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 (
+ <>
+
+
+ >
+ );
+}
+
+function Title({ column }: { column: Column }) {
+ return (
+
+
+
+ {column.title}
+ 1
+
+
+
+ );
+}
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 (
+
+ {isLoading && Array.from({ length: 3 }, (_, index) => )}
+ {data?.data.map((column) => (
+ -
+
+
+ ))}
+
+
+ );
+}
+
+export function SkeletionItem() {
+ return (
+
+
+
+
+ );
+}
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({
+ resolver: zodResolver(columnFormSchema),
+ mode: 'onChange',
+ defaultValues: {
+ title: column.title,
+ },
+ });
+ const updateModalRef = useRef(null);
+ const removeModalRef = useRef(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 (
+
+
updateModalRef.current?.open()} />
+ {/* 컬럼 수정 모달 */}
+
+
+
+ 컬럼 관리
+ handleReset()} className='cursor-pointer' />
+
+
+
+
+ {/* 컬럼 제거 모달 */}
+
+
+ 컬럼의 모든 카드가 삭제됩니다.
+
+
+
+
+
+
+
+ );
+}