diff --git a/cypress/e2e/spec.cy.ts b/cypress/e2e/spec.cy.ts index 1c87fa6..0c05a47 100644 --- a/cypress/e2e/spec.cy.ts +++ b/cypress/e2e/spec.cy.ts @@ -1,40 +1,40 @@ -// describe("로그인 페이지 테스트", () => { -// it("로그인 성공.", () => { -// cy.intercept("/dashboard*").as("dashboard"); - -// cy.visit("http://localhost:3000/login"); - -// cy.get('input[placeholder="이메일"]').type("qq@naver.com"); -// cy.get('input[placeholder="비밀번호"]').type("012345678"); -// cy.contains("로그인").click(); - -// cy.wait("@dashboard").then((_) => { -// cy.contains("대시보드"); -// }); -// }); -// it.only("로그인 실패.", () => { -// cy.visit("http://localhost:3000/login"); - -// cy.get('input[placeholder="이메일"]').type("wrong@naver.com"); -// cy.get('input[placeholder="비밀번호"]').type("wrongnumber"); -// cy.contains("로그인").click(); - -// cy.wait("@dashboard").then(() => { -// cy.contains("대시보드"); -// }); -// }); - -// it("로그인 실패.", () => { -// cy.intercept("/login").as("login"); - -// cy.visit("http://localhost:3000/login"); - -// cy.get('input[placeholder="이메일"]').type("wrong@naver.com"); -// cy.get('input[placeholder="비밀번호"]').type("wrongnumber"); -// cy.contains("로그인").click(); - -// cy.wait("@login").then(() => { -// cy.contains("로그인에 실패했습니다. 다시 시도해 주세요.").should("be.visible"); -// }); -// }); -// }); +describe("로그인 페이지 테스트", () => { + it("로그인 성공.", () => { + cy.intercept("/dashboard*").as("dashboard"); + + cy.visit("http://localhost:3000/login"); + + cy.get('input[placeholder="이메일"]').type("qq@naver.com"); + cy.get('input[placeholder="비밀번호"]').type("012345678"); + cy.contains("로그인").click(); + + cy.wait("@dashboard").then((_) => { + cy.contains("대시보드"); + }); + }); + // it.only("로그인 실패.", () => { + // cy.visit("http://localhost:3000/login"); + + // cy.get('input[placeholder="이메일"]').type("wrong@naver.com"); + // cy.get('input[placeholder="비밀번호"]').type("wrongnumber"); + // cy.contains("로그인").click(); + + // cy.wait("@dashboard").then(() => { + // cy.contains("대시보드"); + // }); + // }); + + // it("로그인 실패.", () => { + // cy.intercept("/login").as("login"); + + // cy.visit("http://localhost:3000/login"); + + // cy.get('input[placeholder="이메일"]').type("wrong@naver.com"); + // cy.get('input[placeholder="비밀번호"]').type("wrongnumber"); + // cy.contains("로그인").click(); + + // cy.wait("@login").then(() => { + // cy.contains("로그인에 실패했습니다. 다시 시도해 주세요.").should("be.visible"); + // }); + // }); +}); diff --git a/public/note-header.webp b/public/note-header.webp new file mode 100644 index 0000000..e782379 Binary files /dev/null and b/public/note-header.webp differ diff --git a/public/note-kebab.webp b/public/note-kebab.webp new file mode 100644 index 0000000..5b6f29d Binary files /dev/null and b/public/note-kebab.webp differ diff --git a/src/api/goalAPI.ts b/src/api/goalAPI.ts index aa038d9..14e334c 100644 --- a/src/api/goalAPI.ts +++ b/src/api/goalAPI.ts @@ -18,6 +18,7 @@ export interface ErrorType { export const getGoals = async () => { try { const response = await api.get(`/goals`); + // console.log(response); return response.data; } catch (error) { console.error("Goals fetch error:", error); @@ -43,6 +44,7 @@ export const PostGoal = async (title: string) => { export const getGoal = async (id: number) => { try { const response = await api.get(`/goals/${id}`); + //console.log(response.data); return response.data; } catch (error) { console.error("Goal fetch error:", error); diff --git a/src/api/noteAPI.ts b/src/api/noteAPI.ts index 2b46f22..27bf8eb 100644 --- a/src/api/noteAPI.ts +++ b/src/api/noteAPI.ts @@ -1,21 +1,31 @@ import { AxiosError } from "axios"; -import { toast } from "react-toastify"; import api from "@/lib/api"; import { ErrorType } from "./goalAPI"; -// 노트 목록 가져오기 (GET) -export const getNotes = async (id: number) => { +// 노트 리스트 가져오기 (GET) +export const getNotes = async (goalId: number) => { try { - const response = await api.get(`/notes/${id}`); + const response = await api.get(`/notes/${goalId}`); return response.data; } catch (error) { const axiosError = error as AxiosError; - toast.error(axiosError.message); + console.error(axiosError.message); } }; +// 단일 노트 조회 +export async function getNote(noteId: number) { + try { + const response = await api.get(`/notes/${noteId}`); + return response.data; + } catch (error) { + const axiosError = error as AxiosError; + console.error(axiosError.message); + } +} + export async function postNotes(todoId: number, title: string, content: string, linkUrl?: string | null) { try { const payload: { @@ -37,7 +47,7 @@ export async function postNotes(todoId: number, title: string, content: string, return response.data; } catch (error) { const axiosError = error as AxiosError; - toast.error(axiosError.message); + console.error(axiosError.message); } } @@ -64,6 +74,18 @@ export async function patchNotes(noteId: number, title: string, content: string, return response.data; } catch (error) { const axiosError = error as AxiosError; - toast.error(axiosError.message); + console.error(axiosError.message); + } +} + +export default async function deleteNote(noteId: number) { + try { + const response = await api.delete(`notes/${noteId}`); + console.log(response); + console.log(response.data); + return response.data; + } catch (e) { + const error = e as ErrorType; + console.log(error); } } diff --git a/src/api/todoAPI.ts b/src/api/todoAPI.ts index fc632ce..fc455c2 100644 --- a/src/api/todoAPI.ts +++ b/src/api/todoAPI.ts @@ -6,7 +6,7 @@ import api from "@/lib/api"; import { ErrorType } from "./goalAPI"; export type TodoType = { - noteId?: number | null; + noteId: number | null; done: boolean; linkUrl?: string | null; fileUrl?: string | null; @@ -31,7 +31,6 @@ export type GoalType = { export const getAllTodos = async () => { try { const response = await api.get(`/todos`); - return response.data; } catch (error) { const axiosError = error as AxiosError; @@ -40,9 +39,12 @@ export const getAllTodos = async () => { } }; -export const getTodos = async (id: number) => { +export const getTodos = async (id: number, done?: boolean, size?: number) => { try { - const response = await api.get(`/todos?goalId=${id}`); + const response = await api.get( + `/todos?goalId=${id}&${done ? `done=${done}&` : done === false ? "done=false&" : ""}${size ? `size=${size}` : "size=27"}`, + ); + //console.log(response.data); return response.data; } catch (error) { const axiosError = error as AxiosError; @@ -58,7 +60,7 @@ export const postFile = async (file: File) => { const response = await api.post(`/files`, formData, { headers: { "Content-Type": "multipart/form-data" }, }); - + console.log(response.data); return response.data; } catch (error) { const axiosError = error as AxiosError; @@ -67,18 +69,22 @@ export const postFile = async (file: File) => { }; export const createTodo = async ( - title: string, - fileUrl: string | null, - linkUrl: string | null, - goalId: number, + title?: string, + goalId?: number, + fileUrl?: string | null, + linkUrl?: string | null, ): Promise => { try { const payload = { title, goalId, fileUrl, linkUrl }; + console.log(title, goalId); + console.log("😲"); const response = await api.post(`/todos`, payload); + console.log(response); return response.data; } catch (error) { + console.log(error); const axiosError = error as AxiosError; - toast.error(axiosError.message); + toast.error(axiosError.response?.data.message); throw error; } }; @@ -86,6 +92,7 @@ export const createTodo = async ( export const updateTodo = async (todoId: number, updates: Partial): Promise => { try { const response = await api.patch(`/todos/${todoId}`, updates); + console.log(); return response.data; } catch (error) { const axiosError = error as AxiosError; @@ -103,3 +110,15 @@ export const deleteTodo = async (id: number): Promise => { throw error; } }; + +// 할 일 상태 토글 함수 +export const toggleTodo = async (todoId: number, updatedTodo: Partial) => { + try { + const response = await api.patch(`/todos/${todoId}`, updatedTodo); + return response.data; + } catch (error) { + const axiosError = error as AxiosError; + toast.error(axiosError.message); + throw error; + } +}; diff --git a/src/app/Types/TodoGoalType.ts b/src/app/Types/TodoGoalType.ts new file mode 100644 index 0000000..6d5ca6c --- /dev/null +++ b/src/app/Types/TodoGoalType.ts @@ -0,0 +1,51 @@ +export type InitialTodoType = { + title?: string; + fileUrl?: string | null; + linkUrl?: string | null; + goalId?: number; +}; + +export type TodoType = { + noteId?: number | null; + done: boolean; + linkUrl?: string | null; + fileUrl?: string | null; + title: string; + id: number; + goal: GoalType; + userId: number; + teamId: string; + updatedAt: string; + createdAt: string; +}; + +export type GoalType = { + id: number; + teamId: string; + title: string; + userId: number; + createdAt: string; + updatedAt: string; +}; + +export type NoteType = { + content: string; + createdAt: string; + goal: { + id: number; + title: string; + }; + id: number; + linkUrl: string; + teamId: string; + title: string; + todo: { + done: boolean; + fileUrl: string | null; + id: number; + linkUrl: string | null; + title: string; + }; + updatedAt: string; + userId: number; +}; diff --git a/src/app/dashboard/components/NoteItem.tsx b/src/app/dashboard/components/NoteItem.tsx new file mode 100644 index 0000000..e6166a3 --- /dev/null +++ b/src/app/dashboard/components/NoteItem.tsx @@ -0,0 +1,80 @@ +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { toast } from "react-toastify"; + +import { NoteType } from "@/app/Types/TodoGoalType"; +import deleteNote, { getNote } from "@/api/noteAPI"; + +import NoteViewer from "./NoteViewer"; + +export default function NoteItem({ note }: { note: NoteType }) { + const router = useRouter(); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isNoteOpen, setIsNoteOpen] = useState(false); + const [noteContent, setNoteContent] = useState(); + + const loadNoteItemData = async () => { + const response = await getNote(note.id); + if (response) { + console.log(isNoteOpen); + console.log(noteContent); + setNoteContent(response.data); + } + }; + + const handleDelete = async () => { + const response = await deleteNote(note.id); + if (response) { + toast.success("삭제되었습니다."); + } + }; + + useEffect(() => { + loadNoteItemData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+ note-header-icon + note-header-icon setIsDropdownOpen((prev) => !prev)} + /> +
+

