diff --git a/src/apis/workspace/workspace.queries.ts b/src/apis/workspace/workspace.queries.ts index 76bd577..27d7b00 100644 --- a/src/apis/workspace/workspace.queries.ts +++ b/src/apis/workspace/workspace.queries.ts @@ -4,6 +4,7 @@ import { workspaceService } from './workspace.service'; import { CreateWorkspaceRequest, InviteWorkspaceRequest, + UpdateWorkspaceSettingsRequest, WorkspaceMemberStatus, } from './workspace.type'; @@ -66,6 +67,18 @@ export const useGetAdminName = (workspaceId: number) => { }); }; +export const useGetWorkspaceMembers = (workspaceId: number, status: WorkspaceMemberStatus) => { + const { data: session } = useSession(); + const accessToken = session?.accessToken as string | undefined; + + return useQuery({ + queryKey: workspaceKeys.members(workspaceId, status), + queryFn: () => workspaceService.getWorkspaceMembers(workspaceId, status, accessToken!), + enabled: !!accessToken && !!workspaceId, + staleTime: 1000 * 60 * 5, + }); +}; + export const useCreateWorkspace = () => { const { data: session } = useSession(); const accessToken = session?.accessToken as string | undefined; @@ -123,14 +136,41 @@ export const useRejectInvitation = () => { }); }; -export const useGetWorkspaceMembers = (workspaceId: number, status: WorkspaceMemberStatus) => { +export const useUpdateWorkspaceSettings = (workspaceId: number) => { const { data: session } = useSession(); const accessToken = session?.accessToken as string | undefined; + const queryClient = useQueryClient(); - return useQuery({ - queryKey: workspaceKeys.members(workspaceId, status), - queryFn: () => workspaceService.getWorkspaceMembers(workspaceId, status, accessToken!), - enabled: !!accessToken && !!workspaceId, - staleTime: 1000 * 60 * 5, + return useMutation({ + mutationFn: (body: UpdateWorkspaceSettingsRequest) => + workspaceService.updateWorkspaceSettings(workspaceId, body, accessToken!), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.detail(workspaceId) }); + queryClient.invalidateQueries({ queryKey: workspaceKeys.my }); + }, + }); +}; + +export const useDeleteWorkspaceMember = (workspaceId: number) => { + const { data: session } = useSession(); + const accessToken = session?.accessToken as string | undefined; + + return useMutation({ + mutationFn: (userId: number) => + workspaceService.deleteWorkspaceMember(workspaceId, userId, accessToken!), + }); +}; + +export const useDeleteWorkspace = () => { + const { data: session } = useSession(); + const accessToken = session?.accessToken as string | undefined; + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (workspaceId: number) => + workspaceService.deleteWorkspace(workspaceId, accessToken!), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.my }); + }, }); }; diff --git a/src/apis/workspace/workspace.service.ts b/src/apis/workspace/workspace.service.ts index 338e1c4..0710562 100644 --- a/src/apis/workspace/workspace.service.ts +++ b/src/apis/workspace/workspace.service.ts @@ -10,6 +10,10 @@ import { AdminNameResponse, WorkspaceMembersResponse, WorkspaceMemberStatus, + UpdateWorkspaceSettingsRequest, + UpdateWorkspaceSettingsResponse, + DeleteWorkspaceMemberResponse, + DeleteWorkspaceResponse, } from './workspace.type'; const BASE_URL = process.env.NEXT_PUBLIC_API_URL; @@ -133,4 +137,55 @@ export const workspaceService = { if (!res.ok) throw new Error('멤버 목록 조회에 실패했습니다.'); return res.json(); }, + + updateWorkspaceSettings: async ( + workspaceId: number, + body: UpdateWorkspaceSettingsRequest, + accessToken: string, + ): Promise => { + const res = await fetch(`${BASE_URL}/api/v1/workspaces/${workspaceId}/settings`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(body), + }); + + if (res.status === 403) throw new Error('수정 권한이 없습니다.'); + if (res.status === 404) throw new Error('워크스페이스를 찾을 수 없습니다.'); + if (!res.ok) throw new Error('워크스페이스 설정 수정에 실패했습니다.'); + return res.json(); + }, + + deleteWorkspaceMember: async ( + workspaceId: number, + userId: number, + accessToken: string, + ): Promise => { + const res = await fetch(`${BASE_URL}/api/v1/workspaces/${workspaceId}/members/${userId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (res.status === 403) throw new Error('강퇴 권한이 없습니다.'); + if (res.status === 404) throw new Error('멤버를 찾을 수 없습니다.'); + if (!res.ok) throw new Error('멤버 강퇴에 실패했습니다.'); + return res.json(); + }, + + deleteWorkspace: async ( + workspaceId: number, + accessToken: string, + ): Promise => { + const res = await fetch(`${BASE_URL}/api/v1/workspaces/${workspaceId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (res.status === 403) throw new Error('삭제 권한이 없습니다.'); + if (res.status === 404) throw new Error('워크스페이스를 찾을 수 없습니다.'); + if (!res.ok) throw new Error('워크스페이스 삭제에 실패했습니다.'); + return res.json(); + }, }; diff --git a/src/apis/workspace/workspace.type.ts b/src/apis/workspace/workspace.type.ts index 42905cd..a178b08 100644 --- a/src/apis/workspace/workspace.type.ts +++ b/src/apis/workspace/workspace.type.ts @@ -121,3 +121,35 @@ export interface WorkspaceMembersResponse { traceId: string; }; } + +export interface UpdateWorkspaceSettingsRequest { + name: string; + color: string; +} + +export interface UpdateWorkspaceSettingsResponse { + success: boolean; + data: null; + meta: { + timestamp: string; + traceId: string; + }; +} + +export interface DeleteWorkspaceMemberResponse { + success: boolean; + data: null; + meta: { + timestamp: string; + traceId: string; + }; +} + +export interface DeleteWorkspaceResponse { + success: boolean; + data: null; + meta: { + timestamp: string; + traceId: string; + }; +} diff --git a/src/app/(after-login)/mypage/page.tsx b/src/app/(after-login)/mypage/page.tsx index 98e6d51..38276bc 100644 --- a/src/app/(after-login)/mypage/page.tsx +++ b/src/app/(after-login)/mypage/page.tsx @@ -157,7 +157,7 @@ export default function Page() { }; return ( -
+
+ +
+

+ {workspace?.workspaceName} +

+ +
+ setName(e.target.value)} + /> + +
+ + +
+ +
+
+

