From b12d3b81fb228919969e1c89deff8fa4d977740c Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:20:46 +0900 Subject: [PATCH 01/48] =?UTF-8?q?#52=20feat:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/assignment/api/assignmentApi.ts | 49 +++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/entities/assignment/api/assignmentApi.ts b/src/entities/assignment/api/assignmentApi.ts index 23940b5..e3af901 100644 --- a/src/entities/assignment/api/assignmentApi.ts +++ b/src/entities/assignment/api/assignmentApi.ts @@ -1,8 +1,55 @@ -import type {DashboardScheduleListResponse} from '@/entities/course/model/types'; +import type { + AssignmentSelectResponse, + DashboardScheduleListResponse, +} from '@/entities/course/model/types'; import {privateAxios} from '@/shared/api/axiosInstance'; +import type {ApiResponse} from '@/shared/model'; +import type {AssignmentsResponse} from '../model/types'; +// 과제 일정 조회 API export const getAssignmentSchedules = async (): Promise => { const response = await privateAxios.get('/assignments/schedule'); return response.data; }; + +// 전체 과제 목록 조회 API +export const getAllAssignments = async (): Promise< + ApiResponse +> => { + const response = await privateAxios.get< + ApiResponse<{ + count: number; + assignments: {assignmentId: number; title: string}[]; + }> + >('/assignments/my'); + const raw = response.data; + return { + ...raw, + response: { + count: raw.response.count, + assignments: raw.response.assignments.map(({assignmentId, title}) => ({ + id: assignmentId, + title, + })), + }, + }; +}; + +// 강의별 과제 목록 조회 API +export const getAssignmentsByCourse = async ( + courseId: number +): Promise => { + const response = await privateAxios.get(`/courses/${courseId}/assignments`); + + console.log('getAssignmentsByCourse API 응답:', response.data); // 응답 데이터 로깅 + return response.data; +}; + +// 과제 삭제 API +export const deleteAssignment = async ( + assignmentId: number +): Promise> => { + const response = await privateAxios.delete(`/assignments/${assignmentId}`); + return response.data; +}; From c17ba820c0e674aa169e1203da70e61e96491a61 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:21:09 +0900 Subject: [PATCH 02/48] =?UTF-8?q?#52=20chore:=20=EC=A3=BC=EC=84=9D=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 --- src/entities/course/api/courseApi.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/entities/course/api/courseApi.ts b/src/entities/course/api/courseApi.ts index deae300..a1afec0 100644 --- a/src/entities/course/api/courseApi.ts +++ b/src/entities/course/api/courseApi.ts @@ -2,11 +2,13 @@ import type {ApiResponse} from '@/shared/model/common'; import type {DashboardCourseListResponse} from '@/entities/course/model/types'; import {privateAxios} from '@/shared/api/axiosInstance'; +// 전체 강의 목록 조회 API export const getAllCourses = async (): Promise => { const response = await privateAxios.get('/courses/my'); return response.data; }; +// 강의 삭제 API export const deleteCourse = async ( courseId: number ): Promise> => { From dd44942af0b2b3043010cd8df425ab4f9c0ea31e Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:21:51 +0900 Subject: [PATCH 03/48] =?UTF-8?q?#52=20feat:=20=EB=8B=A8=EC=9B=90-?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=97=B0=EA=B2=B0=20=ED=95=B4=EC=A0=9C=20?= =?UTF-8?q?api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/unit/api/unitApi.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/entities/unit/api/unitApi.ts b/src/entities/unit/api/unitApi.ts index b716386..428720c 100644 --- a/src/entities/unit/api/unitApi.ts +++ b/src/entities/unit/api/unitApi.ts @@ -44,3 +44,14 @@ export const updateUnit = async ( const response = await privateAxios.put(`/units/${unitId}`, unit); return response.data; }; + +// 단원에 등록된 과제 삭제 +export const deleteAssignmentFromUnit = async ( + unitId: number, + assignmentId: number +): Promise> => { + const response = await privateAxios.delete( + `/units/${unitId}/assignments/${assignmentId}` + ); + return response.data; +}; From c3fb10b0cbc5ad9e1a29d29a780e8388ee9954b3 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:22:59 +0900 Subject: [PATCH 04/48] =?UTF-8?q?#52=20feat:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=BF=BC=EB=A6=AC=20=EC=98=B5=EC=85=98=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 --- .../assignment/api/assignmentQueries.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/entities/assignment/api/assignmentQueries.ts diff --git a/src/entities/assignment/api/assignmentQueries.ts b/src/entities/assignment/api/assignmentQueries.ts new file mode 100644 index 0000000..d2554d8 --- /dev/null +++ b/src/entities/assignment/api/assignmentQueries.ts @@ -0,0 +1,30 @@ +import {queryOptions} from '@tanstack/react-query'; +import { + getAllAssignments, + getAssignmentsByCourse, + getAssignmentSchedules, +} from './assignmentApi'; + +export const assignmentQueries = { + // 과제 일정 조회 쿼리 옵션 + getAssignmentSchedules: () => + queryOptions({ + queryKey: ['schedules'], + queryFn: getAssignmentSchedules, + }), + + // 전체 과제 목록 조회 쿼리 옵션 + getAllAssignments: () => + queryOptions({ + queryKey: ['assignments'], + queryFn: getAllAssignments, + }), + + // 강의별 과제 목록 조회 쿼리 옵션 + getAssignmentsByCourse: (courseId: number) => + queryOptions({ + queryKey: ['courses', courseId, 'assignments'], + queryFn: () => getAssignmentsByCourse(courseId), + enabled: !!courseId, // courseId가 있을 때만 쿼리 실행 + }), +}; From d590227d2d52bad08783e133273613e531c434cb Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:23:41 +0900 Subject: [PATCH 05/48] =?UTF-8?q?#52=20refactor:=20query=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/assignment/api/assignmentQueryOptions.ts | 9 --------- src/entities/course/api/courseQueries.ts | 11 +++++++++++ src/entities/course/api/courseQueryOptions.ts | 9 --------- 3 files changed, 11 insertions(+), 18 deletions(-) delete mode 100644 src/entities/assignment/api/assignmentQueryOptions.ts create mode 100644 src/entities/course/api/courseQueries.ts delete mode 100644 src/entities/course/api/courseQueryOptions.ts diff --git a/src/entities/assignment/api/assignmentQueryOptions.ts b/src/entities/assignment/api/assignmentQueryOptions.ts deleted file mode 100644 index 89af95e..0000000 --- a/src/entities/assignment/api/assignmentQueryOptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {queryOptions} from '@tanstack/react-query'; -import {getAssignmentSchedules} from './assignmentApi'; - -export default function assignmentQueryOptions() { - return queryOptions({ - queryKey: ['schedules'], - queryFn: getAssignmentSchedules, - }); -} diff --git a/src/entities/course/api/courseQueries.ts b/src/entities/course/api/courseQueries.ts new file mode 100644 index 0000000..2999360 --- /dev/null +++ b/src/entities/course/api/courseQueries.ts @@ -0,0 +1,11 @@ +import {getAllCourses} from './courseApi'; +import {queryOptions} from '@tanstack/react-query'; + +export const courseQueries = { + // 전체 강의 조회 쿼리 옵션 + getAllCourses: () => + queryOptions({ + queryKey: ['courses'], + queryFn: getAllCourses, + }), +}; diff --git a/src/entities/course/api/courseQueryOptions.ts b/src/entities/course/api/courseQueryOptions.ts deleted file mode 100644 index ab5d82f..0000000 --- a/src/entities/course/api/courseQueryOptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {getAllCourses} from './courseApi'; -import {queryOptions} from '@tanstack/react-query'; - -export default function courseQueryOptions() { - return queryOptions({ - queryKey: ['courses'], - queryFn: getAllCourses, - }); -} From 46d842f3289179abd3ac55cae419aaba4998fbfc Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:23:57 +0900 Subject: [PATCH 06/48] =?UTF-8?q?#52=20feat:=20=EA=B0=95=EC=9D=98=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=AE=A4=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/course/api/courseMutations.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/entities/course/api/courseMutations.ts diff --git a/src/entities/course/api/courseMutations.ts b/src/entities/course/api/courseMutations.ts new file mode 100644 index 0000000..0eea045 --- /dev/null +++ b/src/entities/course/api/courseMutations.ts @@ -0,0 +1,9 @@ +import {deleteCourse} from './courseApi'; + +export const courseMutations = { + // 강의 삭제 뮤테이션 옵션 + deleteCourse: { + mutationKey: ['deleteCourse'], + mutationFn: (courseId: number) => deleteCourse(courseId), + }, +}; From b5495f54c664ffb5cd8d2c2ad6ef71c46204d6f5 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:24:28 +0900 Subject: [PATCH 07/48] =?UTF-8?q?#52=20chore:=20barrel=20export=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/course/index.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/entities/course/index.ts diff --git a/src/entities/course/index.ts b/src/entities/course/index.ts deleted file mode 100644 index 6b5cfff..0000000 --- a/src/entities/course/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {getAllCourses, deleteCourse} from './api/courseApi'; From c5b95d91123f4bfe0f7604adc3761f03dfc1da15 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:25:04 +0900 Subject: [PATCH 08/48] =?UTF-8?q?#52=20style:=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20items-center=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EC=B7=A8=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/ui/Layout.tsx b/src/shared/ui/Layout.tsx index bf302a7..45913e5 100644 --- a/src/shared/ui/Layout.tsx +++ b/src/shared/ui/Layout.tsx @@ -9,7 +9,7 @@ const Layout = () => { const showHeader = !noHeaderPages.includes(pathname); return ( -
+
{showHeader && (
From 932b8d6008d11f65305bbd2aec8c8b059913a36b Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:25:53 +0900 Subject: [PATCH 09/48] =?UTF-8?q?#52=20feat:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/assignment/model/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/entities/assignment/model/types.ts b/src/entities/assignment/model/types.ts index 331c549..bdbf0a8 100644 --- a/src/entities/assignment/model/types.ts +++ b/src/entities/assignment/model/types.ts @@ -8,3 +8,8 @@ export interface Assignment { title: string; submittedStatus?: SubmissionStatus; } + +export interface AssignmentsResponse { + count: number; + assignments: Assignment[]; +} From 072672a08ed1e0c0cfff6cf64ae7f370ca0444eb Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:27:57 +0900 Subject: [PATCH 10/48] =?UTF-8?q?#52=20refactor:=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=EB=90=9C=20query/mutation=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/dashboard/Dashboard.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index e1ca73a..d9acfd6 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -5,15 +5,15 @@ import AddIcon from '@/assets/svg/addIcon.svg?react'; import ScheduleList from './ui/ScheduleList'; import {Link} from 'react-router-dom'; import {useUserStore} from '@/entities/auth/model/useUserStore'; -import courseQueryOptions from '@/entities/course/api/courseQueryOptions'; import { useMutation, useQueryClient, useSuspenseQueries, } from '@tanstack/react-query'; -import assignmentQueryOptions from '@/entities/assignment/api/assignmentQueryOptions'; -import {deleteCourse} from '@/entities/course'; import {EmptyState} from '@/shared/ui/EmptyState'; +import {courseQueries} from '@/entities/course/api/courseQueries'; +import {courseMutations} from '@/entities/course/api/courseMutations'; +import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; const Dashboard = () => { const userType = useUserStore((state) => state.userType); @@ -21,19 +21,22 @@ const Dashboard = () => { // 강의 및 스케쥴 데이터 패칭 const [{data: courses}, {data: schedules}] = useSuspenseQueries({ - queries: [courseQueryOptions(), assignmentQueryOptions()], + queries: [ + courseQueries.getAllCourses(), + assignmentQueries.getAssignmentSchedules(), + ], }); - // 강의 삭제 뮤테이션 + // 강의 삭제 const {mutate} = useMutation({ - mutationFn: (courseId: number) => deleteCourse(courseId), + ...courseMutations.deleteCourse, onSuccess: () => { // 강의 목록 및 스케쥴 목록 갱신 queryClient.invalidateQueries({ - queryKey: courseQueryOptions().queryKey, + queryKey: courseQueries.getAllCourses().queryKey, }); queryClient.invalidateQueries({ - queryKey: assignmentQueryOptions().queryKey, + queryKey: assignmentQueries.getAssignmentSchedules().queryKey, }); alert('강의가 성공적으로 삭제되었습니다.'); }, From e2286644acae29eceb78a503b19ae34a651906c2 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Tue, 24 Feb 2026 17:28:18 +0900 Subject: [PATCH 11/48] =?UTF-8?q?#52=20feat:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AssignmentSelectPage.tsx | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index b84b174..5c86b09 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -1,24 +1,34 @@ import AssignmentListContainer from './ui/AssignmentListContainer'; import {useState} from 'react'; -import { - response, - courseOptionsResponse, -} from '@/shared/mocks/assignmentSelectResponse'; import {useCourseFilter} from '@/features/course/filter-course/lib/useCourseFilter'; import {AssignmentPageLayout} from '@/widgets/assignment-page-layout'; import ListRow from '@/shared/ui/list-row/ListRow'; +import {useQuery} from '@tanstack/react-query'; +import {courseQueries} from '@/entities/course/api/courseQueries'; +import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; const AssignmentSelectPage = () => { - const {courses} = courseOptionsResponse.response; // /courses/my API 응답 모킹 + const {data: courseList} = useQuery(courseQueries.getAllCourses()); const [selectedAssignments, setSelectedAssignments] = useState([]); // 선택된 문제 ID 목록 + const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( + courseList?.response.courses ?? [] + ); + const {data: allAssignments} = useQuery( + assignmentQueries.getAllAssignments() + ); + const {data: assignments} = useQuery( + assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) + ); - const {courseOptions, handleCourseSelect} = useCourseFilter(courses); - - // 문제 목록 /courses/{courseId}/assignments API 응답 모킹 - const assignmentList = response.response.courses.flatMap( + const filteredAssignments = assignments?.response.courses.flatMap( (course) => course.assignments ); + // 선택된 강의에 따라 보여줄 과제 목록 결정 + const assignmentList = selectedCourseId + ? (filteredAssignments ?? []) + : (allAssignments?.response.assignments ?? []); + // 문제 선택 핸들러 const handleAssignmentSelect = (assignmentId: number) => { setSelectedAssignments((prev) => { @@ -30,6 +40,8 @@ const AssignmentSelectPage = () => { }); }; + console.log('선택된 강의 ID:', selectedAssignments); + return ( Date: Thu, 26 Feb 2026 16:06:39 +0900 Subject: [PATCH 12/48] =?UTF-8?q?#52=20feat:=20=EB=8B=A8=EC=9B=90=20?= =?UTF-8?q?=ED=8F=BC=20=EC=9E=84=EC=8B=9C=20=EC=A0=80=EC=9E=A5=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20useUnitStore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/unit/model/useUnitStore.ts | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/entities/unit/model/useUnitStore.ts diff --git a/src/entities/unit/model/useUnitStore.ts b/src/entities/unit/model/useUnitStore.ts new file mode 100644 index 0000000..ea0def8 --- /dev/null +++ b/src/entities/unit/model/useUnitStore.ts @@ -0,0 +1,51 @@ +import type {Assignment} from '@/entities/assignment/model/types'; +import {create} from 'zustand'; +import {createJSONStorage, persist} from 'zustand/middleware'; + +interface UnitState { + title: string; + releaseDate: string; + dueDate: string; + assignments: Assignment[]; + storeFormData: (title: string, releaseDate: string, dueDate: string) => void; + resetStore: () => void; + setAssignments: (assignments: Assignment[]) => void; +} + +export const useUnitStore = create()( + persist( + (set) => ({ + title: '', + releaseDate: '', + dueDate: '', + assignments: [], + + // 단원 폼 임시 저장 + storeFormData: (title, releaseDate, dueDate) => + set({ + title: title, + releaseDate: releaseDate, + dueDate: dueDate, + }), + + // 선택된 과제 ID 저장 + setAssignments: (assignments) => set({assignments}), + + // 단원 폼 초기화 + resetStore: () => + set({title: '', releaseDate: '', dueDate: '', assignments: []}), + }), + { + name: 'unit-session-storage', + storage: createJSONStorage(() => sessionStorage), + partialize: (state) => ({ + title: state.title, + releaseDate: state.releaseDate, + dueDate: state.dueDate, + assignments: state.assignments, + }), + } + ) +); + +export default useUnitStore; From 2e6b2f48b81014ad762d983cd68ddf1cd89a7558 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Thu, 26 Feb 2026 16:16:27 +0900 Subject: [PATCH 13/48] =?UTF-8?q?#52=20feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20=EB=B3=B5=EA=B7=80=20?= =?UTF-8?q?=EC=8B=9C=20=ED=8F=BC=20=EC=83=81=ED=83=9C=20=EC=9C=A0=EC=A7=80?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/unit-editor/UnitEditorPage.tsx | 25 ++++++++--- src/pages/unit-editor/ui/UnitForm.tsx | 55 +++++++++++++++++++----- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/pages/unit-editor/UnitEditorPage.tsx b/src/pages/unit-editor/UnitEditorPage.tsx index 1e632b9..616354a 100644 --- a/src/pages/unit-editor/UnitEditorPage.tsx +++ b/src/pages/unit-editor/UnitEditorPage.tsx @@ -2,13 +2,14 @@ import {UnitList} from './ui/UnitList'; import {UnitForm} from './ui/UnitForm'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {unitQueries} from '@/entities/unit/api/unitQueries'; -import {useParams} from 'react-router-dom'; +import {useLocation, useParams} from 'react-router-dom'; import {useEffect, useState} from 'react'; import type {Mode} from './model/types'; import {unitMutations} from '@/entities/unit/api/unitMutations'; import type {TUnitFormSchema} from '@/entities/unit/model/types'; import {EmptyState} from '@/shared/ui/EmptyState'; import SurfaceCard from '@/shared/ui/SurfaceCard'; +import useUnitStore from '@/entities/unit/model/useUnitStore'; const UnitEditorPage = () => { const {id} = useParams(); // 강의 ID @@ -18,15 +19,24 @@ const UnitEditorPage = () => { const [currentIndex, setCurrentIndex] = useState(1); const {data: unitList} = useQuery(unitQueries.getUnitList(courseId)); const {data: unit} = useQuery(unitQueries.getUnitDetails(selectedUnitId)); + const {state} = useLocation(); + const {resetStore} = useUnitStore(); useEffect(() => { - if (unitList && unitList?.response.count !== 0 && mode === 'idle') { + if (state?.mode && mode === 'idle') { + setMode(state.mode); + setSelectedUnitId(state.unitId); + setCurrentIndex(state.currentIndex ?? 1); + return; + } + + if (unitList && unitList.response.count !== 0 && mode === 'idle') { setSelectedUnitId(unitList.response.units[0].id); setMode('editing'); // 편집 모드 - } else if (unitList?.response.count === 0 && mode === 'idle') { + } else if (unitList && unitList.response.count === 0 && mode === 'idle') { setMode('creating'); // 생성 모드 } - }, [unitList, mode]); + }, [unitList, mode, state]); const queryClient = useQueryClient(); const invalidateUnitList = () => { @@ -43,6 +53,7 @@ const UnitEditorPage = () => { invalidateUnitList(); setSelectedUnitId(data.response.id); setMode('editing'); // 생성 후 편집 모드로 전환 + resetStore(); // 단원 폼 초기화 alert('새 단원이 성공적으로 생성되었습니다.'); }, onError: (error) => { @@ -69,8 +80,8 @@ const UnitEditorPage = () => { ...unitMutations.deleteUnit, onSuccess: () => { invalidateUnitList(); - setMode('idle'); // 삭제 후 대기 모드로 전환 setSelectedUnitId(null); // 선택된 단원 초기화 + setMode('creating'); alert('단원이 성공적으로 삭제되었습니다.'); }, onError: (error) => { @@ -93,8 +104,8 @@ const UnitEditorPage = () => { }; // 단원 생성 핸들러 - const onCreateUnit = (unit: TUnitFormSchema) => { - addUnit({courseId: courseId, unit}); + const onCreateUnit = (unitForm: TUnitFormSchema) => { + addUnit({courseId, unitForm}); }; // 단원 업데이트 핸들러 diff --git a/src/pages/unit-editor/ui/UnitForm.tsx b/src/pages/unit-editor/ui/UnitForm.tsx index 66cbec5..f50e178 100644 --- a/src/pages/unit-editor/ui/UnitForm.tsx +++ b/src/pages/unit-editor/ui/UnitForm.tsx @@ -7,11 +7,12 @@ import {zodResolver} from '@hookform/resolvers/zod'; import {type UnitFormProps} from '../model/types'; import AddIcon from '@/assets/svg/addIcon.svg?react'; import {EmptyState} from '@/shared/ui/EmptyState'; -// import {useState} from 'react'; import { unitFormSchema, type TUnitFormSchema, } from '@/entities/unit/model/types'; +import {useLocation, useNavigate} from 'react-router-dom'; +import useUnitStore from '@/entities/unit/model/useUnitStore'; export const UnitForm = ({ unit, @@ -21,22 +22,34 @@ export const UnitForm = ({ onUpdateUnit, onDeleteUnit, }: UnitFormProps) => { - // const [assignmentIds, setAssignmentIds] = useState([]); + const location = useLocation(); + const navigate = useNavigate(); + const { + storeFormData, + resetStore, + title: storedTitle, + releaseDate: storedReleaseDate, + dueDate: storedDueDate, + assignments, + } = useUnitStore(); + + const assignmentIds = assignments.map((assignment) => assignment.id); const { register, handleSubmit, reset, + getValues, formState: {errors, isSubmitting}, } = useForm({ resolver: zodResolver(unitFormSchema), values: mode === 'creating' ? { - title: '', - releaseDate: '', - dueDate: '', - assignmentIds: [], + title: storedTitle, + releaseDate: storedReleaseDate, + dueDate: storedDueDate, + assignmentIds: assignmentIds, } : { title: unit?.title || '', @@ -64,9 +77,24 @@ export const UnitForm = ({ // 단원 편집 취소 핸들러 const handleCancel = () => { + resetStore(); // 폼 데이터 초기화 reset(); }; + // 문제 선택 페이지로 이동 핸들러 + const handleAssignmentSelect = () => { + const {title, releaseDate, dueDate} = getValues(); + storeFormData(title, releaseDate, dueDate); + navigate('/admin/assignments/select', { + state: { + mode, + unitId: unit?.id ?? null, + currentIndex: unitIndex, + backPath: location.pathname, + }, + }); + }; + return (
{/* 단원 편집 폼 */} @@ -124,17 +152,24 @@ export const UnitForm = ({

문제 등록

{/* 드래그 앤 드롭 가능한 문제 리스트 */} - {!unit || unit.assignmentCount === 0 ? ( + {mode === 'editing' && unit && unit.assignmentCount > 0 ? ( + + ) : assignments.length > 0 ? ( + + ) : ( 등록된 문제가 없습니다. - ) : ( - )} {/* 문제 연결 버튼 */}
- From 92c4b51a7279b5e2d8764dfad397795e7977cd7a Mon Sep 17 00:00:00 2001 From: suminb99 Date: Thu, 26 Feb 2026 16:17:41 +0900 Subject: [PATCH 14/48] =?UTF-8?q?#52=20feat:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EB=B0=A9=EC=8B=9D=20id=20->=20Assignment?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=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 --- .../AssignmentSelectPage.tsx | 43 ++++++++++++++----- .../ui/AssignmentListContainer.tsx | 9 ++-- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index 5c86b09..e437970 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -6,10 +6,19 @@ import ListRow from '@/shared/ui/list-row/ListRow'; import {useQuery} from '@tanstack/react-query'; import {courseQueries} from '@/entities/course/api/courseQueries'; import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; +import useUnitStore from '@/entities/unit/model/useUnitStore'; +import {useLocation, useNavigate} from 'react-router-dom'; +import type {Assignment} from '@/entities/assignment/model/types'; const AssignmentSelectPage = () => { + const navigate = useNavigate(); + const location = useLocation(); const {data: courseList} = useQuery(courseQueries.getAllCourses()); - const [selectedAssignments, setSelectedAssignments] = useState([]); // 선택된 문제 ID 목록 + const {setAssignments, assignments: currentSelectedAssignments} = + useUnitStore(); + const [selectedAssignments, setSelectedAssignments] = useState( + currentSelectedAssignments ?? [] + ); const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( courseList?.response.courses ?? [] ); @@ -30,17 +39,29 @@ const AssignmentSelectPage = () => { : (allAssignments?.response.assignments ?? []); // 문제 선택 핸들러 - const handleAssignmentSelect = (assignmentId: number) => { + const handleAssignmentSelect = (assignment: Assignment) => { setSelectedAssignments((prev) => { - if (prev.includes(assignmentId)) { - return prev.filter((id) => id !== assignmentId); // 선택 해제 - } else { - return [...prev, assignmentId]; // 선택 추가 + if (prev.some((a) => a.id === assignment.id)) { + return prev.filter((a) => a.id !== assignment.id); } + return [...prev, assignment]; }); }; - console.log('선택된 강의 ID:', selectedAssignments); + const returnToPreviousPage = () => { + navigate(location.state?.backPath ?? -1, { + state: { + mode: location.state?.mode, + unitId: location.state?.unitId, + currentIndex: location.state?.currentIndex, + }, + }); + }; + + const handleConfirm = () => { + setAssignments(selectedAssignments); + returnToPreviousPage(); + }; return ( { renderItem={(assignment) => ( a.id === assignment.id)} /> )} /> } - onCancel={() => {}} - onConfirm={() => {}} + onCancel={() => { + returnToPreviousPage(); + }} + onConfirm={handleConfirm} /> ); }; diff --git a/src/pages/select-assignment/ui/AssignmentListContainer.tsx b/src/pages/select-assignment/ui/AssignmentListContainer.tsx index 94eb164..aa67777 100644 --- a/src/pages/select-assignment/ui/AssignmentListContainer.tsx +++ b/src/pages/select-assignment/ui/AssignmentListContainer.tsx @@ -1,3 +1,4 @@ +import type {Assignment} from '@/entities/assignment/model/types'; import type {AssignmentSelectCourse} from '@/entities/course/model/types'; type T = AssignmentSelectCourse['assignments'][number]; @@ -6,7 +7,7 @@ interface AssignmentListContainerProps { items: T[]; renderItem: (item: T) => React.ReactNode; title: string; - onSelect: (id: number) => void; + onSelect: (item: Assignment) => void; } const AssignmentListContainer = ({ @@ -15,16 +16,16 @@ const AssignmentListContainer = ({ renderItem, title, }: AssignmentListContainerProps) => { - const handleSelect = (id: number, event: React.MouseEvent) => { + const handleSelect = (item: Assignment, event: React.MouseEvent) => { event.stopPropagation(); - onSelect(id); + onSelect(item); }; return (

{title}

    {items.map((item) => ( -
  • handleSelect(item.id, e)} key={item.id}> +
  • handleSelect(item, e)} key={item.id}> {renderItem(item)}
  • ))} From d4428d154ec938f2cf2b0ea925609ad038681371 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Thu, 26 Feb 2026 16:18:09 +0900 Subject: [PATCH 15/48] =?UTF-8?q?#52=20refactor:=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/unit/api/unitMutations.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/entities/unit/api/unitMutations.ts b/src/entities/unit/api/unitMutations.ts index a0e6c91..d114344 100644 --- a/src/entities/unit/api/unitMutations.ts +++ b/src/entities/unit/api/unitMutations.ts @@ -5,8 +5,13 @@ export const unitMutations = { // 단원 추가 뮤테이션 옵션 createUnit: { mutationKey: ['createUnit'], - mutationFn: ({courseId, unit}: {courseId: number; unit: TUnitFormSchema}) => - createUnit(courseId, unit), + mutationFn: ({ + courseId, + unitForm, + }: { + courseId: number; + unitForm: TUnitFormSchema; + }) => createUnit(courseId, unitForm), }, // 단원 수정 뮤테이션 옵션 From 004a045985bc156e97994e047aaab0c584c567f4 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Thu, 26 Feb 2026 22:47:00 +0900 Subject: [PATCH 16/48] =?UTF-8?q?#52=20feat:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EA=B3=BC=EC=A0=9C=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 6 +- .../assignment/api/assignmentMutations.ts | 8 ++ .../AssignmentManagePage.tsx | 94 +++++++++++++++++++ .../ui/AssignmentManageActionsBar.tsx | 38 ++++++++ 4 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/entities/assignment/api/assignmentMutations.ts create mode 100644 src/pages/manage-assignment/AssignmentManagePage.tsx create mode 100644 src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx diff --git a/src/App.tsx b/src/App.tsx index f9fe6d3..35305ce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import KakaoCallbackPage from '@/pages/common/KakaoCallbackPage'; import PrivateRoute from '@/widgets/private-route/ui/PrivateRoute'; import {useSyncUserRole} from '@/features/auth/sync-user-role/model/useSyncUserRole'; import UnitEditorPage from '@/pages/unit-editor/UnitEditorPage'; +import AssignmentManagePage from './pages/manage-assignment/AssignmentManagePage'; const AppRoutes = () => { useSyncUserRole(); @@ -37,7 +38,10 @@ const AppRoutes = () => { }> } /> - {/* } /> */} + } + /> } diff --git a/src/entities/assignment/api/assignmentMutations.ts b/src/entities/assignment/api/assignmentMutations.ts new file mode 100644 index 0000000..5ce85f1 --- /dev/null +++ b/src/entities/assignment/api/assignmentMutations.ts @@ -0,0 +1,8 @@ +import {deleteAssignment} from './assignmentApi'; + +export const assignmentMutations = { + deleteAssignment: { + mutationKey: ['deleteAssignment'], + mutationFn: (assignmentId: number) => deleteAssignment(assignmentId), + }, +}; diff --git a/src/pages/manage-assignment/AssignmentManagePage.tsx b/src/pages/manage-assignment/AssignmentManagePage.tsx new file mode 100644 index 0000000..342aba9 --- /dev/null +++ b/src/pages/manage-assignment/AssignmentManagePage.tsx @@ -0,0 +1,94 @@ +import {AssignmentPageLayout} from '@/widgets/assignment-page-layout'; +import AssignmentListContainer from '../select-assignment/ui/AssignmentListContainer'; +import ListRow from '@/shared/ui/list-row/ListRow'; +import {useCourseFilter} from '@/features/course/filter-course'; +import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; +import {courseQueries} from '@/entities/course/api/courseQueries'; +import {useAssignmentList} from '@/features/assignment/filter-assignmnet/lib/useAssignmentList'; +import {assignmentMutations} from '@/entities/assignment/api/assignmentMutations'; +import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; +import AssignmentManageActionsBar from './ui/AssignmentManageActionsBar'; +import AddIcon from '@/assets/svg/addIcon.svg?react'; +import {Link} from 'react-router-dom'; +import {buttonStyles} from '../../shared/ui/button/button-styles'; + +const AssignmentManagePage = () => { + const {data: courseList} = useQuery(courseQueries.getAllCourses()); + const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( + courseList?.response.courses ?? [] + ); + const assignmentList = useAssignmentList(selectedCourseId); + const queryClient = useQueryClient(); + + // 문제 삭제 뮤테이션 + const {mutate: deleteAssignment} = useMutation({ + ...assignmentMutations.deleteAssignment, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: assignmentQueries.getAllAssignments().queryKey, + }); + queryClient.invalidateQueries({ + queryKey: assignmentQueries.getAssignmentsByCourse( + selectedCourseId ?? 0 + ).queryKey, + }); + alert('문제가 성공적으로 삭제되었습니다.'); + }, + onError: (error) => { + console.error('문제 삭제 실패', error); + alert('문제 삭제에 실패했습니다. 다시 시도해주세요.'); + }, + }); + + // 문제 삭제 핸들러 + const onDeleteAssignment = (id: number) => { + if ( + window.confirm( + '문제를 삭제하시겠습니까? 삭제된 문제는 복구할 수 없습니다.' + ) + ) { + deleteAssignment(id); + } + }; + + return ( + + ( + + } + /> + )} + /> + + + 문제 추가 + + + } + onCancel={() => {}} + onConfirm={() => {}} + /> + ); +}; + +export default AssignmentManagePage; diff --git a/src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx b/src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx new file mode 100644 index 0000000..e1f4815 --- /dev/null +++ b/src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx @@ -0,0 +1,38 @@ +import EditIcon from '@/assets/svg/editIcon.svg?react'; +import DeleteIcon from '@/assets/svg/deleteIcon.svg?react'; +import {useNavigate} from 'react-router-dom'; + +interface AssignmentManageActionsBarProps { + id: number; + onDelete: (id: number) => void; +} + +const AssignmentManageActionsBar = ({ + id, + onDelete, +}: AssignmentManageActionsBarProps) => { + const navigate = useNavigate(); + + const handleOnEdit = () => { + navigate(`/admin/assignments/${id}`); + }; + + const handleOnDelete = () => { + onDelete(id); + }; + + return ( +
    e.stopPropagation()}> + + +
    + ); +}; + +export default AssignmentManageActionsBar; From df245ef3b8a6d8225b27031246ca5c4f9457d527 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Thu, 26 Feb 2026 22:47:36 +0900 Subject: [PATCH 17/48] =?UTF-8?q?#52=20refactor:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=ED=95=84=ED=84=B0=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20useAssignmentList=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/useAssignmentList.ts | 28 +++++++++++++++++++ .../AssignmentSelectPage.tsx | 19 +++---------- 2 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts diff --git a/src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts b/src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts new file mode 100644 index 0000000..2a5fe1d --- /dev/null +++ b/src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts @@ -0,0 +1,28 @@ +import {useQuery} from '@tanstack/react-query'; +import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; +import type {Assignment} from '@/entities/assignment/model/types'; + +// 중복 제거 +const unique = (list: Assignment[]) => + Array.from(new Map(list.map((a) => [a.id, a])).values()); + +export const useAssignmentList = ( + selectedCourseId: number | null +): Assignment[] => { + const {data: allAssignments} = useQuery( + assignmentQueries.getAllAssignments() + ); + const {data: assignments} = useQuery( + assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) + ); + + if (selectedCourseId) { + return unique( + assignments?.response.courses.flatMap((course) => + course.id === selectedCourseId ? course.assignments : [] + ) ?? [] + ); + } + + return unique(allAssignments?.response.assignments ?? []); +}; diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index e437970..15208fb 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -5,10 +5,10 @@ import {AssignmentPageLayout} from '@/widgets/assignment-page-layout'; import ListRow from '@/shared/ui/list-row/ListRow'; import {useQuery} from '@tanstack/react-query'; import {courseQueries} from '@/entities/course/api/courseQueries'; -import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; import useUnitStore from '@/entities/unit/model/useUnitStore'; import {useLocation, useNavigate} from 'react-router-dom'; import type {Assignment} from '@/entities/assignment/model/types'; +import {useAssignmentList} from '@/features/assignment/filter-assignmnet/lib/useAssignmentList'; const AssignmentSelectPage = () => { const navigate = useNavigate(); @@ -22,21 +22,8 @@ const AssignmentSelectPage = () => { const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( courseList?.response.courses ?? [] ); - const {data: allAssignments} = useQuery( - assignmentQueries.getAllAssignments() - ); - const {data: assignments} = useQuery( - assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) - ); - - const filteredAssignments = assignments?.response.courses.flatMap( - (course) => course.assignments - ); - // 선택된 강의에 따라 보여줄 과제 목록 결정 - const assignmentList = selectedCourseId - ? (filteredAssignments ?? []) - : (allAssignments?.response.assignments ?? []); + const assignmentList = useAssignmentList(selectedCourseId); // 문제 선택 핸들러 const handleAssignmentSelect = (assignment: Assignment) => { @@ -48,6 +35,7 @@ const AssignmentSelectPage = () => { }); }; + // 이전 페이지로 돌아가기 const returnToPreviousPage = () => { navigate(location.state?.backPath ?? -1, { state: { @@ -58,6 +46,7 @@ const AssignmentSelectPage = () => { }); }; + // 등록 핸들러 const handleConfirm = () => { setAssignments(selectedAssignments); returnToPreviousPage(); From e7ea6cbd54b41f2b4edddc61653f86a8a940dda9 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Thu, 26 Feb 2026 22:48:03 +0900 Subject: [PATCH 18/48] =?UTF-8?q?#52=20chore:=20ListRow=20=EB=B0=8F=20Assi?= =?UTF-8?q?gnmentListContainer=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/assignment/api/assignmentApi.ts | 2 -- src/pages/select-assignment/ui/AssignmentListContainer.tsx | 4 ++-- src/shared/ui/list-row/ListRow.tsx | 5 +++-- src/shared/ui/list-row/list-row-styles.ts | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/entities/assignment/api/assignmentApi.ts b/src/entities/assignment/api/assignmentApi.ts index e3af901..d118435 100644 --- a/src/entities/assignment/api/assignmentApi.ts +++ b/src/entities/assignment/api/assignmentApi.ts @@ -41,8 +41,6 @@ export const getAssignmentsByCourse = async ( courseId: number ): Promise => { const response = await privateAxios.get(`/courses/${courseId}/assignments`); - - console.log('getAssignmentsByCourse API 응답:', response.data); // 응답 데이터 로깅 return response.data; }; diff --git a/src/pages/select-assignment/ui/AssignmentListContainer.tsx b/src/pages/select-assignment/ui/AssignmentListContainer.tsx index aa67777..c652c5b 100644 --- a/src/pages/select-assignment/ui/AssignmentListContainer.tsx +++ b/src/pages/select-assignment/ui/AssignmentListContainer.tsx @@ -7,7 +7,7 @@ interface AssignmentListContainerProps { items: T[]; renderItem: (item: T) => React.ReactNode; title: string; - onSelect: (item: Assignment) => void; + onSelect?: (item: Assignment) => void; } const AssignmentListContainer = ({ @@ -18,7 +18,7 @@ const AssignmentListContainer = ({ }: AssignmentListContainerProps) => { const handleSelect = (item: Assignment, event: React.MouseEvent) => { event.stopPropagation(); - onSelect(item); + onSelect?.(item); }; return (
    diff --git a/src/shared/ui/list-row/ListRow.tsx b/src/shared/ui/list-row/ListRow.tsx index 91316f0..f81b247 100644 --- a/src/shared/ui/list-row/ListRow.tsx +++ b/src/shared/ui/list-row/ListRow.tsx @@ -1,10 +1,11 @@ import {ListRowStyles, type ListRowVariants} from './list-row-styles'; +import type {ReactNode} from 'react'; interface ListRowProps extends ListRowVariants { selected?: boolean; - leftIcon?: React.ReactNode; + leftIcon?: ReactNode; title: string; - rightIcon?: React.ReactNode; + rightIcon?: ReactNode; className?: string; } diff --git a/src/shared/ui/list-row/list-row-styles.ts b/src/shared/ui/list-row/list-row-styles.ts index 5b4135f..aea2286 100644 --- a/src/shared/ui/list-row/list-row-styles.ts +++ b/src/shared/ui/list-row/list-row-styles.ts @@ -1,7 +1,7 @@ import {tv, type VariantProps} from 'tailwind-variants'; export const ListRowStyles = tv({ - base: 'cursor-pointer bg-background w-full flex items-center rounded-[9px] pl-4.5 pr-7.5 py-4 gap-4 border', + base: 'bg-background w-full flex items-center rounded-[9px] pl-4.5 pr-7.5 py-4 gap-4 border', variants: { selected: { true: 'border-primary', From bbe393882a8aae5832bd841321ed6588149d34c8 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Fri, 27 Feb 2026 22:21:36 +0900 Subject: [PATCH 19/48] =?UTF-8?q?#52=20refactor:=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EB=A0=88=EC=9D=B4=EC=96=B4=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/unit/api/unitQueries.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/entities/unit/api/unitQueries.ts b/src/entities/unit/api/unitQueries.ts index ff10576..8eeaa7b 100644 --- a/src/entities/unit/api/unitQueries.ts +++ b/src/entities/unit/api/unitQueries.ts @@ -7,6 +7,11 @@ export const unitQueries = { queryOptions({ queryKey: ['units', courseId], queryFn: () => getAllUnitsByCourseId(courseId), + select: (data) => ({ + unitList: data.response.units, + unitCount: data.response.count, + firstUnitId: data.response.units[0]?.id ?? null, + }), }), // 단일 단원 조회 쿼리 옵션 @@ -15,5 +20,8 @@ export const unitQueries = { queryKey: ['units', 'detail', unitId], queryFn: () => getUnitById(unitId), enabled: !!unitId, + select: (data) => { + return data.response; + }, }), }; From 191e891c74140117ee3a246c6cd43337167782cd Mon Sep 17 00:00:00 2001 From: suminb99 Date: Fri, 27 Feb 2026 22:22:48 +0900 Subject: [PATCH 20/48] =?UTF-8?q?#52=20refactor:=20=EB=AA=A8=EB=93=9C/?= =?UTF-8?q?=EC=9D=B8=EB=8D=B1=EC=8A=A4=20=EA=B4=80=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=ED=8C=8C=EC=83=9D=20=EC=83=81=ED=83=9C=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/unit-editor/UnitEditorPage.tsx | 72 +++++++++++++----------- src/pages/unit-editor/model/types.ts | 4 +- src/pages/unit-editor/ui/UnitList.tsx | 7 +-- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/pages/unit-editor/UnitEditorPage.tsx b/src/pages/unit-editor/UnitEditorPage.tsx index 616354a..0f0f942 100644 --- a/src/pages/unit-editor/UnitEditorPage.tsx +++ b/src/pages/unit-editor/UnitEditorPage.tsx @@ -2,8 +2,8 @@ import {UnitList} from './ui/UnitList'; import {UnitForm} from './ui/UnitForm'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {unitQueries} from '@/entities/unit/api/unitQueries'; -import {useLocation, useParams} from 'react-router-dom'; -import {useEffect, useState} from 'react'; +import {useParams} from 'react-router-dom'; +import {useState} from 'react'; import type {Mode} from './model/types'; import {unitMutations} from '@/entities/unit/api/unitMutations'; import type {TUnitFormSchema} from '@/entities/unit/model/types'; @@ -14,29 +14,39 @@ import useUnitStore from '@/entities/unit/model/useUnitStore'; const UnitEditorPage = () => { const {id} = useParams(); // 강의 ID const courseId = Number(id); - const [mode, setMode] = useState('idle'); + + const { + resetStore, + title: storedTitle, + assignments: storedAssignments, + } = useUnitStore(); + const {data} = useQuery(unitQueries.getUnitList(courseId)); + const initialMode = + data === undefined ? null : data.unitCount === 0 ? 'creating' : 'editing'; + + const [activeMode, setActiveMode] = useState(null); const [selectedUnitId, setSelectedUnitId] = useState(null); - const [currentIndex, setCurrentIndex] = useState(1); - const {data: unitList} = useQuery(unitQueries.getUnitList(courseId)); - const {data: unit} = useQuery(unitQueries.getUnitDetails(selectedUnitId)); - const {state} = useLocation(); - const {resetStore} = useUnitStore(); - - useEffect(() => { - if (state?.mode && mode === 'idle') { - setMode(state.mode); - setSelectedUnitId(state.unitId); - setCurrentIndex(state.currentIndex ?? 1); - return; - } - if (unitList && unitList.response.count !== 0 && mode === 'idle') { - setSelectedUnitId(unitList.response.units[0].id); - setMode('editing'); // 편집 모드 - } else if (unitList && unitList.response.count === 0 && mode === 'idle') { - setMode('creating'); // 생성 모드 + const hasOngoingCreation = storedTitle !== '' || storedAssignments.length > 0; + const currentMode: Mode | null = + activeMode ?? (hasOngoingCreation ? 'creating' : initialMode); + const currentUnitId = + currentMode === 'creating' + ? null + : (selectedUnitId ?? data?.firstUnitId ?? null); + + // 현재 단원 인덱스 계산 + const currentIndex = (() => { + if (currentMode === 'creating') { + return (data?.unitCount ?? 0) + 1; // 항상 마지막 + 1 } - }, [unitList, mode, state]); + const index = data?.unitList?.findIndex((u) => u.id === currentUnitId); + return index !== undefined && index >= 0 ? index + 1 : 1; + })(); + + const {data: unitDetail} = useQuery( + unitQueries.getUnitDetails(currentUnitId) + ); const queryClient = useQueryClient(); const invalidateUnitList = () => { @@ -52,7 +62,7 @@ const UnitEditorPage = () => { // 단원 목록 갱신 invalidateUnitList(); setSelectedUnitId(data.response.id); - setMode('editing'); // 생성 후 편집 모드로 전환 + setActiveMode('editing'); // 생성 후 편집 모드로 전환 resetStore(); // 단원 폼 초기화 alert('새 단원이 성공적으로 생성되었습니다.'); }, @@ -81,7 +91,7 @@ const UnitEditorPage = () => { onSuccess: () => { invalidateUnitList(); setSelectedUnitId(null); // 선택된 단원 초기화 - setMode('creating'); + setActiveMode('creating'); alert('단원이 성공적으로 삭제되었습니다.'); }, onError: (error) => { @@ -93,14 +103,13 @@ const UnitEditorPage = () => { // 단원 선택 핸들러 const onUnitClick = (id: number) => { setSelectedUnitId(id); - setMode('editing'); // 편집 모드로 전환 + setActiveMode('editing'); // 편집 모드로 전환 }; // 단원 추가 핸들러 const onAddNewUnit = () => { setSelectedUnitId(null); - setCurrentIndex(unitList ? unitList.response.count + 1 : 1); // 새 단원의 인덱스 설정 - setMode('creating'); // 생성 모드로 전환 + setActiveMode('creating'); // 생성 모드로 전환 }; // 단원 생성 핸들러 @@ -130,10 +139,9 @@ const UnitEditorPage = () => { {/* 단원 목차 섹션 */} setCurrentIndex(index)} - selectedUnitId={selectedUnitId} + selectedUnitId={currentUnitId} onAddNewUnit={onAddNewUnit} /> @@ -141,9 +149,9 @@ const UnitEditorPage = () => { {/* 단원 폼 섹션 */} void; onUpdateUnit: (unitId: number, unit: TUnitFormSchema) => void; onDeleteUnit: (unitId: number) => void; diff --git a/src/pages/unit-editor/ui/UnitList.tsx b/src/pages/unit-editor/ui/UnitList.tsx index dab86a7..92a2107 100644 --- a/src/pages/unit-editor/ui/UnitList.tsx +++ b/src/pages/unit-editor/ui/UnitList.tsx @@ -8,7 +8,6 @@ interface UnitListProps { unitList: AllUnitsResponse['response']['units'] | undefined; onUnitClick: (id: number) => void; selectedUnitId?: number | null; - onChangeIndex: (index: number) => void; onAddNewUnit?: () => void; } @@ -16,15 +15,11 @@ export const UnitList = ({ unitList, onUnitClick, selectedUnitId, - onChangeIndex, + onAddNewUnit, }: UnitListProps) => { const handleSelectUnit = (id: number) => { onUnitClick(id); - - // 선택된 단원의 인덱스 찾기 - const index = unitList?.findIndex((unit) => unit.id === id) ?? 0; - onChangeIndex(index + 1); }; return ( From 7bf990a1c477aeb4f92f95dbbbd140bd939d64db Mon Sep 17 00:00:00 2001 From: suminb99 Date: Fri, 27 Feb 2026 22:26:30 +0900 Subject: [PATCH 21/48] =?UTF-8?q?#52=20refactor:=20=EA=B3=BC=EC=A0=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=EB=B0=8F=20form=20prop=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/unit-editor/ui/UnitForm.tsx | 28 ++++++++++++++------------- src/shared/ui/button/Button.tsx | 6 +++--- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pages/unit-editor/ui/UnitForm.tsx b/src/pages/unit-editor/ui/UnitForm.tsx index f50e178..b4ebedf 100644 --- a/src/pages/unit-editor/ui/UnitForm.tsx +++ b/src/pages/unit-editor/ui/UnitForm.tsx @@ -30,10 +30,14 @@ export const UnitForm = ({ title: storedTitle, releaseDate: storedReleaseDate, dueDate: storedDueDate, - assignments, + assignments: storedAssignments, } = useUnitStore(); - const assignmentIds = assignments.map((assignment) => assignment.id); + const currentAssignmentList = + mode === 'editing' ? (unit?.assignments ?? []) : storedAssignments; + + const assignmentListKey = + mode === 'editing' ? `edit-${unit?.id}` : 'creating-assignments'; const { register, @@ -49,7 +53,7 @@ export const UnitForm = ({ title: storedTitle, releaseDate: storedReleaseDate, dueDate: storedDueDate, - assignmentIds: assignmentIds, + assignmentIds: storedAssignments.map((a) => a.id), } : { title: unit?.title || '', @@ -59,7 +63,7 @@ export const UnitForm = ({ }); // 단원 생성/업데이트 핸들러 - const onSubmit = async (data: TUnitFormSchema) => { + const onSubmit = (data: TUnitFormSchema) => { if (mode === 'editing' && unit) { onUpdateUnit(unit.id, data); return; @@ -77,7 +81,7 @@ export const UnitForm = ({ // 단원 편집 취소 핸들러 const handleCancel = () => { - resetStore(); // 폼 데이터 초기화 + if (mode === 'creating') resetStore(); reset(); }; @@ -87,9 +91,6 @@ export const UnitForm = ({ storeFormData(title, releaseDate, dueDate); navigate('/admin/assignments/select', { state: { - mode, - unitId: unit?.id ?? null, - currentIndex: unitIndex, backPath: location.pathname, }, }); @@ -152,10 +153,11 @@ export const UnitForm = ({

    문제 등록

    {/* 드래그 앤 드롭 가능한 문제 리스트 */} - {mode === 'editing' && unit && unit.assignmentCount > 0 ? ( - - ) : assignments.length > 0 ? ( - + {currentAssignmentList.length > 0 ? ( + ) : ( 등록된 문제가 없습니다. @@ -185,7 +187,7 @@ export const UnitForm = ({ diff --git a/src/shared/ui/button/Button.tsx b/src/shared/ui/button/Button.tsx index b9320f2..5e62c0c 100644 --- a/src/shared/ui/button/Button.tsx +++ b/src/shared/ui/button/Button.tsx @@ -6,7 +6,7 @@ interface ButtonProps extends ButtonVariants { className?: string; type?: 'button' | 'submit'; disabled?: boolean; - formID?: string; + form?: string; onClick?: () => void; onMouseEnter?: () => void; onMouseLeave?: () => void; @@ -17,7 +17,7 @@ const Button = ({ onClick, type = 'button', disabled = false, - formID, + form, className, ...props }: ButtonProps) => { @@ -26,7 +26,7 @@ const Button = ({ type={type} onClick={onClick} disabled={disabled} - form={formID} + form={form} className={twMerge(buttonStyles(props), className)}> {children} From 52b6f148c769e3bdb8708394a7230ac6b473b75c Mon Sep 17 00:00:00 2001 From: suminb99 Date: Fri, 27 Feb 2026 22:26:39 +0900 Subject: [PATCH 22/48] =?UTF-8?q?#52=20chore:=20navigate=20state=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20import=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/manage-assignment/AssignmentManagePage.tsx | 2 +- src/pages/select-assignment/AssignmentSelectPage.tsx | 8 +------- .../select-assignment/ui/AssignmentListContainer.tsx | 5 +++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/pages/manage-assignment/AssignmentManagePage.tsx b/src/pages/manage-assignment/AssignmentManagePage.tsx index 342aba9..140d0bb 100644 --- a/src/pages/manage-assignment/AssignmentManagePage.tsx +++ b/src/pages/manage-assignment/AssignmentManagePage.tsx @@ -10,7 +10,7 @@ import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; import AssignmentManageActionsBar from './ui/AssignmentManageActionsBar'; import AddIcon from '@/assets/svg/addIcon.svg?react'; import {Link} from 'react-router-dom'; -import {buttonStyles} from '../../shared/ui/button/button-styles'; +import {buttonStyles} from '@/shared/ui/button/button-styles'; const AssignmentManagePage = () => { const {data: courseList} = useQuery(courseQueries.getAllCourses()); diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index 15208fb..38538d9 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -37,13 +37,7 @@ const AssignmentSelectPage = () => { // 이전 페이지로 돌아가기 const returnToPreviousPage = () => { - navigate(location.state?.backPath ?? -1, { - state: { - mode: location.state?.mode, - unitId: location.state?.unitId, - currentIndex: location.state?.currentIndex, - }, - }); + navigate(location.state?.backPath ?? -1); }; // 등록 핸들러 diff --git a/src/pages/select-assignment/ui/AssignmentListContainer.tsx b/src/pages/select-assignment/ui/AssignmentListContainer.tsx index c652c5b..93129ca 100644 --- a/src/pages/select-assignment/ui/AssignmentListContainer.tsx +++ b/src/pages/select-assignment/ui/AssignmentListContainer.tsx @@ -1,11 +1,12 @@ import type {Assignment} from '@/entities/assignment/model/types'; import type {AssignmentSelectCourse} from '@/entities/course/model/types'; +import type {MouseEvent, ReactNode} from 'react'; type T = AssignmentSelectCourse['assignments'][number]; interface AssignmentListContainerProps { items: T[]; - renderItem: (item: T) => React.ReactNode; + renderItem: (item: T) => ReactNode; title: string; onSelect?: (item: Assignment) => void; } @@ -16,7 +17,7 @@ const AssignmentListContainer = ({ renderItem, title, }: AssignmentListContainerProps) => { - const handleSelect = (item: Assignment, event: React.MouseEvent) => { + const handleSelect = (item: Assignment, event: MouseEvent) => { event.stopPropagation(); onSelect?.(item); }; From 47eda8a81348b697f1715350ebe32f6260f7046a Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 01:51:07 +0900 Subject: [PATCH 23/48] =?UTF-8?q?#62=20chore:=20Zod=20=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=EB=A7=88=20=EA=B8=B0=EB=B0=98=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/assignment/api/assignmentApi.ts | 70 +++++------ src/entities/assignment/model/schemas.ts | 20 ++++ src/entities/assignment/model/types.ts | 17 +-- src/entities/auth/api/authApi.ts | 12 +- src/entities/auth/model/schemas.ts | 10 ++ src/entities/course/api/courseApi.ts | 17 +-- src/entities/course/model/schemas.ts | 38 ++++++ src/entities/course/model/types.ts | 117 +++---------------- src/entities/student/model/schemas.ts | 35 ++++++ src/entities/student/model/types.ts | 39 ++----- src/entities/unit/api/unitApi.ts | 47 ++++---- src/entities/unit/model/types.ts | 14 +++ src/shared/model/index.ts | 1 + src/shared/model/schemas.ts | 17 +++ 14 files changed, 226 insertions(+), 228 deletions(-) create mode 100644 src/entities/assignment/model/schemas.ts create mode 100644 src/entities/auth/model/schemas.ts create mode 100644 src/entities/course/model/schemas.ts create mode 100644 src/entities/student/model/schemas.ts create mode 100644 src/shared/model/schemas.ts diff --git a/src/entities/assignment/api/assignmentApi.ts b/src/entities/assignment/api/assignmentApi.ts index d118435..c415c47 100644 --- a/src/entities/assignment/api/assignmentApi.ts +++ b/src/entities/assignment/api/assignmentApi.ts @@ -1,53 +1,45 @@ -import type { - AssignmentSelectResponse, - DashboardScheduleListResponse, -} from '@/entities/course/model/types'; +import {z} from 'zod'; import {privateAxios} from '@/shared/api/axiosInstance'; -import type {ApiResponse} from '@/shared/model'; -import type {AssignmentsResponse} from '../model/types'; +import {apiResponseSchema} from '@/shared/model/schemas'; +import {assignmentScheduleSchema} from '../model/schemas'; +import {assignmentSelectCourseSchema} from '@/entities/course/model/schemas'; // 과제 일정 조회 API -export const getAssignmentSchedules = - async (): Promise => { - const response = await privateAxios.get('/assignments/schedule'); - return response.data; - }; +export const getAssignmentSchedules = async () => { + const response = await privateAxios.get('/assignments/schedule'); + return apiResponseSchema( + z.object({count: z.number(), schedule: z.array(assignmentScheduleSchema)}) + ).parse(response.data); +}; // 전체 과제 목록 조회 API -export const getAllAssignments = async (): Promise< - ApiResponse -> => { - const response = await privateAxios.get< - ApiResponse<{ - count: number; - assignments: {assignmentId: number; title: string}[]; - }> - >('/assignments/my'); - const raw = response.data; - return { - ...raw, - response: { - count: raw.response.count, - assignments: raw.response.assignments.map(({assignmentId, title}) => ({ - id: assignmentId, - title, - })), - }, - }; +export const getAllAssignments = async () => { + const response = await privateAxios.get('/assignments/my'); + return apiResponseSchema( + z.object({ + count: z.number(), + assignments: z.array( + z + .object({assignmentId: z.number(), title: z.string()}) + .transform(({assignmentId, title}) => ({id: assignmentId, title})) + ), + }) + ).parse(response.data); }; // 강의별 과제 목록 조회 API -export const getAssignmentsByCourse = async ( - courseId: number -): Promise => { +export const getAssignmentsByCourse = async (courseId: number) => { const response = await privateAxios.get(`/courses/${courseId}/assignments`); - return response.data; + return apiResponseSchema( + z.object({ + count: z.number(), + courses: z.array(assignmentSelectCourseSchema), + }) + ).parse(response.data); }; // 과제 삭제 API -export const deleteAssignment = async ( - assignmentId: number -): Promise> => { +export const deleteAssignment = async (assignmentId: number) => { const response = await privateAxios.delete(`/assignments/${assignmentId}`); - return response.data; + return apiResponseSchema(z.string()).parse(response.data); }; diff --git a/src/entities/assignment/model/schemas.ts b/src/entities/assignment/model/schemas.ts new file mode 100644 index 0000000..66da9d5 --- /dev/null +++ b/src/entities/assignment/model/schemas.ts @@ -0,0 +1,20 @@ +import {submissionStatusSchema} from '@/shared/model/schemas'; +import z from 'zod'; + +export const assignmentSchema = z.object({ + id: z.number(), + title: z.string(), + submittedStatus: submissionStatusSchema.optional(), +}); + +export const assignmentScheduleSchema = z.object({ + date: z.string(), + remainingDays: z.number(), + assignments: z.array( + z.object({ + course: z.string(), + section: z.string(), + assignment: z.string(), + }) + ), +}); diff --git a/src/entities/assignment/model/types.ts b/src/entities/assignment/model/types.ts index bdbf0a8..fdef1eb 100644 --- a/src/entities/assignment/model/types.ts +++ b/src/entities/assignment/model/types.ts @@ -1,15 +1,4 @@ -import type {SubmissionStatus} from '@/shared/model/common'; +import type {z} from 'zod'; +import type {assignmentSchema} from './schemas'; -/** - * 과제(Assignment) 인터페이스 정의 - */ -export interface Assignment { - id: number; - title: string; - submittedStatus?: SubmissionStatus; -} - -export interface AssignmentsResponse { - count: number; - assignments: Assignment[]; -} +export type Assignment = z.infer; diff --git a/src/entities/auth/api/authApi.ts b/src/entities/auth/api/authApi.ts index 72c46a2..25cfaa3 100644 --- a/src/entities/auth/api/authApi.ts +++ b/src/entities/auth/api/authApi.ts @@ -1,14 +1,6 @@ import {publicAxios} from '@/shared/api/axiosInstance'; import type {UserType} from '@/shared/model/common'; - -interface KakaoLoginApiResponse { - memberId: number; - name: string; - role: 'ADMIN' | 'USER'; - studentId: string; - email: string; - accessToken: string; -} +import {kakaoLoginResponseSchema} from '../model/schemas'; export const kakaoLogin = async ( oAuthToken: string, @@ -21,7 +13,7 @@ export const kakaoLogin = async ( ...(studentId && {studentId}), OAuthToken: oAuthToken, }); - const data: KakaoLoginApiResponse = response.data.response; + const data = kakaoLoginResponseSchema.parse(response.data.response); return { userName: data.name, userType: (data.role === 'ADMIN' ? 'admin' : 'student') as Exclude< diff --git a/src/entities/auth/model/schemas.ts b/src/entities/auth/model/schemas.ts new file mode 100644 index 0000000..260c6bf --- /dev/null +++ b/src/entities/auth/model/schemas.ts @@ -0,0 +1,10 @@ +import {z} from 'zod'; + +export const kakaoLoginResponseSchema = z.object({ + memberId: z.number(), + name: z.string(), + role: z.enum(['ADMIN', 'USER']), + studentId: z.string(), + email: z.string().nullable(), + accessToken: z.string(), +}); diff --git a/src/entities/course/api/courseApi.ts b/src/entities/course/api/courseApi.ts index a1afec0..2251335 100644 --- a/src/entities/course/api/courseApi.ts +++ b/src/entities/course/api/courseApi.ts @@ -1,17 +1,18 @@ -import type {ApiResponse} from '@/shared/model/common'; -import type {DashboardCourseListResponse} from '@/entities/course/model/types'; +import {z} from 'zod'; import {privateAxios} from '@/shared/api/axiosInstance'; +import {apiResponseSchema} from '@/shared/model/schemas'; +import {dashboardCourseSchema} from '../model/schemas'; // 전체 강의 목록 조회 API -export const getAllCourses = async (): Promise => { +export const getAllCourses = async () => { const response = await privateAxios.get('/courses/my'); - return response.data; + return apiResponseSchema( + z.object({count: z.number(), courses: z.array(dashboardCourseSchema)}) + ).parse(response.data); }; // 강의 삭제 API -export const deleteCourse = async ( - courseId: number -): Promise> => { +export const deleteCourse = async (courseId: number) => { const response = await privateAxios.delete(`/courses/${courseId}`); - return response.data; + return apiResponseSchema(z.string()).parse(response.data); }; diff --git a/src/entities/course/model/schemas.ts b/src/entities/course/model/schemas.ts new file mode 100644 index 0000000..792d379 --- /dev/null +++ b/src/entities/course/model/schemas.ts @@ -0,0 +1,38 @@ +import {z} from 'zod'; +import {semesterCodeSchema} from '@/shared/model/schemas'; +import {assignmentSchema, assignmentScheduleSchema} from '@/entities/assignment/model/schemas'; +import {unitSchema} from '@/entities/unit/model/types'; + +export const courseOverviewSchema = z.object({ + id: z.number(), + title: z.string(), + year: z.number(), + semester: semesterCodeSchema, + section: z.string(), + unitCount: z.number(), + studentCount: z.number().optional(), + units: z.array(unitSchema), +}); + +export const dashboardCourseSchema = z.object({ + id: z.number(), + title: z.string(), + year: z.number(), + semester: semesterCodeSchema, + section: z.string(), + unitCount: z.number(), + description: z.string(), + assignmentCount: z.number(), +}); + +export const assignmentSelectCourseSchema = z.object({ + id: z.number(), + title: z.string(), + year: z.number(), + semester: semesterCodeSchema, + section: z.string(), + count: z.number(), + assignments: z.array(assignmentSchema.pick({id: true, title: true})), +}); + +export {assignmentScheduleSchema as scheduleSchema}; diff --git a/src/entities/course/model/types.ts b/src/entities/course/model/types.ts index 9ca93c8..87f1a15 100644 --- a/src/entities/course/model/types.ts +++ b/src/entities/course/model/types.ts @@ -1,102 +1,15 @@ -import type {Assignment} from '@/entities/assignment/model/types'; -import type {ApiResponse, SemesterCode} from '@/shared/model/common'; - -/** - * 일정(Schedule) 인터페이스 정의 - */ -export interface Schedule { - date: string; - remainingDays: number; - assignments: { - course: string; - section: string; - assignment: string; - }[]; -} - -/** - * 단원(Unit) 인터페이스 정의 - */ -export interface Unit { - id: number; - title: string; - releaseDate: string; - dueDate: string; - isOpen?: boolean; - assignmentCount: number; - assignments: Assignment[]; -} - -/** - * 강의 기본 정보 인터페이스 정의 - */ -export interface BaseCourse { - id: number; - title: string; - year: number; - semester: SemesterCode; - section: string; - unitCount: number; -} - -/** - * 강의 상세 페이지용 (course-overview) 인터페이스 정의 - * 기본 정보 + 단원, 과제 상세 리스트 - */ -export interface CourseOverview extends BaseCourse { - studentCount?: number; - units: Unit[]; -} - -/** - * 대시보드 강의 목록용 인터페이스 정의 - * 기본 정보 + 강의 설명, 과제 개수 - */ -export interface DashboardCourse extends BaseCourse { - description: string; - assignmentCount: number; -} - -/** - * 문제 선택 페이지용 강의 인터페이스 정의 - */ -export interface AssignmentSelectCourse extends Omit { - count: number; - assignments: Pick[]; -} - -// 강의 상세 응답 타입 정의 -export type CourseOverviewResponse = ApiResponse; - -// 대시보드 강의 목록 응답 타입 정의 -export type DashboardCourseListResponse = ApiResponse<{ - count: number; - courses: DashboardCourse[]; -}>; - -// 대시보드 일정 목록 응답 타입 정의 -export type DashboardScheduleListResponse = ApiResponse<{ - count: number; - schedule: Schedule[]; -}>; - -// 문제 선택 페이지 응답 타입 정의 -export type AssignmentSelectResponse = ApiResponse<{ - count: number; - courses: AssignmentSelectCourse[]; -}>; - -// 강의 옵션 목록 응답 타입 정의 -export type CourseOptionsResponse = ApiResponse<{ - count: number; - courses: DashboardCourse[]; -}>; - -// 단원 조회-생성 페이지 응답 타입 정의 - 단일 단원 -export type SingleUnitResponse = ApiResponse; - -// 단원 조회-생성 페이지 응답 타입 정의 - 전체 단원 목록 -export type AllUnitsResponse = ApiResponse<{ - count: number; - units: Pick[]; -}>; +import type {z} from 'zod'; +import type { + courseOverviewSchema, + dashboardCourseSchema, + assignmentSelectCourseSchema, + scheduleSchema, +} from './schemas'; +export type {Unit} from '@/entities/unit/model/types'; + +export type Schedule = z.infer; +export type CourseOverview = z.infer; +export type DashboardCourse = z.infer; +export type AssignmentSelectCourse = z.infer< + typeof assignmentSelectCourseSchema +>; diff --git a/src/entities/student/model/schemas.ts b/src/entities/student/model/schemas.ts new file mode 100644 index 0000000..a038be9 --- /dev/null +++ b/src/entities/student/model/schemas.ts @@ -0,0 +1,35 @@ +import {z} from 'zod'; + +export const progressStatusSchema = z.enum([ + 'PASSED', + 'NOT_SUBMITTED', + 'PARTIAL', + 'FAILED', +]); + +export const studentProgressSchema = z.object({ + status: progressStatusSchema, + assignmentName: z.string().optional(), + score: z.number().optional(), + totalScore: z.number().optional(), + plagiarismRate: z.number().optional(), +}); + +export const studentUnitSchema = z.object({ + id: z.number(), + name: z.string(), + title: z.string(), + startDate: z.string(), + endDate: z.string(), + assignments: z.array(studentProgressSchema), +}); + +export const studentSchema = z.object({ + id: z.number(), + studentId: z.string(), + name: z.string(), + score: z.number(), + totalScore: z.number(), + progress: z.array(studentProgressSchema), + units: z.array(studentUnitSchema).optional(), +}); diff --git a/src/entities/student/model/types.ts b/src/entities/student/model/types.ts index 0a5fc2b..e4d64e8 100644 --- a/src/entities/student/model/types.ts +++ b/src/entities/student/model/types.ts @@ -1,33 +1,16 @@ +import type {z} from 'zod'; import type {ApiResponse} from '@/shared/model/common'; +import type { + progressStatusSchema, + studentProgressSchema, + studentUnitSchema, + studentSchema, +} from './schemas'; -export type ProgressStatus = 'PASSED' | 'NOT_SUBMITTED' | 'PARTIAL' | 'FAILED'; - -export interface StudentProgress { - status: ProgressStatus; - assignmentName?: string; - score?: number; - totalScore?: number; - plagiarismRate?: number; -} - -export interface Unit { - id: number; - name: string; - title: string; - startDate: string; - endDate: string; - assignments: StudentProgress[]; -} - -export interface Student { - id: number; - studentId: string; - name: string; - score: number; - totalScore: number; - progress: StudentProgress[]; - units?: Unit[]; -} +export type ProgressStatus = z.infer; +export type StudentProgress = z.infer; +export type Unit = z.infer; +export type Student = z.infer; export type CourseStudentsResponse = ApiResponse<{ id: number; diff --git a/src/entities/unit/api/unitApi.ts b/src/entities/unit/api/unitApi.ts index 428720c..a1c26ee 100644 --- a/src/entities/unit/api/unitApi.ts +++ b/src/entities/unit/api/unitApi.ts @@ -1,57 +1,50 @@ +import {z} from 'zod'; import {privateAxios} from '@/shared/api/axiosInstance'; -import type {TUnitFormSchema} from '../model/types'; -import type {ApiResponse} from '@/shared/model'; -import type {AllUnitsResponse, Unit} from '@/entities/course/model/types'; +import {apiResponseSchema} from '@/shared/model/schemas'; +import {unitSchema, type TUnitFormSchema} from '../model/types'; // 강의별 전체 단원 조회 -export const getAllUnitsByCourseId = async ( - courseId: number -): Promise => { +export const getAllUnitsByCourseId = async (courseId: number) => { const response = await privateAxios.get(`/courses/${courseId}/units`); - return response.data; + return apiResponseSchema( + z.object({ + count: z.number(), + units: z.array(unitSchema.pick({id: true, title: true, assignmentCount: true})), + }) + ).parse(response.data); }; // 단일 단원 조회 -export const getUnitById = async ( - unitId: number | null -): Promise> => { +export const getUnitById = async (unitId: number | null) => { const response = await privateAxios.get(`/units/${unitId}`); - return response.data; + return apiResponseSchema(unitSchema).parse(response.data); }; // 단원 삭제 -export const deleteUnit = async ( - unitId: number -): Promise> => { +export const deleteUnit = async (unitId: number) => { const response = await privateAxios.delete(`/units/${unitId}`); - return response.data; + return apiResponseSchema(z.string()).parse(response.data); }; // 단원 생성 -export const createUnit = async ( - courseId: number, - unit: TUnitFormSchema -): Promise> => { +export const createUnit = async (courseId: number, unit: TUnitFormSchema) => { const response = await privateAxios.post(`/units/${courseId}`, unit); - return response.data; + return apiResponseSchema(unitSchema).parse(response.data); }; // 단원 수정 -export const updateUnit = async ( - unitId: number, - unit: TUnitFormSchema -): Promise> => { +export const updateUnit = async (unitId: number, unit: TUnitFormSchema) => { const response = await privateAxios.put(`/units/${unitId}`, unit); - return response.data; + return apiResponseSchema(unitSchema).parse(response.data); }; // 단원에 등록된 과제 삭제 export const deleteAssignmentFromUnit = async ( unitId: number, assignmentId: number -): Promise> => { +) => { const response = await privateAxios.delete( `/units/${unitId}/assignments/${assignmentId}` ); - return response.data; + return apiResponseSchema(z.string()).parse(response.data); }; diff --git a/src/entities/unit/model/types.ts b/src/entities/unit/model/types.ts index d6b1c63..2c36b1b 100644 --- a/src/entities/unit/model/types.ts +++ b/src/entities/unit/model/types.ts @@ -1,4 +1,18 @@ import {z} from 'zod'; +import {assignmentSchema} from '@/entities/assignment/model/schemas'; + +// 단원 도메인 스키마 +export const unitSchema = z.object({ + id: z.number(), + title: z.string(), + releaseDate: z.string(), + dueDate: z.string(), + isOpen: z.boolean().optional(), + assignmentCount: z.number(), + assignments: z.array(assignmentSchema), +}); + +export type Unit = z.infer; // 단원 생성/수정 폼 스키마 export const unitFormSchema = z diff --git a/src/shared/model/index.ts b/src/shared/model/index.ts index 7ec0c87..49c528f 100644 --- a/src/shared/model/index.ts +++ b/src/shared/model/index.ts @@ -1 +1,2 @@ export type {ApiResponse, UserType} from './common'; +export {apiResponseSchema} from './schemas'; diff --git a/src/shared/model/schemas.ts b/src/shared/model/schemas.ts new file mode 100644 index 0000000..2de84f4 --- /dev/null +++ b/src/shared/model/schemas.ts @@ -0,0 +1,17 @@ +import {z} from 'zod'; + +export const apiResponseSchema = (dataSchema: T) => + z.object({success: z.boolean(), response: dataSchema}); + +export const semesterCodeSchema = z.enum([ + 'FIRST', + 'SECOND', + 'SUMMER', + 'WINTER', +]); + +export const submissionStatusSchema = z.enum([ + 'NOT_SUBMITTED', + 'CORRECT', + 'INCORRECT', +]); From bd80094495a98609113e4499ab056cae4206337f Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 02:09:02 +0900 Subject: [PATCH 24/48] =?UTF-8?q?#62=20chore:=20common.ts=20->=20type.ts?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/auth/api/authApi.ts | 2 +- src/entities/auth/model/useUserStore.ts | 2 +- src/entities/student/model/types.ts | 2 +- src/pages/course-overview/ui/CourseHero.tsx | 6 +++--- src/shared/lib/course.ts | 2 +- src/shared/model/index.ts | 2 +- src/shared/model/{common.ts => type.ts} | 2 -- 7 files changed, 8 insertions(+), 10 deletions(-) rename src/shared/model/{common.ts => type.ts} (85%) diff --git a/src/entities/auth/api/authApi.ts b/src/entities/auth/api/authApi.ts index 25cfaa3..da80a9b 100644 --- a/src/entities/auth/api/authApi.ts +++ b/src/entities/auth/api/authApi.ts @@ -1,5 +1,5 @@ import {publicAxios} from '@/shared/api/axiosInstance'; -import type {UserType} from '@/shared/model/common'; +import type {UserType} from '@/shared/model/type'; import {kakaoLoginResponseSchema} from '../model/schemas'; export const kakaoLogin = async ( diff --git a/src/entities/auth/model/useUserStore.ts b/src/entities/auth/model/useUserStore.ts index 2a08cb8..e64cf29 100644 --- a/src/entities/auth/model/useUserStore.ts +++ b/src/entities/auth/model/useUserStore.ts @@ -1,6 +1,6 @@ import {create} from 'zustand'; import {persist} from 'zustand/middleware'; -import type {UserType} from '@/shared/model/common'; +import type {UserType} from '@/shared/model/type'; type AuthenticatedUserType = Exclude; diff --git a/src/entities/student/model/types.ts b/src/entities/student/model/types.ts index e4d64e8..4eb8e00 100644 --- a/src/entities/student/model/types.ts +++ b/src/entities/student/model/types.ts @@ -1,5 +1,5 @@ import type {z} from 'zod'; -import type {ApiResponse} from '@/shared/model/common'; +import type {ApiResponse} from '@/shared/model/type'; import type { progressStatusSchema, studentProgressSchema, diff --git a/src/pages/course-overview/ui/CourseHero.tsx b/src/pages/course-overview/ui/CourseHero.tsx index 81a7ff7..553db7f 100644 --- a/src/pages/course-overview/ui/CourseHero.tsx +++ b/src/pages/course-overview/ui/CourseHero.tsx @@ -3,7 +3,7 @@ import {formatCourseTerm} from '@/shared/lib/course'; import CourseActionsBar from './CourseActionsBar'; import {useUserStore} from '@/entities/auth/model/useUserStore'; import type {CourseOverview} from '@/entities/course/model/types'; -import type {SemesterCode} from '@/shared/model/common'; +import type {SemesterCode} from '@/shared/model/type'; interface CourseHeroProps { courseData: Omit; @@ -66,8 +66,8 @@ const CourseInfo = ({title, year, semester, section}: CourseInfoProps) => { return (
    snowCode logo -

    {title}

    -

    +

    {title}

    +

    {formatCourseTerm(year, semester, section)}

    diff --git a/src/shared/lib/course.ts b/src/shared/lib/course.ts index d4eb59c..f966bdb 100644 --- a/src/shared/lib/course.ts +++ b/src/shared/lib/course.ts @@ -1,5 +1,5 @@ import type {Unit} from '@/entities/course/model/types'; -import type {SemesterCode} from '@/shared/model/common'; +import type {SemesterCode} from '@/shared/model/type'; const SEMESTER_MAP: Record = { FIRST: '1', diff --git a/src/shared/model/index.ts b/src/shared/model/index.ts index 49c528f..0c66885 100644 --- a/src/shared/model/index.ts +++ b/src/shared/model/index.ts @@ -1,2 +1,2 @@ -export type {ApiResponse, UserType} from './common'; +export type {ApiResponse, UserType} from './type'; export {apiResponseSchema} from './schemas'; diff --git a/src/shared/model/common.ts b/src/shared/model/type.ts similarity index 85% rename from src/shared/model/common.ts rename to src/shared/model/type.ts index 9842ae5..3047837 100644 --- a/src/shared/model/common.ts +++ b/src/shared/model/type.ts @@ -8,7 +8,5 @@ export interface ApiResponse { } export type UserType = 'admin' | 'student' | 'guest'; - -// 학기 및 제출 상태 상수 타입 정의 export type SemesterCode = 'FIRST' | 'SECOND' | 'SUMMER' | 'WINTER'; export type SubmissionStatus = 'NOT_SUBMITTED' | 'CORRECT' | 'INCORRECT'; From 69590375aec69d3a41c2f554cd3d35baefe09f20 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 02:09:23 +0900 Subject: [PATCH 25/48] =?UTF-8?q?#62=20feat:=20errorResponseSchema=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 --- src/shared/model/schemas.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/shared/model/schemas.ts b/src/shared/model/schemas.ts index 2de84f4..8aa6f72 100644 --- a/src/shared/model/schemas.ts +++ b/src/shared/model/schemas.ts @@ -3,6 +3,18 @@ import {z} from 'zod'; export const apiResponseSchema = (dataSchema: T) => z.object({success: z.boolean(), response: dataSchema}); +export const errorResponseSchema = apiResponseSchema( + z.object({ + errorCode: z.string(), + errorMessage: z.string(), + errors: z.object({ + additionalProp1: z.string(), + additionalProp2: z.string(), + additionalProp3: z.string(), + }), + }) +); + export const semesterCodeSchema = z.enum([ 'FIRST', 'SECOND', From 15eb5025ba83bcfb163833a9329f5df942a187a5 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 16:01:32 +0900 Subject: [PATCH 26/48] =?UTF-8?q?#52=20fix:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- .../lib/useAssignmentList.ts | 0 src/pages/manage-assignment/AssignmentManagePage.tsx | 2 +- src/pages/select-assignment/AssignmentSelectPage.tsx | 3 ++- src/pages/unit-editor/UnitEditorPage.tsx | 9 ++++++++- 5 files changed, 12 insertions(+), 4 deletions(-) rename src/features/assignment/{filter-assignmnet => filter-assignment}/lib/useAssignmentList.ts (100%) diff --git a/src/App.tsx b/src/App.tsx index 35305ce..05debee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import KakaoCallbackPage from '@/pages/common/KakaoCallbackPage'; import PrivateRoute from '@/widgets/private-route/ui/PrivateRoute'; import {useSyncUserRole} from '@/features/auth/sync-user-role/model/useSyncUserRole'; import UnitEditorPage from '@/pages/unit-editor/UnitEditorPage'; -import AssignmentManagePage from './pages/manage-assignment/AssignmentManagePage'; +import AssignmentManagePage from '@/pages/manage-assignment/AssignmentManagePage'; const AppRoutes = () => { useSyncUserRole(); diff --git a/src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts b/src/features/assignment/filter-assignment/lib/useAssignmentList.ts similarity index 100% rename from src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts rename to src/features/assignment/filter-assignment/lib/useAssignmentList.ts diff --git a/src/pages/manage-assignment/AssignmentManagePage.tsx b/src/pages/manage-assignment/AssignmentManagePage.tsx index 140d0bb..f1a0b8f 100644 --- a/src/pages/manage-assignment/AssignmentManagePage.tsx +++ b/src/pages/manage-assignment/AssignmentManagePage.tsx @@ -4,7 +4,7 @@ import ListRow from '@/shared/ui/list-row/ListRow'; import {useCourseFilter} from '@/features/course/filter-course'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {courseQueries} from '@/entities/course/api/courseQueries'; -import {useAssignmentList} from '@/features/assignment/filter-assignmnet/lib/useAssignmentList'; +import {useAssignmentList} from '@/features/assignment/filter-assignment/lib/useAssignmentList'; import {assignmentMutations} from '@/entities/assignment/api/assignmentMutations'; import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; import AssignmentManageActionsBar from './ui/AssignmentManageActionsBar'; diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index 38538d9..ea5c89f 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -8,7 +8,7 @@ import {courseQueries} from '@/entities/course/api/courseQueries'; import useUnitStore from '@/entities/unit/model/useUnitStore'; import {useLocation, useNavigate} from 'react-router-dom'; import type {Assignment} from '@/entities/assignment/model/types'; -import {useAssignmentList} from '@/features/assignment/filter-assignmnet/lib/useAssignmentList'; +import {useAssignmentList} from '@/features/assignment/filter-assignment/lib/useAssignmentList'; const AssignmentSelectPage = () => { const navigate = useNavigate(); @@ -60,6 +60,7 @@ const AssignmentSelectPage = () => { a.id === assignment.id)} + className='cursor-pointer' /> )} /> diff --git a/src/pages/unit-editor/UnitEditorPage.tsx b/src/pages/unit-editor/UnitEditorPage.tsx index 0f0f942..e5abbcc 100644 --- a/src/pages/unit-editor/UnitEditorPage.tsx +++ b/src/pages/unit-editor/UnitEditorPage.tsx @@ -19,6 +19,8 @@ const UnitEditorPage = () => { resetStore, title: storedTitle, assignments: storedAssignments, + releaseDate: storedReleaseDate, + dueDate: storedDueDate, } = useUnitStore(); const {data} = useQuery(unitQueries.getUnitList(courseId)); const initialMode = @@ -27,7 +29,12 @@ const UnitEditorPage = () => { const [activeMode, setActiveMode] = useState(null); const [selectedUnitId, setSelectedUnitId] = useState(null); - const hasOngoingCreation = storedTitle !== '' || storedAssignments.length > 0; + const hasOngoingCreation = + storedTitle !== '' || + storedReleaseDate !== '' || + storedDueDate !== '' || + storedAssignments.length > 0; + const currentMode: Mode | null = activeMode ?? (hasOngoingCreation ? 'creating' : initialMode); const currentUnitId = From d3ddd54ae6aed4e5d81765b558ac7f1d0c9bb807 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 16:33:09 +0900 Subject: [PATCH 27/48] =?UTF-8?q?#52=20refactor:=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EB=A0=88=EC=9D=B4=EC=96=B4=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/assignment/api/assignmentQueries.ts | 5 ++++- src/entities/course/api/courseQueries.ts | 1 + .../filter-assignment/lib/useAssignmentList.ts | 12 +++--------- src/pages/manage-assignment/AssignmentManagePage.tsx | 4 ++-- src/pages/select-assignment/AssignmentSelectPage.tsx | 4 ++-- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/entities/assignment/api/assignmentQueries.ts b/src/entities/assignment/api/assignmentQueries.ts index d2554d8..6603aad 100644 --- a/src/entities/assignment/api/assignmentQueries.ts +++ b/src/entities/assignment/api/assignmentQueries.ts @@ -18,6 +18,7 @@ export const assignmentQueries = { queryOptions({ queryKey: ['assignments'], queryFn: getAllAssignments, + select: (data) => data.response.assignments, }), // 강의별 과제 목록 조회 쿼리 옵션 @@ -25,6 +26,8 @@ export const assignmentQueries = { queryOptions({ queryKey: ['courses', courseId, 'assignments'], queryFn: () => getAssignmentsByCourse(courseId), - enabled: !!courseId, // courseId가 있을 때만 쿼리 실행 + enabled: !!courseId, + select: (data) => + data.response.courses.flatMap((course) => course.assignments), }), }; diff --git a/src/entities/course/api/courseQueries.ts b/src/entities/course/api/courseQueries.ts index 2999360..7bacadd 100644 --- a/src/entities/course/api/courseQueries.ts +++ b/src/entities/course/api/courseQueries.ts @@ -7,5 +7,6 @@ export const courseQueries = { queryOptions({ queryKey: ['courses'], queryFn: getAllCourses, + select: (data) => data.response.courses, }), }; diff --git a/src/features/assignment/filter-assignment/lib/useAssignmentList.ts b/src/features/assignment/filter-assignment/lib/useAssignmentList.ts index 2a5fe1d..acd8458 100644 --- a/src/features/assignment/filter-assignment/lib/useAssignmentList.ts +++ b/src/features/assignment/filter-assignment/lib/useAssignmentList.ts @@ -16,13 +16,7 @@ export const useAssignmentList = ( assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) ); - if (selectedCourseId) { - return unique( - assignments?.response.courses.flatMap((course) => - course.id === selectedCourseId ? course.assignments : [] - ) ?? [] - ); - } - - return unique(allAssignments?.response.assignments ?? []); + return unique( + selectedCourseId ? (assignments ?? []) : (allAssignments ?? []) + ); }; diff --git a/src/pages/manage-assignment/AssignmentManagePage.tsx b/src/pages/manage-assignment/AssignmentManagePage.tsx index f1a0b8f..64970a2 100644 --- a/src/pages/manage-assignment/AssignmentManagePage.tsx +++ b/src/pages/manage-assignment/AssignmentManagePage.tsx @@ -13,9 +13,9 @@ import {Link} from 'react-router-dom'; import {buttonStyles} from '@/shared/ui/button/button-styles'; const AssignmentManagePage = () => { - const {data: courseList} = useQuery(courseQueries.getAllCourses()); + const {data: courses} = useQuery(courseQueries.getAllCourses()); const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( - courseList?.response.courses ?? [] + courses ?? [] ); const assignmentList = useAssignmentList(selectedCourseId); const queryClient = useQueryClient(); diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index ea5c89f..46af7c4 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -13,14 +13,14 @@ import {useAssignmentList} from '@/features/assignment/filter-assignment/lib/use const AssignmentSelectPage = () => { const navigate = useNavigate(); const location = useLocation(); - const {data: courseList} = useQuery(courseQueries.getAllCourses()); + const {data: courses} = useQuery(courseQueries.getAllCourses()); const {setAssignments, assignments: currentSelectedAssignments} = useUnitStore(); const [selectedAssignments, setSelectedAssignments] = useState( currentSelectedAssignments ?? [] ); const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( - courseList?.response.courses ?? [] + courses ?? [] ); const assignmentList = useAssignmentList(selectedCourseId); From e06d73e1141b29c38fb473fbf78a7f5739c43740 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 16:53:01 +0900 Subject: [PATCH 28/48] =?UTF-8?q?#52=20fix:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assignment/api/assignmentQueries.ts | 4 ++++ src/entities/course/api/courseQueries.ts | 5 ++++- src/pages/dashboard/Dashboard.tsx | 20 +++++++++++-------- .../AssignmentManagePage.tsx | 10 ++++++++-- .../AssignmentSelectPage.tsx | 6 ++++-- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/entities/assignment/api/assignmentQueries.ts b/src/entities/assignment/api/assignmentQueries.ts index 6603aad..8fe1d9d 100644 --- a/src/entities/assignment/api/assignmentQueries.ts +++ b/src/entities/assignment/api/assignmentQueries.ts @@ -11,6 +11,10 @@ export const assignmentQueries = { queryOptions({ queryKey: ['schedules'], queryFn: getAssignmentSchedules, + select: (data) => ({ + scheduleCount: data.response.count, + schedules: data.response.schedule, + }), }), // 전체 과제 목록 조회 쿼리 옵션 diff --git a/src/entities/course/api/courseQueries.ts b/src/entities/course/api/courseQueries.ts index 7bacadd..97669c7 100644 --- a/src/entities/course/api/courseQueries.ts +++ b/src/entities/course/api/courseQueries.ts @@ -7,6 +7,9 @@ export const courseQueries = { queryOptions({ queryKey: ['courses'], queryFn: getAllCourses, - select: (data) => data.response.courses, + select: (data) => ({ + courseCount: data.response.count, + courses: data.response.courses, + }), }), }; diff --git a/src/pages/dashboard/Dashboard.tsx b/src/pages/dashboard/Dashboard.tsx index d9acfd6..58d8baa 100644 --- a/src/pages/dashboard/Dashboard.tsx +++ b/src/pages/dashboard/Dashboard.tsx @@ -20,7 +20,14 @@ const Dashboard = () => { const queryClient = useQueryClient(); // 강의 및 스케쥴 데이터 패칭 - const [{data: courses}, {data: schedules}] = useSuspenseQueries({ + const [ + { + data: {courseCount, courses}, + }, + { + data: {scheduleCount, schedules}, + }, + ] = useSuspenseQueries({ queries: [ courseQueries.getAllCourses(), assignmentQueries.getAssignmentSchedules(), @@ -64,13 +71,10 @@ const Dashboard = () => { {userType === 'admin' && }
    - {courses.response.count === 0 ? ( + {courseCount === 0 ? ( 등록된 강의가 없습니다. ) : ( - + )} @@ -80,10 +84,10 @@ const Dashboard = () => {
- {schedules.response.count === 0 ? ( + {scheduleCount === 0 ? ( 예정된 과제가 없습니다. ) : ( - + )}
diff --git a/src/pages/manage-assignment/AssignmentManagePage.tsx b/src/pages/manage-assignment/AssignmentManagePage.tsx index 64970a2..3bf4574 100644 --- a/src/pages/manage-assignment/AssignmentManagePage.tsx +++ b/src/pages/manage-assignment/AssignmentManagePage.tsx @@ -2,7 +2,11 @@ import {AssignmentPageLayout} from '@/widgets/assignment-page-layout'; import AssignmentListContainer from '../select-assignment/ui/AssignmentListContainer'; import ListRow from '@/shared/ui/list-row/ListRow'; import {useCourseFilter} from '@/features/course/filter-course'; -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; import {courseQueries} from '@/entities/course/api/courseQueries'; import {useAssignmentList} from '@/features/assignment/filter-assignment/lib/useAssignmentList'; import {assignmentMutations} from '@/entities/assignment/api/assignmentMutations'; @@ -13,7 +17,9 @@ import {Link} from 'react-router-dom'; import {buttonStyles} from '@/shared/ui/button/button-styles'; const AssignmentManagePage = () => { - const {data: courses} = useQuery(courseQueries.getAllCourses()); + const { + data: {courses}, + } = useSuspenseQuery(courseQueries.getAllCourses()); const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( courses ?? [] ); diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index 46af7c4..aabf0f7 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -3,7 +3,7 @@ import {useState} from 'react'; import {useCourseFilter} from '@/features/course/filter-course/lib/useCourseFilter'; import {AssignmentPageLayout} from '@/widgets/assignment-page-layout'; import ListRow from '@/shared/ui/list-row/ListRow'; -import {useQuery} from '@tanstack/react-query'; +import {useSuspenseQuery} from '@tanstack/react-query'; import {courseQueries} from '@/entities/course/api/courseQueries'; import useUnitStore from '@/entities/unit/model/useUnitStore'; import {useLocation, useNavigate} from 'react-router-dom'; @@ -13,7 +13,9 @@ import {useAssignmentList} from '@/features/assignment/filter-assignment/lib/use const AssignmentSelectPage = () => { const navigate = useNavigate(); const location = useLocation(); - const {data: courses} = useQuery(courseQueries.getAllCourses()); + const { + data: {courses}, + } = useSuspenseQuery(courseQueries.getAllCourses()); const {setAssignments, assignments: currentSelectedAssignments} = useUnitStore(); const [selectedAssignments, setSelectedAssignments] = useState( From 6550556b73a87ced89c15c886b3360020ede93ec Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 17:18:29 +0900 Subject: [PATCH 29/48] =?UTF-8?q?#52=20refactor:=20AssignmentPageLayout=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20render=20prop=EC=9C=BC=EB=A1=9C=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 --- .../AssignmentManagePage.tsx | 15 ++++++++++++--- .../AssignmentSelectPage.tsx | 19 +++++++++++++++---- .../ui/AssignmentPageLayout.tsx | 19 +++++-------------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/pages/manage-assignment/AssignmentManagePage.tsx b/src/pages/manage-assignment/AssignmentManagePage.tsx index 3bf4574..fe8495f 100644 --- a/src/pages/manage-assignment/AssignmentManagePage.tsx +++ b/src/pages/manage-assignment/AssignmentManagePage.tsx @@ -13,10 +13,12 @@ import {assignmentMutations} from '@/entities/assignment/api/assignmentMutations import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries'; import AssignmentManageActionsBar from './ui/AssignmentManageActionsBar'; import AddIcon from '@/assets/svg/addIcon.svg?react'; -import {Link} from 'react-router-dom'; +import {Link, useNavigate} from 'react-router-dom'; import {buttonStyles} from '@/shared/ui/button/button-styles'; +import Button from '@/shared/ui/button/Button'; const AssignmentManagePage = () => { + const navigate = useNavigate(); const { data: {courses}, } = useSuspenseQuery(courseQueries.getAllCourses()); @@ -91,8 +93,15 @@ const AssignmentManagePage = () => { } - onCancel={() => {}} - onConfirm={() => {}} + buttons={ + + } /> ); }; diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index aabf0f7..f034dd8 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -9,6 +9,7 @@ import useUnitStore from '@/entities/unit/model/useUnitStore'; import {useLocation, useNavigate} from 'react-router-dom'; import type {Assignment} from '@/entities/assignment/model/types'; import {useAssignmentList} from '@/features/assignment/filter-assignment/lib/useAssignmentList'; +import Button from '@/shared/ui/button/Button'; const AssignmentSelectPage = () => { const navigate = useNavigate(); @@ -67,10 +68,20 @@ const AssignmentSelectPage = () => { )} /> } - onCancel={() => { - returnToPreviousPage(); - }} - onConfirm={handleConfirm} + buttons={ +
+ + +
+ } /> ); }; diff --git a/src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx b/src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx index 6dc960a..e9e80ed 100644 --- a/src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx +++ b/src/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx @@ -1,23 +1,21 @@ import SurfaceCard from '@/shared/ui/SurfaceCard'; -import Button from '@/shared/ui/button/Button'; import {CourseSelector} from '@/features/course/filter-course'; +import type {ReactNode} from 'react'; interface AssignmentPageLayoutProps { title: string; - list: React.ReactNode; + list: ReactNode; + buttons: ReactNode; courseOptions: string[]; onCourseSelect: (value: string) => void; - onCancel: () => void; - onConfirm: () => void; } export const AssignmentPageLayout = ({ title, list, + buttons, courseOptions, onCourseSelect, - onCancel, - onConfirm, }: AssignmentPageLayoutProps) => { return ( @@ -31,14 +29,7 @@ export const AssignmentPageLayout = ({
{list}
{/* 하단 버튼 영역 */} -
- - -
+
{buttons}
); }; From 15b8a7a3d191500791c329acd3eeed08fa9c1df1 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 17:42:39 +0900 Subject: [PATCH 30/48] =?UTF-8?q?#52=20refactor:=20=ED=8E=B8=EC=A7=91=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EC=8B=9C=20=EB=AC=B8=EC=A0=9C=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EB=B2=84=ED=8A=BC=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit-editor/ui/UnitAssignmentList.tsx | 2 -- src/pages/unit-editor/ui/UnitForm.tsx | 23 ++++++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pages/unit-editor/ui/UnitAssignmentList.tsx b/src/pages/unit-editor/ui/UnitAssignmentList.tsx index 510fc1c..98ce847 100644 --- a/src/pages/unit-editor/ui/UnitAssignmentList.tsx +++ b/src/pages/unit-editor/ui/UnitAssignmentList.tsx @@ -2,7 +2,6 @@ import ListRow from '@/shared/ui/list-row/ListRow'; import type {Assignment} from '@/entities/assignment/model/types'; import {useState} from 'react'; import DragAndDropIcon from '@/assets/svg/dragAndDropIcon.svg?react'; -import DeleteIcon from '@/assets/svg/deleteIcon.svg?react'; import { closestCorners, DndContext, @@ -95,7 +94,6 @@ const DraggableAssignmentItem = ({id, title}: Assignment) => { } - rightIcon={} className={`cursor-grab touch-none bg-white shadow-box active:cursor-grabbing ${isDragging ? 'z-10 opacity-50' : ''}`} /> diff --git a/src/pages/unit-editor/ui/UnitForm.tsx b/src/pages/unit-editor/ui/UnitForm.tsx index b4ebedf..4ec21c6 100644 --- a/src/pages/unit-editor/ui/UnitForm.tsx +++ b/src/pages/unit-editor/ui/UnitForm.tsx @@ -165,17 +165,18 @@ export const UnitForm = ({ )} {/* 문제 연결 버튼 */} -
- -
+ {mode === 'creating' && ( +
+ +
+ )}
From 1597f1509834c2bf610c525f5284e46b9649601e Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 18:01:58 +0900 Subject: [PATCH 31/48] =?UTF-8?q?#52=20fix:=20isSubmitting=EC=9D=84=20isPe?= =?UTF-8?q?nding=EC=9C=BC=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=98=EC=97=AC?= =?UTF-8?q?=20=EC=9D=B4=EC=A4=91=20=EC=A0=9C=EC=B6=9C=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/unit-editor/UnitEditorPage.tsx | 5 +++-- src/pages/unit-editor/model/types.ts | 1 + src/pages/unit-editor/ui/UnitForm.tsx | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/unit-editor/UnitEditorPage.tsx b/src/pages/unit-editor/UnitEditorPage.tsx index e5abbcc..5fee03c 100644 --- a/src/pages/unit-editor/UnitEditorPage.tsx +++ b/src/pages/unit-editor/UnitEditorPage.tsx @@ -63,7 +63,7 @@ const UnitEditorPage = () => { }; // 단원 생성 - const {mutate: addUnit} = useMutation({ + const {mutate: addUnit, isPending: isCreating} = useMutation({ ...unitMutations.createUnit, onSuccess: (data) => { // 단원 목록 갱신 @@ -80,7 +80,7 @@ const UnitEditorPage = () => { }); // 단원 업데이트 - const {mutate: updateUnit} = useMutation({ + const {mutate: updateUnit, isPending: isUpdating} = useMutation({ ...unitMutations.updateUnit, onSuccess: () => { invalidateUnitList(); @@ -162,6 +162,7 @@ const UnitEditorPage = () => { onCreateUnit={onCreateUnit} onUpdateUnit={onUpdateUnit} onDeleteUnit={onDeleteUnit} + isPending={isCreating || isUpdating} />
diff --git a/src/pages/unit-editor/model/types.ts b/src/pages/unit-editor/model/types.ts index 86a78af..f505c89 100644 --- a/src/pages/unit-editor/model/types.ts +++ b/src/pages/unit-editor/model/types.ts @@ -11,4 +11,5 @@ export interface UnitFormProps { onCreateUnit: (unit: TUnitFormSchema) => void; onUpdateUnit: (unitId: number, unit: TUnitFormSchema) => void; onDeleteUnit: (unitId: number) => void; + isPending: boolean; } diff --git a/src/pages/unit-editor/ui/UnitForm.tsx b/src/pages/unit-editor/ui/UnitForm.tsx index 4ec21c6..3985fb4 100644 --- a/src/pages/unit-editor/ui/UnitForm.tsx +++ b/src/pages/unit-editor/ui/UnitForm.tsx @@ -21,6 +21,7 @@ export const UnitForm = ({ onCreateUnit, onUpdateUnit, onDeleteUnit, + isPending, }: UnitFormProps) => { const location = useLocation(); const navigate = useNavigate(); @@ -44,7 +45,7 @@ export const UnitForm = ({ handleSubmit, reset, getValues, - formState: {errors, isSubmitting}, + formState: {errors}, } = useForm({ resolver: zodResolver(unitFormSchema), values: @@ -189,7 +190,7 @@ export const UnitForm = ({
From 9f3fcc68edbea46980a2f5e3adc973f74685418f Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 18:02:45 +0900 Subject: [PATCH 32/48] =?UTF-8?q?#52=20fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20fallback=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../manage-assignment/AssignmentManagePage.tsx | 5 ++--- .../select-assignment/AssignmentSelectPage.tsx | 13 ++++--------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/pages/manage-assignment/AssignmentManagePage.tsx b/src/pages/manage-assignment/AssignmentManagePage.tsx index fe8495f..5bbc7ec 100644 --- a/src/pages/manage-assignment/AssignmentManagePage.tsx +++ b/src/pages/manage-assignment/AssignmentManagePage.tsx @@ -22,9 +22,8 @@ const AssignmentManagePage = () => { const { data: {courses}, } = useSuspenseQuery(courseQueries.getAllCourses()); - const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( - courses ?? [] - ); + const {courseOptions, handleCourseSelect, selectedCourseId} = + useCourseFilter(courses); const assignmentList = useAssignmentList(selectedCourseId); const queryClient = useQueryClient(); diff --git a/src/pages/select-assignment/AssignmentSelectPage.tsx b/src/pages/select-assignment/AssignmentSelectPage.tsx index f034dd8..f9456f1 100644 --- a/src/pages/select-assignment/AssignmentSelectPage.tsx +++ b/src/pages/select-assignment/AssignmentSelectPage.tsx @@ -20,11 +20,10 @@ const AssignmentSelectPage = () => { const {setAssignments, assignments: currentSelectedAssignments} = useUnitStore(); const [selectedAssignments, setSelectedAssignments] = useState( - currentSelectedAssignments ?? [] - ); - const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter( - courses ?? [] + currentSelectedAssignments ); + const {courseOptions, handleCourseSelect, selectedCourseId} = + useCourseFilter(courses); const assignmentList = useAssignmentList(selectedCourseId); @@ -70,11 +69,7 @@ const AssignmentSelectPage = () => { } buttons={
- + + 학생 목록 - - + + 단원 추가 ); From df8758bd70dd920c63db316ee2df0a9660b65be2 Mon Sep 17 00:00:00 2001 From: suminb99 Date: Sat, 28 Feb 2026 22:22:57 +0900 Subject: [PATCH 48/48] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20mock=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/unit-editor/mock/unitMock.ts | 61 ---------- src/shared/mocks/assignmentDummyResponse.ts | 119 ------------------- src/shared/mocks/assignmentDummyTypes.ts | 34 ------ src/shared/mocks/assignmentSelectResponse.ts | 104 ---------------- src/shared/mocks/courseOverviewResponse.ts | 115 ------------------ src/shared/mocks/dashboardCourseList.ts | 32 ----- src/shared/mocks/dashboardScheduleList.ts | 59 --------- 7 files changed, 524 deletions(-) delete mode 100644 src/pages/unit-editor/mock/unitMock.ts delete mode 100644 src/shared/mocks/assignmentDummyResponse.ts delete mode 100644 src/shared/mocks/assignmentDummyTypes.ts delete mode 100644 src/shared/mocks/assignmentSelectResponse.ts delete mode 100644 src/shared/mocks/courseOverviewResponse.ts delete mode 100644 src/shared/mocks/dashboardCourseList.ts delete mode 100644 src/shared/mocks/dashboardScheduleList.ts diff --git a/src/pages/unit-editor/mock/unitMock.ts b/src/pages/unit-editor/mock/unitMock.ts deleted file mode 100644 index 9d9d2f9..0000000 --- a/src/pages/unit-editor/mock/unitMock.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { - AllUnitsResponse, - SingleUnitResponse, -} from '@/entities/course/model/types'; - -export const singleUnitResponse: SingleUnitResponse = { - success: true, - response: { - id: 1, - title: '날씨 데이터 확인하기', - releaseDate: '2025-07-27', - dueDate: '2025-07-27', - assignmentCount: 5, - assignments: [ - { - id: 1, - title: '날씨 데이터를 활용한 앱 만들기 1', - }, - { - id: 2, - title: '날씨 데이터를 활용한 앱 만들기 2', - }, - { - id: 3, - title: '날씨 데이터를 활용한 앱 만들기 3', - }, - { - id: 4, - title: '날씨 데이터를 활용한 앱 만들기 4', - }, - { - id: 5, - title: '날씨 데이터를 활용한 앱 만들기 4', - }, - ], - }, -}; - -export const allUnitsResponse: AllUnitsResponse = { - success: true, - response: { - count: 3, - units: [ - { - id: 1, - title: '변수와 수식 (수정)', - assignmentCount: 2, - }, - { - id: 2, - title: '날씨 데이터 확인하기', - assignmentCount: 1, - }, - { - id: 3, - title: '날씨 데이터 확인하기', - assignmentCount: 3, - }, - ], - }, -}; diff --git a/src/shared/mocks/assignmentDummyResponse.ts b/src/shared/mocks/assignmentDummyResponse.ts deleted file mode 100644 index 55f4810..0000000 --- a/src/shared/mocks/assignmentDummyResponse.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type {CoursesResponse} from '@/shared/mocks/assignmentDummyTypes'; - -export const coursesResponse: CoursesResponse = { - success: true, - response: { - count: 5, - courses: [ - { - id: 1, - title: '데이터구조와 알고리즘', - year: 2025, - semester: 'FIRST', - section: '002', - count: 5, - assignments: [ - {id: 1, title: '날씨 데이터로 크롤링하기'}, - {id: 3, title: '정렬 알고리즘 구현하기'}, - {id: 5, title: '스택과 큐로 계산기 만들기'}, - {id: 6, title: '이진 트리 순회'}, - {id: 7, title: '그래프 탐색 시각화'}, - ], - }, - { - id: 2, - title: '컴퓨터수학', - year: 2025, - semester: 'SECOND', - section: '003', - count: 2, - assignments: [ - {id: 2, title: '행렬 연산 연습문제'}, - {id: 4, title: '이산 수학과 부울 대수'}, - ], - }, - { - id: 3, - title: '운영체제', - year: 2025, - semester: 'SECOND', - section: '001', - count: 3, - assignments: [ - {id: 8, title: 'CPU 스케줄링 시뮬레이터'}, - {id: 9, title: '가상 메모리 구조 분석'}, - {id: 10, title: '프로세스 동기화 실습'}, - ], - }, - { - id: 4, - title: '웹프로그래밍', - year: 2025, - semester: 'SUMMER', - section: '101', - count: 4, - assignments: [ - {id: 11, title: 'HTML/CSS로 개인 홈페이지 만들기'}, - {id: 12, title: 'JavaScript 인터랙션 추가하기'}, - {id: 13, title: 'React로 게시판 구현'}, - {id: 14, title: 'API 연동 과제'}, - ], - }, - { - id: 5, - title: 'AI기초', - year: 2025, - semester: 'WINTER', - section: '201', - count: 3, - assignments: [ - {id: 15, title: '선형회귀 모델 구현'}, - {id: 16, title: '데이터 전처리 실습'}, - {id: 17, title: 'PyTorch 기초 과제'}, - ], - }, - { - id: 6, - title: '소프트웨어의 이해', - year: 2025, - semester: 'WINTER', - section: '005', - count: 3, - assignments: [ - {id: 18, title: '음수 구별하기'}, - {id: 19, title: '조건문과 반복문을 활용한 계산기 만들기'}, - {id: 20, title: '리스트와 튜플의 활용'}, - ], - }, - { - id: 7, - title: '소프트웨어의 이해', - year: 2025, - semester: 'FIRST', - section: '005', - count: 5, - assignments: [ - {id: 21, title: '음수 구별하기'}, - {id: 22, title: '조건문과 반복문을 활용한 계산기 만들기'}, - {id: 23, title: '딕셔너리와 집합의 활용'}, - {id: 24, title: '파이썬에서의 날짜와 시간 처리'}, - {id: 25, title: '클래스와 객체 지향 프로그래밍'}, - ], - }, - { - id: 8, - title: '소프트웨어의 이해', - year: 2025, - semester: 'SECOND', - section: '001', - count: 4, - assignments: [ - {id: 26, title: '음수 구별하기'}, - {id: 27, title: '조건문과 반복문을 활용한 계산기 만들기'}, - {id: 28, title: '리스트와 튜플의 활용'}, - {id: 29, title: '파이썬에서의 예외 처리'}, - ], - }, - ], - }, -}; diff --git a/src/shared/mocks/assignmentDummyTypes.ts b/src/shared/mocks/assignmentDummyTypes.ts deleted file mode 100644 index c07b420..0000000 --- a/src/shared/mocks/assignmentDummyTypes.ts +++ /dev/null @@ -1,34 +0,0 @@ -type SemesterCode = 'FIRST' | 'SECOND' | 'SUMMER' | 'WINTER'; -type SubmissionStatus = 'NOT_SUBMITTED' | 'CORRECT' | 'INCORRECT'; - -interface Assignment { - id: number; - title: string; - submittedStatus?: SubmissionStatus; -} - -interface Course { - id: number; - title: string; - year: number; - semester: SemesterCode; - section: string; - count: number; - assignments: Assignment[]; -} - -interface CoursesResponse { - success: boolean; - response: { - count: number; - courses: Course[]; - }; -} - -export type { - Assignment, - Course, - CoursesResponse, - SemesterCode, - SubmissionStatus, -}; diff --git a/src/shared/mocks/assignmentSelectResponse.ts b/src/shared/mocks/assignmentSelectResponse.ts deleted file mode 100644 index 015419d..0000000 --- a/src/shared/mocks/assignmentSelectResponse.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { - AssignmentSelectResponse, - CourseOptionsResponse, -} from '@/entities/course/model/types'; - -export const response: AssignmentSelectResponse = { - success: true, - response: { - count: 2, - courses: [ - { - id: 1, - title: '데이터구조와 알고리즘', - year: 2025, - semester: 'FIRST', - section: '002', - count: 2, - assignments: [ - { - id: 1, - title: '날씨 데이터로 크롤링하기 1', - }, - { - id: 3, - title: '날씨 데이터로 크롤링하기 2', - }, - { - id: 7, - title: '날씨 데이터로 크롤링하기 3', - }, - { - id: 8, - title: '날씨 데이터로 크롤링하기 4', - }, - ], - }, - { - id: 2, - title: '데이터구조와 알고리즘', - year: 2025, - semester: 'SECOND', - section: '003', - count: 2, - assignments: [ - { - id: 2, - title: '날씨 데이터로 크롤링하기 5', - }, - { - id: 4, - title: '날씨 데이터로 크롤링하기 6', - }, - { - id: 5, - title: '날씨 데이터로 크롤링하기 7', - }, - { - id: 6, - title: '날씨 데이터로 크롤링하기 8', - }, - ], - }, - ], - }, -}; - -export const courseOptionsResponse: CourseOptionsResponse = { - success: true, - response: { - count: 3, - courses: [ - { - id: 1, - year: 2025, - semester: 'SUMMER', - section: '001', - title: '소프트웨어의이해', - description: '소프트웨어의이해 강의는 파이썬을 배웁니다.', - unitCount: 3, - assignmentCount: 10, - }, - { - id: 2, - year: 2025, - semester: 'SUMMER', - section: '001', - title: '소프트웨어의이해', - description: '소프트웨어의이해 강의는 파이썬을 배웁니다.', - unitCount: 3, - assignmentCount: 10, - }, - { - id: 3, - year: 2025, - semester: 'SUMMER', - section: '001', - title: '소프트웨어의이해', - description: '소프트웨어의이해 강의는 파이썬을 배웁니다.', - unitCount: 3, - assignmentCount: 10, - }, - ], - }, -}; diff --git a/src/shared/mocks/courseOverviewResponse.ts b/src/shared/mocks/courseOverviewResponse.ts deleted file mode 100644 index 0ac91fc..0000000 --- a/src/shared/mocks/courseOverviewResponse.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type {CourseOverviewResponse} from '@/entities/course/model/types'; - -export const courseResponse: CourseOverviewResponse = { - success: true, - response: { - id: 1, - title: '소프트웨어의 이해', - year: 2025, - semester: 'FIRST', - section: '005', - studentCount: 56, - unitCount: 2, - units: [ - { - id: 1, - title: '변수와 수식', - releaseDate: '2025-06-19', - dueDate: '2025-06-25', - isOpen: true, - assignmentCount: 2, - assignments: [ - { - id: 1, - title: '음수 구별하기', - submittedStatus: 'CORRECT', - }, - { - id: 2, - title: '수식 작성 및 결과 확인 실습 과제', - submittedStatus: 'CORRECT', - }, - { - id: 3, - title: '변수 선언 후 값 출력해보기', - submittedStatus: 'NOT_SUBMITTED', - }, - { - id: 4, - title: '변수 수식 결과를 저장해보세요', - submittedStatus: 'INCORRECT', - }, - ], - }, - { - id: 3, - title: '반복문과 조건문', - releaseDate: '2025-06-19', - dueDate: '2025-06-25', - isOpen: false, - assignmentCount: 2, - assignments: [ - { - id: 4, - title: '짝수와 음수 구별하기', - submittedStatus: 'NOT_SUBMITTED', - }, - { - id: 5, - title: '숫자 맞히기 프로그램 만들기 실습', - submittedStatus: 'NOT_SUBMITTED', - }, - ], - }, - ], - }, -}; - -// export const courseResponse: CourseResponse = { -// success: true, -// response: { -// id: 1, -// title: '소프트웨어의 이해', -// year: 2025, -// semester: 'FIRST', -// section: '005', -// studentCount: 56, -// unitCount: 2, -// units: [ -// { -// id: 1, -// title: '변수와 수식', -// releaseDate: '2025-06-19', -// dueDate: '2025-06-25', -// assignmentCount: 2, -// assignments: [ -// { -// id: 1, -// title: '음수 구별하기', -// }, -// { -// id: 3, -// title: '변수 선언 후 값 출력해보기', -// }, -// ], -// }, -// { -// id: 3, -// title: '반복문과 조건문', -// releaseDate: '2025-06-19', -// dueDate: '2025-06-25', -// assignmentCount: 2, -// assignments: [ -// { -// id: 4, -// title: '짝수와 음수 구별하기', -// }, -// { -// id: 5, -// title: '숫자 맞히기 프로그램 만들기 실습', -// }, -// ], -// }, -// ], -// }, -// }; diff --git a/src/shared/mocks/dashboardCourseList.ts b/src/shared/mocks/dashboardCourseList.ts deleted file mode 100644 index e5edffe..0000000 --- a/src/shared/mocks/dashboardCourseList.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type {DashboardCourseListResponse} from '@/entities/course/model/types'; - -export const responseCourseList: DashboardCourseListResponse = { - success: true, - response: { - count: 2, - courses: [ - { - id: 1, - year: 2025, - semester: 'FIRST', - section: '005', - title: '소프트웨어의 이해', - description: - 'Python 언어를 기반으로 하여 프로그래밍에 대한 기본 원리를 학습한다.', - unitCount: 3, - assignmentCount: 2, - }, - { - id: 2, - year: 2025, - semester: 'FIRST', - section: '005', - title: '소프트웨어의 이해', - description: - 'Python 언어를 기반으로 하여 프로그래밍에 대한 기본 원리를 학습한다.', - unitCount: 3, - assignmentCount: 2, - }, - ], - }, -}; diff --git a/src/shared/mocks/dashboardScheduleList.ts b/src/shared/mocks/dashboardScheduleList.ts deleted file mode 100644 index 9f81edd..0000000 --- a/src/shared/mocks/dashboardScheduleList.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type {DashboardScheduleListResponse} from '@/entities/course/model/types'; - -export const responseScheduleList: DashboardScheduleListResponse = { - success: true, - response: { - count: 4, - schedule: [ - { - date: '2025-07-20', - remainingDays: 0, - assignments: [ - { - course: '데이터구조와 알고리즘', - section: '002', - assignment: 'final', - }, - ], - }, - { - date: '2025-07-21', - remainingDays: 1, - assignments: [ - { - course: '데이터구조와 알고리즘', - section: '002', - assignment: 'file', - }, - ], - }, - { - date: '2025-07-22', - remainingDays: 2, - assignments: [ - { - course: 'test', - section: '003', - assignment: '확인용', - }, - ], - }, - { - date: '2025-07-26', - remainingDays: 6, - assignments: [ - { - course: 'Python을 활용한 데이터 분석', - section: '005', - assignment: '날씨 데이터를 활용한 기온 변화 분석', - }, - { - course: '소프트웨어의이해', - section: '005', - assignment: '날씨 확인', - }, - ], - }, - ], - }, -};