Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions src/apis/dashboards/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from 'zod';
import { DASHBOARD_FORM_ERROR_MESSAGE, DASHBOARD_FORM_VALID_LENGTH } from '@/constants/dashboard';
import { DEFAULT_COLORS } from '@/constants/colors';
import { userSchema } from '@/apis/users/types';

// base pagination params 타입 (필요시 공용으로 추출)
export type BasePaginationParams = {
Expand Down Expand Up @@ -45,13 +46,6 @@ export const dashboardFormSchema = z.object({
});
export type DashboardFormType = z.infer<typeof dashboardFormSchema>;

// TODO : 임시 유저 스키마(추후 /apis/auth 쪽에서 작성된 schema 임포트 필요)
export const userSchema = z.object({
id: z.number(),
email: z.string().email(),
nickname: z.string(),
});

export const invitationUserSchema = userSchema.pick({
id: true,
email: true,
Expand Down
23 changes: 23 additions & 0 deletions src/apis/members/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import axiosClientHelper from '@/utils/network/axiosClientHelper';
import { MembersParams, MembersRespons, membersResponseSchema } from './types';

export const getMembers = async ({ page, size, dashboardId }: MembersParams) => {
const response = await axiosClientHelper.get<MembersRespons>('/members', {
params: {
size: size || 20,
page: page || 1,
dashboardId,
},
});

const result = membersResponseSchema.safeParse(response.data);
if (!result.success) {
throw new Error('서버에서 받은 데이터가 예상과 다릅니다.');
}
return result.data;
};

export const deleteMember = async (memberId: number) => {
const response = await axiosClientHelper.delete<void>(`/members/${memberId}`);
return response.data;
};
31 changes: 31 additions & 0 deletions src/apis/members/quries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { deleteMember, getMembers } from '.';

export const useMembersQuery = (page: number, size: number, dashboardId: number) => {
return useQuery({
queryKey: ['members', dashboardId, page, size],
queryFn: () =>
getMembers({
page,
size,
dashboardId,
}),
});
};

export const useMembersMutation = () => {
const queryClient = useQueryClient();

const remove = useMutation({
mutationFn: ({ memberId }: { memberId: number; dashboardId: number }) => {
return deleteMember(memberId);
},
onSuccess: (_, { dashboardId }) => {
queryClient.invalidateQueries({ queryKey: ['members', dashboardId] });
},
});

return {
remove: remove.mutateAsync,
};
};
20 changes: 20 additions & 0 deletions src/apis/members/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod';
import { userSchema } from '../users/types';
import { BasePaginationParams } from '../dashboards/types';

export type MembersParams = BasePaginationParams & {
dashboardId: number;
};

export const memberSchema = userSchema.extend({
userId: z.number(),
isOwner: z.boolean(),
});
export type Member = z.infer<typeof memberSchema>;

export const membersResponseSchema = z.object({
totalCount: z.number(),
members: z.array(memberSchema),
});

export type MembersRespons = z.infer<typeof membersResponseSchema>;
59 changes: 25 additions & 34 deletions src/app/(after-login)/dashboard/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,43 @@
'use client';

import { useParams, useRouter } from 'next/navigation';
import { useDashboardMutation } from '@/apis/dashboards/queries';
import Button from '@/components/ui/Button/Button';
import useAlert from '@/hooks/useAlert';
import { getErrorMessage } from '@/utils/errorMessage';
import DetailModify from '@/components/dashboard/DetailModify';
import DetailMembers from '@/components/dashboard/DetailMembers';
import DetailInvited from '@/components/dashboard/DetailInvited';
import GoBackLink from '@/components/ui/Link/GoBackLink';

export default function DashboardEditPage() {
const router = useRouter();
const alert = useAlert();
const { id } = useParams<{ id: string }>();
const { remove } = useDashboardMutation();
import DetailDelete from '@/components/dashboard/DetailDelete';
import { Suspense } from 'react';
import axiosServerHelper from '@/utils/network/axiosServerHelper';
import { redirect } from 'next/navigation';
import { Dashboard } from '@/apis/dashboards/types';

export default async function DashboardEditPage({ params }: { params: Promise<{ id: string }> }) {
const id = (await params).id;
const response = await axiosServerHelper<Dashboard>(`/dashboards/${id}`);
const { createdByMe } = response.data;

const handleDelete = async () => {
try {
await remove(Number(id));
alert('삭제했습니다.');
router.push(`/mydashboard`);
} catch (error) {
const message = getErrorMessage(error);
alert(message);
}
};
if (!createdByMe) {
redirect('/mydashboard');
}

return (
<div className='p-10'>
<div className='mb-8'>
<GoBackLink href={`/dashboard/${id}`} />
</div>
<div className='grid w-full max-w-[620px] gap-4'>
{/* 대시보드 정보 */}
<DetailModify />
<Suspense fallback={<div>loading...</div>}>
<div className='grid w-full max-w-[620px] gap-4'>
{/* 대시보드 정보 */}
<DetailModify data={response.data} />

{/* 구성원 리스트 */}
<DetailMembers />
{/* 구성원 리스트 */}
<DetailMembers />

{/* 초대내역 */}
<DetailInvited />
{/* 초대내역 */}
<DetailInvited />

{/* 대시보드 삭제 */}
<Button variant='outline' onClick={handleDelete}>
대시보드 삭제하기
</Button>
</div>
{/* 대시보드 삭제 */}
<DetailDelete />
</div>
</Suspense>
</div>
);
}
31 changes: 31 additions & 0 deletions src/components/dashboard/DetailDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { useParams, useRouter } from 'next/navigation';
import { useDashboardMutation } from '@/apis/dashboards/queries';
import Button from '@/components/ui/Button/Button';
import useAlert from '@/hooks/useAlert';
import { getErrorMessage } from '@/utils/errorMessage';

export default function DetailDelete() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
const alert = useAlert();
const { remove } = useDashboardMutation();

const handleDelete = async () => {
try {
await remove(Number(id));
alert('삭제했습니다.');
router.push(`/mydashboard`);
} catch (error) {
const message = getErrorMessage(error);
alert(message);
}
};

return (
<Button variant='outline' onClick={handleDelete}>
대시보드 삭제하기
</Button>
);
}
2 changes: 2 additions & 0 deletions src/components/dashboard/DetailInvited.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

import { useRef, useState } from 'react';
import Button from '@/components/ui/Button/Button';
import { Card, CardTitle } from '@/components/ui/Card/Card';
Expand Down
94 changes: 92 additions & 2 deletions src/components/dashboard/DetailMembers.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,100 @@
'use client';

import { Card, CardTitle } from '@/components/ui/Card/Card';
import { useState } from 'react';
import { useParams } from 'next/navigation';
import useAlert from '@/hooks/useAlert';
import PaginationWithCounter from '@/components/pagination/PaginationWithCounter';
import { useMembersMutation, useMembersQuery } from '@/apis/members/quries';
import { Table, TableBody, TableCell, TableCol, TableColGroup, TableHead, TableHeadCell, TableRow } from '@/components/ui/Table/Table';
import { isAxiosError } from 'axios';
import Button from '../ui/Button/Button';
import { getErrorMessage } from '@/utils/errorMessage';
import Avatar from '../ui/Avatar/Avatar';

const PAGE_SIZE = 5;

export default function DetailMembers() {
const { id } = useParams<{ id: string }>();
const [page, setPage] = useState(1);
const { data, error, isLoading } = useMembersQuery(page, PAGE_SIZE, Number(id));
const { remove } = useMembersMutation();
const alert = useAlert();

const removeMember = async (memberId: number) => {
try {
await remove({ memberId, dashboardId: Number(id) });
alert('맴버를 삭제했습니다.');
} catch (error) {
const message = getErrorMessage(error);
alert(message);
}
};
const notAllowed = isAxiosError(error) && error.status === 403;

return (
<Card>
<CardTitle>구성원</CardTitle>
<div>구성원 리스트(작업필요)</div>
<CardTitle>
구성원
<div className='leading-none'>
<PaginationWithCounter //
totalCount={data?.totalCount || 0}
page={page}
setPage={setPage}
pageSize={PAGE_SIZE}
/>
</div>
</CardTitle>
<Table className='mb-4'>
<TableColGroup>
<TableCol />
<TableCol className='w-24' />
</TableColGroup>
<TableHead>
<TableRow>
<TableHeadCell>이름</TableHeadCell>
<TableHeadCell></TableHeadCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && (
<TableRow>
<TableCell colSpan={2}>
<div className='p-4 text-center'>구성원을 가져오는 중입니다.</div>
</TableCell>
</TableRow>
)}
{notAllowed && (
<TableRow>
<TableCell colSpan={2}>
<div className='p-4 text-center'>권한이 없습니다.</div>
</TableCell>
</TableRow>
)}
{data?.members.length === 0 && (
<TableRow>
<TableCell colSpan={2}>
<div className='p-4 text-center'>구성원이 없습니다.</div>
</TableCell>
</TableRow>
)}
{data?.members.map((item) => (
<TableRow key={item.id}>
<TableCell>
<div className='flex items-center gap-3'>
<Avatar email={item.email} />
{item.nickname}
</div>
</TableCell>
<TableCell>
<Button variant='outline' size='sm' onClick={() => removeMember(item.id)} disabled={item.isOwner}>
{item.isOwner ? '주인' : '취소'}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
);
}
Loading