diff --git a/what_team_my_team/.storybook/preview.ts b/what_team_my_team/.storybook/preview.ts index f6c2ff5..c96a5d1 100644 --- a/what_team_my_team/.storybook/preview.ts +++ b/what_team_my_team/.storybook/preview.ts @@ -1,12 +1,10 @@ -import React from 'react' import { Preview } from '@storybook/react' import '../styles/global.css' -import Layout from '../app/layout' -import {fn} from '@storybook/test' +import { fn } from '@storybook/test' const preview: Preview = { parameters: { - actions: { handleClick: fn()}, + actions: { handleClick: fn() }, controls: { matchers: { color: /(background|color)$/i, @@ -14,17 +12,9 @@ const preview: Preview = { }, }, nextjs: { - appDirectory: true + appDirectory: true, }, }, } -// export const decorators = [ -// (Story) => { -// return React.createElement(Layout,{ -// children:React.createElement(Story) -// }) -// } -// ] - export default preview diff --git a/what_team_my_team/_components/ProfileMenu.tsx b/what_team_my_team/_components/ProfileMenu.tsx index 28c4165..b7bcfa8 100644 --- a/what_team_my_team/_components/ProfileMenu.tsx +++ b/what_team_my_team/_components/ProfileMenu.tsx @@ -55,7 +55,7 @@ const ProfileMenu = ({ user }: ProfileMenuProps) => { }, { label: '팀관리', - href: `/profile/${user?.id}/team`, + href: `/profile/${user?.id}/teamManage`, onClick: handleItemClick, }, ] diff --git a/what_team_my_team/_components/ui/Avatar/Root.tsx b/what_team_my_team/_components/ui/Avatar/Root.tsx index 6806ccf..5e1e6cf 100644 --- a/what_team_my_team/_components/ui/Avatar/Root.tsx +++ b/what_team_my_team/_components/ui/Avatar/Root.tsx @@ -14,6 +14,7 @@ const AvatarRootVariants = cva(`inline-flex items-center justify-center`, { }, rounded: { full: 'rounded-full', + rect: 'rounded-xl', }, }, defaultVariants: { diff --git a/what_team_my_team/_components/ui/DropdownMenu/Content.tsx b/what_team_my_team/_components/ui/DropdownMenu/Content.tsx index 4b0ef4c..a40b580 100644 --- a/what_team_my_team/_components/ui/DropdownMenu/Content.tsx +++ b/what_team_my_team/_components/ui/DropdownMenu/Content.tsx @@ -37,7 +37,6 @@ const DropdownMenuContent = React.forwardRef< ({ + name: '장호정', + student_num: '2020131313', + id: idx, + image_url: 'https://avatars.githubusercontent.com/u/71972587?v=4', + position: '백엔드', + category: '게임 기획', + })), + member_count: 2, + team_id: 49, +} +const notApprovedMemberData = Array.from({ length: 4 }, (_, idx) => ({ + id: idx, + team_id: 36, + created_at: '2024-04-26T02:24:42.910791', + bio: '열심히 하겠습니다!!', + tech: '웹', + user_info: { + id: 127, + name: '이경자', + image_url: + 'https://wtnt-bucket.s3.ap-northeast-2.amazonaws.com/default/thumnail.jpg', + position: '디자이너', + }, +})) + export { AssignData, EntireData, @@ -249,4 +312,7 @@ export { MainPageProjectListData, assignTeamListData, entireTeamData, + myTeamData, + myTeamDetailData, + notApprovedMemberData, } diff --git a/what_team_my_team/_mocks/handlers.ts b/what_team_my_team/_mocks/handlers.ts index 00213b2..f652972 100644 --- a/what_team_my_team/_mocks/handlers.ts +++ b/what_team_my_team/_mocks/handlers.ts @@ -10,6 +10,9 @@ import { MainPageProjectListData, entireTeamData, assignTeamListData, + myTeamData, + myTeamDetailData, + notApprovedMemberData, } from './datas' export const handlers = [ @@ -44,9 +47,18 @@ export const handlers = [ return HttpResponse.json(AccomplishedData) } }), + http.get(`*/user/profile/team-manage/detail/*`, () => { + return HttpResponse.json(myTeamDetailData) + }), + http.get(`*/user/profile/team-manage/*`, () => { + return HttpResponse.json(myTeamData) + }), http.get(`*/user/profile/*`, () => { return HttpResponse.json(ProfileData) }), + http.get(`*/apply/*`, () => { + return HttpResponse.json(notApprovedMemberData) + }), http.get(`*/team/detail/*`, () => { return HttpResponse.json(ProjectDetailData) }), diff --git a/what_team_my_team/_services/mutations/useAcceptMemberWithLeader.ts b/what_team_my_team/_services/mutations/useAcceptMemberWithLeader.ts new file mode 100644 index 0000000..1b76ca5 --- /dev/null +++ b/what_team_my_team/_services/mutations/useAcceptMemberWithLeader.ts @@ -0,0 +1,20 @@ +import axiosInstance from '@/_lib/axios' +import { useMutation } from '@tanstack/react-query' + +export const acceptMemberWithLeaderApi = async ({ + requestId, +}: { + requestId: number +}) => { + const response = await axiosInstance.patch(`/apply/${requestId}`) + + return response.data +} + +export const useAcceptMemberWithLeader = () => { + const acceptMemberWithLeaderMutation = useMutation({ + mutationFn: acceptMemberWithLeaderApi, + }) + + return acceptMemberWithLeaderMutation +} diff --git a/what_team_my_team/_services/mutations/useBanMemberFromProject.ts b/what_team_my_team/_services/mutations/useBanMemberFromProject.ts new file mode 100644 index 0000000..372045d --- /dev/null +++ b/what_team_my_team/_services/mutations/useBanMemberFromProject.ts @@ -0,0 +1,27 @@ +import axiosInstance from '@/_lib/axios' +import { useMutation } from '@tanstack/react-query' + +const banMemberFromProject = async ({ + teamId, + userId, +}: { + teamId: string + userId: number +}) => { + const body = { + ban_user: userId, + } + + const response = await axiosInstance.delete( + `/user/profile/team-manage/detail/${teamId}`, + { data: body }, + ) + + return response.data +} + +export const useBanMemberFromProject = () => { + const mutation = useMutation({ mutationFn: banMemberFromProject }) + + return mutation +} diff --git a/what_team_my_team/_services/mutations/useDeleteTeamWithLeader.ts b/what_team_my_team/_services/mutations/useDeleteTeamWithLeader.ts new file mode 100644 index 0000000..54d58a9 --- /dev/null +++ b/what_team_my_team/_services/mutations/useDeleteTeamWithLeader.ts @@ -0,0 +1,22 @@ +import axiosInstance from '@/_lib/axios' +import { useMutation } from '@tanstack/react-query' + +export const deleteTeamWithLeaderApi = async ({ + teamId, +}: { + teamId: string +}) => { + const response = await axiosInstance.delete( + `/user/profile/team-manage/${teamId}`, + ) + + return response.data +} + +export const useDeleteTeamWithLeader = () => { + const deleteTeamWithLeaderMutation = useMutation({ + mutationFn: deleteTeamWithLeaderApi, + }) + + return deleteTeamWithLeaderMutation +} diff --git a/what_team_my_team/_services/mutations/useLeaveTeam.ts b/what_team_my_team/_services/mutations/useLeaveTeam.ts new file mode 100644 index 0000000..8fd546b --- /dev/null +++ b/what_team_my_team/_services/mutations/useLeaveTeam.ts @@ -0,0 +1,16 @@ +import axiosInstance from '@/_lib/axios' +import { useMutation } from '@tanstack/react-query' + +export const leaveTeamApi = async ({ teamId }: { teamId: string }) => { + const response = await axiosInstance.patch( + `/user/profile/team-manage/${teamId}`, + ) + + return response.data +} + +export const useLeaveTeam = () => { + const leaveTeamMutation = useMutation({ mutationFn: leaveTeamApi }) + + return leaveTeamMutation +} diff --git a/what_team_my_team/_services/mutations/useRejectMemberWithLeader.ts b/what_team_my_team/_services/mutations/useRejectMemberWithLeader.ts new file mode 100644 index 0000000..184e689 --- /dev/null +++ b/what_team_my_team/_services/mutations/useRejectMemberWithLeader.ts @@ -0,0 +1,20 @@ +import axiosInstance from '@/_lib/axios' +import { useMutation } from '@tanstack/react-query' + +export const rejectMemberWithLeaderApi = async ({ + requestId, +}: { + requestId: number +}) => { + const response = await axiosInstance.delete(`/apply/${requestId}`) + + return response.data +} + +export const useRejectMemberWithLeader = () => { + const rejectMemberWithLeaderMutation = useMutation({ + mutationFn: rejectMemberWithLeaderApi, + }) + + return rejectMemberWithLeaderMutation +} diff --git a/what_team_my_team/_services/queries/useCheckLeader.ts b/what_team_my_team/_services/queries/useCheckLeader.ts new file mode 100644 index 0000000..2940974 --- /dev/null +++ b/what_team_my_team/_services/queries/useCheckLeader.ts @@ -0,0 +1,37 @@ +import axiosInstance from '@/_lib/axios' +import { + ConvertSnakeToCamel, + convertSnakeToCamel, +} from '@/_utils/convertSnakeToCamel' +import { useQuery } from '@tanstack/react-query' +import { AxiosError } from 'axios' + +export interface CheckLeaderApiResponse { + is_leader: boolean +} + +export const checkLeaderApi = async ({ teamId }: { teamId: string }) => { + const response = await axiosInstance.get(`/team/check-leader/${teamId}`) + + return response.data +} + +const IS_LEADER_KEY = 'is-leader' + +export const useCheckLeader = ({ teamId }: { teamId: string }) => { + const query = useQuery< + CheckLeaderApiResponse, + AxiosError, + ConvertSnakeToCamel + >({ + queryKey: [IS_LEADER_KEY, teamId], + queryFn: () => checkLeaderApi({ teamId }), + select: (data) => { + const convertedData = convertSnakeToCamel(data) + + return convertedData + }, + }) + + return query +} diff --git a/what_team_my_team/_services/queries/useMyTeam.ts b/what_team_my_team/_services/queries/useMyTeam.ts new file mode 100644 index 0000000..909b472 --- /dev/null +++ b/what_team_my_team/_services/queries/useMyTeam.ts @@ -0,0 +1,50 @@ +import axiosInstance from '@/_lib/axios' +import { + convertSnakeToCamel, + ConvertSnakeToCamel, +} from '@/_utils/convertSnakeToCamel' +import { useQuery } from '@tanstack/react-query' +import { AxiosError } from 'axios' + +export type Team = { + id: number + title: string + member_count: number + leader_info: { + name: string + id: number + image_url: string + is_leader: boolean + } +} +export type TeamCamel = ConvertSnakeToCamel + +const getManageTeamApi = async ({ userId }: { userId: string }) => { + const response = await axiosInstance.get( + `/user/profile/team-manage/${userId}`, + ) + + return response.data +} + +const MANAGE_MY_TEAM_KEY = 'team-manage' + +const useMyTeam = ({ userId }: { userId: string }) => { + const query = useQuery< + { team: Team[] }, + AxiosError, + ConvertSnakeToCamel<{ team: Team[] }> + >({ + queryFn: () => getManageTeamApi({ userId }), + queryKey: [MANAGE_MY_TEAM_KEY, userId], + select: (data) => { + const camelData = convertSnakeToCamel(data) + + return camelData + }, + }) + + return query +} + +export default useMyTeam diff --git a/what_team_my_team/_services/queries/useMyTeamDetail.ts b/what_team_my_team/_services/queries/useMyTeamDetail.ts new file mode 100644 index 0000000..ae813f0 --- /dev/null +++ b/what_team_my_team/_services/queries/useMyTeamDetail.ts @@ -0,0 +1,59 @@ +import axiosInstance from '@/_lib/axios' +import { + convertSnakeToCamel, + ConvertSnakeToCamel, +} from '@/_utils/convertSnakeToCamel' +import { useQuery } from '@tanstack/react-query' +import { AxiosError } from 'axios' + +export interface MyTeamDetailResponse { + title: string + leader_info: { + name: string + student_num: string + id: number + image_url: string + position: string + category: string + } + members_info: Member[] + member_count: number + team_id: number +} + +export type Member = { + name: string + student_num: string + id: number + image_url: string + position: string + category: string +} + +const getMyTeamDetailApi = async ({ teamId }: { teamId: string }) => { + const response = await axiosInstance.get( + `/user/profile/team-manage/detail/${teamId}`, + ) + + return response.data +} + +export const MANAGE_MY_TEAM_DETAIL_KEY = 'team-manage-detail' + +const useMyTeamDetail = ({ teamId }: { teamId: string }) => { + const myTeamDetailQuery = useQuery< + MyTeamDetailResponse, + AxiosError, + ConvertSnakeToCamel + >({ + queryFn: () => getMyTeamDetailApi({ teamId }), + queryKey: [MANAGE_MY_TEAM_DETAIL_KEY, teamId], + select: (data) => { + return convertSnakeToCamel(data) + }, + }) + + return myTeamDetailQuery +} + +export default useMyTeamDetail diff --git a/what_team_my_team/_services/queries/useNotApprovedMember.ts b/what_team_my_team/_services/queries/useNotApprovedMember.ts new file mode 100644 index 0000000..cfec0d7 --- /dev/null +++ b/what_team_my_team/_services/queries/useNotApprovedMember.ts @@ -0,0 +1,47 @@ +import axiosInstance from '@/_lib/axios' +import { + convertSnakeToCamel, + ConvertSnakeToCamel, +} from '@/_utils/convertSnakeToCamel' +import { useQuery } from '@tanstack/react-query' +import { AxiosError } from 'axios' + +export interface NotApprovedMemberReturn { + id: number + team_id: number + created_at: string + bio: string + tech: string + user_info: { + id: number + name: string + image_url: string + position: string + } +} + +export const notApprovedMemberApi = async ({ teamId }: { teamId: string }) => { + const response = await axiosInstance.get(`/apply/${teamId}`) + + return response.data +} + +export const NOT_APPROVED_MEMBER_KEY = 'not-approved-member' + +export const useNotApprovedMember = ({ teamId }: { teamId: string }) => { + const notApprovedMemberQuery = useQuery< + NotApprovedMemberReturn[], + AxiosError, + ConvertSnakeToCamel + >({ + queryFn: () => notApprovedMemberApi({ teamId }), + queryKey: [NOT_APPROVED_MEMBER_KEY, teamId], + select: (data) => { + const convertedData = convertSnakeToCamel(data) + + return convertedData + }, + }) + + return notApprovedMemberQuery +} diff --git a/what_team_my_team/_stores/atoms/dialog.ts b/what_team_my_team/_stores/atoms/dialog.ts index 1efa426..7c5bf7c 100644 --- a/what_team_my_team/_stores/atoms/dialog.ts +++ b/what_team_my_team/_stores/atoms/dialog.ts @@ -1,3 +1,5 @@ import { atom } from 'jotai' export const applyDialogAtom = atom(false) +export const leaveTeamDialogAtom = atom(false) +export const deleteTeamDialogAtom = atom(false) diff --git a/what_team_my_team/app/(profile)/profile/[slug]/ProfileSideMenu.tsx b/what_team_my_team/app/(profile)/profile/[slug]/_components/ProfileSideMenu.tsx similarity index 88% rename from what_team_my_team/app/(profile)/profile/[slug]/ProfileSideMenu.tsx rename to what_team_my_team/app/(profile)/profile/[slug]/_components/ProfileSideMenu.tsx index de5c248..e89fdc4 100644 --- a/what_team_my_team/app/(profile)/profile/[slug]/ProfileSideMenu.tsx +++ b/what_team_my_team/app/(profile)/profile/[slug]/_components/ProfileSideMenu.tsx @@ -12,7 +12,7 @@ const ProfileSideMenu = ({ userId }: { userId: string }) => { const menuItems = [ { href: `/profile/${userId}`, label: '프로필' }, { href: `/profile/${userId}/active`, label: '내 활동' }, - { href: `/profile/${userId}/team`, label: '팀 관리' }, + { href: `/profile/${userId}/teamManage`, label: '팀 관리' }, ] return ( @@ -31,7 +31,7 @@ const ProfileSideMenu = ({ userId }: { userId: string }) => { ))} - diff --git a/what_team_my_team/app/(profile)/profile/[slug]/layout.tsx b/what_team_my_team/app/(profile)/profile/[slug]/layout.tsx index 0e4fc35..d7c9d61 100644 --- a/what_team_my_team/app/(profile)/profile/[slug]/layout.tsx +++ b/what_team_my_team/app/(profile)/profile/[slug]/layout.tsx @@ -1,4 +1,4 @@ -import ProfileSideMenu from './ProfileSideMenu' +import ProfileSideMenu from './_components/ProfileSideMenu' const layout = ({ children, diff --git a/what_team_my_team/app/(profile)/profile/[slug]/teamManage/_components/Team.tsx b/what_team_my_team/app/(profile)/profile/[slug]/teamManage/_components/Team.tsx new file mode 100644 index 0000000..adc945b --- /dev/null +++ b/what_team_my_team/app/(profile)/profile/[slug]/teamManage/_components/Team.tsx @@ -0,0 +1,46 @@ +'use client' + +import ProfileAvatar from '@/_components/ProfileAvatar' +import { TeamCamel } from '@/_services/queries/useMyTeam' +import Image from 'next/image' +import Link from 'next/link' +import React from 'react' + +interface TeamProps { + item: TeamCamel +} + +const Team = ({ item }: TeamProps) => { + return ( +
  • + + {/*
    + + + + {item.leaderInfo.name} +
    */} +
  • + ) +} + +export default Team diff --git a/what_team_my_team/app/(profile)/profile/[slug]/teamManage/_components/TeamListContainer.tsx b/what_team_my_team/app/(profile)/profile/[slug]/teamManage/_components/TeamListContainer.tsx new file mode 100644 index 0000000..ec0476d --- /dev/null +++ b/what_team_my_team/app/(profile)/profile/[slug]/teamManage/_components/TeamListContainer.tsx @@ -0,0 +1,27 @@ +'use client' + +import useMyTeam from '@/_services/queries/useMyTeam' +import React from 'react' +import Team from './Team' + +interface ContainerProps { + userId: string +} + +const TeamListContainer = ({ userId }: ContainerProps) => { + const { data } = useMyTeam({ userId }) + + return ( +
    + {data && ( +
      + {data.team.map((item) => ( + + ))} +
    + )} +
    + ) +} + +export default TeamListContainer diff --git a/what_team_my_team/app/(profile)/profile/[slug]/teamManage/page.tsx b/what_team_my_team/app/(profile)/profile/[slug]/teamManage/page.tsx new file mode 100644 index 0000000..05275e2 --- /dev/null +++ b/what_team_my_team/app/(profile)/profile/[slug]/teamManage/page.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import TeamListContainer from './_components/TeamListContainer' + +const page = ({ params }: { params: { slug: string } }) => { + const userId = params.slug + return ( +
    + +
    + ) +} + +export default page diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/ApplyDialog.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/ApplyDialog.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/ApplyDialog.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/ApplyDialog.tsx diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/LinkList.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/LinkList.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/LinkList.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/LinkList.tsx diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/PositionItem.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/PositionItem.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/PositionItem.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/PositionItem.tsx diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/PositionList.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/PositionList.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/PositionList.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/PositionList.tsx diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/PrimaryInfo.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/PrimaryInfo.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/PrimaryInfo.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/PrimaryInfo.tsx diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/ProjectContainer.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/ProjectContainer.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/ProjectContainer.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/ProjectContainer.tsx diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/ProjectExplain.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/ProjectExplain.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/ProjectExplain.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/ProjectExplain.tsx diff --git a/what_team_my_team/app/(project)/project/[...slug]/_components/SideMenu.tsx b/what_team_my_team/app/(project)/project/[slug]/_components/SideMenu.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/_components/SideMenu.tsx rename to what_team_my_team/app/(project)/project/[slug]/_components/SideMenu.tsx diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/_components/DeleteTeamConfirmDialog.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/_components/DeleteTeamConfirmDialog.tsx new file mode 100644 index 0000000..65f57f7 --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/_components/DeleteTeamConfirmDialog.tsx @@ -0,0 +1,60 @@ +'use client' + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogPortal, +} from '@/_components/ui/AlertDialog' +import Button from '@/_components/ui/Button' +import { useDeleteTeamWithLeader } from '@/_services/mutations/useDeleteTeamWithLeader' +import { deleteTeamDialogAtom } from '@/_stores/atoms/dialog' +import { useAtom } from 'jotai' +import { useRouter } from 'next/navigation' + +interface DeleteTeamConfirmDialogProps { + teamId: string +} + +const DeleteTeamConfirmDialog = ({ teamId }: DeleteTeamConfirmDialogProps) => { + const [isOpen, setIsOpen] = useAtom(deleteTeamDialogAtom) + const router = useRouter() + const { mutate } = useDeleteTeamWithLeader() + + const handleActionBtn = () => { + mutate( + { teamId }, + { + onSuccess: () => { + alert('팀이 해체되었습니다.') + router.push('/') + }, + onError: (error) => console.log(error), + }, + ) + } + + return ( + + + +

    정말로 팀을 해체하시겠습니까?

    +

    + 팀을 해체한 후에는 되돌릴 수 없습니다. 신중하게 선택해주세요. +

    +
    + + + + + + +
    +
    +
    +
    + ) +} + +export default DeleteTeamConfirmDialog diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/_components/LeaveTeamConfirmDialog.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/_components/LeaveTeamConfirmDialog.tsx new file mode 100644 index 0000000..719c7c6 --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/_components/LeaveTeamConfirmDialog.tsx @@ -0,0 +1,48 @@ +'use client' + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogPortal, +} from '@/_components/ui/AlertDialog' +import Button from '@/_components/ui/Button' +import { useLeaveTeam } from '@/_services/mutations/useLeaveTeam' +import { leaveTeamDialogAtom } from '@/_stores/atoms/dialog' +import { useAtom } from 'jotai' + +interface LeaveTeamConfirmDialogProps { + teamId: string +} + +const LeaveTeamConfirmDialog = ({ teamId }: LeaveTeamConfirmDialogProps) => { + const [isOpen, setIsOpen] = useAtom(leaveTeamDialogAtom) + const { mutate } = useLeaveTeam() + const handleActionBtn = () => { + mutate({ teamId }, { onSuccess: () => console.log('success') }) + } + + return ( + + + +

    정말로 팀을 탈퇴하시겠습니까?

    +

    + 팀을 탈퇴한 후에는 되돌릴 수 없습니다. 신중하게 선택해주세요. +

    +
    + + + + + + +
    +
    +
    +
    + ) +} + +export default LeaveTeamConfirmDialog diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/_components/ManageNavigation.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/_components/ManageNavigation.tsx new file mode 100644 index 0000000..ac790ed --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/_components/ManageNavigation.tsx @@ -0,0 +1,107 @@ +'use client' + +import Button from '@/_components/ui/Button' +import { cn } from '@/_lib/utils' +import { useCheckLeader } from '@/_services/queries/useCheckLeader' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import React, { useEffect, useState } from 'react' +import LeaveTeamConfirmDialog from './LeaveTeamConfirmDialog' +import { useSetAtom } from 'jotai' +import { + deleteTeamDialogAtom, + leaveTeamDialogAtom, +} from '@/_stores/atoms/dialog' +import DeleteTeamConfirmDialog from './DeleteTeamConfirmDialog' + +interface ManageNavigationProps { + teamId: string +} + +export const ManageNavigation = ({ teamId }: ManageNavigationProps) => { + const [isLeader, setIsLeader] = useState(false) + const { data } = useCheckLeader({ teamId }) + const pathname = usePathname() + const segments = pathname.split('/') + + const path = segments[segments.length - 1] + + useEffect(() => { + if (data) { + setIsLeader(data.isLeader) + } + }, [data]) + + const navigationButtons = [ + { + path: `/project/${teamId}/manage/member`, + text: '팀원', + id: 'member', + }, + { + path: `/project/${teamId}/manage/member/assign`, + text: '팀원 승인', + id: 'assign', + forLeader: true, + }, + ] + + return ( +
    + + + +
    + ) +} + +const DeleteTeamButton = () => { + const setIsOpen = useSetAtom(deleteTeamDialogAtom) + const handleClickDeleteTeam = () => { + setIsOpen(true) + } + + return ( + + ) +} + +const LeaveTeamButton = () => { + const setIsOpen = useSetAtom(leaveTeamDialogAtom) + const handleClickLeaveBtn = () => { + setIsOpen(true) + } + + return ( + + ) +} diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/layout.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/layout.tsx new file mode 100644 index 0000000..e8407c2 --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/layout.tsx @@ -0,0 +1,17 @@ +import { ManageNavigation } from './_components/ManageNavigation' + +const layout = ({ + children, + params, +}: Readonly<{ children: React.ReactNode; params: { slug: string } }>) => { + const teamId = params.slug + + return ( +
    + + {children} +
    + ) +} + +export default layout diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/MemberItem.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/MemberItem.tsx new file mode 100644 index 0000000..3120c23 --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/MemberItem.tsx @@ -0,0 +1,100 @@ +'use client' + +import ProfileAvatar from '@/_components/ProfileAvatar' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuPortal, + DropdownMenuTrigger, +} from '@/_components/ui/DropdownMenu' +import { useBanMemberFromProject } from '@/_services/mutations/useBanMemberFromProject' +import { Member } from '@/_services/queries/useMyTeamDetail' +import { userState } from '@/_stores/atoms/user' +import { ConvertSnakeToCamel } from '@/_utils/convertSnakeToCamel' +import { useAtomValue } from 'jotai' +import React from 'react' +import { FaEllipsisV, FaTrash } from 'react-icons/fa' + +interface MemberItemProps { + teamId: string + memberData: ConvertSnakeToCamel + leaderId: number +} + +const MemberItem = ({ teamId, memberData, leaderId }: MemberItemProps) => { + const user = useAtomValue(userState) + + const isLeader = leaderId === user?.id + + return ( +
  • +
    + + {isLeader && ( + + )} +
    +
    + + {memberData.name} + {memberData.studentNum} + + {memberData.position} +
    +
    +
    +
    역할
    + {memberData.category} +
    +
  • + ) +} + +interface MemberManageDropdownProps { + teamId: string + userId: number +} + +const MemberManageDropdown = ({ + teamId, + userId, +}: MemberManageDropdownProps) => { + const { mutate } = useBanMemberFromProject() + const handleBanMember = () => { + mutate( + { teamId, userId }, + { + onError: (err) => console.log(err), + onSuccess: () => console.log('success'), + }, + ) + } + + return ( + + + + + + + + + + + + + ) +} + +export default MemberItem diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/MemberList.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/MemberList.tsx new file mode 100644 index 0000000..1c555c8 --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/MemberList.tsx @@ -0,0 +1,49 @@ +'use client' + +import { MyTeamDetailResponse } from '@/_services/queries/useMyTeamDetail' +import { ConvertSnakeToCamel } from '@/_utils/convertSnakeToCamel' +import MemberItem from './MemberItem' +import { cn } from '@/_lib/utils' + +interface MemberListProps { + teamId: string + isLoading: boolean + data: ConvertSnakeToCamel | undefined +} + +const MemberList = ({ teamId, isLoading, data }: MemberListProps) => { + return ( + <> +

    승인 멤버

    + {isLoading &&
    로딩중...
    } + {data && + (data.membersInfo.length === 0 ? ( +
    + + + 팀원이 없습니다. + +

    함께할 팀원들을 구해보아요!

    +
    + ) : ( +
      + {data.membersInfo.map((item) => ( + + ))} +
    + ))} + + ) +} + +export default MemberList diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/TeamMemberContainer.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/TeamMemberContainer.tsx new file mode 100644 index 0000000..679508c --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/member/_components/TeamMemberContainer.tsx @@ -0,0 +1,24 @@ +'use client' + +import useMyTeamDetail from '@/_services/queries/useMyTeamDetail' +import React from 'react' +import { useAtomValue } from 'jotai' +import { userState } from '@/_stores/atoms/user' +import MemberList from './MemberList' + +interface TeamMemberContainerProps { + teamId: string +} + +const TeamMemberContainer = ({ teamId }: TeamMemberContainerProps) => { + const { data, isLoading } = useMyTeamDetail({ teamId }) + const user = useAtomValue(userState) + + return ( +
    + +
    + ) +} + +export default TeamMemberContainer diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/member/assign/_components/MemberContainer.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/member/assign/_components/MemberContainer.tsx new file mode 100644 index 0000000..58fa624 --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/member/assign/_components/MemberContainer.tsx @@ -0,0 +1,116 @@ +'use client' + +import { + NOT_APPROVED_MEMBER_KEY, + useNotApprovedMember, +} from '@/_services/queries/useNotApprovedMember' +import ProfileAvatar from '@/_components/ProfileAvatar' +import Link from 'next/link' +import Button from '@/_components/ui/Button' +import { useAcceptMemberWithLeader } from '@/_services/mutations/useAcceptMemberWithLeader' +import { useRejectMemberWithLeader } from '@/_services/mutations/useRejectMemberWithLeader' +import { useQueryClient } from '@tanstack/react-query' + +interface MemberContainerProps { + teamId: string +} + +export const MemberContainer = ({ teamId }: MemberContainerProps) => { + const { data, isLoading } = useNotApprovedMember({ teamId }) + + return ( + <> + {isLoading &&
    로딩중...
    } + {data && data.length === 0 ? ( +
    승인 대기 목록이 비어있습니다.
    + ) : ( + data && ( +
      + {data.map((item) => ( +
    • +
      + + + {item.userInfo.name} + +
      +
      + + +
      +
    • + ))} +
    + ) + )} + + ) +} + +const AcceptBtn = ({ + requestId, + teamId, +}: { + requestId: number + teamId: string +}) => { + const queryClient = useQueryClient() + const { mutate } = useAcceptMemberWithLeader() + + const handleClickBtn = () => { + mutate( + { requestId }, + { + onError: (err) => console.log(err), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [NOT_APPROVED_MEMBER_KEY, teamId], + }) + alert('성공') + }, + }, + ) + } + + return ( + + ) +} + +const RejectBtn = ({ + requestId, + teamId, +}: { + requestId: number + teamId: string +}) => { + const queryClient = useQueryClient() + + const { mutate } = useRejectMemberWithLeader() + const handleClickBtn = () => { + mutate( + { requestId }, + { + onError: () => alert('알 수 없는 에러가 발생하였습니다.'), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [NOT_APPROVED_MEMBER_KEY, teamId], + }) + alert('성공적입니다.') + }, + }, + ) + } + + return ( + + ) +} diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/member/assign/page.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/member/assign/page.tsx new file mode 100644 index 0000000..d1d30c2 --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/member/assign/page.tsx @@ -0,0 +1,14 @@ +import { MemberContainer } from './_components/MemberContainer' + +const page = ({ params }: { params: { slug: string } }) => { + const teamId = params.slug + + console.log(teamId) + return ( +
    + +
    + ) +} + +export default page diff --git a/what_team_my_team/app/(project)/project/[slug]/manage/member/page.tsx b/what_team_my_team/app/(project)/project/[slug]/manage/member/page.tsx new file mode 100644 index 0000000..b5f541e --- /dev/null +++ b/what_team_my_team/app/(project)/project/[slug]/manage/member/page.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import TeamMemberContainer from './_components/TeamMemberContainer' +import { cookies } from 'next/headers' +import { getQueryClient } from '@/app/getQueryClient' +import { MANAGE_MY_TEAM_DETAIL_KEY } from '@/_services/queries/useMyTeamDetail' +import axios from 'axios' +import { dehydrate } from '@tanstack/react-query' +import Hydrate from '@/_lib/hydrate.client' +import { baseURL } from '@/_lib/axios' + +const page = async ({ params }: { params: { slug: string } }) => { + const teamId = params.slug + const cookieStore = cookies() + const accessCookie = cookieStore.get('access')?.value + + const queryClient = getQueryClient() + await queryClient.prefetchQuery({ + queryKey: [MANAGE_MY_TEAM_DETAIL_KEY, teamId], + queryFn: async () => { + const response = await axios.get( + `${baseURL}/user/profile/team-manage/detail/${teamId}`, + { + headers: { Authorization: `Bearer ${accessCookie}` }, + }, + ) + + return response.data + }, + }) + + const dehydratedState = dehydrate(queryClient) + + return ( + +
    + +
    +
    + ) +} + +export default page diff --git a/what_team_my_team/app/(project)/project/[...slug]/page.tsx b/what_team_my_team/app/(project)/project/[slug]/page.tsx similarity index 100% rename from what_team_my_team/app/(project)/project/[...slug]/page.tsx rename to what_team_my_team/app/(project)/project/[slug]/page.tsx diff --git a/what_team_my_team/next.config.mjs b/what_team_my_team/next.config.mjs index 9452277..f3e36a5 100644 --- a/what_team_my_team/next.config.mjs +++ b/what_team_my_team/next.config.mjs @@ -15,6 +15,15 @@ const nextConfig = { }, ], }, + redirects() { + return [ + { + source: '/project/:slug/manage', + destination: '/project/:slug/manage/member', + permanent: true, + }, + ] + }, } export default nextConfig diff --git a/what_team_my_team/public/assets/stakeholder.png b/what_team_my_team/public/assets/stakeholder.png new file mode 100644 index 0000000..62fa838 Binary files /dev/null and b/what_team_my_team/public/assets/stakeholder.png differ diff --git a/what_team_my_team/stories/MemberItem.stories.tsx b/what_team_my_team/stories/MemberItem.stories.tsx new file mode 100644 index 0000000..88155d5 --- /dev/null +++ b/what_team_my_team/stories/MemberItem.stories.tsx @@ -0,0 +1,24 @@ +import { convertSnakeToCamel } from '../_utils/convertSnakeToCamel' +import type { Meta, StoryObj } from '@storybook/react' +import MemberItem from '@/app/(project)/project/[slug]/manage/member/_components/MemberItem' +import { myTeamDetailData } from '@/_mocks/datas' + +const meta = { + title: 'Components/MemberItem', + component: MemberItem, + parameters: { + layout: 'centered', + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Primary: Story = { + args: { + memberData: convertSnakeToCamel(myTeamDetailData.members_info[0]), + teamId: '1', + leaderId: 1, + }, +} diff --git a/what_team_my_team/stories/PositionItem.stories.ts b/what_team_my_team/stories/PositionItem.stories.ts index afc6a17..8842b35 100644 --- a/what_team_my_team/stories/PositionItem.stories.ts +++ b/what_team_my_team/stories/PositionItem.stories.ts @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react' -import PositionItem from '@/app/(project)/project/[...slug]/_components/PositionItem' +import PositionItem from '@/app/(project)/project/[slug]/_components/PositionItem' import { Tech } from '@/_services/queries/useProjectDetail' const meta = {