diff --git a/src/apis/columns/queries.ts b/src/apis/columns/queries.ts new file mode 100644 index 0000000..4aca819 --- /dev/null +++ b/src/apis/columns/queries.ts @@ -0,0 +1,51 @@ +'use client'; + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { deleteColumn, getColumns, postColumn, putColumn } from '.'; +import { ColumnForm } from './types'; + +export const useColumnsQuery = (dashboardId: number) => { + return useQuery({ + queryKey: ['columns', dashboardId], + queryFn: () => + getColumns({ + dashboardId, + }), + }); +}; + +export const useColumnMutation = (dashboardId: number) => { + const queryClient = useQueryClient(); + + const post = useMutation({ + mutationFn: (data: ColumnForm) => { + return postColumn(dashboardId, data); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['columns', dashboardId] }); + }, + }); + + const put = useMutation({ + mutationFn: ({ id, formData }: { id: number; formData: ColumnForm }) => { + return putColumn(id, formData); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['columns', dashboardId] }); + }, + }); + + const remove = useMutation({ + mutationFn: (id: number) => { + return deleteColumn(id); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['columns', dashboardId] }); + }, + }); + return { + create: post.mutateAsync, + update: put.mutateAsync, + remove: remove.mutateAsync, + }; +}; diff --git a/src/app/(after-login)/dashboard/[id]/page.tsx b/src/app/(after-login)/dashboard/[id]/page.tsx index 4c1d799..f2d14db 100644 --- a/src/app/(after-login)/dashboard/[id]/page.tsx +++ b/src/app/(after-login)/dashboard/[id]/page.tsx @@ -1,16 +1,5 @@ -import Link from 'next/link'; -import Button from '@/components/ui/Button/Button'; +import ColumnList from '@/components/columns/ColumnList'; -export default async function DashboardDetailPage({ params }: { params: Promise<{ id: string }> }) { - const id = (await params).id; - - return ( -
-
- - - -
-
- ); +export default function DashboardDetailPage() { + return ; } diff --git a/src/components/columns/AddColumnBtn.tsx b/src/components/columns/AddColumnBtn.tsx new file mode 100644 index 0000000..801ffaa --- /dev/null +++ b/src/components/columns/AddColumnBtn.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { ColumnForm, columnFormSchema } from '@/apis/columns/types'; +import DashboardButton from '@/components/ui/Button/DashboardButton'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { Modal, ModalContent, ModalFooter, ModalHandle, ModalHeader } from '@/components/ui/Modal/Modal'; +import { useRef } from 'react'; +import useAlert from '@/hooks/useAlert'; +import { Input } from '@/components/ui/Field'; +import Button from '../ui/Button/Button'; +import { useColumnMutation } from '@/apis/columns/queries'; +import { getErrorMessage } from '@/utils/errorMessage'; +import xIcon from '@/assets/icons/x.svg'; +import Image from 'next/image'; + +export default function AddColumnBtn({ dashboardId }: { dashboardId: number }) { + const { + handleSubmit, + register, + reset, + formState: { errors, isValid, isSubmitting, isDirty }, + } = useForm({ + resolver: zodResolver(columnFormSchema), + mode: 'onChange', + defaultValues: { + title: '', + }, + }); + const modalRef = useRef(null); + const { create } = useColumnMutation(dashboardId); + const alert = useAlert(); + + const handleReset = () => { + reset(); + modalRef.current?.close(); + }; + + const onSubmit = async (formData: ColumnForm) => { + try { + await create(formData); + handleReset(); + alert('컬럼이 생성되었습니다!'); + } catch (error) { + const message = getErrorMessage(error); + handleReset(); + alert(message); + } + }; + + const isDisabled = !isDirty || !isValid || isSubmitting; + + return ( +
  • +
    + modalRef.current?.open()} /> + + + + 새 컬럼 생성 + 컬럼 생성 취소 아이콘 handleReset()} className='cursor-pointer' /> + +
    + + + + + +
    +
    +
    +
  • + ); +} diff --git a/src/components/columns/ColumnItem.tsx b/src/components/columns/ColumnItem.tsx new file mode 100644 index 0000000..3bea323 --- /dev/null +++ b/src/components/columns/ColumnItem.tsx @@ -0,0 +1,31 @@ +import { Column } from '@/apis/columns/types'; +import ColumnSettingBtn from './ColumnSettingBtn'; +import Dot from '@/components/ui/Dot/Dot'; +import DashboardButton from '@/components/ui/Button/DashboardButton'; + +interface ColumnItemProps { + column: Column; +} + +//TODO: 할 일 카드 리스트 구현 예정 +export default function ColumnItem({ column }: ColumnItemProps) { + return ( + <> + + <DashboardButton variant='addTodo' /> + </> + ); +} + +function Title({ column }: { column: Column }) { + return ( + <div className='flex h-7 justify-between'> + <div className='flex items-center gap-4'> + <Dot color='#760DDE' /> + <span className='text-2lg font-bold text-gray-70'>{column.title}</span> + <span className='rounded-[4px] bg-gray-20 px-2 py-0.5 text-xs font-medium text-gray-50'>1</span> + </div> + <ColumnSettingBtn column={column} /> + </div> + ); +} diff --git a/src/components/columns/ColumnList.tsx b/src/components/columns/ColumnList.tsx new file mode 100644 index 0000000..fb13264 --- /dev/null +++ b/src/components/columns/ColumnList.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useColumnsQuery } from '@/apis/columns/queries'; +import { useParams } from 'next/navigation'; +import ColumnItem from './ColumnItem'; +import AddColumnBtn from './AddColumnBtn'; + +export default function ColumnList() { + const params = useParams(); + const dashbaordId = Number(params.id); + const { data, isLoading } = useColumnsQuery(dashbaordId); + + return ( + <ul className='flex flex-col lg:flex-row'> + {isLoading && Array.from({ length: 3 }, (_, index) => <SkeletionItem key={index} />)} + {data?.data.map((column) => ( + <li key={column.id} className='flex flex-col gap-4 border-b border-r-0 p-6 lg:min-h-[calc(100dvh-70px)] lg:border-b-0 lg:border-r'> + <ColumnItem column={column} /> + </li> + ))} + <AddColumnBtn dashboardId={dashbaordId} /> + </ul> + ); +} + +export function SkeletionItem() { + return ( + <li className='flex animate-pulse flex-col gap-4 border-b border-r-0 p-6 lg:min-h-[calc(100dvh-70px)] lg:border-b-0 lg:border-r'> + <div className='flex h-7 items-center justify-between'> + <div className='flex items-center gap-2'> + <div className='h-3 w-3 rounded-full bg-gray-300' /> + <div className='h-5 w-56 rounded bg-gray-300' /> + <div className='h-6 w-6 rounded bg-gray-300' /> + </div> + <div className='ml-4 h-5 w-5 rounded-full bg-gray-300' /> + </div> + <div className='flex h-12 w-full items-center justify-center rounded bg-gray-200'> + <div className='h-6 w-6 rounded bg-gray-300' /> + </div> + </li> + ); +} diff --git a/src/components/columns/ColumnSettingBtn.tsx b/src/components/columns/ColumnSettingBtn.tsx new file mode 100644 index 0000000..43712dc --- /dev/null +++ b/src/components/columns/ColumnSettingBtn.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { useRef } from 'react'; +import { Modal, ModalContent, ModalFooter, ModalHandle, ModalHeader } from '@/components/ui/Modal/Modal'; +import Image from 'next/image'; +import Setting from '@/assets/icons/setting.svg'; +import Button from '@/components/ui/Button/Button'; +import { useForm } from 'react-hook-form'; +import { Column, ColumnForm, columnFormSchema } from '@/apis/columns/types'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Input } from '@/components/ui/Field'; +import { getErrorMessage } from '@/utils/errorMessage'; +import useAlert from '@/hooks/useAlert'; +import { useColumnMutation } from '@/apis/columns/queries'; +import xIcon from '@/assets/icons/x.svg'; + +export default function ColumnSettingBtn({ column }: { column: Column }) { + const { + handleSubmit, + register, + reset, + formState: { errors, isValid, isSubmitting, isDirty }, + } = useForm<ColumnForm>({ + resolver: zodResolver(columnFormSchema), + mode: 'onChange', + defaultValues: { + title: column.title, + }, + }); + const updateModalRef = useRef<ModalHandle>(null); + const removeModalRef = useRef<ModalHandle>(null); + const { update, remove } = useColumnMutation(column.dashboardId); + const alert = useAlert(); + + const handleReset = (updatedTitle?: string) => { + reset({ + title: updatedTitle ?? column.title, + }); + updateModalRef.current?.close(); + }; + + const onClick = () => { + updateModalRef.current?.close(); + removeModalRef.current?.open(); + }; + + const onSubmit = async (formData: ColumnForm) => { + try { + await update({ id: column.id, formData }); + handleReset(formData.title); + alert('수정이 완료되었습니다!'); + } catch (error) { + const message = getErrorMessage(error); + handleReset(); + alert(message); + } + }; + + const onDelete = async () => { + try { + await remove(column.id); + removeModalRef.current?.close(); + alert('컬럼이 삭제되었습니다!'); + } catch (error) { + const message = getErrorMessage(error); + removeModalRef.current?.close(); + alert(message); + } + }; + + const isDisabled = !isDirty || !isValid || isSubmitting; + + return ( + <div className='cursor-pointer'> + <Image src={Setting} alt='관리 버튼' width={18} height={18} onClick={() => updateModalRef.current?.open()} /> + {/* 컬럼 수정 모달 */} + <Modal ref={updateModalRef}> + <ModalContent> + <ModalHeader className='flex justify-between'> + <span>컬럼 관리</span> + <Image src={xIcon} alt='컬럼 관리 취소 아이콘' width={24} height={24} onClick={() => handleReset()} className='cursor-pointer' /> + </ModalHeader> + <form onSubmit={handleSubmit(onSubmit)}> + <Input label='이름' error={errors.title?.message} placeholder='컬럼 이름을 입력해주세요' {...register('title')} /> + <ModalFooter> + <Button type='button' variant='outline' onClick={onClick}> + 삭제 + </Button> + <Button type='submit' disabled={isDisabled}> + {isSubmitting ? '수정중' : '수정'} + </Button> + </ModalFooter> + </form> + </ModalContent> + </Modal> + {/* 컬럼 제거 모달 */} + <Modal ref={removeModalRef}> + <ModalContent> + <div className='py-3 text-center'>컬럼의 모든 카드가 삭제됩니다.</div> + <ModalFooter> + <Button + variant='outline' + onClick={() => { + removeModalRef.current?.close(); + updateModalRef.current?.open(); + }} + > + 취소 + </Button> + <Button onClick={async () => await onDelete()}>삭제</Button> + </ModalFooter> + </ModalContent> + </Modal> + </div> + ); +}