From 91a4d5e6e883178d6a67878810ed7049f11a570c Mon Sep 17 00:00:00 2001 From: hyejin Date: Mon, 26 May 2025 10:00:44 +0900 Subject: [PATCH 01/37] =?UTF-8?q?Fix=20:=20nogroup=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=EC=9D=B4=20=EC=95=88?= =?UTF-8?q?=EB=82=98=EC=98=A4=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/gnb/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/layout/gnb/Header.tsx b/src/components/layout/gnb/Header.tsx index a79ecd6a..7b69c7ca 100644 --- a/src/components/layout/gnb/Header.tsx +++ b/src/components/layout/gnb/Header.tsx @@ -111,7 +111,7 @@ export default function Header() {
- {user && ( + {!isLoading && user && ( Date: Mon, 26 May 2025 10:11:33 +0900 Subject: [PATCH 02/37] =?UTF-8?q?Fix=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=A7=81=ED=9B=84=20fetchuser=ED=98=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(form-layout)/signup/_signup/SignupForm.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/(form-layout)/signup/_signup/SignupForm.tsx b/src/app/(form-layout)/signup/_signup/SignupForm.tsx index 6700cb95..3d0f0ebf 100644 --- a/src/app/(form-layout)/signup/_signup/SignupForm.tsx +++ b/src/app/(form-layout)/signup/_signup/SignupForm.tsx @@ -18,6 +18,7 @@ import { import { AUTH_ERROR_MESSAGES } from '@/constants/messages/signup'; import { Toast } from '@/components/common/Toastify'; import { setClientCookie } from '@/lib/cookie/client'; +import { useUser } from '@/contexts/UserContext'; interface ErrorResponse { response?: { @@ -31,6 +32,7 @@ export default function SignupForm() { const { openModal } = useModalContext(); const router = useRouter(); const { isPasswordVisible, togglePasswordVisibility } = usePasswordVisibility(); + const { fetchUser } = useUser(); const [formData, setFormData] = useState({ nickname: '', email: '', @@ -163,6 +165,8 @@ export default function SignupForm() { setClientCookie('accessToken', accessToken); setClientCookie('refreshToken', refreshToken); + await fetchUser(); + router.push('/nogroup'); } catch { Toast.error('자동 로그인 실패. 로그인 페이지로 이동합니다.'); From 2ab4e6a05599c7a000bb7fd3dd757ddc38763cf3 Mon Sep 17 00:00:00 2001 From: hyejin Date: Mon, 26 May 2025 10:17:01 +0900 Subject: [PATCH 03/37] =?UTF-8?q?Style=20:=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(form-layout)/signup/_signup/SignupForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(form-layout)/signup/_signup/SignupForm.tsx b/src/app/(form-layout)/signup/_signup/SignupForm.tsx index 3d0f0ebf..db8a9c8b 100644 --- a/src/app/(form-layout)/signup/_signup/SignupForm.tsx +++ b/src/app/(form-layout)/signup/_signup/SignupForm.tsx @@ -169,7 +169,7 @@ export default function SignupForm() { router.push('/nogroup'); } catch { - Toast.error('자동 로그인 실패. 로그인 페이지로 이동합니다.'); + Toast.error('자동 로그인 실패.'); router.push('/login'); } }, 5000); From bdb4e7c43c24d108376e54430daa94655388bfc0 Mon Sep 17 00:00:00 2001 From: nerte Date: Wed, 28 May 2025 16:21:11 +0900 Subject: [PATCH 04/37] =?UTF-8?q?Fix=20:=20dnd=EC=8B=9C=20taskListId?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=98=EB=AA=BB=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[groupId]/tasklist/(tasklist)/page.tsx | 2 +- .../[groupId]/tasklist/_tasklist/components/TaskLists.tsx | 5 ++++- .../[groupId]/tasklist/_tasklist/components/Tasks.tsx | 8 ++++---- .../[groupId]/tasklist/_tasklist/hooks/use-dnd-kit.ts | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/page.tsx b/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/page.tsx index d87af77d..252d8d86 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/page.tsx @@ -66,7 +66,7 @@ export default async function Page({ params, searchParams }: Props) {

할 일

- +
); diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TaskLists.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TaskLists.tsx index 5a3d1251..946032cb 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TaskLists.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TaskLists.tsx @@ -14,6 +14,9 @@ interface Props { export default function TaskLists({ taskLists, currentTaskListId }: Props) { const router = useRouter(); const searchParams = useSearchParams(); + const sortedTaskLists = taskLists.sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); const handleClickChangeCurrentTaskList = async (taskList: TaskList) => { const params = new URLSearchParams(searchParams.toString()); @@ -27,7 +30,7 @@ export default function TaskLists({ taskLists, currentTaskListId }: Props) { return ( }>
- {taskLists.map((taskList) => { + {sortedTaskLists.map((taskList) => { return (

(tasks); const [isClient, setIsClient] = useState(false); @@ -26,7 +26,7 @@ export default function Tasks({ groupId, tasks, currentTaskList }: Props) { setIsClient(true); }, [tasks]); - const { sensors, handleDragEnd } = useDndKit(currentTasks, currentTaskList!, orderCurrentTasks); + const { sensors, handleDragEnd } = useDndKit(currentTasks, taskListId!, orderCurrentTasks); if (!isClient) return; @@ -43,7 +43,7 @@ export default function Tasks({ groupId, tasks, currentTaskList }: Props) { {currentTasks.map((task) => { return ( void ) { const { saveNewTaskOrder } = useTaskActions(); @@ -31,7 +31,7 @@ export default function useDndKit( const newIndex = currentTasks.findIndex((task) => task.id === over?.id); if (oldIndex !== -1 && newIndex !== -1) { sortCurrentTasks(arrayMove(currentTasks, oldIndex, newIndex)); - await saveNewTaskOrder(currentTaskList!.id, Number(active.id), newIndex); + await saveNewTaskOrder(taskListId, Number(active.id), newIndex); } } }; From abe18848be79ca28f2352ae519275e93672d9a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=84=A0=ED=96=A5?= <160606117+grimza99@users.noreply.github.com> Date: Wed, 28 May 2025 16:51:37 +0900 Subject: [PATCH 05/37] Create README.md --- README.md | 80 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e215bc4c..2f5ef8dd 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,70 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# Coworkers -## Getting Started +## **💡** 프로젝트 개요 -First, run the development server: -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +> ▫️ **Coworkers** 는 가족, 회사 등 다양한 커뮤니티에서 일정을 관리하고 공유할 수 있는 웹 애플리케이션입니다. +▫️ 멤버 초대, 할 일 목록 CRUD, 댓글 작성 등 유기적인 커뮤니티 기능을 제공 합니다. +> -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +**🔹 유저 기능** -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +- 초대된 멤버들만 접근 할 수 있어, 종속된 공간에서 공유와 의견을 나눌 수 있습니다. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +🔹 **그룹, 할일목록, 할일 생성** -## Learn More +- 할일의 반복 설정, 할일 목록 생성 수정등 마감일, 담당자 등 업무에 필요한 내용을 추가할수 있어, 전반적인 내용을 쉽게 공유할 수 있습니다. +- 댓글 남길 수 있어 실시간으로 현황이나 의견을 공유할 수 있습니다. +- drag & drop으로 쉽게 할일의 우선순위를 부여하여 사용자의 편의성을 높였습니다. -To learn more about Next.js, take a look at the following resources: + +
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +## **🛠** 개발환경 -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +- **Front-End : Next.js(App Router) , Axios , tailwind, dnd-kit,…** +- **Back-end : Swagger** +- **배포 환경 : Vercel , AWS** +- **디자인 : Figma** +- **버전 및 이슈 관리: Github , Git ,Github action** +- **협업 툴: Discord, Notion** -## Deploy on Vercel +
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +## 📂 폴더 구조 + +스크린샷 2025-05-28 오후 4 51 02 + + +
+ +# **👥** 프로젝트 팀 구성 및 역할 + + + +--- + +## 🧑‍💻👩‍💻👩‍💻👩‍💻 팀원 소개와 역할 + +| 이름 | 주요 역할 | GitHub | +| --- | --- | --- | +|강석준 | modal ,interceptor, 댓글, 랜딩페이지, 로그인 페이지, 그룹 페이지, 자유게시판 생성,수정, AWS 배포, docker 설정, ... | https://github.com/KSJ27 +|김희진 | button , toast, input, 투두리스트 아이템, 팀생성페이지, 팀 수정페이지, 할일 생성,수정, 자유게시판 상세 페이지, ... | https://github.com/heewls +|유선향 | dropdown , svgr, OAuth, 비번 재설정 페이지, 리스트 페이지, 히스토리페이지, 할일 상세, Dnd ,... | https://github.com/grimza99 +|황혜진 | header , global css, 회원가입, 팀참여하기 페이지, 계정 관리 페이지, 자유게시판 페이지, ... | https://github.com/hhjin1 + + +
+ +# 프로젝트 수행 절차 및 컨벤션 + +### 프로젝트 기간 : 25.04.21 ~ 25.05.25 +### 리팩토링 기간 : 25.05.28 ~ 25.06.08 +### ⏰ 코어 타임: 월-금, 오후 2시~5시 + +### 🛠 데일리 스크럼: 월- 토, 오후 2시 + +### 📌 **오류 발생 시, 1~2시간 내로 해결이 안되면 공유하기!** + +### 📖 데일리 스크럼시, 팀 규칙에 따른 해당 팀원이 노션에 진행 내용과 진행 예정을 기록 -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. From 3462f77d771302a877c7b529936e41d79959b44a Mon Sep 17 00:00:00 2001 From: nerte Date: Fri, 30 May 2025 19:15:15 +0900 Subject: [PATCH 06/37] =?UTF-8?q?Refactor=20:=20interception=20route?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => (..)tasks/[taskId]}/page.tsx | 4 +-- .../@detailTask/_components/DetailTask.tsx | 12 +++---- .../_tasklist/components/TasksWiseTask.tsx | 35 ++++++++++--------- .../_tasklist/hooks/use-task-actions.ts | 9 ++--- .../[groupId]/tasks/[taskId]/page.tsx | 12 +++++++ 5 files changed, 38 insertions(+), 34 deletions(-) rename src/app/(content-layout)/[groupId]/tasklist/@detailTask/{ => (..)tasks/[taskId]}/page.tsx (93%) create mode 100644 src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/page.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx similarity index 93% rename from src/app/(content-layout)/[groupId]/tasklist/@detailTask/page.tsx rename to src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx index 54f4634c..ba53fef1 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx @@ -1,8 +1,8 @@ 'use client'; +import DetailTaskPage from '@/app/(content-layout)/[groupId]/tasks/[taskId]/page'; import Image from 'next/image'; import { useCallback, useEffect, useRef } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import DetailTask from './_components/DetailTask'; interface Props { groupId: string; @@ -53,7 +53,7 @@ export default function DetailTaskContainer({ taskId, isOpen, closeDetailTask, . x 해당 태스크를 불러올 수 없습니다.

}> - +
diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx index df845d95..e85fbb1a 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx @@ -10,14 +10,12 @@ import clsx from 'clsx'; import { DetailTaskType } from '../../_tasklist/types/task-type'; interface Props { - groupId: string; - taskListId: number; isDone: boolean; setIsDone: () => void; taskId: number; } -export default function DetailTask({ taskId, groupId, taskListId, isDone, setIsDone }: Props) { +export default function DetailTask({ taskId, isDone, setIsDone }: Props) { const [currentTask, setCurrentTask] = useState(); const [error, setError] = useState(null); const { toggleTaskDone } = useTaskActions(currentTask); @@ -26,15 +24,13 @@ export default function DetailTask({ taskId, groupId, taskListId, isDone, setIsD const fetchTask = useCallback(async () => { if (!taskId) return; try { - const { data } = await axiosClient( - `/groups/${groupId}/task-lists/${taskListId}/tasks/${taskId}` - ); + const { data } = await axiosClient(`/groups/groupId/task-lists/taskListId/tasks/${taskId}`); setCurrentTask(data); } catch { setError(new Error('Task를 불러오는데 오류가 발생했습니다.')); } - }, [groupId, taskListId, taskId]); + }, [taskId]); useEffect(() => { fetchTask(); @@ -48,7 +44,7 @@ export default function DetailTask({ taskId, groupId, taskListId, isDone, setIsD 해당 태스크를 불러올 수 없습니다.}> - + diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx index e85fbb1a..03c93263 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx @@ -40,7 +40,7 @@ export default function DetailTask({ taskId, isDone, setIsDone }: Props) { if (!currentTask) return; return ( -
+
+ ); +} diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx index 26aba3f2..ada152d8 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx @@ -106,7 +106,7 @@ export default function TasksWiseTask({ task, groupId, taskListId }: Props) { onCheckStatusChange={() => toggleTaskDone(isDone, toggleTaskStatus)} onEdit={() => popUpEditTaskModal(createOrEditModalId)} onDelete={() => popUpDeleteTaskModal(taskDeleteModalId)} - // onClick={() => openDetailTask()} + onClick={() => openDetailTask()} isDone={isDone} name={task.name} commentCount={task.commentCount} diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts index b4c750d6..74481732 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts @@ -22,7 +22,7 @@ export function useTaskActions(task?: Task) { } }; - const toggleTaskDone = async (doneState: boolean, toggleDoneState: () => void) => { + const toggleTaskDone = async (doneState: boolean, toggleDoneState?: () => void) => { if (!task) return; try { await axiosClient.patch(`/groups/groupId/task-lists/taskListId/tasks/${task.id}`, { @@ -30,7 +30,7 @@ export function useTaskActions(task?: Task) { description: task.description, done: !doneState, }); - toggleDoneState(); + toggleDoneState?.(); if (doneState) return Toast.success('할 일 완료 취소 성공'); if (!doneState) return Toast.success('할 일 완료 성공'); diff --git a/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx b/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx index 08e50e08..e481c56d 100644 --- a/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx @@ -1,12 +1,24 @@ -'use client'; - -import DetailTask from '../../tasklist/@detailTask/_components/DetailTask'; +import DetailTaskCommentField from '../../tasklist/@detailTask/_components/DetailTaskCommentsField'; +import Content from '../../tasklist/@detailTask/_components/DetailTaskContentField'; +import axiosServer from '@/lib/axiosServer'; +import { DetailTaskType } from '../../tasklist/_tasklist/types/task-type'; +import ToggleDoneButton from '../../tasklist/@detailTask/_components/ToggleDoneButton'; interface Props { - isDone: boolean; - setIsDone: () => void; - taskId: number; + params: Promise<{ taskId: string }>; } -export default function DetailTaskPage({ taskId, ...props }: Props) { - return ; +export default async function DetailTaskPage({ params }: Props) { + const taskId = (await params).taskId; + + const { data } = await axiosServer(`/groups/groupId/task-lists/taskListId/tasks/${taskId}`); + const task = data; + const isDone = Boolean(task.doneAt); + + return ( +
+ + + +
+ ); } From bb5ade2f4141525ba5a4af8a64f33d18cceffdfe Mon Sep 17 00:00:00 2001 From: nerte Date: Fri, 30 May 2025 20:02:43 +0900 Subject: [PATCH 08/37] =?UTF-8?q?Fix=20:=20=EB=94=94=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=83=9C=EC=8A=A4=ED=81=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=86=A0=EA=B8=80=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=81=B4=EB=A6=AD=EC=8B=9C=20=EB=B0=94=EB=A1=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20revalidateTag=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tasklist/_tasklist/actions/task-actions.ts | 14 ++++++++++++++ .../tasklist/_tasklist/hooks/use-task-actions.ts | 3 ++- .../[groupId]/tasks/[taskId]/page.tsx | 8 ++++---- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/actions/task-actions.ts b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/actions/task-actions.ts index d5ad847c..4af13840 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/actions/task-actions.ts +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/actions/task-actions.ts @@ -12,6 +12,10 @@ export const revalidateTaskLists = async () => { revalidateTag(`getTaskList`); }; +export const revalidateDetailTask = async () => { + revalidateTag(`getDetailTask`); +}; + export const getTaskLists = async (groupId: string) => { try { const { data: taskListsData } = await axiosServer(`/groups/${groupId}`, { @@ -41,3 +45,13 @@ export const getTasks = async (groupId: string, taskListId: number, date: Date | } } }; +export const getDetailTask = async (taskId: string) => { + try { + const { data } = await axiosServer(`/groups/groupId/task-lists/taskListId/tasks/${taskId}`, { + fetchOptions: { next: { tags: ['getDetailTask'] } }, + }); + return data; + } catch (error: unknown) { + console.error(error); + } +}; diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts index 74481732..32831bdb 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts @@ -1,6 +1,7 @@ import axiosClient from '@/lib/axiosClient'; import { Task } from '../types/task-type'; import { Toast } from '@/components/common/Toastify'; +import { revalidateDetailTask } from '../actions/task-actions'; export function useTaskActions(task?: Task) { const deleteTask = async ( @@ -31,7 +32,7 @@ export function useTaskActions(task?: Task) { done: !doneState, }); toggleDoneState?.(); - + revalidateDetailTask(); if (doneState) return Toast.success('할 일 완료 취소 성공'); if (!doneState) return Toast.success('할 일 완료 성공'); } catch { diff --git a/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx b/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx index e481c56d..3ce820a0 100644 --- a/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx @@ -1,8 +1,8 @@ import DetailTaskCommentField from '../../tasklist/@detailTask/_components/DetailTaskCommentsField'; import Content from '../../tasklist/@detailTask/_components/DetailTaskContentField'; -import axiosServer from '@/lib/axiosServer'; -import { DetailTaskType } from '../../tasklist/_tasklist/types/task-type'; import ToggleDoneButton from '../../tasklist/@detailTask/_components/ToggleDoneButton'; +import { getDetailTask } from '../../tasklist/_tasklist/actions/task-actions'; +import { DetailTaskType } from '../../tasklist/_tasklist/types/task-type'; interface Props { params: Promise<{ taskId: string }>; @@ -10,8 +10,8 @@ interface Props { export default async function DetailTaskPage({ params }: Props) { const taskId = (await params).taskId; - const { data } = await axiosServer(`/groups/groupId/task-lists/taskListId/tasks/${taskId}`); - const task = data; + const task: DetailTaskType = await getDetailTask(taskId); + const isDone = Boolean(task.doneAt); return ( From 5ab896141763c619628d66008e69937ff2220481 Mon Sep 17 00:00:00 2001 From: nerte Date: Fri, 30 May 2025 20:11:37 +0900 Subject: [PATCH 09/37] =?UTF-8?q?Feat=20:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=82=98=EC=98=A8=20=EB=94=94=ED=85=8C=EC=9D=BC=20?= =?UTF-8?q?=ED=83=9C=EC=8A=A4=ED=81=AC=EC=97=90=EC=84=9C=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=20=EB=B2=84=ED=8A=BC=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20?= =?UTF-8?q?=EB=94=94=ED=85=8C=EC=9D=BC=20=ED=83=9C=EC=8A=A4=ED=81=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/expand-icon.svg | 1 + .../@detailTask/(..)tasks/[taskId]/page.tsx | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 public/icons/expand-icon.svg diff --git a/public/icons/expand-icon.svg b/public/icons/expand-icon.svg new file mode 100644 index 00000000..75b29dd9 --- /dev/null +++ b/public/icons/expand-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx index b242055a..c475ddb0 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx @@ -4,6 +4,7 @@ import Image from 'next/image'; import { useCallback, useEffect, useRef } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import DetailTask from '../../_components/DetailTask'; +import { useRouter } from 'next/navigation'; interface Props { groupId: string; @@ -17,6 +18,7 @@ interface Props { export default function DetailTaskContainer({ taskId, isOpen, closeDetailTask, ...props }: Props) { const detailTaskRef = useRef(null); + const router = useRouter(); const closingDetailTaskOutsideClick = useCallback( (e: MouseEvent) => { @@ -50,9 +52,19 @@ export default function DetailTaskContainer({ taskId, isOpen, closeDetailTask, . className="bg-bg200 animate-detail-task fixed top-15 right-0 z-500 flex h-[calc(100%-60px)] w-full flex-col gap-25 px-4 py-4 md:max-w-[700px] md:gap-45.5 md:px-6 md:py-6 lg:max-w-[779px] lg:px-10 lg:py-10" >
- +
+ + +
해당 태스크를 불러올 수 없습니다.
}> From deb6ffcceb0d65f2480e534edb5c5ce0aed70a8e Mon Sep 17 00:00:00 2001 From: nerte Date: Fri, 30 May 2025 20:25:14 +0900 Subject: [PATCH 10/37] =?UTF-8?q?Chore=20:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20import,=20props=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx | 3 --- .../[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx index c475ddb0..6a091142 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx @@ -1,5 +1,4 @@ 'use client'; -import DetailTaskPage from '@/app/(content-layout)/[groupId]/tasks/[taskId]/page'; import Image from 'next/image'; import { useCallback, useEffect, useRef } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; @@ -7,8 +6,6 @@ import DetailTask from '../../_components/DetailTask'; import { useRouter } from 'next/navigation'; interface Props { - groupId: string; - taskListId: number; isOpen: boolean; isDone: boolean; setIsDone: () => void; diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx index ada152d8..3efe2bb6 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx @@ -118,8 +118,6 @@ export default function TasksWiseTask({ task, groupId, taskListId }: Props) { isDone={isDone} setIsDone={toggleTaskStatus} taskId={task.id} - groupId={groupId} - taskListId={taskListId} closeDetailTask={closeDetailTask} isOpen={isDetailTaskOpen} /> From 1e46ef79186971100fa33e3dbe956408216978a8 Mon Sep 17 00:00:00 2001 From: "Seok-jun.Kang" Date: Fri, 30 May 2025 20:34:48 +0900 Subject: [PATCH 11/37] =?UTF-8?q?Refactor=20:=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=EC=9D=84=20=EB=B3=84=EA=B0=9C=EC=9D=98=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EA=B5=AC=ED=98=84=20-=20parallel=20&=20in?= =?UTF-8?q?tercepting=20routes=20=ED=99=9C=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(groupId)/@modal/(.)create/page.tsx | 36 +++++++++++++++++++ .../[groupId]/(groupId)/@modal/default.tsx | 3 ++ .../(groupId)/_[groupId]/Tasklists/index.tsx | 10 ++++-- .../[groupId]/(groupId)/create/page.tsx | 3 ++ .../[groupId]/(groupId)/layout.tsx | 14 ++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx create mode 100644 src/app/(content-layout)/[groupId]/(groupId)/@modal/default.tsx create mode 100644 src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx create mode 100644 src/app/(content-layout)/[groupId]/(groupId)/layout.tsx diff --git a/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx new file mode 100644 index 00000000..62f7a7d9 --- /dev/null +++ b/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx @@ -0,0 +1,36 @@ +'use client'; +import { useEffect, useState } from 'react'; +import Button from '@/components/common/Button'; +import FormField from '@/components/common/formField'; +import { + ModalCloseButton, + ModalContainer, + ModalFooter, + ModalHeading, + ModalOverlay, + ModalPortal, +} from '@/components/common/modal'; +import useModalContext from '@/components/common/modal/core/useModalContext'; +import BouncingDots from '@/components/common/loading/BouncingDots'; +import { validateEmptyValue } from '@/utils/validators'; + +export default function Page() { + const { openModal } = useModalContext(); + useEffect(() => { + openModal('modalId'); + }, []); + + return ( + + + + +
+ 할 일 목록 추가 +
+ +
+
+
+ ); +} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/@modal/default.tsx b/src/app/(content-layout)/[groupId]/(groupId)/@modal/default.tsx new file mode 100644 index 00000000..6ddf1b76 --- /dev/null +++ b/src/app/(content-layout)/[groupId]/(groupId)/@modal/default.tsx @@ -0,0 +1,3 @@ +export default function Default() { + return null; +} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx index be7c9450..68755d88 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx @@ -7,6 +7,8 @@ import TasklistDeleteModal from '@/app/(content-layout)/[groupId]/(groupId)/_[gr import { ModalTrigger } from '@/components/common/modal'; import { Group } from '@/types/group'; import { Tasklist } from '@/types/tasklist'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; type TasklistsProps = { groupId: Group['id']; @@ -30,6 +32,7 @@ export default function Tasklists({ groupId, tasklists }: TasklistsProps) { const tasklistUpdateModalId = selectedTasklist ? `tasklistUpdate-${selectedTasklist.id}` : ''; const tasklistDeleteModalId = selectedTasklist ? `tasklistDelete-${selectedTasklist.id}` : ''; const totalTasklistCount = optimisticTasklists.length; + const pathname = usePathname(); return ( <> @@ -38,9 +41,12 @@ export default function Tasklists({ groupId, tasklists }: TasklistsProps) {

할 일 목록 ({totalTasklistCount}개)

- + {/* + 새로운 목록 추가하기 - + */} + + + 새로운 목록 추가하기 +
    {tasklists.map((tasklist, index) => ( diff --git a/src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx new file mode 100644 index 00000000..55bc0258 --- /dev/null +++ b/src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return <>zz; +} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/layout.tsx b/src/app/(content-layout)/[groupId]/(groupId)/layout.tsx new file mode 100644 index 00000000..73e93591 --- /dev/null +++ b/src/app/(content-layout)/[groupId]/(groupId)/layout.tsx @@ -0,0 +1,14 @@ +export default function Layout({ + children, + modal, +}: { + children: React.ReactNode; + modal: React.ReactNode; +}) { + return ( + <> +
    {children}
    +
    {modal}
    + + ); +} From fe85046dc4b8cf574f8cf9412d136202e003b37a Mon Sep 17 00:00:00 2001 From: heejin Date: Fri, 30 May 2025 20:52:59 +0900 Subject: [PATCH 12/37] Chore : install react-hook-form & zod --- package-lock.json | 48 ++++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 ++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17295683..39c6eae2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@hookform/resolvers": "^5.0.1", "axios": "^1.9.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -19,8 +20,10 @@ "react-calendar": "^5.1.0", "react-dom": "^19.0.0", "react-error-boundary": "^6.0.0", + "react-hook-form": "^7.56.4", "react-intersection-observer": "^9.16.0", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "zod": "^3.25.41" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -2048,6 +2051,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz", + "integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2774,6 +2789,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -8181,6 +8202,22 @@ "react": ">=16.13.1" } }, + "node_modules/react-hook-form": { + "version": "7.56.4", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.4.tgz", + "integrity": "sha512-Rob7Ftz2vyZ/ZGsQZPaRdIefkgOSrQSPXfqBdvOPwJfoGnjwRJUs7EM7Kc1mcoDv3NOtqBzPGbcMB8CGn9CKgw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-intersection-observer": { "version": "9.16.0", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.16.0.tgz", @@ -9718,6 +9755,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.41", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.41.tgz", + "integrity": "sha512-8+sDJTGtCYIDBhdqDygp0ffj8kzziRKqAJPhpYObbElJ+3TRe/mnlnwH+/OMa3kKhueS4Drm5UMW00/u1p07zA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index fe185f42..53b9ef08 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", + "@hookform/resolvers": "^5.0.1", "axios": "^1.9.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -20,8 +21,10 @@ "react-calendar": "^5.1.0", "react-dom": "^19.0.0", "react-error-boundary": "^6.0.0", + "react-hook-form": "^7.56.4", "react-intersection-observer": "^9.16.0", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "zod": "^3.25.41" }, "devDependencies": { "@eslint/eslintrc": "^3", From b4182b98567d27703dc5e5b6617f9dfbdc22058c Mon Sep 17 00:00:00 2001 From: "Seok-jun.Kang" Date: Fri, 30 May 2025 21:12:02 +0900 Subject: [PATCH 13/37] =?UTF-8?q?Refactor=20:=20Modal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20modal=20=EA=B8=B0=EB=8A=A5=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=EB=8A=94=20=ED=95=98=EB=82=98?= =?UTF-8?q?=EC=9D=98=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=AC=B6=EA=B3=A0,?= =?UTF-8?q?=20Context=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EC=98=AE=EA=B9=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(groupId)/@modal/(.)create/page.tsx | 12 +-- .../_[groupId]/Members/MemberDeleteModal.tsx | 5 +- .../_[groupId]/Members/MemberDetailModal.tsx | 5 +- .../Members/MemberInvitationModal.tsx | 5 +- .../Tasklists/TasklistCreateModal.tsx | 5 +- .../Tasklists/TasklistDeleteModal.tsx | 5 +- .../Tasklists/TasklistItemDropdown.tsx | 5 +- .../Tasklists/TasklistUpdateModal.tsx | 5 +- .../_editgroup/DeleteGroupButton.tsx | 4 +- .../@detailTask/_components/CommentField.tsx | 4 +- .../ModalContents/CreateTaskListModal.tsx | 5 +- .../ModalContents/RemoveCommentModal.tsx | 14 +--- .../ModalContents/RemoveTaskModal.tsx | 6 +- .../MangeTaskItemModal.tsx | 3 +- .../_tasklist/hooks/use-task-modals.ts | 5 +- .../_articleId/components/CommentList.tsx | 4 +- .../components/DetailArticleInfo.tsx | 4 +- .../login/_login/SendResetPassword.tsx | 5 +- .../signup/_signup/SignupForm.tsx | 4 +- src/app/layout.tsx | 2 +- src/app/mypage/MypageClient.tsx | 4 +- .../mypage-modal/ChangePasswordModal.tsx | 12 +-- .../ConfirmDeleteAccountModal.tsx | 5 +- .../mypage-modal/DeleteAccountModal.tsx | 5 +- src/components/common/ErrorModal/index.tsx | 5 +- .../modal/{ui => }/ModalCloseButton.tsx | 5 +- .../common/modal/{ui => }/ModalContainer.tsx | 0 .../modal/{ui => }/ModalDescription.tsx | 0 .../common/modal/{ui => }/ModalFooter.tsx | 0 .../common/modal/{ui => }/ModalHeading.tsx | 0 .../common/modal/{ui => }/ModalOverlay.tsx | 5 +- .../common/modal/{ui => }/ModalTrigger.tsx | 5 +- .../common/modal/core/ModalContext.tsx | 13 --- .../common/modal/core/ModalPortal.tsx | 27 ------- .../common/modal/core/ModalProvider.tsx | 49 ----------- .../common/modal/core/useModalContext.tsx | 9 --- src/components/common/modal/index.ts | 16 ++-- src/components/danger-modal/index.tsx | 5 +- .../manage-task-item/components/Frequency.tsx | 4 +- .../manage-task-item/useManageTaskItem.ts | 4 +- .../signup-alert-modal/SignupFailModal.tsx | 5 +- .../signup-alert-modal/SignupSuccessModal.tsx | 2 +- src/contexts/ModalContext.tsx | 81 +++++++++++++++++++ 43 files changed, 155 insertions(+), 213 deletions(-) rename src/components/common/modal/{ui => }/ModalCloseButton.tsx (85%) rename src/components/common/modal/{ui => }/ModalContainer.tsx (100%) rename src/components/common/modal/{ui => }/ModalDescription.tsx (100%) rename src/components/common/modal/{ui => }/ModalFooter.tsx (100%) rename src/components/common/modal/{ui => }/ModalHeading.tsx (100%) rename src/components/common/modal/{ui => }/ModalOverlay.tsx (90%) rename src/components/common/modal/{ui => }/ModalTrigger.tsx (78%) delete mode 100644 src/components/common/modal/core/ModalContext.tsx delete mode 100644 src/components/common/modal/core/ModalPortal.tsx delete mode 100644 src/components/common/modal/core/ModalProvider.tsx delete mode 100644 src/components/common/modal/core/useModalContext.tsx create mode 100644 src/contexts/ModalContext.tsx diff --git a/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx index 62f7a7d9..d26ae3f0 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx @@ -1,21 +1,15 @@ 'use client'; -import { useEffect, useState } from 'react'; -import Button from '@/components/common/Button'; -import FormField from '@/components/common/formField'; +import { useEffect } from 'react'; import { ModalCloseButton, ModalContainer, ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; -import BouncingDots from '@/components/common/loading/BouncingDots'; -import { validateEmptyValue } from '@/utils/validators'; - +import { useModal, ModalPortal } from '@/contexts/ModalContext'; export default function Page() { - const { openModal } = useModalContext(); + const { openModal } = useModal(); useEffect(() => { openModal('modalId'); }, []); diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDeleteModal.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDeleteModal.tsx index d4d749d5..6a072034 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDeleteModal.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDeleteModal.tsx @@ -8,9 +8,8 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import BouncingDots from '@/components/common/loading/BouncingDots'; import { Member } from '@/types/user'; @@ -28,7 +27,7 @@ export default function MemberDeleteModal({ deleteMember, }: MemberDeleteModalProps) { const { userName } = member; - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const handleClickDeleteButton = async () => { deleteMember(); diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDetailModal.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDetailModal.tsx index 8b602c7e..2766889e 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDetailModal.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberDetailModal.tsx @@ -8,9 +8,8 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import { Member } from '@/types/user'; type MemberDetailModalProps = { @@ -20,7 +19,7 @@ type MemberDetailModalProps = { export default function MemberDetailModal({ modalId, member }: MemberDetailModalProps) { const { userName, userImage, userEmail } = member; - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const copyEmailToClipboard = () => { navigator.clipboard.writeText(userEmail); }; diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberInvitationModal.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberInvitationModal.tsx index 68b660b7..6fbd6444 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberInvitationModal.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Members/MemberInvitationModal.tsx @@ -10,9 +10,8 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import { Toast } from '@/components/common/Toastify'; import BouncingDots from '@/components/common/loading/BouncingDots'; import { getInvitationToken } from '@/api/group'; @@ -32,7 +31,7 @@ export default function MemberInvitationModal({ isLoading, addMember, }: MemberInvitationModalProps) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const [isTokenMethod, setIsTokenMethod] = useState(true); const [email, setEmail] = useState(''); diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx index 07ad5f1b..f2521707 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx @@ -8,9 +8,8 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import BouncingDots from '@/components/common/loading/BouncingDots'; import { validateEmptyValue } from '@/utils/validators'; @@ -26,7 +25,7 @@ export default function TasklistCreateModal({ createTasklist, }: TasklistCreateModalProps) { const [name, setName] = useState(''); - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const handleChangeName = (e: React.ChangeEvent) => { setName(e.target.value); diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistDeleteModal.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistDeleteModal.tsx index c685ce59..98a54c4c 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistDeleteModal.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistDeleteModal.tsx @@ -7,9 +7,8 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import BouncingDots from '@/components/common/loading/BouncingDots'; import { Tasklist } from '@/types/tasklist'; @@ -27,7 +26,7 @@ export default function TasklistDeleteModal({ deleteTasklist, }: TasklistDeleteModalProps) { const { name } = tasklist; - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const handleClickDeleteButton = async () => { deleteTasklist(tasklist); diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistItemDropdown.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistItemDropdown.tsx index 8561bb51..8f56a670 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistItemDropdown.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistItemDropdown.tsx @@ -3,8 +3,7 @@ import Image from 'next/image'; import DropDown from '@/components/common/dropdown'; import kebabIcon from '@/../public/icons/kebab-icon.svg'; import { Tasklist } from '@/types/tasklist'; -import useModalContext from '@/components/common/modal/core/useModalContext'; - +import { useModal } from '@/contexts/ModalContext'; const ITEM_DROPDOWN_VALUE = ['수정하기', '삭제하기']; type TasklistItemDropdownProps = { @@ -16,7 +15,7 @@ export default function TasklistItemDropdown({ onTriggerClick, tasklist, }: TasklistItemDropdownProps) { - const { openModal } = useModalContext(); + const { openModal } = useModal(); return (
    ) => { setName(e.target.value); diff --git a/src/app/(content-layout)/[groupId]/editgroup/_editgroup/DeleteGroupButton.tsx b/src/app/(content-layout)/[groupId]/editgroup/_editgroup/DeleteGroupButton.tsx index 9878f298..516d1c6d 100644 --- a/src/app/(content-layout)/[groupId]/editgroup/_editgroup/DeleteGroupButton.tsx +++ b/src/app/(content-layout)/[groupId]/editgroup/_editgroup/DeleteGroupButton.tsx @@ -5,14 +5,14 @@ import { useState } from 'react'; import { Toast } from '@/components/common/Toastify'; import DangerModal from '@/components/danger-modal'; import BouncingDots from '@/components/common/loading/BouncingDots'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import TrashCan from '@/assets/TrashCan'; import { deleteGroup } from '../action'; import { useUser } from '@/contexts/UserContext'; export default function DeleteGroupButton({ groupId }: { groupId: number }) { const [isPending, setIsPending] = useState(false); - const { openModal } = useModalContext(); + const { openModal } = useModal(); const { fetchUser } = useUser(); const router = useRouter(); diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx index f2f215b6..5751ed75 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx @@ -4,7 +4,7 @@ import { Comment } from '@/components/comment/types'; import { useState } from 'react'; import EditCommentInput from './EditCommentInput'; import axiosClient from '@/lib/axiosClient'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import RemoveCommentModal from '../../_tasklist/components/ModalContents/RemoveCommentModal'; import { Toast } from '@/components/common/Toastify'; import { revalidateTasks } from '../../_tasklist/actions/task-actions'; @@ -17,7 +17,7 @@ interface Props { export default function CommentField({ comment, taskId }: Props) { const [isEdit, setIsEdit] = useState(false); const [isDelete, setIsDelete] = useState(false); - const { openModal } = useModalContext(); + const { openModal } = useModal(); const [currentComment, setCurrentComment] = useState(comment); const [currentContent, setCurrentContent] = useState(comment.content); diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/CreateTaskListModal.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/CreateTaskListModal.tsx index b4bb5465..5bad5bb8 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/CreateTaskListModal.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/CreateTaskListModal.tsx @@ -9,10 +9,9 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, ModalTrigger, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import axiosClient from '@/lib/axiosClient'; import { revalidateTaskLists } from '../../actions/task-actions'; import { Toast } from '@/components/common/Toastify'; @@ -28,7 +27,7 @@ export default function CreateTaskListModal({ groupId }: Props) { const [errorMessage, setErrorMessage] = useState(''); const [isForceShowError, setIsForceShowError] = useState(false); - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const clearState = () => { setCurrentValue(''); diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx index a3de62fc..dce4ba12 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx @@ -1,23 +1,15 @@ 'use client'; -import { - ModalContainer, - ModalDescription, - ModalFooter, - ModalHeading, - ModalOverlay, - ModalPortal, -} from '@/components/common/modal'; +import { ModalContainer, ModalFooter, ModalHeading, ModalOverlay } from '@/components/common/modal'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import Image from 'next/image'; import Button from '@/components/common/Button'; -import useModalContext from '@/components/common/modal/core/useModalContext'; - interface Props { modalId: string; onDelete: () => void; } export default function RemoveCommentModal({ modalId, onDelete }: Props) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); return ( <> diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveTaskModal.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveTaskModal.tsx index 7100bec1..f3d4f86d 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveTaskModal.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveTaskModal.tsx @@ -5,12 +5,10 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import Image from 'next/image'; import Button from '@/components/common/Button'; -import useModalContext from '@/components/common/modal/core/useModalContext'; - interface Props { taskName: string; modalId: string; @@ -18,7 +16,7 @@ interface Props { } export default function RemoveTaskModal({ taskName, modalId, deleteTask }: Props) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); return ( <> diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/manage-task-item-modal/MangeTaskItemModal.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/manage-task-item-modal/MangeTaskItemModal.tsx index d7712f5c..d393d24f 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/manage-task-item-modal/MangeTaskItemModal.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/manage-task-item-modal/MangeTaskItemModal.tsx @@ -1,4 +1,5 @@ -import { ModalContainer, ModalOverlay, ModalPortal, ModalTrigger } from '@/components/common/modal'; +import { ModalContainer, ModalOverlay, ModalTrigger } from '@/components/common/modal'; +import { ModalPortal } from '@/contexts/ModalContext'; import ManageTaskItem from '@/components/manage-task-item/components/ManageTaskItem'; import Plus from '@/assets/Plus'; import { TaskItemProps } from '@/components/manage-task-item/type'; diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-modals.ts b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-modals.ts index b87d90e0..e5d97a9d 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-modals.ts +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-modals.ts @@ -3,10 +3,9 @@ type TaskModals = { popUpEditTaskModal: (modalId: string) => void; }; -import useModalContext from '@/components/common/modal/core/useModalContext'; - +import { useModal } from '@/contexts/ModalContext'; export function useTaskModals(): TaskModals { - const { openModal } = useModalContext(); + const { openModal } = useModal(); const popUpEditTaskModal = (modalId: string) => { openModal(modalId); diff --git a/src/app/(content-layout)/articles/[articleId]/_articleId/components/CommentList.tsx b/src/app/(content-layout)/articles/[articleId]/_articleId/components/CommentList.tsx index 64d69094..5f7850ff 100644 --- a/src/app/(content-layout)/articles/[articleId]/_articleId/components/CommentList.tsx +++ b/src/app/(content-layout)/articles/[articleId]/_articleId/components/CommentList.tsx @@ -8,7 +8,7 @@ import DangerModal from '@/components/danger-modal'; import { Toast } from '@/components/common/Toastify'; import CommentItem from '@/components/comment'; import BouncingDots from '@/components/common/loading/BouncingDots'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import { ArticleComment, ArticleComments } from '@/components/comment/types'; import { deleteArticleComment, getArticleComments, patchArticleComment } from '../action'; import { validateEmptyValue } from '@/utils/validators'; @@ -25,7 +25,7 @@ export default function CommentList({ const [commentIdToDelete, setCommentIdToDelete] = useState(null); const [commentToEdit, setCommentToEdit] = useState<{ id: number; content: string } | null>(null); const [isPending, setIsPending] = useState(false); - const { openModal } = useModalContext(); + const { openModal } = useModal(); const [ref, inView] = useInView({ threshold: 1.0 }); const [moreComments, setMoreComments] = useState([]); diff --git a/src/app/(content-layout)/articles/[articleId]/_articleId/components/DetailArticleInfo.tsx b/src/app/(content-layout)/articles/[articleId]/_articleId/components/DetailArticleInfo.tsx index 35ba9d41..f07f58cb 100644 --- a/src/app/(content-layout)/articles/[articleId]/_articleId/components/DetailArticleInfo.tsx +++ b/src/app/(content-layout)/articles/[articleId]/_articleId/components/DetailArticleInfo.tsx @@ -10,7 +10,7 @@ import { GetArticleDetailResponse } from '@/types/article'; import { formatTimeDistance } from '@/utils/date'; import LikeToggleButton from '../../../_articles/components/LikeToggleButton'; import { useUser } from '@/contexts/UserContext'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import DetailArticleDropdown from './DetailArticleDropdown'; import axiosClient from '@/lib/axiosClient'; @@ -19,7 +19,7 @@ const DEFAULT_IMAGE = process.env.NEXT_PUBLIC_DEFAULT_IMAGE; export default function DetailArticleInfo({ detail }: { detail: GetArticleDetailResponse }) { const router = useRouter(); const { user } = useUser(); - const { openModal } = useModalContext(); + const { openModal } = useModal(); const [isPending, setIsPending] = useState(false); const handleArticleDelete = async () => { diff --git a/src/app/(form-layout)/login/_login/SendResetPassword.tsx b/src/app/(form-layout)/login/_login/SendResetPassword.tsx index 1e111367..91e4e918 100644 --- a/src/app/(form-layout)/login/_login/SendResetPassword.tsx +++ b/src/app/(form-layout)/login/_login/SendResetPassword.tsx @@ -10,10 +10,9 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, ModalTrigger, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import axiosClient from '@/lib/axiosClient'; import { validateEmail } from '@/utils/validators'; import { AUTH_ERROR_MESSAGES } from '@/constants/messages/signup'; @@ -22,7 +21,7 @@ import BouncingDots from '@/components/common/loading/BouncingDots'; const redirectUrl = process.env.NEXT_PUBLIC_RESET_PASSWORD; export default function SendResetPassword() { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const modalId = `resetPassword`; const errorMessageConstant = AUTH_ERROR_MESSAGES.sendResetPassword; diff --git a/src/app/(form-layout)/signup/_signup/SignupForm.tsx b/src/app/(form-layout)/signup/_signup/SignupForm.tsx index 6700cb95..44a5c8a8 100644 --- a/src/app/(form-layout)/signup/_signup/SignupForm.tsx +++ b/src/app/(form-layout)/signup/_signup/SignupForm.tsx @@ -7,7 +7,7 @@ import FormField from '@/components/common/formField'; import Button from '@/components/common/Button'; import PasswordToggleButton from './PasswordToggleButton'; import usePasswordVisibility from '@/utils/use-password-visibility'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import SignupSuccessModal from '@/components/signup-alert-modal/SignupSuccessModal'; import { validateEmail, @@ -28,7 +28,7 @@ interface ErrorResponse { } export default function SignupForm() { - const { openModal } = useModalContext(); + const { openModal } = useModal(); const router = useRouter(); const { isPasswordVisible, togglePasswordVisibility } = usePasswordVisibility(); const [formData, setFormData] = useState({ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9670033b..de8e1aee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,9 @@ import type { Metadata } from 'next'; import './globals.css'; import Header from '@/components/layout/gnb/Header'; -import { ModalProvider } from '@/components/common/modal'; import ToastProvider from '@/components/common/Toastify/ToasProvider'; import { UserProvider } from '@/contexts/UserContext'; +import { ModalProvider } from '@/contexts/ModalContext'; export const metadata: Metadata = { title: 'Coworkers', diff --git a/src/app/mypage/MypageClient.tsx b/src/app/mypage/MypageClient.tsx index 2ee7a466..6e2aa560 100644 --- a/src/app/mypage/MypageClient.tsx +++ b/src/app/mypage/MypageClient.tsx @@ -10,7 +10,7 @@ import { Toast } from '@/components/common/Toastify'; import ChangePasswordModal from './_mypage/mypage-modal/ChangePasswordModal'; import DeleteAccountModal from './_mypage/mypage-modal/DeleteAccountModal'; import ConfirmDeleteAccountModal from './_mypage/mypage-modal/ConfirmDeleteAccountModal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import FormField from '@/components/common/formField'; import { useUser } from '@/contexts/UserContext'; @@ -20,7 +20,7 @@ export default function MyPageClient() { const [nickname, setNickname] = useState(user?.nickname ?? ''); const [nicknameError, setNicknameError] = useState(''); const [password, setPassword] = useState(''); - const { openModal, closeModal } = useModalContext(); + const { openModal, closeModal } = useModal(); useEffect(() => { if (user?.image) { diff --git a/src/app/mypage/_mypage/mypage-modal/ChangePasswordModal.tsx b/src/app/mypage/_mypage/mypage-modal/ChangePasswordModal.tsx index 984bb9b5..e4e5afac 100644 --- a/src/app/mypage/_mypage/mypage-modal/ChangePasswordModal.tsx +++ b/src/app/mypage/_mypage/mypage-modal/ChangePasswordModal.tsx @@ -2,14 +2,8 @@ import { useState, useTransition } from 'react'; import { useRouter } from 'next/navigation'; -import { - ModalContainer, - ModalFooter, - ModalHeading, - ModalOverlay, - ModalPortal, -} from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { ModalContainer, ModalFooter, ModalHeading, ModalOverlay } from '@/components/common/modal'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import { validatePassword, validateConfirmPassword } from '@/utils/validators'; import FormField from '@/components/common/formField'; import Button from '@/components/common/Button'; @@ -26,7 +20,7 @@ interface PasswordChangeSuccessModalProps { } export default function ChangePasswordModal({ onClose }: PasswordChangeSuccessModalProps) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const router = useRouter(); const [isPending, startTransition] = useTransition(); const { logoutUser } = useUser(); diff --git a/src/app/mypage/_mypage/mypage-modal/ConfirmDeleteAccountModal.tsx b/src/app/mypage/_mypage/mypage-modal/ConfirmDeleteAccountModal.tsx index 0629d6c7..2bb8f339 100644 --- a/src/app/mypage/_mypage/mypage-modal/ConfirmDeleteAccountModal.tsx +++ b/src/app/mypage/_mypage/mypage-modal/ConfirmDeleteAccountModal.tsx @@ -8,16 +8,15 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import axiosClient from '@/lib/axiosClient'; import Button from '@/components/common/Button'; import { Toast } from '@/components/common/Toastify'; import { deleteClientCookie } from '@/lib/cookie/client'; export default function ConfirmDeleteAccountModal() { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const { logoutUser } = useUser(); return ( diff --git a/src/app/mypage/_mypage/mypage-modal/DeleteAccountModal.tsx b/src/app/mypage/_mypage/mypage-modal/DeleteAccountModal.tsx index b214bcfb..a257cb3e 100644 --- a/src/app/mypage/_mypage/mypage-modal/DeleteAccountModal.tsx +++ b/src/app/mypage/_mypage/mypage-modal/DeleteAccountModal.tsx @@ -7,13 +7,12 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import Button from '@/components/common/Button'; export default function DeleteAccountModal() { - const { closeModal, openModal } = useModalContext(); + const { closeModal, openModal } = useModal(); return ( <> diff --git a/src/components/common/ErrorModal/index.tsx b/src/components/common/ErrorModal/index.tsx index e43ea4c5..5484ee81 100644 --- a/src/components/common/ErrorModal/index.tsx +++ b/src/components/common/ErrorModal/index.tsx @@ -4,11 +4,10 @@ import { ModalDescription, ModalFooter, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import Image from 'next/image'; import Button from '@/components/common/Button'; -import useModalContext from '@/components/common/modal/core/useModalContext'; interface Props { modalId: string; @@ -17,7 +16,7 @@ interface Props { buttonText?: string; } export default function ErrorModal({ modalId, description, onClick, buttonText }: Props) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const modalButtonText = buttonText ? buttonText : '닫기'; return ( diff --git a/src/components/common/modal/ui/ModalCloseButton.tsx b/src/components/common/modal/ModalCloseButton.tsx similarity index 85% rename from src/components/common/modal/ui/ModalCloseButton.tsx rename to src/components/common/modal/ModalCloseButton.tsx index c42f269c..0e88343a 100644 --- a/src/components/common/modal/ui/ModalCloseButton.tsx +++ b/src/components/common/modal/ModalCloseButton.tsx @@ -2,8 +2,7 @@ import Image from 'next/image'; import clsx from 'clsx'; import xIcon from '@/../public/icons/x-icon.svg'; -import useModalContext from '@/components/common/modal/core/useModalContext'; - +import { useModal } from '@/contexts/ModalContext'; interface ModalCloseButtonProps extends React.ComponentProps<'button'> { modalId: string; } @@ -14,7 +13,7 @@ export default function ModalCloseButton({ onClick, ...props }: ModalCloseButtonProps) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const handleClick = (e: React.MouseEvent) => { if (onClick) { diff --git a/src/components/common/modal/ui/ModalContainer.tsx b/src/components/common/modal/ModalContainer.tsx similarity index 100% rename from src/components/common/modal/ui/ModalContainer.tsx rename to src/components/common/modal/ModalContainer.tsx diff --git a/src/components/common/modal/ui/ModalDescription.tsx b/src/components/common/modal/ModalDescription.tsx similarity index 100% rename from src/components/common/modal/ui/ModalDescription.tsx rename to src/components/common/modal/ModalDescription.tsx diff --git a/src/components/common/modal/ui/ModalFooter.tsx b/src/components/common/modal/ModalFooter.tsx similarity index 100% rename from src/components/common/modal/ui/ModalFooter.tsx rename to src/components/common/modal/ModalFooter.tsx diff --git a/src/components/common/modal/ui/ModalHeading.tsx b/src/components/common/modal/ModalHeading.tsx similarity index 100% rename from src/components/common/modal/ui/ModalHeading.tsx rename to src/components/common/modal/ModalHeading.tsx diff --git a/src/components/common/modal/ui/ModalOverlay.tsx b/src/components/common/modal/ModalOverlay.tsx similarity index 90% rename from src/components/common/modal/ui/ModalOverlay.tsx rename to src/components/common/modal/ModalOverlay.tsx index 5ea431da..58f201a2 100644 --- a/src/components/common/modal/ui/ModalOverlay.tsx +++ b/src/components/common/modal/ModalOverlay.tsx @@ -1,8 +1,7 @@ 'use client'; import { useEffect } from 'react'; import clsx from 'clsx'; -import useModalContext from '@/components/common/modal/core/useModalContext'; - +import { useModal } from '@/contexts/ModalContext'; interface ModalOverlayProps extends React.ComponentProps<'div'> { modalId: string; disableOverlayClose?: boolean; @@ -17,7 +16,7 @@ export default function ModalOverlay({ children, ...props }: ModalOverlayProps) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const handleClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget && !disableOverlayClose) { diff --git a/src/components/common/modal/ui/ModalTrigger.tsx b/src/components/common/modal/ModalTrigger.tsx similarity index 78% rename from src/components/common/modal/ui/ModalTrigger.tsx rename to src/components/common/modal/ModalTrigger.tsx index 9743c6e1..129ef707 100644 --- a/src/components/common/modal/ui/ModalTrigger.tsx +++ b/src/components/common/modal/ModalTrigger.tsx @@ -1,12 +1,11 @@ 'use client'; -import useModalContext from '@/components/common/modal/core/useModalContext'; - +import { useModal } from '@/contexts/ModalContext'; interface ModalTriggerProps extends React.ComponentProps<'button'> { modalId: string; } export default function ModalTrigger({ modalId, onClick, children, ...props }: ModalTriggerProps) { - const { openModal } = useModalContext(); + const { openModal } = useModal(); const handleClick = (e: React.MouseEvent) => { openModal(modalId); if (onClick) { diff --git a/src/components/common/modal/core/ModalContext.tsx b/src/components/common/modal/core/ModalContext.tsx deleted file mode 100644 index 4f1f2fbe..00000000 --- a/src/components/common/modal/core/ModalContext.tsx +++ /dev/null @@ -1,13 +0,0 @@ -'use client'; -import { createContext } from 'react'; - -type ModalContextValue = { - toggleModal: (id: string) => void; - openModal: (id: string) => void; - closeModal: (id: string) => void; - checkIsModalOpen: (id: string) => boolean; -}; - -const ModalContext = createContext(null); - -export default ModalContext; diff --git a/src/components/common/modal/core/ModalPortal.tsx b/src/components/common/modal/core/ModalPortal.tsx deleted file mode 100644 index 213ab4bf..00000000 --- a/src/components/common/modal/core/ModalPortal.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client'; -import { useState, useEffect } from 'react'; -import { createPortal } from 'react-dom'; -import useModalContext from '@/components/common/modal/core/useModalContext'; - -export default function ModalPortal({ - children, - modalId, -}: { - children: React.ReactNode; - modalId: string; -}) { - const [modalPortal, setModalPortal] = useState(null); - const { checkIsModalOpen } = useModalContext(); - - useEffect( - function initializeModalPortal() { - const modalElement = document.querySelector('#modal-container'); - setModalPortal(modalElement); - }, - [setModalPortal] - ); - - if (!modalPortal) return null; - - return <>{createPortal(checkIsModalOpen(modalId) ? children : null, modalPortal)}; -} diff --git a/src/components/common/modal/core/ModalProvider.tsx b/src/components/common/modal/core/ModalProvider.tsx deleted file mode 100644 index 08e16299..00000000 --- a/src/components/common/modal/core/ModalProvider.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; -import { useState } from 'react'; -import ModalContext from '@/components/common/modal/core/ModalContext'; - -export interface ModalState { - [id: string]: { isOpen: boolean }; -} - -export default function ModalProvider({ children }: { children: React.ReactNode }) { - const [modals, setModals] = useState({}); - - const openModal = (id: string) => { - setModals((prev) => ({ - ...prev, - [id]: { isOpen: true }, - })); - }; - - const closeModal = (id: string) => { - setModals((prev) => ({ - ...prev, - [id]: { isOpen: false }, - })); - }; - - const toggleModal = (id: string) => { - setModals((prev) => ({ - ...prev, - [id]: { isOpen: prev[id]?.isOpen !== true }, - })); - }; - - const checkIsModalOpen = (id: string) => { - return !!modals[id]?.isOpen; - }; - - return ( - - {children} - - ); -} diff --git a/src/components/common/modal/core/useModalContext.tsx b/src/components/common/modal/core/useModalContext.tsx deleted file mode 100644 index 320d0321..00000000 --- a/src/components/common/modal/core/useModalContext.tsx +++ /dev/null @@ -1,9 +0,0 @@ -'use client'; -import { use } from 'react'; -import ModalContext from '@/components/common/modal/core/ModalContext'; - -export default function useModalContext() { - const ctx = use(ModalContext); - if (!ctx) throw new Error('useModal must be used within ModalProvider'); - return ctx; -} diff --git a/src/components/common/modal/index.ts b/src/components/common/modal/index.ts index 4ae22a42..f5a6b0fe 100644 --- a/src/components/common/modal/index.ts +++ b/src/components/common/modal/index.ts @@ -1,9 +1,7 @@ -export { default as ModalProvider } from './core/ModalProvider'; -export { default as ModalPortal } from './core/ModalPortal'; -export { default as ModalCloseButton } from './ui/ModalCloseButton'; -export { default as ModalContainer } from './ui/ModalContainer'; -export { default as ModalDescription } from './ui/ModalDescription'; -export { default as ModalFooter } from './ui/ModalFooter'; -export { default as ModalHeading } from './ui/ModalHeading'; -export { default as ModalOverlay } from './ui/ModalOverlay'; -export { default as ModalTrigger } from './ui/ModalTrigger'; +export { default as ModalCloseButton } from './ModalCloseButton'; +export { default as ModalContainer } from './ModalContainer'; +export { default as ModalDescription } from './ModalDescription'; +export { default as ModalFooter } from './ModalFooter'; +export { default as ModalHeading } from './ModalHeading'; +export { default as ModalOverlay } from './ModalOverlay'; +export { default as ModalTrigger } from './ModalTrigger'; diff --git a/src/components/danger-modal/index.tsx b/src/components/danger-modal/index.tsx index 8c2d1599..61e5c0c5 100644 --- a/src/components/danger-modal/index.tsx +++ b/src/components/danger-modal/index.tsx @@ -5,11 +5,10 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import Image from 'next/image'; import Button from '@/components/common/Button'; -import useModalContext from '@/components/common/modal/core/useModalContext'; interface DangerModalProps { modalId: string; @@ -29,7 +28,7 @@ export default function DangerModal({ onConfirm, disabled, }: DangerModalProps) { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const closeButton = closeButtonText ? closeButtonText : '닫기'; diff --git a/src/components/manage-task-item/components/Frequency.tsx b/src/components/manage-task-item/components/Frequency.tsx index 7fd57921..68a444f9 100644 --- a/src/components/manage-task-item/components/Frequency.tsx +++ b/src/components/manage-task-item/components/Frequency.tsx @@ -1,6 +1,6 @@ import Button from '@/components/common/Button'; import { OptionSelector } from '@/components/common/dropdown/OptionSelector'; -import useModalContext from '@/components/common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import DangerModal from '@/components/danger-modal'; interface Frequency { @@ -14,7 +14,7 @@ const DELETE_FREQUENCY_MODAL_ID = 'delete-frequency'; const FREQUENCY_LIST = ['한 번', '매일', '주 반복', '월 반복']; export default function Frequency({ isEdit, isOnce, handleFrequencyChange }: Frequency) { - const { openModal } = useModalContext(); + const { openModal } = useModal(); return isEdit ? ( <> diff --git a/src/components/manage-task-item/useManageTaskItem.ts b/src/components/manage-task-item/useManageTaskItem.ts index d48a0d60..60b0d797 100644 --- a/src/components/manage-task-item/useManageTaskItem.ts +++ b/src/components/manage-task-item/useManageTaskItem.ts @@ -4,7 +4,7 @@ import { Frequency } from '@/app/(content-layout)/[groupId]/tasklist/_tasklist/t import generateTime from './time-table'; import { TaskItemProps, TaskItem, Time } from './type'; import axiosClient from '@/lib/axiosClient'; -import useModalContext from '../common/modal/core/useModalContext'; +import { useModal } from '@/contexts/ModalContext'; import { validateEmptyValue } from '@/utils/validators'; import { Toast } from '../common/Toastify'; import { revalidateTasks } from '@/app/(content-layout)/[groupId]/tasklist/_tasklist/actions/task-actions'; @@ -25,7 +25,7 @@ export default function useManageTaskItem({ createOrEditModalId, }: TaskItemProps) { const { am, pm } = generateTime(); - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); const task = detailTask?.recurring; const router = useRouter(); diff --git a/src/components/signup-alert-modal/SignupFailModal.tsx b/src/components/signup-alert-modal/SignupFailModal.tsx index 4c3a6168..35c7686e 100644 --- a/src/components/signup-alert-modal/SignupFailModal.tsx +++ b/src/components/signup-alert-modal/SignupFailModal.tsx @@ -7,13 +7,12 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; +import { useModal, ModalPortal } from '@/contexts/ModalContext'; import Button from '../common/Button'; -import useModalContext from '@/components/common/modal/core/useModalContext'; export default function SignupFailModal() { - const { closeModal } = useModalContext(); + const { closeModal } = useModal(); return ( diff --git a/src/components/signup-alert-modal/SignupSuccessModal.tsx b/src/components/signup-alert-modal/SignupSuccessModal.tsx index 78377458..d109eca9 100644 --- a/src/components/signup-alert-modal/SignupSuccessModal.tsx +++ b/src/components/signup-alert-modal/SignupSuccessModal.tsx @@ -7,8 +7,8 @@ import { ModalFooter, ModalHeading, ModalOverlay, - ModalPortal, } from '@/components/common/modal'; +import { ModalPortal } from '@/contexts/ModalContext'; import Button from '../common/Button'; interface Props { diff --git a/src/contexts/ModalContext.tsx b/src/contexts/ModalContext.tsx new file mode 100644 index 00000000..8d8a8d86 --- /dev/null +++ b/src/contexts/ModalContext.tsx @@ -0,0 +1,81 @@ +'use client'; +import { useContext, createContext, useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; + +type ModalContextValue = { + toggleModal: (id: string) => void; + openModal: (id: string) => void; + closeModal: (id: string) => void; + checkIsModalOpen: (id: string) => boolean; +}; + +const ModalContext = createContext(null); + +type ModalState = { + [id: string]: { isOpen: boolean }; +}; + +export const ModalProvider = ({ children }: { children: React.ReactNode }) => { + const [modals, setModals] = useState({}); + + const openModal = (id: string) => { + setModals((prev) => ({ + ...prev, + [id]: { isOpen: true }, + })); + }; + + const closeModal = (id: string) => { + setModals((prev) => ({ + ...prev, + [id]: { isOpen: false }, + })); + }; + + const toggleModal = (id: string) => { + setModals((prev) => ({ + ...prev, + [id]: { isOpen: prev[id]?.isOpen !== true }, + })); + }; + + const checkIsModalOpen = (id: string) => { + return !!modals[id]?.isOpen; + }; + + return ( + + {children} + + ); +}; + +export const useModal = () => { + const ctx = useContext(ModalContext); + if (!ctx) throw new Error('useModal must be used within ModalProvider'); + return ctx; +}; + +export function ModalPortal({ children, modalId }: { children: React.ReactNode; modalId: string }) { + const [modalPortal, setModalPortal] = useState(null); + const { checkIsModalOpen } = useModal(); + + useEffect( + function initializeModalPortal() { + const modalElement = document.querySelector('#modal-container'); + setModalPortal(modalElement); + }, + [setModalPortal] + ); + + if (!modalPortal) return null; + + return <>{createPortal(checkIsModalOpen(modalId) ? children : null, modalPortal)}; +} From 4237ab6f5d749f8512222c17fea31d577090bfae Mon Sep 17 00:00:00 2001 From: nerte Date: Sat, 31 May 2025 17:35:50 +0900 Subject: [PATCH 14/37] =?UTF-8?q?Refactor=20:=20=ED=8D=BC=EB=A0=90?= =?UTF-8?q?=EB=9F=B4=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@detailTask/(..)tasks/[taskId]/page.tsx | 77 ++++--------------- .../@detailTask/_components/Background.tsx | 43 +++++++++++ .../@detailTask/_components/CloseButton.tsx | 18 +++++ .../@detailTask/_components/CommentField.tsx | 2 +- .../@detailTask/_components/DetailTask.tsx | 2 +- .../_components/DetailTaskContentField.tsx | 2 +- .../@detailTask/_components/ExpandButton.tsx | 11 +++ .../_components/ToggleDoneButton.tsx | 2 +- .../tasklist/@detailTask/default.tsx | 3 + .../_tasklist/components/TasksWiseTask.tsx | 45 ++++------- .../_tasklist/hooks/use-task-actions.ts | 2 +- .../tasklist/{(tasklist) => }/layout.tsx | 4 +- .../tasklist/{(tasklist) => }/loading.tsx | 2 +- .../tasklist/{(tasklist) => }/page.tsx | 10 +-- .../[groupId]/tasks/[taskId]/page.tsx | 13 ++-- 15 files changed, 129 insertions(+), 107 deletions(-) create mode 100644 src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx create mode 100644 src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CloseButton.tsx create mode 100644 src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ExpandButton.tsx create mode 100644 src/app/(content-layout)/[groupId]/tasklist/@detailTask/default.tsx rename src/app/(content-layout)/[groupId]/tasklist/{(tasklist) => }/layout.tsx (90%) rename src/app/(content-layout)/[groupId]/tasklist/{(tasklist) => }/loading.tsx (90%) rename src/app/(content-layout)/[groupId]/tasklist/{(tasklist) => }/page.tsx (86%) diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx index 6a091142..45dd0d58 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/(..)tasks/[taskId]/page.tsx @@ -1,72 +1,29 @@ -'use client'; -import Image from 'next/image'; -import { useCallback, useEffect, useRef } from 'react'; -import { ErrorBoundary } from 'react-error-boundary'; -import DetailTask from '../../_components/DetailTask'; -import { useRouter } from 'next/navigation'; +import Background from '../../_components/Background'; +import DetailTaskPage from '@/app/(content-layout)/[groupId]/tasks/[taskId]/page'; +import CloseButton from '../../_components/CloseButton'; +import ExpandButton from '../../_components/ExpandButton'; interface Props { - isOpen: boolean; - isDone: boolean; - setIsDone: () => void; - taskId: number; - closeDetailTask: () => void; + params: Promise<{ taskId: string }>; } -export default function DetailTaskContainer({ taskId, isOpen, closeDetailTask, ...props }: Props) { - const detailTaskRef = useRef(null); - const router = useRouter(); - - const closingDetailTaskOutsideClick = useCallback( - (e: MouseEvent) => { - if (!isOpen || !taskId) return; - - const target = e.target as Node; - - const isInsideDetail = detailTaskRef.current?.contains(target); - const modalPortal = document.querySelector('#modal-container'); - const isInsidePortal = modalPortal?.contains(target); - - if (!isInsideDetail && !isInsidePortal) { - closeDetailTask(); - } - }, - [isOpen, taskId, closeDetailTask] - ); - - useEffect(() => { - document.addEventListener('mousedown', closingDetailTaskOutsideClick); - return () => { - document.removeEventListener('mousedown', closingDetailTaskOutsideClick); - }; - }, [isOpen, closingDetailTaskOutsideClick]); +export default async function DetailTaskContainer({ params }: Props) { + const taskId = (await params).taskId; return ( <> - {isOpen && ( -
    -
    -
    - - + {!!taskId && ( + +
    +
    +
    + + +
    +
    - 해당 태스크를 불러올 수 없습니다.
    }> - -
    -
    + )} ); diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx new file mode 100644 index 00000000..6b592f79 --- /dev/null +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { ReactNode, useCallback, useEffect, useRef } from 'react'; + +interface Props { + children: ReactNode; + isOpen: boolean; +} +export default function Background({ children, isOpen }: Props) { + const detailTaskRef = useRef(null); + const router = useRouter(); + + const closingDetailTaskOutsideClick = useCallback( + (e: MouseEvent) => { + if (!isOpen) return; + + const target = e.target as Node; + + const isInsideDetail = detailTaskRef.current?.contains(target); + const modalPortal = document.querySelector('#modal-container'); + const isInsidePortal = modalPortal?.contains(target); + + if (!isInsideDetail && !isInsidePortal) { + router.back(); + } + }, + [isOpen] + ); + + useEffect(() => { + document.addEventListener('mousedown', closingDetailTaskOutsideClick); + return () => { + document.removeEventListener('mousedown', closingDetailTaskOutsideClick); + }; + }, [isOpen, closingDetailTaskOutsideClick]); + + return ( +
    +
    {children}
    +
    + ); +} diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CloseButton.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CloseButton.tsx new file mode 100644 index 00000000..97df9fa8 --- /dev/null +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CloseButton.tsx @@ -0,0 +1,18 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +export default function CloseButton() { + const router = useRouter(); + + return ( + + ); +} diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx index f2f215b6..011e12b7 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/CommentField.tsx @@ -5,9 +5,9 @@ import { useState } from 'react'; import EditCommentInput from './EditCommentInput'; import axiosClient from '@/lib/axiosClient'; import useModalContext from '@/components/common/modal/core/useModalContext'; -import RemoveCommentModal from '../../_tasklist/components/ModalContents/RemoveCommentModal'; import { Toast } from '@/components/common/Toastify'; import { revalidateTasks } from '../../_tasklist/actions/task-actions'; +import RemoveCommentModal from '../../_tasklist/components/ModalContents/RemoveCommentModal'; interface Props { comment: Comment; diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx index 03c93263..e42485cf 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx @@ -1,13 +1,13 @@ 'use client'; import axiosClient from '@/lib/axiosClient'; import { useCallback, useEffect, useState } from 'react'; -import { useTaskActions } from '../../_tasklist/hooks/use-task-actions'; import Content from './DetailTaskContentField'; import DetailTaskCommentField from './DetailTaskCommentsField'; import Button from '@/components/common/Button'; import Check from '@/assets/Check'; import clsx from 'clsx'; import { DetailTaskType } from '../../_tasklist/types/task-type'; +import { useTaskActions } from '../../_tasklist/hooks/use-task-actions'; interface Props { isDone: boolean; diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTaskContentField.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTaskContentField.tsx index f53926b1..afd7e95f 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTaskContentField.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTaskContentField.tsx @@ -5,8 +5,8 @@ import ProfileBadge from '@/components/profile-badge'; import Repeat from '@/assets/Repeat'; import { format } from 'date-fns'; import clsx from 'clsx'; -import { useTaskModals } from '../../_tasklist/hooks/use-task-modals'; import { getRepeatDescription } from '../../_tasklist/utils/format-repeat-schedule'; +import { useTaskModals } from '../../_tasklist/hooks/use-task-modals'; import { DetailTaskType } from '../../_tasklist/types/task-type'; interface Props { diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ExpandButton.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ExpandButton.tsx new file mode 100644 index 00000000..c341b27f --- /dev/null +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ExpandButton.tsx @@ -0,0 +1,11 @@ +'use client'; + +import Image from 'next/image'; + +export default function ExpandButton() { + return ( + + ); +} diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ToggleDoneButton.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ToggleDoneButton.tsx index b028e45b..9abf15c5 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ToggleDoneButton.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/ToggleDoneButton.tsx @@ -2,8 +2,8 @@ import Check from '@/assets/Check'; import Button from '@/components/common/Button'; import clsx from 'clsx'; -import { useTaskActions } from '../../_tasklist/hooks/use-task-actions'; import { DetailTaskType } from '../../_tasklist/types/task-type'; +import { useTaskActions } from '../../_tasklist/hooks/use-task-actions'; interface Props { isDone: boolean; diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/default.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/default.tsx new file mode 100644 index 00000000..ce313c06 --- /dev/null +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/default.tsx @@ -0,0 +1,3 @@ +export default function DefaultRender() { + return null; +} diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx index 3efe2bb6..566de0d6 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/TasksWiseTask.tsx @@ -11,8 +11,7 @@ import getDetailTaskItem from '@/lib/api/detail-task-item'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { useDndMonitor } from '@dnd-kit/core'; -import Link from 'next/link'; -import DetailTaskContainer from '../../@detailTask/(..)tasks/[taskId]/page'; +import { useRouter } from 'next/navigation'; interface Props { task: Task; @@ -23,7 +22,6 @@ interface Props { export default function TasksWiseTask({ task, groupId, taskListId }: Props) { const [isDone, setIsDone] = useState(!!task.doneAt); const [isDelete, setIsDelete] = useState(false); - const [isDetailTaskOpen, setIsDetailTaskOpen] = useState(false); const [detailTask, setDetailTask] = useState(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ @@ -31,6 +29,8 @@ export default function TasksWiseTask({ task, groupId, taskListId }: Props) { }); const [onDrag, setOnDrag] = useState(false); + const router = useRouter(); + useDndMonitor({ onDragStart: () => setOnDrag(true), onDragEnd: () => setOnDrag(false), @@ -78,11 +78,7 @@ export default function TasksWiseTask({ task, groupId, taskListId }: Props) { }; const openDetailTask = () => { - setIsDetailTaskOpen(true); - }; - - const closeDetailTask = () => { - setIsDetailTaskOpen(false); + router.push(`/${groupId}/tasks/${task.id}`); }; return ( @@ -98,28 +94,19 @@ export default function TasksWiseTask({ task, groupId, taskListId }: Props) { opacity: isDragging ? 0.5 : 1, }} > - - toggleTaskDone(isDone, toggleTaskStatus)} - onEdit={() => popUpEditTaskModal(createOrEditModalId)} - onDelete={() => popUpDeleteTaskModal(taskDeleteModalId)} - onClick={() => openDetailTask()} - isDone={isDone} - name={task.name} - commentCount={task.commentCount} - date={safeFormatDate(task.date)} - frequency={task.frequency} - /> - - toggleTaskDone(isDone, toggleTaskStatus)} + onEdit={() => popUpEditTaskModal(createOrEditModalId)} + onDelete={() => popUpDeleteTaskModal(taskDeleteModalId)} isDone={isDone} - setIsDone={toggleTaskStatus} - taskId={task.id} - closeDetailTask={closeDetailTask} - isOpen={isDetailTaskOpen} + onClick={() => openDetailTask()} + name={task.name} + commentCount={task.commentCount} + date={safeFormatDate(task.date)} + frequency={task.frequency} /> ) { +}) { return ( <> {children} diff --git a/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/loading.tsx b/src/app/(content-layout)/[groupId]/tasklist/loading.tsx similarity index 90% rename from src/app/(content-layout)/[groupId]/tasklist/(tasklist)/loading.tsx rename to src/app/(content-layout)/[groupId]/tasklist/loading.tsx index 15b1d56b..f45d0734 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/loading.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/loading.tsx @@ -1,4 +1,4 @@ -import { DateSkeleton, TaskListsSkeleton, TaskSkeleton } from '../_tasklist/components/Skeleton'; +import { DateSkeleton, TaskListsSkeleton, TaskSkeleton } from './_tasklist/components/Skeleton'; export default function TaskListPageLoading() { return ( diff --git a/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/page.tsx b/src/app/(content-layout)/[groupId]/tasklist/page.tsx similarity index 86% rename from src/app/(content-layout)/[groupId]/tasklist/(tasklist)/page.tsx rename to src/app/(content-layout)/[groupId]/tasklist/page.tsx index 252d8d86..35ef56cc 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/(tasklist)/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/page.tsx @@ -1,13 +1,13 @@ -import { getTaskLists, getTasks } from '../_tasklist/actions/task-actions'; -import ManageTaskItemModal from '../_tasklist/components/manage-task-item-modal/MangeTaskItemModal'; -import DateSwitcher from '../_tasklist/components/DateSwitcher'; -import TaskLists from '../_tasklist/components/TaskLists'; -import Tasks from '../_tasklist/components/Tasks'; import { notFound } from 'next/navigation'; import { Metadata } from 'next'; import { cache } from 'react'; import { getGroupApiResponse, Group } from '@/types/group'; import axiosServer from '@/lib/axiosServer'; +import DateSwitcher from './_tasklist/components/DateSwitcher'; +import TaskLists from './_tasklist/components/TaskLists'; +import Tasks from './_tasklist/components/Tasks'; +import ManageTaskItemModal from './_tasklist/components/manage-task-item-modal/MangeTaskItemModal'; +import { getTaskLists, getTasks } from './_tasklist/actions/task-actions'; interface Props { params: Promise<{ groupId: string }>; diff --git a/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx b/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx index 3ce820a0..1be737ee 100644 --- a/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx +++ b/src/app/(content-layout)/[groupId]/tasks/[taskId]/page.tsx @@ -1,3 +1,4 @@ +import { ErrorBoundary } from 'react-error-boundary'; import DetailTaskCommentField from '../../tasklist/@detailTask/_components/DetailTaskCommentsField'; import Content from '../../tasklist/@detailTask/_components/DetailTaskContentField'; import ToggleDoneButton from '../../tasklist/@detailTask/_components/ToggleDoneButton'; @@ -15,10 +16,12 @@ export default async function DetailTaskPage({ params }: Props) { const isDone = Boolean(task.doneAt); return ( -
    - - - -
    + 해당 태스크를 불러올 수 없습니다.
    }> +
    + + + +
    + ); } From 489063412b38750bf65f843bba0332808c88df2f Mon Sep 17 00:00:00 2001 From: nerte Date: Sun, 1 Jun 2025 15:22:14 +0900 Subject: [PATCH 15/37] =?UTF-8?q?Build=20:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20import=EB=AC=B8=20=EC=82=AD=EC=A0=9C,=20=EB=94=94?= =?UTF-8?q?=ED=8E=9C=EB=8D=98=EC=8B=9C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[groupId]/tasklist/@detailTask/_components/Background.tsx | 2 +- .../_tasklist/components/ModalContents/RemoveCommentModal.tsx | 1 - .../[groupId]/tasklist/_tasklist/components/Tasks.tsx | 2 +- .../[groupId]/tasklist/_tasklist/hooks/use-dnd-kit.ts | 2 +- .../[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx index 6b592f79..3f4cb1d6 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/Background.tsx @@ -25,7 +25,7 @@ export default function Background({ children, isOpen }: Props) { router.back(); } }, - [isOpen] + [isOpen, router] ); useEffect(() => { diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx index a3de62fc..7ecbf14f 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/ModalContents/RemoveCommentModal.tsx @@ -1,7 +1,6 @@ 'use client'; import { ModalContainer, - ModalDescription, ModalFooter, ModalHeading, ModalOverlay, diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/Tasks.tsx b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/Tasks.tsx index 0be0ba41..25c15a0f 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/Tasks.tsx +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/components/Tasks.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import useDndKit from '../hooks/use-dnd-kit'; import TaskListPageFallBack from '../../error'; -import { Task, TaskList } from '../types/task-type'; +import { Task } from '../types/task-type'; import TasksWiseTask from './TasksWiseTask'; interface Props { diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-dnd-kit.ts b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-dnd-kit.ts index 2bf7f2e2..d607e7bb 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-dnd-kit.ts +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-dnd-kit.ts @@ -1,6 +1,6 @@ import { DragEndEvent, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; import { arrayMove } from '@dnd-kit/sortable'; -import { TaskList, Task } from '../types/task-type'; +import { Task } from '../types/task-type'; import { useTaskActions } from './use-task-actions'; export default function useDndKit( diff --git a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts index eed77902..32831bdb 100644 --- a/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts +++ b/src/app/(content-layout)/[groupId]/tasklist/_tasklist/hooks/use-task-actions.ts @@ -1,7 +1,7 @@ import axiosClient from '@/lib/axiosClient'; import { Task } from '../types/task-type'; import { Toast } from '@/components/common/Toastify'; -import { revalidateDetailTask, revalidateTasks } from '../actions/task-actions'; +import { revalidateDetailTask } from '../actions/task-actions'; export function useTaskActions(task?: Task) { const deleteTask = async ( From bba3522eb0d042317b5f5747c0d7a8c337ba9bec Mon Sep 17 00:00:00 2001 From: nerte Date: Sun, 1 Jun 2025 15:35:35 +0900 Subject: [PATCH 16/37] =?UTF-8?q?Chore=20:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@detailTask/_components/DetailTask.tsx | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx diff --git a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx b/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx deleted file mode 100644 index e42485cf..00000000 --- a/src/app/(content-layout)/[groupId]/tasklist/@detailTask/_components/DetailTask.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; -import axiosClient from '@/lib/axiosClient'; -import { useCallback, useEffect, useState } from 'react'; -import Content from './DetailTaskContentField'; -import DetailTaskCommentField from './DetailTaskCommentsField'; -import Button from '@/components/common/Button'; -import Check from '@/assets/Check'; -import clsx from 'clsx'; -import { DetailTaskType } from '../../_tasklist/types/task-type'; -import { useTaskActions } from '../../_tasklist/hooks/use-task-actions'; - -interface Props { - isDone: boolean; - setIsDone: () => void; - taskId: number; -} - -export default function DetailTask({ taskId, isDone, setIsDone }: Props) { - const [currentTask, setCurrentTask] = useState(); - const [error, setError] = useState(null); - const { toggleTaskDone } = useTaskActions(currentTask); - const buttonText = isDone ? '완료 취소하기' : '완료 하기'; - - const fetchTask = useCallback(async () => { - if (!taskId) return; - try { - const { data } = await axiosClient(`/groups/groupId/task-lists/taskListId/tasks/${taskId}`); - - setCurrentTask(data); - } catch { - setError(new Error('Task를 불러오는데 오류가 발생했습니다.')); - } - }, [taskId]); - - useEffect(() => { - fetchTask(); - }, [fetchTask]); - - if (error) throw error; - if (!currentTask) return; - - return ( -
    - - - -
    - ); -} From 6b4995f6a1d25f7aef9a3c43e5a92edc686404d0 Mon Sep 17 00:00:00 2001 From: heejin Date: Sun, 1 Jun 2025 20:52:14 +0900 Subject: [PATCH 17/37] Refactor : new form field --- src/components/common/formField/Fields.tsx | 32 +++++++++++++++++++ .../common/formField/compound/Input.tsx | 3 +- src/components/common/formField/style.ts | 1 + src/components/common/formField/type.ts | 19 +++++++++-- 4 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/components/common/formField/Fields.tsx diff --git a/src/components/common/formField/Fields.tsx b/src/components/common/formField/Fields.tsx new file mode 100644 index 00000000..94b82820 --- /dev/null +++ b/src/components/common/formField/Fields.tsx @@ -0,0 +1,32 @@ +'use client'; + +import clsx from 'clsx'; +import { GAP_SIZE, LABEL_SIZE } from './style'; +import { FieldProps } from './type'; + +export default function Fields({ + label, + required, + errorMessage, + gapSize = '12', + labelSize = '16/16', + render, +}: FieldProps) { + const showError = !!errorMessage; + + return ( +
    +
    + {label && ( + + )} + {render()} +
    + + {showError && {errorMessage}} +
    + ); +} diff --git a/src/components/common/formField/compound/Input.tsx b/src/components/common/formField/compound/Input.tsx index 7803517a..631f6cee 100644 --- a/src/components/common/formField/compound/Input.tsx +++ b/src/components/common/formField/compound/Input.tsx @@ -7,6 +7,7 @@ import { InputProps } from '../type'; export default function Input({ leftSlot = null, rightSlot = null, + hasError, borderClassName = 'border-border', className, ref, @@ -16,7 +17,7 @@ export default function Input({
    diff --git a/src/components/common/formField/style.ts b/src/components/common/formField/style.ts index 5a285b87..4b5b3aa2 100644 --- a/src/components/common/formField/style.ts +++ b/src/components/common/formField/style.ts @@ -14,6 +14,7 @@ export const LABEL_SIZE = { '16/20': 'text-lg-md sm:text-xl-bold', } as const; +// 삭제 예정 export const getBorderClassName = ({ isFocused, showSuccess, diff --git a/src/components/common/formField/type.ts b/src/components/common/formField/type.ts index 9118aadc..32d44d27 100644 --- a/src/components/common/formField/type.ts +++ b/src/components/common/formField/type.ts @@ -1,8 +1,10 @@ import { InputHTMLAttributes, TextareaHTMLAttributes } from 'react'; +import { GAP_SIZE, LABEL_SIZE } from './style'; export interface InputProps extends InputHTMLAttributes { leftSlot?: React.ReactNode; rightSlot?: React.ReactNode; + hasError?: boolean; borderClassName?: string; className?: string; ref?: React.Ref; @@ -15,14 +17,17 @@ export interface TextareaProps extends TextareaHTMLAttributes { + // imageUploaderType 삭제 예정 imageUploaderType?: ImageUploaderType; image: string | null; onImageChange: (e: React.ChangeEvent) => void; } +// 삭제 예정 export interface FormFieldProps { field: 'input' | 'textarea' | 'file-input'; imageUploaderType?: ImageUploaderType; @@ -34,10 +39,20 @@ export interface FormFieldProps { errorMessage?: string; gapSize?: '12' | '16' | '24' | '32'; labelSize?: '16/16' | '14/16' | '16/20'; - onFieldFocus?: () => void; - onFieldBlur?: () => void; + onFieldFocus?: (e: React.FocusEvent) => void; + onFieldBlur?: (e: React.FocusEvent) => void; } +export interface FieldProps { + label?: string; + required?: boolean; + errorMessage?: string; + gapSize?: keyof typeof GAP_SIZE; + labelSize?: keyof typeof LABEL_SIZE; + render: () => React.ReactNode; +} + +// 삭제 에정 export type FieldComponentProps = | (InputProps & FormFieldProps & { field?: 'input' }) | (TextareaProps & FormFieldProps & { field?: 'textarea' }) From 0fd2311715fc9a1039b4c2fc7cbe0c9d9257a8e5 Mon Sep 17 00:00:00 2001 From: heejin Date: Mon, 2 Jun 2025 03:35:38 +0900 Subject: [PATCH 18/37] Refactor : useZodForm --- src/hooks/useZodForm.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/hooks/useZodForm.ts diff --git a/src/hooks/useZodForm.ts b/src/hooks/useZodForm.ts new file mode 100644 index 00000000..f9770ed1 --- /dev/null +++ b/src/hooks/useZodForm.ts @@ -0,0 +1,19 @@ +import { useForm, UseFormProps, UseFormReturn } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { ZodSchema } from 'zod'; + +export default function useZodForm>({ + validationSchema, + defaultValues, + ...rest +}: { + validationSchema: ZodSchema; + defaultValues: UseFormProps['defaultValues']; +} & Omit, 'resolver' | 'defaultValues'>): UseFormReturn { + return useForm({ + resolver: zodResolver(validationSchema), + defaultValues, + mode: 'all', + ...rest, + }); +} From 9336cbe7bad97b1ce144e4d500a1cabf99e33bb9 Mon Sep 17 00:00:00 2001 From: heejin Date: Mon, 2 Jun 2025 03:57:34 +0900 Subject: [PATCH 19/37] Refactor : image uploader object-cover --- src/components/common/formField/compound/ImageUploader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/common/formField/compound/ImageUploader.tsx b/src/components/common/formField/compound/ImageUploader.tsx index 30691c3f..4fef0de7 100644 --- a/src/components/common/formField/compound/ImageUploader.tsx +++ b/src/components/common/formField/compound/ImageUploader.tsx @@ -41,12 +41,12 @@ export default function ImageUploader({ imageUploaderType, image, inputRef }: Im + ); +} diff --git a/src/components/common/modal/newModal/Container.tsx b/src/components/common/modal/newModal/Container.tsx new file mode 100644 index 00000000..e95b4a6b --- /dev/null +++ b/src/components/common/modal/newModal/Container.tsx @@ -0,0 +1,15 @@ +import clsx from 'clsx'; + +export default function Container({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( +
    + {children} +
    + ); +} diff --git a/src/components/common/modal/newModal/Description.tsx b/src/components/common/modal/newModal/Description.tsx new file mode 100644 index 00000000..75ffbb04 --- /dev/null +++ b/src/components/common/modal/newModal/Description.tsx @@ -0,0 +1,15 @@ +import clsx from 'clsx'; + +export default function Description({ className, children, ...props }: React.ComponentProps<'p'>) { + return ( +

    + {children} +

    + ); +} diff --git a/src/components/common/modal/newModal/Footer.tsx b/src/components/common/modal/newModal/Footer.tsx new file mode 100644 index 00000000..1ff370c8 --- /dev/null +++ b/src/components/common/modal/newModal/Footer.tsx @@ -0,0 +1,9 @@ +import clsx from 'clsx'; + +export default function Footer({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( +
    + {children} +
    + ); +} diff --git a/src/components/common/modal/newModal/Heading.tsx b/src/components/common/modal/newModal/Heading.tsx new file mode 100644 index 00000000..59aa69a0 --- /dev/null +++ b/src/components/common/modal/newModal/Heading.tsx @@ -0,0 +1,12 @@ +import clsx from 'clsx'; + +export default function Heading({ className, children, ...props }: React.ComponentProps<'h2'>) { + return ( +

    + {children} +

    + ); +} diff --git a/src/components/common/modal/newModal/Overlay.tsx b/src/components/common/modal/newModal/Overlay.tsx new file mode 100644 index 00000000..4b823283 --- /dev/null +++ b/src/components/common/modal/newModal/Overlay.tsx @@ -0,0 +1,47 @@ +'use client'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import clsx from 'clsx'; + +interface OverlayProps extends React.ComponentProps<'div'> { + disableOverlayClose?: boolean; +} + +export default function Overlay({ + disableOverlayClose = false, + onClick, + className, + children, + ...props +}: OverlayProps) { + const router = useRouter(); + + const handleClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget && !disableOverlayClose) { + onClick?.(e); + router.back(); + } + }; + + useEffect(function lockBodyScroll() { + const originalStyle = window.getComputedStyle(document.body).overflow; + document.body.style.overflow = 'hidden'; + + return () => { + document.body.style.overflow = originalStyle; + }; + }, []); + + return ( +
    + {children} +
    + ); +} diff --git a/src/components/common/modal/newModal/index.ts b/src/components/common/modal/newModal/index.ts new file mode 100644 index 00000000..d0138340 --- /dev/null +++ b/src/components/common/modal/newModal/index.ts @@ -0,0 +1,17 @@ +import CloseButton from './CloseButton'; +import Container from './Container'; +import Description from './Description'; +import Footer from './Footer'; +import Heading from './Heading'; +import Overlay from './Overlay'; + +const Modal = { + CloseButton, + Container, + Description, + Footer, + Heading, + Overlay, +}; + +export default Modal; From e77ac26939757356a073c32a1b1d370a3f5edf36 Mon Sep 17 00:00:00 2001 From: "Seok-jun.Kang" Date: Mon, 2 Jun 2025 17:11:59 +0900 Subject: [PATCH 24/37] =?UTF-8?q?Feat=20:=20intercepting=20routing=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=AA=A8=EB=8B=AC=EC=9D=98=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8C=85=20=EC=A3=BC=EC=86=8C=EB=A5=BC=20=EB=B0=94?= =?UTF-8?q?=EA=BE=B8=EA=B3=A0,=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=EB=A5=BC=20=EB=A3=A8=ED=8A=B8=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=EC=9C=BC=EB=A1=9C=20=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(groupId)/@modal/(.)create/page.tsx | 30 ---------- .../(groupId)/_[groupId]/Tasklists/index.tsx | 6 +- .../(groupId)/create-tasklist/page.tsx | 5 ++ .../[groupId]/(groupId)/create/page.tsx | 3 - .../[groupId]/(groupId)/layout.tsx | 14 ----- .../(.)[groupId]/create-tasklist/page.tsx | 58 +++++++++++++++++++ .../(groupId) => }/@modal/default.tsx | 0 src/app/layout.tsx | 3 + 8 files changed, 67 insertions(+), 52 deletions(-) delete mode 100644 src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx create mode 100644 src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx delete mode 100644 src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx delete mode 100644 src/app/(content-layout)/[groupId]/(groupId)/layout.tsx create mode 100644 src/app/@modal/(.)[groupId]/create-tasklist/page.tsx rename src/app/{(content-layout)/[groupId]/(groupId) => }/@modal/default.tsx (100%) diff --git a/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx deleted file mode 100644 index d26ae3f0..00000000 --- a/src/app/(content-layout)/[groupId]/(groupId)/@modal/(.)create/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; -import { useEffect } from 'react'; -import { - ModalCloseButton, - ModalContainer, - ModalFooter, - ModalHeading, - ModalOverlay, -} from '@/components/common/modal'; -import { useModal, ModalPortal } from '@/contexts/ModalContext'; -export default function Page() { - const { openModal } = useModal(); - useEffect(() => { - openModal('modalId'); - }, []); - - return ( - - - - -
    - 할 일 목록 추가 -
    - -
    -
    -
    - ); -} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx index 68755d88..51c99634 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx @@ -4,7 +4,6 @@ import TasklistItem from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/ import TasklistCreateModal from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal'; import TasklistUpdateModal from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistUpdateModal'; import TasklistDeleteModal from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistDeleteModal'; -import { ModalTrigger } from '@/components/common/modal'; import { Group } from '@/types/group'; import { Tasklist } from '@/types/tasklist'; import Link from 'next/link'; @@ -41,10 +40,7 @@ export default function Tasklists({ groupId, tasklists }: TasklistsProps) {

    할 일 목록 ({totalTasklistCount}개)

    - {/* - + 새로운 목록 추가하기 - */} - + + 새로운 목록 추가하기
    diff --git a/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx new file mode 100644 index 00000000..b302afcf --- /dev/null +++ b/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function Page() { + return
    Page
    ; +} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx deleted file mode 100644 index 55bc0258..00000000 --- a/src/app/(content-layout)/[groupId]/(groupId)/create/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return <>zz; -} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/layout.tsx b/src/app/(content-layout)/[groupId]/(groupId)/layout.tsx deleted file mode 100644 index 73e93591..00000000 --- a/src/app/(content-layout)/[groupId]/(groupId)/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export default function Layout({ - children, - modal, -}: { - children: React.ReactNode; - modal: React.ReactNode; -}) { - return ( - <> -
    {children}
    -
    {modal}
    - - ); -} diff --git a/src/app/@modal/(.)[groupId]/create-tasklist/page.tsx b/src/app/@modal/(.)[groupId]/create-tasklist/page.tsx new file mode 100644 index 00000000..046265f8 --- /dev/null +++ b/src/app/@modal/(.)[groupId]/create-tasklist/page.tsx @@ -0,0 +1,58 @@ +'use client'; +import { useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import Button from '@/components/common/Button'; +import FormField from '@/components/common/formField'; +import Modal from '@/components/common/modal/newModal'; +import BouncingDots from '@/components/common/loading/BouncingDots'; +import { validateEmptyValue } from '@/utils/validators'; +import { createTasklistAction } from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/actions'; + +export default function TasklistCreateModal() { + const { groupId } = useParams<{ groupId: string }>(); + const router = useRouter(); + const [name, setName] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleChangeName = (e: React.ChangeEvent) => { + setName(e.target.value); + }; + + const handleClickAddButton = async () => { + setIsLoading(true); + if (validateEmptyValue(name)) { + setIsLoading(false); + return; + } + createTasklistAction(Number(groupId), name).then(() => { + setIsLoading(false); + router.back(); + }); + }; + + return ( + + + +
    + 할 일 목록 추가 + +
    + + + +
    +
    + ); +} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/@modal/default.tsx b/src/app/@modal/default.tsx similarity index 100% rename from src/app/(content-layout)/[groupId]/(groupId)/@modal/default.tsx rename to src/app/@modal/default.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index de8e1aee..0871d184 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,8 +11,10 @@ export const metadata: Metadata = { export default function RootLayout({ children, + modal, }: Readonly<{ children: React.ReactNode; + modal: React.ReactNode; }>) { return ( @@ -24,6 +26,7 @@ export default function RootLayout({
    {children}
    +
    {modal}
    From 9ff07e651b4e552d3bc8707fe7f8e9cfc4802272 Mon Sep 17 00:00:00 2001 From: "Seok-jun.Kang" Date: Mon, 2 Jun 2025 17:29:56 +0900 Subject: [PATCH 25/37] =?UTF-8?q?Feat=20:=20=EB=AA=A8=EB=8B=AC=20=EC=95=88?= =?UTF-8?q?=20=EC=97=B4=EC=97=88=EC=9D=84=20=EB=95=8C=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(groupId)/create-tasklist/page.tsx | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx index b302afcf..747f525c 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx @@ -1,5 +1,56 @@ -import React from 'react'; +'use client'; +import { useState } from 'react'; +import { useParams } from 'next/navigation'; +import Button from '@/components/common/Button'; +import FormField from '@/components/common/formField'; +import Modal from '@/components/common/modal/newModal'; +import BouncingDots from '@/components/common/loading/BouncingDots'; +import { validateEmptyValue } from '@/utils/validators'; +import { createTasklistAction } from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/actions'; export default function Page() { - return
    Page
    ; + const { groupId } = useParams<{ groupId: string }>(); + const [name, setName] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const handleChangeName = (e: React.ChangeEvent) => { + setName(e.target.value); + }; + + const handleClickAddButton = async () => { + setIsLoading(true); + if (validateEmptyValue(name)) { + setIsLoading(false); + return; + } + createTasklistAction(Number(groupId), name).then(() => { + setIsLoading(false); + }); + }; + + return ( +
    +
    + +
    + 할 일 목록 추가 + +
    + + + +
    +
    + ); } From 61b86ca8ffa91bd0a1f703fb7fe0c4ca9b7e3695 Mon Sep 17 00:00:00 2001 From: heejin Date: Tue, 3 Jun 2025 01:03:32 +0900 Subject: [PATCH 26/37] Feat : query provider --- package-lock.json | 27 +++++++++++++++++++++++++++ package.json | 1 + src/app/layout.tsx | 25 ++++++++++++++----------- src/lib/query/queryProvider.tsx | 10 ++++++++++ 4 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 src/lib/query/queryProvider.tsx diff --git a/package-lock.json b/package-lock.json index 39c6eae2..6ddd3290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@hookform/resolvers": "^5.0.1", + "@tanstack/react-query": "^5.79.0", "axios": "^1.9.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -3349,6 +3350,32 @@ "tailwindcss": "4.1.4" } }, + "node_modules/@tanstack/query-core": { + "version": "5.79.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.79.0.tgz", + "integrity": "sha512-s+epTqqLM0/TbJzMAK7OEhZIzh63P9sWz5HEFc5XHL4FvKQXQkcjI8F3nee+H/xVVn7mrP610nVXwOytTSYd0w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.79.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.79.0.tgz", + "integrity": "sha512-DjC4JIYZnYzxaTzbg3osOU63VNLP67dOrWet2cZvXgmgwAXNxfS52AMq86M5++ILuzW+BqTUEVMTjhrZ7/XBuA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.79.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", diff --git a/package.json b/package.json index 53b9ef08..553e1fae 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@hookform/resolvers": "^5.0.1", + "@tanstack/react-query": "^5.79.0", "axios": "^1.9.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index de8e1aee..0b310fb0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import Header from '@/components/layout/gnb/Header'; import ToastProvider from '@/components/common/Toastify/ToasProvider'; import { UserProvider } from '@/contexts/UserContext'; import { ModalProvider } from '@/contexts/ModalContext'; +import QueryProvider from '@/lib/query/queryProvider'; export const metadata: Metadata = { title: 'Coworkers', @@ -17,17 +18,19 @@ export default function RootLayout({ return ( - - - -
    -
    - {children} -
    - - - - + + + + +
    +
    + {children} +
    + + + + + ); diff --git a/src/lib/query/queryProvider.tsx b/src/lib/query/queryProvider.tsx new file mode 100644 index 00000000..a9630dbe --- /dev/null +++ b/src/lib/query/queryProvider.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export default function QueryProvider({ children }: { children: React.ReactNode }) { + const [queryClient] = useState(() => new QueryClient()); + + return {children}; +} From 57a0885343bf3ae3913bd0b9c20e20854f413923 Mon Sep 17 00:00:00 2001 From: "Seok-jun.Kang" Date: Wed, 4 Jun 2025 15:28:33 +0900 Subject: [PATCH 27/37] =?UTF-8?q?Refactor=20:=20tasklist=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=AA=A8=EB=8B=AC=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Tasklists/TasklistCreateModal.tsx | 74 ------------------- .../(groupId)/_[groupId]/Tasklists/index.tsx | 10 --- .../create-tasklist/page.tsx | 2 +- src/app/[...modal]/page.tsx | 13 ++++ 4 files changed, 14 insertions(+), 85 deletions(-) delete mode 100644 src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx rename src/app/@modal/{(.)[groupId] => (...)[...modal]}/create-tasklist/page.tsx (97%) create mode 100644 src/app/[...modal]/page.tsx diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx deleted file mode 100644 index f2521707..00000000 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; -import { useState } from 'react'; -import Button from '@/components/common/Button'; -import FormField from '@/components/common/formField'; -import { - ModalCloseButton, - ModalContainer, - ModalFooter, - ModalHeading, - ModalOverlay, -} from '@/components/common/modal'; -import { useModal, ModalPortal } from '@/contexts/ModalContext'; -import BouncingDots from '@/components/common/loading/BouncingDots'; -import { validateEmptyValue } from '@/utils/validators'; - -interface TasklistCreateModalProps { - modalId: string; - isLoading: boolean; - createTasklist: (name: string) => void; -} - -export default function TasklistCreateModal({ - modalId, - isLoading, - createTasklist, -}: TasklistCreateModalProps) { - const [name, setName] = useState(''); - const { closeModal } = useModal(); - - const handleChangeName = (e: React.ChangeEvent) => { - setName(e.target.value); - }; - - const clearName = () => { - setName(''); - }; - - const handleClickAddButton = async () => { - if (validateEmptyValue(name)) return; - createTasklist(name); - clearName(); - closeModal(modalId); - }; - - return ( - <> - - - - -
    - 할 일 목록 추가 - -
    - - - -
    -
    -
    - - ); -} diff --git a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx index 51c99634..183ac8f4 100644 --- a/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx +++ b/src/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/index.tsx @@ -1,7 +1,6 @@ 'use client'; import useTasklists from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/useTasklists'; import TasklistItem from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistItem'; -import TasklistCreateModal from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistCreateModal'; import TasklistUpdateModal from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistUpdateModal'; import TasklistDeleteModal from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/TasklistDeleteModal'; import { Group } from '@/types/group'; @@ -19,15 +18,12 @@ export default function Tasklists({ groupId, tasklists }: TasklistsProps) { optimisticTasklists, selectedTasklist, setSelectedTasklist, - isCreateLoading, isUpdateLoading, isDeleteLoading, - createTasklist, updateTasklist, deleteTasklist, } = useTasklists(groupId, tasklists); - const tasklistCreateModalId = `tasklistCreate-${groupId}`; const tasklistUpdateModalId = selectedTasklist ? `tasklistUpdate-${selectedTasklist.id}` : ''; const tasklistDeleteModalId = selectedTasklist ? `tasklistDelete-${selectedTasklist.id}` : ''; const totalTasklistCount = optimisticTasklists.length; @@ -56,12 +52,6 @@ export default function Tasklists({ groupId, tasklists }: TasklistsProps) {
- - {selectedTasklist && ( (); const router = useRouter(); const [name, setName] = useState(''); diff --git a/src/app/[...modal]/page.tsx b/src/app/[...modal]/page.tsx new file mode 100644 index 00000000..abb52786 --- /dev/null +++ b/src/app/[...modal]/page.tsx @@ -0,0 +1,13 @@ +'use client'; +import { useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import Button from '@/components/common/Button'; +import FormField from '@/components/common/formField'; +import Modal from '@/components/common/modal/newModal'; +import BouncingDots from '@/components/common/loading/BouncingDots'; +import { validateEmptyValue } from '@/utils/validators'; +import { createTasklistAction } from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/actions'; + +export default function MymodalPage() { + return <>zz; +} From 93475d0d8a2e4bd0d58484390b663a811d170171 Mon Sep 17 00:00:00 2001 From: "Seok-jun.Kang" Date: Wed, 4 Jun 2025 15:33:59 +0900 Subject: [PATCH 28/37] =?UTF-8?q?Fix=20:=20tasklist=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=AA=A8=EB=8B=AC=20=EA=B2=BD=EB=A1=9C=20=EC=A7=81=EC=A0=91?= =?UTF-8?q?=20=EB=B0=A9=EB=AC=B8=20=EC=8B=9C=20not=20found=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(groupId)/create-tasklist/page.tsx | 56 ------------------- src/app/[...modal]/page.tsx | 14 +---- 2 files changed, 3 insertions(+), 67 deletions(-) delete mode 100644 src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx diff --git a/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx b/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx deleted file mode 100644 index 747f525c..00000000 --- a/src/app/(content-layout)/[groupId]/(groupId)/create-tasklist/page.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; -import { useState } from 'react'; -import { useParams } from 'next/navigation'; -import Button from '@/components/common/Button'; -import FormField from '@/components/common/formField'; -import Modal from '@/components/common/modal/newModal'; -import BouncingDots from '@/components/common/loading/BouncingDots'; -import { validateEmptyValue } from '@/utils/validators'; -import { createTasklistAction } from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/actions'; - -export default function Page() { - const { groupId } = useParams<{ groupId: string }>(); - const [name, setName] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const handleChangeName = (e: React.ChangeEvent) => { - setName(e.target.value); - }; - - const handleClickAddButton = async () => { - setIsLoading(true); - if (validateEmptyValue(name)) { - setIsLoading(false); - return; - } - createTasklistAction(Number(groupId), name).then(() => { - setIsLoading(false); - }); - }; - - return ( -
-
- -
- 할 일 목록 추가 - -
- - - -
-
- ); -} diff --git a/src/app/[...modal]/page.tsx b/src/app/[...modal]/page.tsx index abb52786..e7c23934 100644 --- a/src/app/[...modal]/page.tsx +++ b/src/app/[...modal]/page.tsx @@ -1,13 +1,5 @@ 'use client'; -import { useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import Button from '@/components/common/Button'; -import FormField from '@/components/common/formField'; -import Modal from '@/components/common/modal/newModal'; -import BouncingDots from '@/components/common/loading/BouncingDots'; -import { validateEmptyValue } from '@/utils/validators'; -import { createTasklistAction } from '@/app/(content-layout)/[groupId]/(groupId)/_[groupId]/Tasklists/actions'; -export default function MymodalPage() { - return <>zz; -} +import notFound from '@/app/not-found'; + +export default notFound; From 6c06c40d064fd73a20dc2a2e945baba0447d5716 Mon Sep 17 00:00:00 2001 From: hyejin Date: Thu, 5 Jun 2025 19:36:45 +0900 Subject: [PATCH 29/37] =?UTF-8?q?Chore=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B4=80=EB=A0=A8=20=EB=AA=A8=EB=8B=AC=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signup/_signup/SignupForm.tsx | 156 +++++++---- .../signup/_signup}/SignupSuccessModal.tsx | 2 +- .../signup/_signup/hooks/useSignup.ts | 242 ++++++++++++++++++ .../signup-alert-modal/SignupFailModal.tsx | 41 --- 4 files changed, 347 insertions(+), 94 deletions(-) rename src/{components/signup-alert-modal => app/(form-layout)/signup/_signup}/SignupSuccessModal.tsx (96%) create mode 100644 src/app/(form-layout)/signup/_signup/hooks/useSignup.ts delete mode 100644 src/components/signup-alert-modal/SignupFailModal.tsx diff --git a/src/app/(form-layout)/signup/_signup/SignupForm.tsx b/src/app/(form-layout)/signup/_signup/SignupForm.tsx index 9664a91c..1c279c00 100644 --- a/src/app/(form-layout)/signup/_signup/SignupForm.tsx +++ b/src/app/(form-layout)/signup/_signup/SignupForm.tsx @@ -2,13 +2,13 @@ import { useState, ChangeEvent } from 'react'; import { useRouter } from 'next/navigation'; +import { useMutation } from '@tanstack/react-query'; import axiosClient from '@/lib/axiosClient'; import FormField from '@/components/common/formField'; import Button from '@/components/common/Button'; import PasswordToggleButton from './PasswordToggleButton'; import usePasswordVisibility from '@/utils/use-password-visibility'; import { useModal } from '@/contexts/ModalContext'; -import SignupSuccessModal from '@/components/signup-alert-modal/SignupSuccessModal'; import { validateEmail, validatePassword, @@ -19,6 +19,24 @@ import { AUTH_ERROR_MESSAGES } from '@/constants/messages/signup'; import { Toast } from '@/components/common/Toastify'; import { setClientCookie } from '@/lib/cookie/client'; import { useUser } from '@/contexts/UserContext'; +import SignupSuccessModal from './SignupSuccessModal'; + +interface SignupRequest { + email: string; + password: string; + passwordConfirmation: string; + nickname: string; +} + +interface LoginRequest { + email: string; + password: string; +} + +interface LoginResponse { + accessToken: string; + refreshToken: string; +} interface ErrorResponse { response?: { @@ -28,30 +46,92 @@ interface ErrorResponse { }; } +// 회원가입 API 함수 +const signupUser = async (data: SignupRequest): Promise => { + await axiosClient.post('/auth/signUp', data); +}; + +// 로그인 API 함수 +const loginUser = async (data: LoginRequest): Promise => { + const response = await axiosClient.post('/auth/signIn', data); + return response.data; +}; + export default function SignupForm() { const { openModal } = useModal(); const router = useRouter(); const { isPasswordVisible, togglePasswordVisibility } = usePasswordVisibility(); const { fetchUser } = useUser(); + const [formData, setFormData] = useState({ nickname: '', email: '', password: '', passwordConfirmation: '', }); - const setFieldValue = (key: keyof typeof formData, value: string) => { - setFormData((prev) => ({ - ...prev, - [key]: value.trim(), - })); - }; + const [duplicateError, setDuplicateError] = useState({ nickname: false, email: false, }); + const [isSuccess, setIsSuccess] = useState(false); const [loginTimeoutId, setLoginTimeoutId] = useState(null); + // 회원가입 mutation + const signupMutation = useMutation({ + mutationFn: signupUser, + onSuccess: () => { + // 회원가입 성공 후 자동 로그인 시도 + const timeoutId = setTimeout(() => { + loginMutation.mutate({ + email: formData.email, + password: formData.password, + }); + }, 5000); + + setLoginTimeoutId(timeoutId); + openModal('signup-success'); + setIsSuccess(true); + }, + onError: (error: unknown) => { + const err = error as ErrorResponse; + const message = err.response?.data?.message || ''; + + setDuplicateError({ + email: message.includes('이메일'), + nickname: message.includes('닉네임'), + }); + + Toast.error('회원가입 실패'); + }, + }); + + // 자동 로그인 mutation + const loginMutation = useMutation({ + mutationFn: loginUser, + onSuccess: async (data: LoginResponse) => { + const { accessToken, refreshToken } = data; + + setClientCookie('accessToken', accessToken); + setClientCookie('refreshToken', refreshToken); + + await fetchUser(); + router.push('/nogroup'); + }, + onError: () => { + Toast.error('자동 로그인 실패.'); + router.push('/login'); + }, + }); + + const setFieldValue = (key: keyof typeof formData, value: string) => { + setFormData((prev) => ({ + ...prev, + [key]: value.trim(), + })); + }; + function getNicknameErrorMessage() { if (formData.nickname.trim() === '') { return AUTH_ERROR_MESSAGES.nickname.required; @@ -145,50 +225,13 @@ export default function SignupForm() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - try { - await axiosClient.post('/auth/signUp', { - email: formData.email, - password: formData.password, - passwordConfirmation: formData.passwordConfirmation, - nickname: formData.nickname, - }); - - const timeoutId = setTimeout(async () => { - try { - const loginRes = await axiosClient.post('/auth/signIn', { - email: formData.email, - password: formData.password, - }); - - const { accessToken, refreshToken } = loginRes.data; - - setClientCookie('accessToken', accessToken); - setClientCookie('refreshToken', refreshToken); - - await fetchUser(); - router.push('/nogroup'); - } catch { - Toast.error('자동 로그인 실패.'); - router.push('/login'); - } - }, 5000); - - setLoginTimeoutId(timeoutId); - - openModal('signup-success'); - setIsSuccess(true); - } catch (error: unknown) { - const err = error as ErrorResponse; - const message = err.response?.data?.message || ''; - - setDuplicateError({ - email: message.includes('이메일'), - nickname: message.includes('닉네임'), - }); - - Toast.error('회원가입 실패'); - } + signupMutation.mutate({ + email: formData.email, + password: formData.password, + passwordConfirmation: formData.passwordConfirmation, + nickname: formData.nickname, + }); }; const isFormInvalid = @@ -197,6 +240,9 @@ export default function SignupForm() { !validatePassword(formData.password) || !validateConfirmPassword(formData.password, formData.passwordConfirmation); + // 로딩 상태: 회원가입 중이거나 자동 로그인 중일 때 + const isLoading = signupMutation.isPending || loginMutation.isPending; + return (
@@ -222,8 +268,14 @@ export default function SignupForm() { /> ))}
- {isSuccess && ( => { + await axiosClient.post('/auth/signUp', data); +}; + +const loginUser = async (data: LoginRequest): Promise => { + const response = await axiosClient.post('/auth/signIn', data); + return response.data; +}; + +export const useSignup = () => { + const { openModal } = useModal(); + const router = useRouter(); + const { fetchUser } = useUser(); + + const [duplicateError, setDuplicateError] = useState({ + nickname: false, + email: false, + }); + + const [isSuccess, setIsSuccess] = useState(false); + const [loginTimeoutId, setLoginTimeoutId] = useState(null); + + // 회원가입 mutation + const signupMutation = useMutation({ + mutationFn: signupUser, + onSuccess: () => { + openModal('signup-success'); + setIsSuccess(true); + }, + onError: (error: unknown) => { + const err = error as ErrorResponse; + const message = err.response?.data?.message || ''; + + setDuplicateError({ + email: message.includes('이메일'), + nickname: message.includes('닉네임'), + }); + + Toast.error('회원가입 실패'); + }, + }); + + // 자동 로그인 mutation + const loginMutation = useMutation({ + mutationFn: loginUser, + onSuccess: async (data: LoginResponse) => { + const { accessToken, refreshToken } = data; + + setClientCookie('accessToken', accessToken); + setClientCookie('refreshToken', refreshToken); + + await fetchUser(); + router.push('/nogroup'); + }, + onError: () => { + Toast.error('자동 로그인 실패.'); + router.push('/login'); + }, + }); + + const handleSignup = (formData: SignupRequest) => { + signupMutation.mutate(formData); + }; + + const handleAutoLogin = (email: string, password: string, delay: number = 5000) => { + const timeoutId = setTimeout(() => { + loginMutation.mutate({ email, password }); + }, delay); + + setLoginTimeoutId(timeoutId); + }; + + const cancelAutoLogin = () => { + if (loginTimeoutId) { + clearTimeout(loginTimeoutId); + setLoginTimeoutId(null); + } + }; + + const clearDuplicateError = (field: 'email' | 'nickname') => { + setDuplicateError((prev) => ({ ...prev, [field]: false })); + }; + + const isLoading = signupMutation.isPending || loginMutation.isPending; + + return { + // States + duplicateError, + isSuccess, + isLoading, + + // Mutations + signupMutation, + loginMutation, + + // Actions + handleSignup, + handleAutoLogin, + cancelAutoLogin, + clearDuplicateError, + }; +}; + +// 사용 예시 - SignupForm 컴포넌트에서: +/* +'use client'; + +import { useState, ChangeEvent } from 'react'; +import FormField from '@/components/common/formField'; +import Button from '@/components/common/Button'; +import PasswordToggleButton from './PasswordToggleButton'; +import usePasswordVisibility from '@/utils/use-password-visibility'; +import SignupSuccessModal from '@/components/signup-alert-modal/SignupSuccessModal'; +import { + validateEmail, + validatePassword, + validateConfirmPassword, + validateLengthLimit, +} from '@/utils/validators'; +import { AUTH_ERROR_MESSAGES } from '@/constants/messages/signup'; +import { useSignup } from '@/hooks/useSignup'; + +export default function SignupForm() { + const { isPasswordVisible, togglePasswordVisibility } = usePasswordVisibility(); + const { + duplicateError, + isSuccess, + isLoading, + handleSignup, + handleAutoLogin, + cancelAutoLogin, + clearDuplicateError, + } = useSignup(); + + const [formData, setFormData] = useState({ + nickname: '', + email: '', + password: '', + passwordConfirmation: '', + }); + + const setFieldValue = (key: keyof typeof formData, value: string) => { + setFormData((prev) => ({ + ...prev, + [key]: value.trim(), + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // 회원가입 실행 + handleSignup(formData); + + // 회원가입 성공 시 자동 로그인 스케줄링 + handleAutoLogin(formData.email, formData.password); + }; + + // ... 나머지 validation 로직과 formFields 배열은 동일 + + return ( + +
+ {formFields.map((field) => ( + ) => { + const key = field.name as keyof typeof formData; + setFieldValue(key, e.target.value); + + if (key === 'email' || key === 'nickname') { + clearDuplicateError(key); + } + }} + placeholder={field.placeholder} + rightSlot={field.rightSlot} + /> + ))} +
+ + {isSuccess && ( + { + cancelAutoLogin(); + router.push('/login'); + }} + /> + )} + + ); +} +*/ diff --git a/src/components/signup-alert-modal/SignupFailModal.tsx b/src/components/signup-alert-modal/SignupFailModal.tsx deleted file mode 100644 index 35c7686e..00000000 --- a/src/components/signup-alert-modal/SignupFailModal.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { - ModalContainer, - ModalDescription, - ModalFooter, - ModalHeading, - ModalOverlay, -} from '@/components/common/modal'; -import { useModal, ModalPortal } from '@/contexts/ModalContext'; -import Button from '../common/Button'; - -export default function SignupFailModal() { - const { closeModal } = useModal(); - - return ( - - - - ! - 회원가입 실패 - - 회원가입 도중 문제가 발생했습니다.
- 다시 시도해주세요. -
- - - -
-
-
- ); -} From 795c35dfc0f3692277015aebc195efdb0f6deb06 Mon Sep 17 00:00:00 2001 From: hyejin Date: Thu, 5 Jun 2025 19:40:51 +0900 Subject: [PATCH 30/37] =?UTF-8?q?Feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=8F=BC=EC=97=90=20TanStack=20Query=20useMutation?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signup/_signup/SignupForm.tsx | 5 - .../signup/_signup/hooks/useSignup.ts | 111 ------------------ 2 files changed, 116 deletions(-) diff --git a/src/app/(form-layout)/signup/_signup/SignupForm.tsx b/src/app/(form-layout)/signup/_signup/SignupForm.tsx index 1c279c00..dd9b3fb3 100644 --- a/src/app/(form-layout)/signup/_signup/SignupForm.tsx +++ b/src/app/(form-layout)/signup/_signup/SignupForm.tsx @@ -46,12 +46,10 @@ interface ErrorResponse { }; } -// 회원가입 API 함수 const signupUser = async (data: SignupRequest): Promise => { await axiosClient.post('/auth/signUp', data); }; -// 로그인 API 함수 const loginUser = async (data: LoginRequest): Promise => { const response = await axiosClient.post('/auth/signIn', data); return response.data; @@ -78,11 +76,9 @@ export default function SignupForm() { const [isSuccess, setIsSuccess] = useState(false); const [loginTimeoutId, setLoginTimeoutId] = useState(null); - // 회원가입 mutation const signupMutation = useMutation({ mutationFn: signupUser, onSuccess: () => { - // 회원가입 성공 후 자동 로그인 시도 const timeoutId = setTimeout(() => { loginMutation.mutate({ email: formData.email, @@ -240,7 +236,6 @@ export default function SignupForm() { !validatePassword(formData.password) || !validateConfirmPassword(formData.password, formData.passwordConfirmation); - // 로딩 상태: 회원가입 중이거나 자동 로그인 중일 때 const isLoading = signupMutation.isPending || loginMutation.isPending; return ( diff --git a/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts b/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts index cd911b03..c3bbcbed 100644 --- a/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts +++ b/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts @@ -1,4 +1,3 @@ -// hooks/useSignup.ts import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { useMutation } from '@tanstack/react-query'; @@ -55,7 +54,6 @@ export const useSignup = () => { const [isSuccess, setIsSuccess] = useState(false); const [loginTimeoutId, setLoginTimeoutId] = useState(null); - // 회원가입 mutation const signupMutation = useMutation({ mutationFn: signupUser, onSuccess: () => { @@ -75,7 +73,6 @@ export const useSignup = () => { }, }); - // 자동 로그인 mutation const loginMutation = useMutation({ mutationFn: loginUser, onSuccess: async (data: LoginResponse) => { @@ -119,124 +116,16 @@ export const useSignup = () => { const isLoading = signupMutation.isPending || loginMutation.isPending; return { - // States duplicateError, isSuccess, isLoading, - // Mutations signupMutation, loginMutation, - // Actions handleSignup, handleAutoLogin, cancelAutoLogin, clearDuplicateError, }; }; - -// 사용 예시 - SignupForm 컴포넌트에서: -/* -'use client'; - -import { useState, ChangeEvent } from 'react'; -import FormField from '@/components/common/formField'; -import Button from '@/components/common/Button'; -import PasswordToggleButton from './PasswordToggleButton'; -import usePasswordVisibility from '@/utils/use-password-visibility'; -import SignupSuccessModal from '@/components/signup-alert-modal/SignupSuccessModal'; -import { - validateEmail, - validatePassword, - validateConfirmPassword, - validateLengthLimit, -} from '@/utils/validators'; -import { AUTH_ERROR_MESSAGES } from '@/constants/messages/signup'; -import { useSignup } from '@/hooks/useSignup'; - -export default function SignupForm() { - const { isPasswordVisible, togglePasswordVisibility } = usePasswordVisibility(); - const { - duplicateError, - isSuccess, - isLoading, - handleSignup, - handleAutoLogin, - cancelAutoLogin, - clearDuplicateError, - } = useSignup(); - - const [formData, setFormData] = useState({ - nickname: '', - email: '', - password: '', - passwordConfirmation: '', - }); - - const setFieldValue = (key: keyof typeof formData, value: string) => { - setFormData((prev) => ({ - ...prev, - [key]: value.trim(), - })); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - // 회원가입 실행 - handleSignup(formData); - - // 회원가입 성공 시 자동 로그인 스케줄링 - handleAutoLogin(formData.email, formData.password); - }; - - // ... 나머지 validation 로직과 formFields 배열은 동일 - - return ( -
-
- {formFields.map((field) => ( - ) => { - const key = field.name as keyof typeof formData; - setFieldValue(key, e.target.value); - - if (key === 'email' || key === 'nickname') { - clearDuplicateError(key); - } - }} - placeholder={field.placeholder} - rightSlot={field.rightSlot} - /> - ))} -
- - {isSuccess && ( - { - cancelAutoLogin(); - router.push('/login'); - }} - /> - )} - - ); -} -*/ From 66dd00bdeedf36abe6c7a2353e9735d61833bab1 Mon Sep 17 00:00:00 2001 From: hyejin Date: Thu, 5 Jun 2025 19:59:04 +0900 Subject: [PATCH 31/37] =?UTF-8?q?Feat=20:=20=EA=B3=84=EC=A0=95=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20TanStack=20Quer?= =?UTF-8?q?y=20useMutation=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/mypage/MypageClient.tsx | 72 ++++++++++++++++-------- src/app/mypage/_mypage/NicknameField.tsx | 17 ++++-- src/app/mypage/_mypage/PasswordField.tsx | 21 +++++-- 3 files changed, 78 insertions(+), 32 deletions(-) diff --git a/src/app/mypage/MypageClient.tsx b/src/app/mypage/MypageClient.tsx index 6e2aa560..08e498c3 100644 --- a/src/app/mypage/MypageClient.tsx +++ b/src/app/mypage/MypageClient.tsx @@ -2,6 +2,7 @@ import Image from 'next/image'; import { useState, useEffect } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { updateUserNickname, verifyPassword } from './_mypage/action'; import ProfileImageUploader from './_mypage/ProfileImageUploader'; import NicknameField from './_mypage/NicknameField'; @@ -21,6 +22,39 @@ export default function MyPageClient() { const [nicknameError, setNicknameError] = useState(''); const [password, setPassword] = useState(''); const { openModal, closeModal } = useModal(); + const queryClient = useQueryClient(); + + // 닉네임 업데이트 mutation + const updateNicknameMutation = useMutation({ + mutationFn: (newNickname: string) => updateUserNickname(newNickname), + onSuccess: async () => { + // 사용자 데이터 다시 불러오기 + await fetchUser(); + // 관련된 쿼리 캐시 무효화 + queryClient.invalidateQueries({ queryKey: ['user'] }); + Toast.success('닉네임 변경 성공'); + }, + onError: (error) => { + const errorObj = error as { response?: { data?: { message?: string } } }; + const message = errorObj?.response?.data?.message || '닉네임을 변경할 수 없습니다.'; + setNicknameError(message); + Toast.error('닉네임 변경에 실패했습니다.'); + }, + }); + + // 비밀번호 검증 mutation + const verifyPasswordMutation = useMutation({ + mutationFn: ({ email, password }: { email: string; password: string }) => + verifyPassword(email, password), + onSuccess: (res) => { + if (res?.accessToken) { + openModal('change-password'); + } + }, + onError: () => { + Toast.error('비밀번호 인증 실패'); + }, + }); useEffect(() => { if (user?.image) { @@ -42,6 +76,16 @@ export default function MyPageClient() { return false; }; + const handleNicknameUpdate = async () => { + if (isSameNickname()) return; + updateNicknameMutation.mutate(nickname); + }; + + const handlePasswordVerification = async () => { + if (!email) return; + verifyPasswordMutation.mutate({ email, password }); + }; + return (
@@ -60,35 +104,15 @@ export default function MyPageClient() { nicknameError={nicknameError} setNickname={setNickname} setNicknameError={setNicknameError} - onClick={async () => { - if (isSameNickname()) return; - try { - await updateUserNickname(nickname); - await fetchUser(); - Toast.success('닉네임 변경 성공'); - } catch (error: unknown) { - const errorObj = error as { response?: { data?: { message?: string } } }; - const message = - errorObj?.response?.data?.message || '닉네임을 변경할 수 없습니다.'; - setNicknameError(message); - Toast.error('닉네임 변경에 실패했습니다.'); - } - }} + onClick={handleNicknameUpdate} + isLoading={updateNicknameMutation.isPending} /> { - try { - const res = await verifyPassword(email!, password); - if (res?.accessToken) { - openModal('change-password'); - } - } catch (e) { - Toast.error('비밀번호 인증 실패'); - } - }} + onClick={handlePasswordVerification} + isLoading={verifyPasswordMutation.isPending} />
} diff --git a/src/app/mypage/_mypage/PasswordField.tsx b/src/app/mypage/_mypage/PasswordField.tsx index d280032f..b993655b 100644 --- a/src/app/mypage/_mypage/PasswordField.tsx +++ b/src/app/mypage/_mypage/PasswordField.tsx @@ -2,20 +2,27 @@ import Button from '@/components/common/Button'; import FormField from '@/components/common/formField'; +import BouncingDots from '@/components/common/loading/BouncingDots'; interface PasswordFieldProps { password: string; setPassword: (value: string) => void; onClick: () => void; + isLoading?: boolean; // 로딩 상태 추가 } -export default function PasswordField({ password, setPassword, onClick }: PasswordFieldProps) { +export default function PasswordField({ + password, + setPassword, + onClick, + isLoading = false, +}: PasswordFieldProps) { return ( setPassword(e.target.value)} onKeyDown={(e) => { @@ -26,8 +33,14 @@ export default function PasswordField({ password, setPassword, onClick }: Passwo }} rightSlot={
-
} From 28123c16651ceb2d231abdef2badc062130d5b09 Mon Sep 17 00:00:00 2001 From: hyejin Date: Thu, 5 Jun 2025 20:23:38 +0900 Subject: [PATCH 32/37] =?UTF-8?q?Fix=20:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=8B=89=EB=84=A4=EC=9E=84/=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/mypage/MypageClient.tsx | 9 +++++---- src/app/mypage/_mypage/NicknameField.tsx | 3 ++- src/app/mypage/_mypage/PasswordField.tsx | 13 +++++++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/mypage/MypageClient.tsx b/src/app/mypage/MypageClient.tsx index 08e498c3..ae3aa102 100644 --- a/src/app/mypage/MypageClient.tsx +++ b/src/app/mypage/MypageClient.tsx @@ -21,16 +21,14 @@ export default function MyPageClient() { const [nickname, setNickname] = useState(user?.nickname ?? ''); const [nicknameError, setNicknameError] = useState(''); const [password, setPassword] = useState(''); + const [passwordError, setPasswordError] = useState(''); const { openModal, closeModal } = useModal(); const queryClient = useQueryClient(); - // 닉네임 업데이트 mutation const updateNicknameMutation = useMutation({ mutationFn: (newNickname: string) => updateUserNickname(newNickname), onSuccess: async () => { - // 사용자 데이터 다시 불러오기 await fetchUser(); - // 관련된 쿼리 캐시 무효화 queryClient.invalidateQueries({ queryKey: ['user'] }); Toast.success('닉네임 변경 성공'); }, @@ -42,16 +40,17 @@ export default function MyPageClient() { }, }); - // 비밀번호 검증 mutation const verifyPasswordMutation = useMutation({ mutationFn: ({ email, password }: { email: string; password: string }) => verifyPassword(email, password), onSuccess: (res) => { if (res?.accessToken) { + setPasswordError(''); openModal('change-password'); } }, onError: () => { + setPasswordError('올바른 비밀번호를 입력해 주세요.'); Toast.error('비밀번호 인증 실패'); }, }); @@ -113,6 +112,8 @@ export default function MyPageClient() { setPassword={setPassword} onClick={handlePasswordVerification} isLoading={verifyPasswordMutation.isPending} + passwordError={passwordError} + setPasswordError={setPasswordError} /> {isSuccess && ( Date: Thu, 5 Jun 2025 21:15:33 +0900 Subject: [PATCH 34/37] =?UTF-8?q?Refactor=20:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EB=90=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signup/_signup/SignupForm.tsx | 120 +++--------------- .../signup/_signup/hooks/useSignup.ts | 26 ++-- 2 files changed, 31 insertions(+), 115 deletions(-) diff --git a/src/app/(form-layout)/signup/_signup/SignupForm.tsx b/src/app/(form-layout)/signup/_signup/SignupForm.tsx index c7b30ced..1924fa1c 100644 --- a/src/app/(form-layout)/signup/_signup/SignupForm.tsx +++ b/src/app/(form-layout)/signup/_signup/SignupForm.tsx @@ -2,13 +2,10 @@ import { useState, ChangeEvent } from 'react'; import { useRouter } from 'next/navigation'; -import { useMutation } from '@tanstack/react-query'; -import axiosClient from '@/lib/axiosClient'; import FormField from '@/components/common/formField'; import Button from '@/components/common/Button'; import PasswordToggleButton from './PasswordToggleButton'; import usePasswordVisibility from '@/utils/use-password-visibility'; -import { useModal } from '@/contexts/ModalContext'; import { validateEmail, validatePassword, @@ -16,51 +13,22 @@ import { validateLengthLimit, } from '@/utils/validators'; import { AUTH_ERROR_MESSAGES } from '@/constants/messages/signup'; -import { Toast } from '@/components/common/Toastify'; -import { setClientCookie } from '@/lib/cookie/client'; -import { useUser } from '@/contexts/UserContext'; import SignupSuccessModal from './SignupSuccessModal'; import BouncingDots from '@/components/common/loading/BouncingDots'; - -interface SignupRequest { - email: string; - password: string; - passwordConfirmation: string; - nickname: string; -} - -interface LoginRequest { - email: string; - password: string; -} - -interface LoginResponse { - accessToken: string; - refreshToken: string; -} - -interface ErrorResponse { - response?: { - data: { - message: string; - }; - }; -} - -const signupUser = async (data: SignupRequest): Promise => { - await axiosClient.post('/auth/signUp', data); -}; - -const loginUser = async (data: LoginRequest): Promise => { - const response = await axiosClient.post('/auth/signIn', data); - return response.data; -}; +import { useSignup } from '@/app/(form-layout)/signup/_signup/hooks/useSignup'; export default function SignupForm() { - const { openModal } = useModal(); const router = useRouter(); const { isPasswordVisible, togglePasswordVisibility } = usePasswordVisibility(); - const { fetchUser } = useUser(); + const { + duplicateError, + isSuccess, + isLoading, + handleSignup, + handleAutoLogin, + cancelAutoLogin, + clearDuplicateError, + } = useSignup(); const [formData, setFormData] = useState({ nickname: '', @@ -69,59 +37,6 @@ export default function SignupForm() { passwordConfirmation: '', }); - const [duplicateError, setDuplicateError] = useState({ - nickname: false, - email: false, - }); - - const [isSuccess, setIsSuccess] = useState(false); - const [loginTimeoutId, setLoginTimeoutId] = useState(null); - - const signupMutation = useMutation({ - mutationFn: signupUser, - onSuccess: () => { - const timeoutId = setTimeout(() => { - loginMutation.mutate({ - email: formData.email, - password: formData.password, - }); - }, 5000); - - setLoginTimeoutId(timeoutId); - openModal('signup-success'); - setIsSuccess(true); - }, - onError: (error: unknown) => { - const err = error as ErrorResponse; - const message = err.response?.data?.message || ''; - - setDuplicateError({ - email: message.includes('이메일'), - nickname: message.includes('닉네임'), - }); - - Toast.error('회원가입 실패'); - }, - }); - - // 자동 로그인 mutation - const loginMutation = useMutation({ - mutationFn: loginUser, - onSuccess: async (data: LoginResponse) => { - const { accessToken, refreshToken } = data; - - setClientCookie('accessToken', accessToken); - setClientCookie('refreshToken', refreshToken); - - await fetchUser(); - router.push('/nogroup'); - }, - onError: () => { - Toast.error('자동 로그인 실패.'); - router.push('/login'); - }, - }); - const setFieldValue = (key: keyof typeof formData, value: string) => { setFormData((prev) => ({ ...prev, @@ -223,12 +138,9 @@ export default function SignupForm() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - signupMutation.mutate({ - email: formData.email, - password: formData.password, - passwordConfirmation: formData.passwordConfirmation, - nickname: formData.nickname, - }); + handleSignup(formData); + + handleAutoLogin(formData.email, formData.password); }; const isFormInvalid = @@ -237,8 +149,6 @@ export default function SignupForm() { !validatePassword(formData.password) || !validateConfirmPassword(formData.password, formData.passwordConfirmation); - const isLoading = signupMutation.isPending || loginMutation.isPending; - return (
@@ -256,7 +166,7 @@ export default function SignupForm() { setFieldValue(key, e.target.value); if (key === 'email' || key === 'nickname') { - setDuplicateError((prev) => ({ ...prev, [key]: false })); + clearDuplicateError(key); } }} placeholder={field.placeholder} @@ -277,7 +187,7 @@ export default function SignupForm() { { - if (loginTimeoutId) clearTimeout(loginTimeoutId); + cancelAutoLogin(); router.push('/login'); }} /> diff --git a/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts b/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts index c3bbcbed..72de01fc 100644 --- a/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts +++ b/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts @@ -7,24 +7,24 @@ import { Toast } from '@/components/common/Toastify'; import { setClientCookie } from '@/lib/cookie/client'; import { useUser } from '@/contexts/UserContext'; -interface SignupRequest { +export interface SignupRequest { email: string; password: string; passwordConfirmation: string; nickname: string; } -interface LoginRequest { +export interface LoginRequest { email: string; password: string; } -interface LoginResponse { +export interface LoginResponse { accessToken: string; refreshToken: string; } -interface ErrorResponse { +export interface ErrorResponse { response?: { data: { message: string; @@ -53,12 +53,20 @@ export const useSignup = () => { const [isSuccess, setIsSuccess] = useState(false); const [loginTimeoutId, setLoginTimeoutId] = useState(null); + const [pendingLogin, setPendingLogin] = useState(null); const signupMutation = useMutation({ mutationFn: signupUser, onSuccess: () => { openModal('signup-success'); setIsSuccess(true); + + if (pendingLogin) { + const timeoutId = setTimeout(() => { + loginMutation.mutate(pendingLogin); + }, 5000); + setLoginTimeoutId(timeoutId); + } }, onError: (error: unknown) => { const err = error as ErrorResponse; @@ -70,6 +78,7 @@ export const useSignup = () => { }); Toast.error('회원가입 실패'); + setPendingLogin(null); }, }); @@ -94,12 +103,8 @@ export const useSignup = () => { signupMutation.mutate(formData); }; - const handleAutoLogin = (email: string, password: string, delay: number = 5000) => { - const timeoutId = setTimeout(() => { - loginMutation.mutate({ email, password }); - }, delay); - - setLoginTimeoutId(timeoutId); + const handleAutoLogin = (email: string, password: string) => { + setPendingLogin({ email, password }); }; const cancelAutoLogin = () => { @@ -107,6 +112,7 @@ export const useSignup = () => { clearTimeout(loginTimeoutId); setLoginTimeoutId(null); } + setPendingLogin(null); }; const clearDuplicateError = (field: 'email' | 'nickname') => { From cb582c091ad20f9d6fd1cf22628184706aa93781 Mon Sep 17 00:00:00 2001 From: hyejin Date: Fri, 6 Jun 2025 00:22:57 +0900 Subject: [PATCH 35/37] =?UTF-8?q?Chore=20:=20placeholder=20=EB=AC=B8?= =?UTF-8?q?=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/mypage/_mypage/PasswordField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/mypage/_mypage/PasswordField.tsx b/src/app/mypage/_mypage/PasswordField.tsx index 1297bcc5..f1b33dfa 100644 --- a/src/app/mypage/_mypage/PasswordField.tsx +++ b/src/app/mypage/_mypage/PasswordField.tsx @@ -26,7 +26,7 @@ export default function PasswordField({ field="input" type="password" label="비밀번호" - placeholder="기존 비밀번호 입력" + placeholder="비밀번호를 입력해 주세요." value={password} onChange={(e) => { setPassword(e.target.value); From 717ed2240976987e5ffdea90201b869b211765c7 Mon Sep 17 00:00:00 2001 From: hyejin Date: Fri, 6 Jun 2025 00:38:58 +0900 Subject: [PATCH 36/37] =?UTF-8?q?Fix=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=9E=90=EB=8F=99=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=EB=B0=8D=20=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EB=B0=A9=EC=A7=80=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signup/_signup/hooks/useSignup.ts | 67 ++++++++++++++----- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts b/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts index 72de01fc..6078bfed 100644 --- a/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts +++ b/src/app/(form-layout)/signup/_signup/hooks/useSignup.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useMutation } from '@tanstack/react-query'; import axiosClient from '@/lib/axiosClient'; @@ -52,20 +52,24 @@ export const useSignup = () => { }); const [isSuccess, setIsSuccess] = useState(false); - const [loginTimeoutId, setLoginTimeoutId] = useState(null); const [pendingLogin, setPendingLogin] = useState(null); + const loginTimeoutRef = useRef(null); + const autoLoginCancelledRef = useRef(false); + const signupMutation = useMutation({ mutationFn: signupUser, onSuccess: () => { openModal('signup-success'); setIsSuccess(true); + autoLoginCancelledRef.current = false; if (pendingLogin) { - const timeoutId = setTimeout(() => { - loginMutation.mutate(pendingLogin); + loginTimeoutRef.current = setTimeout(() => { + if (!autoLoginCancelledRef.current) { + loginMutation.mutate(pendingLogin); + } }, 5000); - setLoginTimeoutId(timeoutId); } }, onError: (error: unknown) => { @@ -79,6 +83,7 @@ export const useSignup = () => { Toast.error('회원가입 실패'); setPendingLogin(null); + autoLoginCancelledRef.current = true; }, }); @@ -99,25 +104,49 @@ export const useSignup = () => { }, }); - const handleSignup = (formData: SignupRequest) => { - signupMutation.mutate(formData); - }; + const handleSignup = useCallback( + (formData: SignupRequest) => { + if (signupMutation.isPending || loginMutation.isPending) { + return; + } + + if (loginTimeoutRef.current) { + clearTimeout(loginTimeoutRef.current); + loginTimeoutRef.current = null; + } + + signupMutation.mutate(formData); + }, + [signupMutation, loginMutation] + ); - const handleAutoLogin = (email: string, password: string) => { + const handleAutoLogin = useCallback((email: string, password: string) => { setPendingLogin({ email, password }); - }; + }, []); - const cancelAutoLogin = () => { - if (loginTimeoutId) { - clearTimeout(loginTimeoutId); - setLoginTimeoutId(null); + const cancelAutoLogin = useCallback(() => { + if (loginTimeoutRef.current) { + clearTimeout(loginTimeoutRef.current); + loginTimeoutRef.current = null; } + + autoLoginCancelledRef.current = true; setPendingLogin(null); - }; + }, []); - const clearDuplicateError = (field: 'email' | 'nickname') => { + const clearDuplicateError = useCallback((field: 'email' | 'nickname') => { setDuplicateError((prev) => ({ ...prev, [field]: false })); - }; + }, []); + + const cleanup = useCallback(() => { + if (loginTimeoutRef.current) { + clearTimeout(loginTimeoutRef.current); + loginTimeoutRef.current = null; + } + autoLoginCancelledRef.current = true; + }, []); + + const isFormDisabled = signupMutation.isPending || loginMutation.isPending; const isLoading = signupMutation.isPending || loginMutation.isPending; @@ -125,6 +154,7 @@ export const useSignup = () => { duplicateError, isSuccess, isLoading, + isFormDisabled, signupMutation, loginMutation, @@ -133,5 +163,8 @@ export const useSignup = () => { handleAutoLogin, cancelAutoLogin, clearDuplicateError, + cleanup, + + isPendingAutoLogin: !!pendingLogin && !autoLoginCancelledRef.current, }; }; From d98709f7eae9494ad5774b50b1360fb3193d5b88 Mon Sep 17 00:00:00 2001 From: hyejin Date: Mon, 9 Jun 2025 07:44:53 +0900 Subject: [PATCH 37/37] =?UTF-8?q?Chore=20:=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/mypage/MypageClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/mypage/MypageClient.tsx b/src/app/mypage/MypageClient.tsx index ae3aa102..d97fb150 100644 --- a/src/app/mypage/MypageClient.tsx +++ b/src/app/mypage/MypageClient.tsx @@ -36,7 +36,7 @@ export default function MyPageClient() { const errorObj = error as { response?: { data?: { message?: string } } }; const message = errorObj?.response?.data?.message || '닉네임을 변경할 수 없습니다.'; setNicknameError(message); - Toast.error('닉네임 변경에 실패했습니다.'); + Toast.error('닉네임 변경 실패'); }, });