멤버

+
+ +

이름

+ +
+ {members.length === 0 ? ( +

멤버가 없습니다.

+ ) : ( +
    + {members.map((member) => ( +
  • + + {member.role !== 'ADMIN' && ( + + )} +
  • + ))} +
+ )} +
+
+ +
+
+

초대 내역

+ +
+ +

이메일

+ +
+ {pendingMembers.length === 0 ? ( +

초대 내역이 없습니다.

+ ) : ( +
    + {pendingMembers.map((member) => ( +
  • + {member.email} + +
  • + ))} +
+ )} +
+
+ +
+ +
+ + {kickTarget && ( + setKickTarget(null)}> + handleKickMember(kickTarget)} + onClose={() => setKickTarget(null)} + /> + + )} + + {cancelTarget && ( + setCancelTarget(null)}> + handleCancelInvite(cancelTarget)} + onClose={() => setCancelTarget(null)} + /> + + )} + + {showDeleteWorkspaceModal && ( + setShowDeleteWorkspaceModal(false)}> + setShowDeleteWorkspaceModal(false)} + /> + + )} + + {showInviteModal && ( + setShowInviteModal(false)}> + setShowInviteModal(false)} /> + + )} +
+ ); } diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index c8c129f..58e9280 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -2,39 +2,44 @@ import Image from 'next/image'; import { cn } from '@/utils/cn'; -import Icon from './Icon'; interface AvatarProps { name: string; picture: string | null; member?: boolean; + className?: string; } const BASE_URL = process.env.NEXT_PUBLIC_API_URL; -export default function Avatar({ name, picture, member = false }: AvatarProps) { +export default function Avatar({ name, picture, member = false, className }: AvatarProps) { const imageUrl = picture ? picture.startsWith('http') ? picture : `${BASE_URL}${picture}` : null; + const initial = name.charAt(0); + + const sizeClass = member ? 'h-8.5 w-8.5 md:h-9.5 md:w-9.5' : 'h-7.5 w-7.5'; + const textClass = member ? 'text-sm md:text-base' : 'text-xs'; + return ( -
+
{imageUrl ? ( +
+ {name} +
+ ) : (
- {name} + {initial}
- ) : ( - )}
{name}
diff --git a/src/components/header/HeaderUserProfile.tsx b/src/components/header/HeaderUserProfile.tsx index 3e77df3..80415b0 100644 --- a/src/components/header/HeaderUserProfile.tsx +++ b/src/components/header/HeaderUserProfile.tsx @@ -5,7 +5,6 @@ import { signOut } from 'next-auth/react'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import Dropdown from '@/components/Dropdown'; -import Icon from '@/components/Icon'; interface HeaderUserProfileProps { name: string; @@ -26,6 +25,8 @@ export default function HeaderUserProfile({ name, picture }: HeaderUserProfilePr : `${BASE_URL}${picture}` : null; + const initial = name.charAt(0); + useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { @@ -51,11 +52,13 @@ export default function HeaderUserProfile({ name, picture }: HeaderUserProfilePr onClick={() => setOpen((prev) => !prev)} > {imageUrl ? ( -
+
{name}
) : ( - +
+ {initial} +
)}