setIsNoteOpen((prev) => !prev)}> + {note.title} +

+
+
+ + {note.todo.done ? "Done" : "To do"} + +

{note.todo.title}

+
+ {isDropdownOpen && ( +
setIsDropdownOpen(false)} + > +

router.push(`/dashboard/note/${note.todo.id}?goalId=${note.goal.id}`)} + > + 수정하기 +

+

+ 삭제하기 +

+
+ )} + +
+ ); +} diff --git a/src/app/dashboard/components/NoteViewer.tsx b/src/app/dashboard/components/NoteViewer.tsx index 23f73a4..33e9ad9 100644 --- a/src/app/dashboard/components/NoteViewer.tsx +++ b/src/app/dashboard/components/NoteViewer.tsx @@ -1,27 +1,7 @@ import Image from "next/image"; import { Dispatch, SetStateAction } from "react"; -export interface NoteType { - content: string; - createdAt: string; - goal: { - id: number; - title: string; - }; - id: number; - linkUrl: string; - teamId: string; - title: string; - todo: { - done: boolean; - fileUrl: string | null; - id: number; - linkUrl: string | null; - title: string; - }; - updatedAt: string; - userId: number; -} +import { NoteType } from "@/app/Types/TodoGoalType"; type NoteViewerProps = { isNoteOpen: boolean; @@ -41,6 +21,8 @@ export default function NoteViewer({ isNoteOpen, setIsNoteOpen, noteContent }: N window.open(url, "_blank", `width=${windowWidth},height=${windowHeight},left=${windowLeft},top=${windowTop}`); }; + console.log(noteContent); + return (
{isNoteOpen && ( diff --git a/src/app/dashboard/components/ProgressBar.tsx b/src/app/dashboard/components/ProgressBar.tsx index da58299..80ab620 100644 --- a/src/app/dashboard/components/ProgressBar.tsx +++ b/src/app/dashboard/components/ProgressBar.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from "react"; -export default function ProgressBar({ progress }: { progress?: number }) { +export default function ProgressBar({ progressPercentage }: { progressPercentage?: number }) { const [animatedProgress, setAnimatedProgress] = useState(0); useEffect(() => { - setAnimatedProgress(progress || 0); - }, [progress]); + setAnimatedProgress(progressPercentage || 0); + }, [progressPercentage]); return (
@@ -18,7 +18,7 @@ export default function ProgressBar({ progress }: { progress?: number }) { className="absolute h-[4px] rounded-[6px] bg-[#000]" >
- {progress ? Math.round(progress) : 0}% + {progressPercentage ? Math.round(progressPercentage) : 0}% ); } diff --git a/src/app/dashboard/components/ProgressTracker.tsx b/src/app/dashboard/components/ProgressTracker.tsx index 1913d94..3ed3e1c 100644 --- a/src/app/dashboard/components/ProgressTracker.tsx +++ b/src/app/dashboard/components/ProgressTracker.tsx @@ -4,19 +4,23 @@ import Image from "next/image"; import CircleProcess, { setProgress } from "./CircleProgress"; type ProgressTrackerType = { - ratio: number; - progressValue: number; - setProgressValue: React.Dispatch>; + completionRatio: number; + progressPercentage: number; + setProgressPercentage: React.Dispatch>; }; -export default function ProgressTracker({ ratio, progressValue, setProgressValue }: ProgressTrackerType) { +export default function ProgressTracker({ + completionRatio, + progressPercentage, + setProgressPercentage, +}: ProgressTrackerType) { useEffect(() => { // 숫자 애니메이션 설정 const interval = setInterval(() => { - setProgressValue((prev) => { - if (prev < ratio) { + setProgressPercentage((prev) => { + if (prev < completionRatio) { return prev + 1; // 비율이 증가할 때 - } else if (prev > ratio) { + } else if (prev > completionRatio) { return prev - 1; // 비율이 감소할 때 } else { clearInterval(interval); // 비율이 같아지면 인터벌 종료 @@ -26,16 +30,16 @@ export default function ProgressTracker({ ratio, progressValue, setProgressValue }, 10); // 숫자가 증가하는 속도 조절 return () => clearInterval(interval); // 컴포넌트 언마운트 시 인터벌 클리어 - }, [ratio, setProgressValue]); + }, [completionRatio, setProgressPercentage]); - setProgress(ratio ? 100 - ratio : 100); + setProgress(completionRatio ? 100 - completionRatio : 100); return ( -
+
pregress-task-icon

내 진행 상황

- {progressValue}% + {progressPercentage}%

diff --git a/src/app/dashboard/components/Sidebar.tsx b/src/app/dashboard/components/Sidebar.tsx index 77e8250..2bfd66b 100644 --- a/src/app/dashboard/components/Sidebar.tsx +++ b/src/app/dashboard/components/Sidebar.tsx @@ -8,20 +8,12 @@ import Link from "next/link"; import { useQueryClient } from "@tanstack/react-query"; import { logout } from "@/utils/authUtils"; -import CreateNewTodo from "@/components/CreateNewTodo"; import useModal from "@/hook/useModal"; import AnimatedText from "@/utils/AnimatedText"; import { useGoalStore } from "@/store/goalStore"; import { useAuthStore } from "@/store/authStore"; - -export interface GoalType { - id: number; - teamId: string; - userId: number; - title: string; - createdAt: string; - updatedAt: string; -} +import CreateNewTodo from "@/components/CreateNewTodo"; +import { GoalType } from "@/app/Types/TodoGoalType"; interface GoalsPage { goals: GoalType[]; @@ -65,17 +57,25 @@ export default function SideBar() { // 무한 스크롤 데이터를 업데이트 queryClient.setQueryData(["goals"], (oldData) => { - if (oldData) { - return { - ...oldData, - pages: oldData.pages.map((page) => ({ + if (!oldData) return { pages: [{ goals: [newGoal], nextCursor: null }], pageParams: [undefined] }; + + const newPages = oldData.pages.map((page, index) => { + if (index === 0) { + return { ...page, - goals: [newGoal, ...page.goals] as GoalType[], // 명시적으로 GoalType 배열로 캐스팅 - })), - }; - } - return { pages: [{ goals: [newGoal] }] }; // 초기 데이터 설정 + goals: [newGoal, ...page.goals], + }; + } + return page; + }); + + return { + ...oldData, + pages: newPages, + }; }); + + queryClient.invalidateQueries({ queryKey: ["goals"] }); } else { toast.warn("목표를 입력해 주세요."); setInputVisible(false); diff --git a/src/app/dashboard/components/TodoCard.tsx b/src/app/dashboard/components/TodoCard.tsx index 3c2dac2..488b429 100644 --- a/src/app/dashboard/components/TodoCard.tsx +++ b/src/app/dashboard/components/TodoCard.tsx @@ -3,90 +3,49 @@ import Image from "next/image"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { useQuery } from "@tanstack/react-query"; import { getGoal } from "@/api/goalAPI"; -import { getTodos } from "@/api/todoAPI"; -import CreateNewTodo from "@/components/CreateNewTodo"; import useModal from "@/hook/useModal"; +import CreateNewTodo from "@/components/CreateNewTodo"; +import { GoalType, TodoType } from "@/app/Types/TodoGoalType"; +import { getTodos } from "@/api/todoAPI"; -import Todos from "./Todos"; import ProgressBar from "./ProgressBar"; +import TodoItem from "./TodoItem"; export type TodoCardProps = { id: number; }; -export type GoalType = { - updatedAt: string; - createdAt: string; - title: string; - id: number; - userId: number; - teamId: string; -}; - -export type TodoType = { - noteId: number | null; - done: boolean; - linkUrl: string | null; - fileUrl: string | null; - title: string; - id: number; - goal: GoalType; - userId: number; - teamId: string; - updatedAt: string; - createdAt: string; -}; - export default function TodoCard({ id }: TodoCardProps) { const router = useRouter(); - const { Modal, openModal, closeModal } = useModal(); - const [progress, setProgress] = useState(0); const [goal, setGoal] = useState(null); + const [selectedGoal, setSelectedGoal] = useState(); + const [progressPercentage, setProgressPercentage] = useState(0); + const [hasMoreThanFiveTodos, setHasMoreThanFiveTodos] = useState(true); + const [completedTodos, setCompletedTodos] = useState([]); + const [activeTodos, setAcitveTodos] = useState([]); + const { Modal, openModal, closeModal } = useModal(); - // 목표 데이터 가져오기 - const { data: goalData, error: goalError } = useQuery({ - queryKey: ["goal", id], // 쿼리 키 - queryFn: () => getGoal(id), // 쿼리 함수 - }); - - // 할 일 목록 가져오기 - const { data: todosData, error: todosError } = useQuery({ - queryKey: ["todos", goalData?.id], // 쿼리 키 - queryFn: () => getTodos(goalData.id), // 쿼리 함수 - enabled: !!goalData?.id, // 목표가 존재할 때만 쿼리 실행 - }); - - // 목표 데이터가 변경될 때마다 setGoal 호출 - useEffect(() => { - if (goalData) { - setGoal(goalData); - } - }, [goalData]); - - console.log(todosData); // todosData 로그 출력 + const loadTodoCardData = async () => { + const goalResponse = await getGoal(id); + const activeResponse = await getTodos(id, false, 5); + const completedResponse = await getTodos(id, true, 5); - // todosData에서 todos 배열 가져오기 - const activeTodos = Array.isArray(todosData?.todos) ? todosData.todos.filter((todo: TodoType) => !todo.done) : []; - const completedTodos = Array.isArray(todosData?.todos) ? todosData.todos.filter((todo: TodoType) => todo.done) : []; - const showMore = activeTodos.length > 5 || completedTodos.length > 5; + setGoal(goalResponse); + setSelectedGoal(goalResponse); + setAcitveTodos(activeResponse.todos); + setCompletedTodos(completedResponse.todos); - // const handleTodoUpdate = async (updatedTodo: TodoType) => { - // onTodoUpdate(updatedTodo); - // setTodos((prevTodos) => prevTodos.map((todo) => (todo.id === updatedTodo.id ? { ...todo, ...updatedTodo } : todo))); - // }; + const totalTaskCount = activeResponse.totalCount + completedResponse.totalCount; + setProgressPercentage(Math.round((completedResponse.totalCount / totalTaskCount) * 100)); + setHasMoreThanFiveTodos(5 < activeResponse.totalCount || 5 < completedResponse.totalCount); + }; useEffect(() => { - // completedTodos.length가 0일 경우를 고려하여 0으로 나누는 것을 방지 - const totalTodos = todosData?.todos.length || 1; // todosData가 없거나 todos 배열이 없을 때 1로 설정 - setProgress(Math.round((completedTodos.length / totalTodos) * 100)); - }, [completedTodos.length, todosData?.todos.length]); // todosData의 todos 길이로 의존성 추가 - - if (goalError || todosError) { - return
데이터를 가져오는 중 오류가 발생했습니다.
; - } + loadTodoCardData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return (
@@ -103,39 +62,33 @@ export default function TodoCard({ id }: TodoCardProps) {
- {" "} + {" "}

To do

    - {activeTodos.slice(0, 5).map((todo: TodoType) => ( - - ))} + {activeTodos.length > 0 ? ( + activeTodos.map((todo) => ) + ) : ( +
  • 아직 해야할 일이 없어요
  • + )}
- {activeTodos.length === 0 && ( -
-
해야할 일이 아직 없어요.
-
- )}

Done

    - {completedTodos.slice(0, 5).map((todo: TodoType) => ( - - ))} + {completedTodos.length > 0 ? ( + completedTodos.map((todo) => ) + ) : ( +
  • 아직 다 한 일이 없어요
  • + )}
- {completedTodos.length === 0 && ( -
-
다 한 일이 아직 없어요.
-
- )}
- {showMore && ( + {hasMoreThanFiveTodos && (
); } diff --git a/src/app/dashboard/components/Todos.tsx b/src/app/dashboard/components/TodoItem.tsx similarity index 58% rename from src/app/dashboard/components/Todos.tsx rename to src/app/dashboard/components/TodoItem.tsx index d94fba5..c47cb23 100644 --- a/src/app/dashboard/components/Todos.tsx +++ b/src/app/dashboard/components/TodoItem.tsx @@ -3,119 +3,59 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; // useQueryClient 추가 import useModal from "@/hook/useModal"; import CreateNewTodo from "@/components/CreateNewTodo"; +import { NoteType, TodoType } from "@/app/Types/TodoGoalType"; +import { deleteTodo, toggleTodo } from "@/api/todoAPI"; import { getNotes } from "@/api/noteAPI"; -import { TodoType } from "@/store/todoStore"; -import { deleteTodo, updateTodo } from "@/api/todoAPI"; import NoteViewer from "./NoteViewer"; -export type GoalType = { - updatedAt: string; - createdAt: string; - title: string; - id: number; - userId: number; - teamId: string; -}; - type TodoProps = { todo: TodoType; - isGoal?: boolean; - isInGoalSection?: boolean; + isTodoCardRelated?: boolean; + inTodoCard?: boolean; }; -// type toggleTodoStatusType = { -// title: string; -// goalId: number; -// fileUrl: string; -// linkUrl: string; -// done: boolean; -// todoId: number; -// }; - -export interface NoteType { - content: string; - createdAt: string; - goal: { - id: number; - title: string; - }; - id: number; - linkUrl: string; - teamId: string; - title: string; - todo: { - done: boolean; - fileUrl: string | null; - id: number; - linkUrl: string | null; - title: string; - }; - updatedAt: string; - userId: number; -} - -export default function Todos({ todo, isGoal = false, isInGoalSection = false }: TodoProps) { +export default function TodoItem({ todo, isTodoCardRelated = true, inTodoCard }: TodoProps) { const router = useRouter(); - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); const { Modal, openModal, closeModal } = useModal(); + const dropdownRef = useRef(null); + const [isDropdownOpen, setDropdownOpen] = useState(false); const [isNoteOpen, setIsNoteOpen] = useState(false); const [noteContent, setNoteContent] = useState(); - const queryClient = useQueryClient(); - - const updateTodoMutation = useMutation({ - mutationFn: (updatedTodo: TodoType) => updateTodo(todo.id, updatedTodo), - onSuccess: () => { - // 할 일 목록 쿼리 무효화 - queryClient.invalidateQueries({ queryKey: ["todos", todo.goal.id] }); - }, - }); - - const toggleTodoStatus = async () => { - try { - await updateTodoMutation.mutateAsync({ ...todo, done: !todo.done }); - } catch (error) { - console.error("할 일 상태 변경 중 오류 발생:", error); - } - }; - - // const toggleTodoStatus = async (updatedTodo: TodoType) => { - // try { - // updateTodo(todo.id, updatedTodo); - // } catch (error) { - // console.error("할 일 상태 변경 중 오류 발생:", error); - // } - // }; - - const fetchNoteContent = async () => { + const loadNoteContent = async () => { if (todo.noteId) { const response = await getNotes(todo.noteId); if (response) { + console.log("🖐️🖐️🖐️🖐️🖐️🖐️🖐️🖐️"); + console.log(response); setNoteContent(response); } } }; - // const handleTodoUpdate = (updatedTodo: TodoType) => { - // setTodos((prevTodos) => - // prevTodos.map((prevTodo) => (prevTodo.id === updatedTodo.id ? { ...prevTodo, ...updatedTodo } : prevTodo)), - // ); - // }; - const handleDelete = async () => { - await deleteTodo(todo.id); // Use deleteTodo from store + await deleteTodo(todo.id); + // TODO: 삭제 후 새로고침하도록 구현 + }; + + const toggleTodoStatus = async (todo: TodoType) => { + try { + const updatedTodo = { done: !todo.done }; + await toggleTodo(todo.id, updatedTodo); + // TODO: 토글됐다면 할 일 목록 업데이트 + } catch (error) { + console.error("할 일 상태 변경에 실패했습니다.", error); + } }; useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (isOpen && dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); + if (isDropdownOpen && dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setDropdownOpen(false); } }; @@ -124,16 +64,18 @@ export default function Todos({ todo, isGoal = false, isInGoalSection = false }: return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, [isOpen]); + }, [isDropdownOpen]); useEffect(() => { - fetchNoteContent(); - // eslint-disable-next-line react-hooks/exhaustive-deps + loadNoteContent(); }, []); return (
-
    +
    • checkbox-icon toggleTodoStatus} + onClick={() => { + toggleTodoStatus(todo); + }} /> {todo.title}
    • - {isGoal && ( + {isTodoCardRelated && (
      goal-summit-icon

      {todo.goal?.title}

      @@ -154,8 +98,8 @@ export default function Todos({ todo, isGoal = false, isInGoalSection = false }: )}
      {todo.fileUrl && ( @@ -210,11 +154,11 @@ export default function Todos({ todo, isGoal = false, isInGoalSection = false }: alt="수정/삭제" title="수정 / 삭제" onClick={() => { - setIsOpen((prev) => !prev); + setDropdownOpen((prev) => !prev); }} /> - {isOpen && ( + {isDropdownOpen && (
      { openModal("EDIT_TODO"); - if (isOpen) setIsOpen(false); + if (isDropdownOpen) setDropdownOpen(false); }} > 수정하기

      -

      +

      삭제하기

      @@ -236,15 +180,7 @@ export default function Todos({ todo, isGoal = false, isInGoalSection = false }:
    - +
diff --git a/src/app/dashboard/goal/[goalId]/page.tsx b/src/app/dashboard/goal/[goalId]/page.tsx index 6e78577..061124e 100644 --- a/src/app/dashboard/goal/[goalId]/page.tsx +++ b/src/app/dashboard/goal/[goalId]/page.tsx @@ -4,109 +4,82 @@ import Image from "next/image"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { AxiosError } from "axios"; -import { useQuery } from "@tanstack/react-query"; -import { useGoalStore } from "@/store/goalStore"; -import { deleteGoal, ErrorType } from "@/api/goalAPI"; +import { deleteGoal, ErrorType, getGoal } from "@/api/goalAPI"; import { getTodos } from "@/api/todoAPI"; import useModal from "@/hook/useModal"; import CreateNewTodo from "@/components/CreateNewTodo"; import EditGoalTitleModal from "@/components/EditGoalTitleModal"; -import LoadingScreen from "@/components/LoadingScreen"; -import useTodoStore, { TodoType } from "@/store/todoStore"; +import { GoalType, TodoType } from "@/app/Types/TodoGoalType"; -import Todos from "../../components/Todos"; import ProgressBar from "../../components/ProgressBar"; +import TodoItem from "../../components/TodoItem"; type GoalPageProps = { params: { goalId: string }; }; -export type GoalType = { - id: number; - teamId: string; - title: string; - userId: number; - createdAt: string; - updatedAt: string; -}; - export default function GoalPage({ params }: GoalPageProps) { const { goalId } = params; const router = useRouter(); const { Modal, openModal, closeModal } = useModal(); - const { goals, refreshGoals } = useGoalStore(); - const { todos, setTodos } = useTodoStore(); + const [goal, setGoal] = useState(null); - const [progress, setProgress] = useState(0); + const [todos, setTodos] = useState([]); + const [progressPercentage, setProgressPercentage] = useState(0); const [isOpen, setIsOpen] = useState(false); + const [selectedTodo, setSelectedTodo] = useState(undefined); const dropdownRef = useRef(null); - // 목표 찾기 - useEffect(() => { - setGoal(goals.find((goal) => goal.id === Number(goalId)) || null); - }, [goals, goalId]); - // 할 일 데이터 가져오기 - const fetchTodos = async (goalId: number) => { - const todoResponse = await getTodos(goalId); - return Array.isArray(todoResponse?.todos) ? todoResponse.todos : []; - }; - - // React Query를 사용하여 할 일 데이터를 가져옴 - const { data: todosData, isLoading: isTodosLoading } = useQuery({ - queryKey: ["todos", goalId], - queryFn: () => fetchTodos(goal ? goal.id : 0), // goal이 있을 때만 호출 - enabled: !!goal, // goal이 있을 때만 쿼리 실행 - }); + const loadGoalData = async () => { + const goalResponse = await getGoal(Number(goalId)); + const todoResponse = await getTodos(Number(goalId)); - // 할 일 업데이트 및 진행률 계산 - useEffect(() => { - if (todosData) { - setTodos(todosData); - updateProgress(todosData); - } - }, [setTodos, todosData]); + setGoal(goalResponse); + setSelectedTodo(todoResponse); + setTodos(todoResponse.todos); - const updateProgress = (todosArray: TodoType[]) => { - if (todosArray.length > 0) { - const completedTodos = todosArray.filter((todo: TodoType) => todo.done === true).length; - const ratio = (completedTodos / todosArray.length) * 100; - setProgress(ratio); - } else { - setProgress(0); - } + const completedTodos = todoResponse.todos.filter((todo: TodoType) => todo.done === true).length; + const completionRatio = (completedTodos / todoResponse.todos.length) * 100; + setProgressPercentage(completionRatio); }; // 목표 삭제 처리 const handleDelete = async () => { - if (goal) { - try { - const response = await deleteGoal(goal.id); - if (response) { - toast.success("목표가 삭제되었습니다."); - await refreshGoals(); // 사이드바의 목표 상태를 업데이트 - router.push("/"); - } - } catch (error) { - const axiosError = error as AxiosError; - toast.error(axiosError.message); + try { + const response = await deleteGoal(goal?.id as number); + if (response) { + toast.success("목표가 삭제되었습니다."); + // await refreshGoals(); // 사이드바의 목표 상태를 업데이트 + router.push("/"); } + } catch (error) { + const axiosError = error as AxiosError; + toast.error(axiosError.message); } }; // 목표 제목 수정 모달 열기 - const handleEditGoal = () => { + const handleEditGoalTitle = () => { if (goal) { openModal("EDIT_GOAL_TITLE"); } setIsOpen(false); }; - // 이거 있어야 될 것 같은데? - // useEffect(() => { - // updateProgress(todos); - // }, [todos]); + const checkIfNotesExist = (todos: TodoType[]) => { + const noteExists = todos.some((todo) => todo.noteId !== null); + if (noteExists) { + router.push(`/dashboard/notes/${goal?.id}`); + } else { + toast.warn("해당 목표에 작성된 노트가 없습니다."); + } + }; + + useEffect(() => { + loadGoalData(); + }, [progressPercentage]); // 외부 클릭 시 드롭다운 닫기 useEffect(() => { @@ -123,9 +96,9 @@ export default function GoalPage({ params }: GoalPageProps) { }; }, [isOpen]); - if (isTodosLoading) { - return ; - } + // if (isTodosLoading) { + // return ; + // } return (
@@ -153,7 +126,7 @@ export default function GoalPage({ params }: GoalPageProps) { {isOpen && (
-

+

수정하기

@@ -164,13 +137,15 @@ export default function GoalPage({ params }: GoalPageProps) {

Progress

- +
note-icon -

노트 모아보기

+

checkIfNotesExist(todos)} className="cursor-pointer text-lg font-semibold"> + 노트 모아보기 +

@@ -183,10 +158,13 @@ export default function GoalPage({ params }: GoalPageProps) {
    - {Array.isArray(todos) && - todos.filter((todo) => !todo.done).map((todo) => )} + {todos + .filter((todo) => todo.done === false) + .map((todo) => ( + + ))}
- {Array.isArray(todos) && todos.filter((todo) => !todo.done).length === 0 && ( + {todos.filter((todo) => todo.done === false).length === 0 && (
해야할 일이 아직 없어요.
)}
@@ -196,18 +174,23 @@ export default function GoalPage({ params }: GoalPageProps) {

Done

    - {Array.isArray(todos) && - todos.filter((todo) => todo.done).map((todo) => )} + {todos + .filter((todo) => todo.done === true) + .map((todo) => ( + + ))}
- {Array.isArray(todos) && todos.filter((todo) => todo.done).length === 0 && ( + {todos.filter((todo) => todo.done === true).length === 0 && (
다 한 일이 아직 없어요.
)} - - - + {goal && ( + + + + )} diff --git a/src/app/dashboard/note/[noteId]/page.tsx b/src/app/dashboard/note/[noteId]/page.tsx index f0f22ff..1b9598e 100644 --- a/src/app/dashboard/note/[noteId]/page.tsx +++ b/src/app/dashboard/note/[noteId]/page.tsx @@ -6,55 +6,11 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import useModal from "@/hook/useModal"; -import { getNotes, patchNotes, postNotes } from "@/api/noteAPI"; +import { getNote, getNotes, patchNotes, postNotes } from "@/api/noteAPI"; import { getTodos } from "@/api/todoAPI"; import UploadLinkModal from "@/components/UploadLinkModal"; import LoadingScreen from "@/components/LoadingScreen"; - -export type GoalType = { - id: number; - teamId: string; - title: string; - userId: number; - createdAt: string; - updatedAt: string; -}; - -export type TodoType = { - noteId: number | null; - done: boolean; - linkUrl: string | null; - fileUrl: string | null; - title: string; - id: number; - goal: GoalType; - userId: number; - teamId: string; - updatedAt: string; - createdAt: string; -}; - -export interface NoteType { - content: string; - createdAt: string; - goal: { - id: number; - title: string; - }; - id: number; - linkUrl: string; - teamId: string; - title: string; - todo: { - done: boolean; - fileUrl: string | null; - id: number; - linkUrl: string | null; - title: string; - }; - updatedAt: string; - userId: number; -} +import { NoteType, TodoType } from "@/app/Types/TodoGoalType"; export default function NotePage() { const router = useRouter(); @@ -69,42 +25,22 @@ export default function NotePage() { const todoId = Number(pathName.split("/").at(-1)); const goalId = Number(searchParams.get("goalId")); - // 노트와 할 일 가져오는 함수 - const fetchNote = async () => { + const loadNoteData = async () => { const todoResponse = await getTodos(goalId); - const findTodo = todoResponse.todos.find((todo: TodoType) => todo.id === todoId); - setTodo(findTodo); + const selectedTodo = todoResponse.todos.find((todo: TodoType) => todo.id === todoId); + console.log(selectedTodo); + setTodo(selectedTodo); - if (findTodo?.noteId) { - const noteResponse = await getNotes(findTodo.noteId); - return noteResponse; // 노트 데이터 반환 - } - - return null; // 노트가 없을 경우 null 반환 - }; - - // React Query를 사용하여 노트 데이터를 가져옴 - const { - data: fetchedNote, - isLoading, - isError, - } = useQuery({ - queryKey: ["note", todoId], // 고유 쿼리 키 설정 - queryFn: fetchNote, - enabled: !!todoId, // todoId가 존재할 때만 쿼리 실행 - }); + const noteResponse = await getNote(selectedTodo.noteId); - // fetchedNote가 변경되면 상태 업데이트 - useEffect(() => { - if (fetchedNote) { - setNote(fetchedNote); - setTitle(fetchedNote.title); - setContent(fetchedNote.content); - setLink(fetchedNote.linkUrl); + if (noteResponse) { + setNote(noteResponse); + setContent(noteResponse.content); + setTitle(noteResponse.title); + setLink(noteResponse.linkUrl); } - }, [fetchedNote]); + }; - // 제출 처리 const handleSubmit = async (type: "write" | "edit") => { const response = type === "write" @@ -118,6 +54,64 @@ export default function NotePage() { } }; + useEffect(() => { + loadNoteData(); + }, []); + + // 노트와 할 일 가져오는 함수 + // const fetchNote = async () => { + // const todoResponse = await getTodos(goalId); + // const findTodo = todoResponse.todos.find((todo: TodoType) => todo.id === todoId); + // setTodo(findTodo); + + // if (findTodo?.noteId) { + // const noteResponse = await getNotes(findTodo.noteId); + // return noteResponse; // 노트 데이터 반환 + // } + + // return null; // 노트가 없을 경우 null 반환 + // }; + + // // React Query를 사용하여 노트 데이터를 가져옴 + // const { + // data: noteData, + // isLoading, + // isError, + // } = useQuery({ + // queryKey: ["note", todoId], // 고유 쿼리 키 설정 + // queryFn: fetchNote, + // enabled: !!todoId, // todoId가 존재할 때만 쿼리 실행 + // }); + + // noteData가 변경되면 상태 업데이트 + // useEffect(() => { + // if (noteData) { + // setNote(noteData); + // setTitle(noteData.title); + // setContent(noteData.content); + // setLink(noteData.linkUrl); + // } + // }, [noteData]); + + // 제출 처리 + // const handleSubmit = async (type: string) => { + // if (type === "create") { + // const response = await postNotes(todoId, title, content, link ? link : null); + // if (response) { + // setNote(response); + // router.push(`/dashboard/goal/${goalId}`); + // toast.success("작성완료"); + // } + // } else { + // const response = await patchNotes(Number(note?.id), title, content, link ? link : null); + // if (response) { + // setNote(response.data); + // router.push(`/dashboard/goal/${goalId}`); + // toast.success("수정완료"); + // } + // } + // }; + const handleTitleChange = (e: ChangeEvent) => { const value = e.target.value; @@ -131,16 +125,16 @@ export default function NotePage() { toast.success("임시 저장이 완료되었습니다."); }; - const autoSaveDraft = () => { - const intervalId = setInterval( - () => { - saveDraft(); - }, - 5 * 60 * 1000, - ); // 5분마다 저장 + // const autoSaveDraft = () => { + // const intervalId = setInterval( + // () => { + // saveDraft(); + // }, + // 5 * 60 * 1000, + // ); // 5분마다 저장 - return () => clearInterval(intervalId); - }; + // return () => clearInterval(intervalId); + // }; const loadSavedDraft = () => { const savedNote = localStorage.getItem(`note${todoId}`); @@ -156,18 +150,17 @@ export default function NotePage() { } }; - useEffect(() => { - autoSaveDraft(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // useEffect(() => { + // autoSaveDraft(); + // }, []); - if (isError) { - return

노트를 불러오는 데 오류가 발생했습니다.

; - } + // if (isError) { + // return

노트를 불러오는 데 오류가 발생했습니다.

; + // } - if (isLoading) { - return ; - } + // if (isLoading) { + // return ; + // } return (
diff --git a/src/app/dashboard/notes/[noteId]/page.tsx b/src/app/dashboard/notes/[noteId]/page.tsx new file mode 100644 index 0000000..f3396f3 --- /dev/null +++ b/src/app/dashboard/notes/[noteId]/page.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { useEffect, useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; + +import { getNotes } from "@/api/noteAPI"; +import { NoteType } from "@/app/Types/TodoGoalType"; + +import NoteItem from "../../components/NoteItem"; + +export default function NotesPage() { + const path = usePathname(); + const goalId = Number(path.split("/").at(-1)); + const [notes, setNotes] = useState(); + + const loadNotesData = async () => { + const response = await getNotes(goalId); + console.log(response); + + if (response) { + setNotes(response.notes); + } + }; + + useEffect(() => { + loadNotesData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+
+
+
+

노트 모아보기

+
+
+
+ recent-task-icon +
+

+ + {notes && notes[0] ? notes[0].goal.title : "작성된 노트가 없습니다."} + +

+
+
{notes?.map((note) => )}
+
+
+
+ ); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 83408bd..d856038 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { useEffect, useState } from "react"; -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { useInView } from "react-intersection-observer"; import LoadingSpinner from "@/components/LoadingSpinner"; @@ -11,63 +11,21 @@ import LoadingScreen from "@/components/LoadingScreen"; import { getInfinityScrollGoals } from "@/api/goalAPI"; import { getAllTodos } from "@/api/todoAPI"; +import { GoalType, TodoType } from "../Types/TodoGoalType"; + import TodoCard from "./components/TodoCard"; -import Todos from "./components/Todos"; import ProgressTracker from "./components/ProgressTracker"; +import TodoItem from "./components/TodoItem"; -export type TodoType = { - noteId?: number | null; - done: boolean; - linkUrl?: string | null; - fileUrl?: string | null; - title: string; - id: number; - goal: GoalType; - userId: number; - teamId: string; - updatedAt: string; - createdAt: string; -}; - -export type GoalType = { - id: number; - teamId: string; - title: string; - userId: number; - createdAt: string; - updatedAt: string; -}; - -export default function Dashboard() { +export default function DashboardPage() { const [recentTodos, setRecentTodos] = useState([]); - const [progressValue, setProgressValue] = useState(0); - const [ratio, setRatio] = useState(0); + const [progressPercentage, setProgressPercentage] = useState(0); + const [completionRatio, setCompletionRatio] = useState(0); const { ref, inView } = useInView(); - // 최근 할 일 데이터 가져오기 - const { data: todosData, isLoading: isTodosLoading } = useQuery({ - queryKey: ["todos"], - queryFn: getAllTodos, - //staleTime: 60000, - }); - - // 정렬 및 최근 할 일 설정 - useEffect(() => { - if (todosData && Array.isArray(todosData.todos)) { - const sortedTodos = [...todosData.todos].sort( - (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), - ); - setRecentTodos(sortedTodos.slice(0, 4)); - - const total = todosData.totalCount; // 총 개수는 totalCount에서 가져옴 - const dones = todosData.todos.filter((todo: TodoType) => todo.done); - setRatio(Math.round((dones.length / total) * 100)); - } - }, [todosData]); - // 목표 데이터 무한 스크롤 const { - data, + data: infiniteGoalPages, fetchNextPage, hasNextPage, isFetchingNextPage, @@ -75,21 +33,18 @@ export default function Dashboard() { } = useInfiniteQuery({ queryKey: ["goals"], queryFn: async ({ pageParam }) => { - const response = await getInfinityScrollGoals({ cursor: pageParam, size: 3, sortOrder: "newest" }); + const response = await getInfinityScrollGoals({ cursor: pageParam, size: 3, sortOrder: "oldest" }); return response; }, initialPageParam: undefined, getNextPageParam: (lastPage) => lastPage.nextCursor, }); - const getAllGoals = () => { - if (!data) return []; - return data.pages.reduce((acc, page) => { - return [...acc, ...page.goals]; - }, []); - }; - - const allGoals = getAllGoals(); + const allGoals = infiniteGoalPages + ? infiniteGoalPages.pages.reduce((acc, page) => { + return acc.concat(page.goals); + }, []) + : []; // 목표 데이터가 화면에 보일 때 다음 페이지 로드 useEffect(() => { @@ -98,13 +53,31 @@ export default function Dashboard() { } }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + // 정렬 및 최근 할 일 설정 + const loadDashboardData = async () => { + const todosResponse = await getAllTodos(); + if (todosResponse) { + const todoTotalCount = todosResponse.totalCount; + const doneTodos = todosResponse.todos.filter((todo: TodoType) => todo.done === true); + setCompletionRatio(Math.round((doneTodos.length / todoTotalCount) * 100)); + const sortedTodos = [...todosResponse.todos].sort( + (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(), + ); + setRecentTodos(sortedTodos.slice(0, 4)); + } + }; + + useEffect(() => { + loadDashboardData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // 로딩 상태 처리 - if (isGoalsLoading || isTodosLoading) { + if (isGoalsLoading) { + console.log("여기 들리니?"); return ; } - //const allGoals = data?.pages.flatMap((page) => page.goals) || []; - return (
@@ -127,12 +100,16 @@ export default function Dashboard() { {recentTodos.length === 0 ? (

최근에 등록한 할 일이 없어요

) : ( - recentTodos.map((todo) => ) + recentTodos.map((todo) => ) )}
{/* 진행 상황 트래커 */} - +
{/* 목표 별 할 일 */} diff --git a/src/app/dashboard/todoboard/page.tsx b/src/app/dashboard/todoboard/page.tsx index 1255e7c..04c00e5 100644 --- a/src/app/dashboard/todoboard/page.tsx +++ b/src/app/dashboard/todoboard/page.tsx @@ -8,103 +8,43 @@ import CreateNewTodo from "@/components/CreateNewTodo"; import useModal from "@/hook/useModal"; import { getAllTodos } from "@/api/todoAPI"; -import Todos from "../components/Todos"; - -export type TodosResponse = { - totalCount: number; - nextCursor: number; - todos: TodoType[]; -}; - -export type GoalType = { - updatedAt: string; - createdAt: string; - title: string; - id: number; - userId: number; - teamId: string; -}; - -export type TodoType = { - noteId: number | null; - done: boolean; - linkUrl: string | null; - fileUrl: string | null; - title: string; - id: number; - goal: GoalType; - userId: number; - teamId: string; - updatedAt: string; - createdAt: string; -}; +import TodoItem from "../components/TodoItem"; +import { TodoType } from "@/app/Types/TodoGoalType"; type StatusType = "All" | "Todo" | "Done"; const statuses: StatusType[] = ["All", "Todo", "Done"]; export default function TodoboardPage() { - //const [todos, setTodos] = useState([]); + const [todos, setTodos] = useState([]); const [status, setStatus] = useState("All"); const { Modal, openModal, closeModal } = useModal(); - // useQuery를 사용하여 할 일 데이터를 가져옴 - const { data: todosData, error } = useQuery({ - queryKey: ["todos"], - queryFn: async () => { - const response = await getAllTodos(); // API 호출 - if (response && response.data) { - return response.data; // 데이터 구조가 TodosResponse와 일치해야 합니다. - } - throw new Error("데이터를 불러오는 데 실패했습니다."); // 데이터가 없으면 에러 던지기 - }, - select: (data) => data.todos, // 최종 데이터에서 todos만 선택 - }); - - console.log(todosData); + const loadTodoboardData = async () => { + const response = await getAllTodos(); + setTodos(response.todos); + }; - // const handleTodoUpdate = useCallback(async (updatedTodo: TodoType) => { - // try { - // const response = await patchTodo( - // updatedTodo.title, - // updatedTodo.goal.id, - // updatedTodo.done, - // updatedTodo?.id, - // updatedTodo.fileUrl || "", - // updatedTodo.linkUrl || "", - // ); - // if (response) { - // // setTodos((prevTodos) => - // // prevTodos.map((todo) => (todo.id === updatedTodo.id ? { ...todo, ...updatedTodo } : todo)) - // // ); - // } - // } catch (error) { - // console.error("할 일 업데이트 중 오류 발생:", error); - // } - // }, []); + useEffect(() => { + loadTodoboardData(); + }, []); const renderTodos = () => { switch (status) { case "Todo": - return todosData.filter((todo: TodoType) => !todo.done); + return todos.filter((todo: TodoType) => !todo.done); case "Done": - return todosData.filter((todo: TodoType) => todo.done); + return todos.filter((todo: TodoType) => todo.done); default: - return todosData; + return todos; } }; - useEffect(() => { - if (error) { - toast.error(`데이터 로딩 중 오류 발생: ${error.message}`); - } - }, [error]); - return (
-

모든 할 일 {`(${todosData?.length})`}

+

모든 할 일 {`(${todos?.length})`}

openModal("CREATE_NEW_TODO")}> + 할일 추가 @@ -128,7 +68,7 @@ export default function TodoboardPage() {
    {Array.isArray(renderTodos()) && - renderTodos().map((todo: TodoType) => )} + renderTodos().map((todo: TodoType) => )}
diff --git a/src/components/CreateNewTodo.tsx b/src/components/CreateNewTodo.tsx index 4d979f1..960b38f 100644 --- a/src/components/CreateNewTodo.tsx +++ b/src/components/CreateNewTodo.tsx @@ -1,182 +1,137 @@ -"use client"; - +import React, { useState, useRef, useEffect } from "react"; import { toast } from "react-toastify"; import Image from "next/image"; -import { ChangeEvent, useEffect, useRef, useState } from "react"; -import { createTodo, postFile } from "@/api/todoAPI"; +import { createTodo, postFile, updateTodo } from "@/api/todoAPI"; import useModal from "@/hook/useModal"; -import { useGoalStore } from "@/store/goalStore"; -import useTodoStore from "@/store/todoStore"; +import { GoalType, InitialTodoType, TodoType } from "@/app/Types/TodoGoalType"; +import { getGoals } from "@/api/goalAPI"; import LinkUpload from "./LinkUpload"; -export type TodoType = { - noteId?: number | null; - done: boolean; - linkUrl?: string | null; - fileUrl?: string | null; - title: string; - id: number; - goal: GoalType; - userId: number; - teamId: string; - updatedAt: string; - createdAt: string; -}; -export type GoalType = { - id: number; - teamId: string; - title: string; - userId: number; - createdAt: string; - updatedAt: string; -}; - -export type FileType = { - url?: string | null; -}; - -export type CreateNewTodoProps = { +type CreateNewTodoProps = { closeCreateNewTodo: () => void; - goal?: GoalType; - title?: string; - fileUrl?: string | undefined; - linkUrl?: string | undefined; - todoId?: number; - done?: boolean; - isEdit?: boolean; - goalId?: number | undefined; // 목표 ID를 받아옴 + todo?: TodoType | undefined; + isEditing?: boolean; + goals?: GoalType[]; + selectedGoalId?: number; }; -export default function CreateNewTodo({ - closeCreateNewTodo, - goal, - title, - fileUrl, - linkUrl, - todoId, - isEdit, - goalId, // 목표 ID를 받아옴 -}: CreateNewTodoProps) { +export default function CreateNewTodo({ closeCreateNewTodo, todo, isEditing, selectedGoalId }: CreateNewTodoProps) { + console.log(selectedGoalId); + const titleRef = useRef(null); const fileInputRef = useRef(null); - const [isOpenGoals, setIsOpenGoals] = useState(false); - const [isFileUpload, setIsFileUpload] = useState(false); - const [fileTitle, setFileTitle] = useState(""); const { Modal, openModal, closeModal } = useModal(); - const { goals } = useGoalStore(); - const { updateTodo } = useTodoStore(); - - const [todo, setTodo] = useState({ - title: "", - fileUrl: null, - linkUrl: null, - goal: goal || { - id: 0, - teamId: "", - title: "", - userId: 0, - createdAt: "", - updatedAt: "", - }, - done: false, - noteId: null, - id: 0, - userId: 0, - teamId: "", - updatedAt: "", - createdAt: "", + const [fileUploaded, setFileUploaded] = useState(false); + const [fileName, setFileName] = useState(""); + const [fileUrl, setFileUrl] = useState(""); + const [isGoalListOpen, setIsGoalListOpen] = useState(false); + const [goalList, setGoalList] = useState([]); + const [goalTitle, setGoalTitle] = useState(selectedGoalId as number); + const [initialTodo, setInitialTodo] = useState({ + title: todo?.title || "", + linkUrl: todo?.linkUrl || "", + goalId: selectedGoalId || todo?.goal?.id || 0, }); - useEffect(() => { - // 할 일 추가할 때 목표가 설정되어 있지 않으면 전역 상태에서 기본값 설정 - if (!goal && goals.length > 0) { - const defaultGoal = goals.find((g) => g.id === goalId); // goalId와 일치하는 목표 찾기 - if (defaultGoal) { - setTodo((prevTodo) => ({ - ...prevTodo, - goal: defaultGoal, // 일치하는 목표가 있으면 todo에 설정 - })); - } + const fetchGoalList = async () => { + const response = await getGoals(); + if (response) { + setGoalList(response.goals); } - }, [goal, goals, goalId]); - - const handleTitleChange = (e: React.ChangeEvent) => { - setTodo({ ...todo, title: e.target.value }); }; - const handleFileChange = async (e: ChangeEvent) => { - const MAX_FILE_SIZE = 3 * 1024 * 1024; // 3MB 제한 - const selectedFile = e.target.files?.[0]; + const handleFileUpload = async () => { + const MAX_FILE_SIZE = 3 * 1024 * 1024; + const file = fileInputRef.current?.files?.[0]; - if (!selectedFile) { - toast.error("파일이 선택되지 않았습니다."); - return; + try { + if (file && file.size < MAX_FILE_SIZE) { + const uploadResponse = await postFile(file); + setFileUrl(uploadResponse.url); + setFileUploaded(true); + setFileName(file.name); + } else { + toast.error("파일은 3MB 이하만 업로드 가능합니다."); + } + } catch (error) { + console.log(error); + setFileUploaded(false); + toast.error("파일 업로드 중 오류가 발생했습니다."); } + }; - if (selectedFile.size > MAX_FILE_SIZE) { - toast.error("파일은 3MB 이하만 업로드 가능합니다."); - setIsFileUpload(false); - return; - } + const handleSubmit = async () => { + const todoPayload = { + title: initialTodo.title, + goalId: selectedGoalId || initialTodo.goalId, + fileUrl: fileUrl || null, // file이 존재하지 않으면 null로 처리 + linkUrl: initialTodo.linkUrl || null, // linkUrl이 없으면 null로 처리 + }; - const response = await postFile(selectedFile); - if (response) { - setTodo((prevTodo) => ({ - ...prevTodo, - fileUrl: response.url, - })); - setFileTitle(selectedFile.name); - setIsFileUpload(true); - toast.success("파일이 성공적으로 업로드되었습니다"); - } else { - toast.error("파일 업로드 실패"); + try { + if (isEditing) { + await updateTodo(todo?.id as number, todoPayload); // 업데이트할 경우 + toast.success("할 일이 수정되었습니다."); + } else { + await createTodo( + todoPayload.title, + todoPayload.goalId, + todoPayload.fileUrl ?? undefined, + todoPayload.linkUrl ?? undefined, + ); // 새로 생성할 경우 + toast.success("할 일이 생성되었습니다."); + } + //updateTodos(); // 상태 업데이트 + closeCreateNewTodo(); // 모달 닫기 + } catch (error) { + console.error(error); + toast.error("작업을 처리하는 중 오류가 발생했습니다."); } }; - const handleGoalSelect = (goalId: number, goalTitle: string) => { - setTodo((prevTodo) => ({ - ...prevTodo, - goal: { ...prevTodo.goal, id: goalId, title: goalTitle }, - })); - setIsOpenGoals(false); - }; + useEffect(() => { + fetchGoalList(); + }, []); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!todo) return; + useEffect(() => { + if (todo) { + // 데이터를 먼저 초기화 + setInitialTodo({ + title: todo.title || "", + linkUrl: todo.linkUrl || "", + goalId: todo.goal?.id || 0, + }); - if (isEdit && todoId) { - await updateTodo(todoId, { title: todo.title, fileUrl: todo.fileUrl, linkUrl: todo.linkUrl, id: todo.id }); + // 파일 URL 처리 + if (todo.fileUrl) { + setFileUrl(todo.fileUrl); + setFileUploaded(true); + } else { + setFileUrl(""); // 파일 URL이 없을 경우 빈 값 처리 + setFileUploaded(false); + } } else { - await createTodo(todo.title, todo.fileUrl || null, todo.linkUrl || null, todo.goal.id); + // todo가 없을 때 초기화 + setInitialTodo({ + title: "", + linkUrl: "", + goalId: 0, + }); + setFileUrl(""); // 파일 URL 초기화 + setFileUploaded(false); // 파일 업로드 상태 초기화 } - - toast.success(isEdit ? "할 일이 수정되었습니다" : "할 일이 생성되었습니다"); - closeCreateNewTodo(); - }; + }, [todo]); useEffect(() => { - console.log(isEdit, goal); - - if (isEdit && goal) { - setTodo((prevTodo) => ({ - ...prevTodo, - title: title || "", - linkUrl: linkUrl || null, - fileUrl: fileUrl || null, - goal: goal, - })); - } - - if (fileUrl) { - setIsFileUpload(true); + if (titleRef.current) { + titleRef.current.focus(); } - }, [isEdit, title, fileUrl, linkUrl, goal]); + }, [todo]); return ( <> -
+

제목

+

자료

setFileUploaded(!fileUploaded)} > checkbox-icon 파일 업로드
openModal("LINK_ATTACHMENT")} + onClick={() => openModal("LINK_ATTACHMENT")} // 링크 첨부 모달 열기 > checkbox-icon 링크 첨부
+
- {fileTitle ? ( -

{fileTitle}

+ {fileName ? ( +

{fileName}

) : ( -
{ - e.stopPropagation(); - fileInputRef.current?.click(); - }} - > +
fileInputRef.current?.click()}>

+

파일을 업로드해주세요

@@ -247,7 +201,7 @@ export default function CreateNewTodo({ id="file-upload" type="file" ref={fileInputRef} - onChange={handleFileChange} + onChange={handleFileUpload} className="absolute opacity-0" />
@@ -260,24 +214,33 @@ export default function CreateNewTodo({ 목표
setIsOpenGoals((prev) => !prev)} + onClick={() => setIsGoalListOpen((prev) => !prev)} className="flex w-full cursor-pointer justify-between rounded-xl bg-slate-50 px-[20px] py-3" > -

- {todo.goal.id ? todo.goal.title : "목표를 선택해주세요"} +

+ {initialTodo.goalId || selectedGoalId + ? goalList.find((goal) => goal.id === initialTodo.goalId || selectedGoalId)?.title + : "목표를 선택해주세요"}

arrowdown-icon
- {isOpenGoals && ( + {isGoalListOpen && (
    - {goals.length > 0 ? ( - goals.map((goal) => ( + {goalList?.length > 0 ? ( + goalList.map((goal) => (
  • handleGoalSelect(goal.id, goal.title)} + onClick={() => { + console.log(`목표 선택: ${selectedGoalId}`); // 디버깅 로그 추가 + console.log(`목표 선택: ${goal.id}`); // 디버깅 로그 추가 + console.log(`목표 선택: ${goal.title}`); // 디버깅 로그 추가 + setGoalTitle(goal.id); + setInitialTodo({ ...initialTodo, goalId: goal.id }); + setIsGoalListOpen(false); + }} > {goal.title}
  • @@ -294,13 +257,15 @@ export default function CreateNewTodo({ + + {/* 링크 첨부 모달 */} - + ); diff --git a/src/components/LinkUpload.tsx b/src/components/LinkUpload.tsx index 04aec00..7610e09 100644 --- a/src/components/LinkUpload.tsx +++ b/src/components/LinkUpload.tsx @@ -2,12 +2,12 @@ import { useState } from "react"; -import { TodoType } from "./CreateNewTodo"; +import { InitialTodoType } from "@/app/Types/TodoGoalType"; type LinkUploadProps = { closeSecond: () => void; - todo?: TodoType; - setTodo: React.Dispatch>; + todo?: InitialTodoType; + setTodo: React.Dispatch>; }; export default function LinkUpload({ closeSecond, todo, setTodo }: LinkUploadProps) { diff --git a/src/store/goalStore.ts b/src/store/goalStore.ts index af53520..3ba84d5 100644 --- a/src/store/goalStore.ts +++ b/src/store/goalStore.ts @@ -13,7 +13,7 @@ export type GoalType = { export type GoalState = { goals: GoalType[]; - addGoal: (little: string) => Promise; + addGoal: (tittle: string) => Promise; updateGoal: (id: number, title: string) => Promise; refreshGoals: () => Promise; }; diff --git a/src/store/todoStore.ts b/src/store/todoStore.ts index 0fc84c7..f6afafc 100644 --- a/src/store/todoStore.ts +++ b/src/store/todoStore.ts @@ -1,9 +1,6 @@ import { create } from "zustand"; -import { toast } from "react-toastify"; -import { AxiosError } from "axios"; - -import api from "@/lib/api"; +import { createTodo, getAllTodos, getTodos, updateTodo } from "@/api/todoAPI"; export type TodoType = { noteId?: number | null; @@ -28,37 +25,90 @@ export type GoalType = { updatedAt: string; }; -type ErrorType = { - message: string; -}; - -type TodoStore = { +// Todo 상태 정의 +export type TodoState = { todos: TodoType[]; - updateTodo: (todoId: number, updates: Partial) => Promise; - setTodos: (todos: TodoType[]) => void; + fetchTodos: () => Promise; // 모든 할 일을 가져오는 메서드 + fetchTodosByGoal: (goalId: number) => Promise; // 특정 목표의 할 일을 가져오는 메서드 + setTodos: (todos: TodoType[]) => void; // setTodos 추가 + addTodo: (title: string, goalId: number, fileUrl?: string | null, linkUrl?: string | null) => Promise; + updateTodo: (todoId: number, updates: Partial) => Promise; + refreshTodos: () => Promise; + toggleTodo: (id: number) => void; // id를 받아서 toggle하도록 수정 }; -const useTodoStore = create((set) => ({ +export const useTodoStore = create((set) => ({ todos: [], - - updateTodo: async (todoId, updates) => { + fetchTodos: async () => { try { - const response = await api.patch(`/todos/${todoId}`, updates); - const updatedTodo = response.data; - - set((state) => ({ - todos: state.todos.map((todo) => (todo.id === updatedTodo.id ? updatedTodo : todo)), - })); - - // toast.success("할일을 수정했습니다."); + const response = await getAllTodos(); + const todos = response.todos; // todos 속성에서 배열을 가져옴 + console.log(todos); + if (Array.isArray(todos)) { + set({ todos }); + } else { + console.error("fetchTodos에서 반환된 값이 배열이 아닙니다.", todos); + } } catch (error) { - const axiosError = error as AxiosError; - toast.error(axiosError.response?.data.message || "할 일 수정을 실패했습니다."); - throw error; + console.error("모든 할 일 가져오는 중 오류 발생:", error); + } + }, + fetchTodosByGoal: async (goalId: number) => { + try { + const todos = await getTodos(goalId); // 목표에 해당하는 할 일 가져오기 + if (Array.isArray(todos)) { + set({ todos }); + } else { + console.error(`fetchTodosByGoal에서 반환된 값이 배열이 아닙니다.`, todos); + } + } catch (error) { + console.error(`목표 ID ${goalId}의 할 일 가져오는 중 오류 발생:`, error); + } + }, + setTodos: (todos: TodoType[]) => { + if (Array.isArray(todos)) { + set({ todos }); // todos가 배열이면 상태 업데이트 + } else { + console.error("setTodos에 전달된 값이 배열이 아닙니다.", todos); } }, + addTodo: async ( + title: string, + goalId: number, + fileUrl?: string | null, + linkUrl?: string | null, + ): Promise => { + const response = await createTodo(title, goalId, fileUrl, linkUrl); + const newTodo: TodoType = response; - setTodos: (todos) => set({ todos }), -})); + set((state) => ({ + todos: [...state.todos, newTodo], // 새 할 일을 배열의 끝에 추가 + })); + return newTodo; + }, -export default useTodoStore; + updateTodo: async (todoId: number, updates: Partial): Promise => { + const response = await updateTodo(todoId, updates); // PatchGoal 호출 + const updatedTodo: TodoType = response; // 응답의 data에서 GoalType 추출 + + set((state) => ({ + todos: state.todos.map((todo) => (todo.id === todoId ? { ...todo, ...updatedTodo } : todo)), + })); + + return updatedTodo; + }, + + refreshTodos: async () => { + const todoListResponse = await getAllTodos(); + const todoList = todoListResponse?.todos || []; // 할 일 목록을 가져옴 + set({ todos: todoList }); // 상태 업데이트 + }, + + toggleTodo: (id: number) => { + set((state) => ({ + todos: state.todos.map( + (t) => (t.id === id ? { ...t, done: !t.done } : t), // 현재 todo의 done 상태를 반전 + ), + })); + }, +})); diff --git a/src/utils/authUtils.ts b/src/utils/authUtils.ts index bd05779..f798cb1 100644 --- a/src/utils/authUtils.ts +++ b/src/utils/authUtils.ts @@ -1,6 +1,7 @@ import api from "@/lib/api"; import { useAuthStore } from "@/store/authStore"; +// 로그인 할 때, 바디에 받아서 export const login = async (email: string, password: string) => { try { const { data } = await api.post("/auth/login", { email, password });