Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b12d3b8
#52 feat: 과제 관련 API 추가
suminb99 Feb 24, 2026
c17ba82
#52 chore: 주석 추가
suminb99 Feb 24, 2026
dd44942
#52 feat: 단원-문제 연결 해제 api 추가
suminb99 Feb 24, 2026
c3fb10b
#52 feat: 과제 관련 쿼리 옵션 추가
suminb99 Feb 24, 2026
d590227
#52 refactor: query 파일 구조 개선
suminb99 Feb 24, 2026
46d842f
#52 feat: 강의 삭제 뮤테이션 옵션 추가
suminb99 Feb 24, 2026
b5495f5
#52 chore: barrel export 파일 삭제
suminb99 Feb 24, 2026
c5b95d9
#52 style: 전체 레이아웃 items-center 적용 취소
suminb99 Feb 24, 2026
932b8d6
#52 feat: 과제 응답 타입 추가
suminb99 Feb 24, 2026
072672a
#52 refactor: 리팩토링된 query/mutation 적용
suminb99 Feb 24, 2026
e228664
#52 feat: 과제 선택 페이지 API 연동
suminb99 Feb 24, 2026
3c49ba1
#52 feat: 단원 폼 임시 저장을 위한 useUnitStore 추가
suminb99 Feb 26, 2026
2e6b2f4
#52 feat: 페이지 이동 및 복귀 시 폼 상태 유지 구현
suminb99 Feb 26, 2026
92c4b51
#52 feat: 과제 선택 방식 id -> Assignment 객체 기반으로 변경
suminb99 Feb 26, 2026
d4428d1
#52 refactor: 파라미터명 변경
suminb99 Feb 26, 2026
004a045
#52 feat: 과제 관리 페이지 구현 및 과제 삭제 기능 추가
suminb99 Feb 26, 2026
df245ef
#52 refactor: 과제 목록 필터 로직을 useAssignmentList 훅으로 분리
suminb99 Feb 26, 2026
e7ea6cb
#52 chore: ListRow 및 AssignmentListContainer 공통 컴포넌트 정리
suminb99 Feb 26, 2026
bbe3938
#52 refactor: 데이터 변환 로직 쿼리 레이어로 이동
suminb99 Feb 27, 2026
191e891
#52 refactor: 모드/인덱스 관리를 파생 상태로 전환
suminb99 Feb 27, 2026
7bf990a
#52 refactor: 과제 목록 로직 통합 및 form prop 네이밍 수정
suminb99 Feb 27, 2026
52b6f14
#52 chore: navigate state 정리 및 import 경로 수정
suminb99 Feb 27, 2026
47eda8a
#62 chore: Zod 스키마 기반 API 응답 타입 시스템 도입
suminb99 Feb 27, 2026
bd80094
#62 chore: common.ts -> type.ts 파일명 변경
suminb99 Feb 27, 2026
6959037
#62 feat: errorResponseSchema 추가
suminb99 Feb 27, 2026
628a411
fix: import 경로 수정
suminb99 Feb 28, 2026
f91092f
#62 fix: enabled 대신 skipToken으로 null unitId 타입 에러 해결
suminb99 Feb 28, 2026
2607964
#62 fix: 스키마 import에서 타입 제거
suminb99 Feb 28, 2026
1dbdb31
#62 refactor: 스키마-타입 파일 분리 및 불필요한 네이밍 접두사 제거
suminb99 Feb 28, 2026
1047e28
#62 fix: Unit 타입 import 경로 변경
suminb99 Feb 28, 2026
292ca07
#62 refactor: 불필요한 네이밍 접두사 제거 및 Unit 타입 직접 참조로 변경
suminb99 Feb 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -37,7 +38,10 @@ const AppRoutes = () => {
<Route element={<PrivateRoute allowedRoles={['admin']} />}>
<Route path='admin'>
<Route index element={<Dashboard />} />
{/* <Route path='assignments' element={<AssignmentsPage />} /> */}
<Route
path='assignments/manage'
element={<AssignmentManagePage />}
/>
<Route
path='assignments/create'
element={<AssignmentCreatePage />}
Expand Down
49 changes: 43 additions & 6 deletions src/entities/assignment/api/assignmentApi.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,45 @@
import type {DashboardScheduleListResponse} from '@/entities/course/model/types';
import {z} from 'zod';
import {privateAxios} from '@/shared/api/axiosInstance';
import {apiResponseSchema} from '@/shared/model/schemas';
import {assignmentScheduleSchema} from '../model/schemas';
import {assignmentCourseSchema} from '@/entities/course/model/schemas';

export const getAssignmentSchedules =
async (): Promise<DashboardScheduleListResponse> => {
const response = await privateAxios.get('/assignments/schedule');
return response.data;
};
// 과제 일정 조회 API
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 () => {
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) => {
const response = await privateAxios.get(`/courses/${courseId}/assignments`);
return apiResponseSchema(
z.object({
count: z.number(),
courses: z.array(assignmentCourseSchema),
})
).parse(response.data);
};

// 과제 삭제 API
export const deleteAssignment = async (assignmentId: number) => {
const response = await privateAxios.delete(`/assignments/${assignmentId}`);
return apiResponseSchema(z.string()).parse(response.data);
};
8 changes: 8 additions & 0 deletions src/entities/assignment/api/assignmentMutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {deleteAssignment} from './assignmentApi';

export const assignmentMutations = {
deleteAssignment: {
mutationKey: ['deleteAssignment'],
mutationFn: (assignmentId: number) => deleteAssignment(assignmentId),
},
};
30 changes: 30 additions & 0 deletions src/entities/assignment/api/assignmentQueries.ts
Original file line number Diff line number Diff line change
@@ -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가 있을 때만 쿼리 실행
}),
};
9 changes: 0 additions & 9 deletions src/entities/assignment/api/assignmentQueryOptions.ts

This file was deleted.

20 changes: 20 additions & 0 deletions src/entities/assignment/model/schemas.ts
Original file line number Diff line number Diff line change
@@ -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(),
})
),
});
13 changes: 4 additions & 9 deletions src/entities/assignment/model/types.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스키마로 옮겼으면 type.ts의 수동 타입 정의는 삭제하고 z.infer<>로 통일하면 될 것 같아요!

Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import type {SubmissionStatus} from '@/shared/model/common';
import type {z} from 'zod';
import type {assignmentScheduleSchema, assignmentSchema} from './schemas';

/**
* 과제(Assignment) 인터페이스 정의
*/
export interface Assignment {
id: number;
title: string;
submittedStatus?: SubmissionStatus;
}
export type Assignment = z.infer<typeof assignmentSchema>;
export type AssignmentSchedule = z.infer<typeof assignmentScheduleSchema>;
14 changes: 3 additions & 11 deletions src/entities/auth/api/authApi.ts
Original file line number Diff line number Diff line change
@@ -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 type {UserType} from '@/shared/model/type';
import {kakaoLoginResponseSchema} from '../model/schemas';

export const kakaoLogin = async (
oAuthToken: string,
Expand All @@ -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<
Expand Down
10 changes: 10 additions & 0 deletions src/entities/auth/model/schemas.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
Comment on lines +3 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

studentId 스키마가 역할별 응답 차이를 막아 로그인 실패를 유발할 수 있어요

Line 7의 studentId: z.string()는 항상 필수 문자열을 강제합니다. 그런데 현재 호출부(kakaoLogin)는 roleADMIN | USER이고 studentId는 optional이라, ADMIN 응답에서 studentId가 없거나 null이면 parse가 즉시 throw 되어 로그인 플로우가 깨질 수 있습니다.

개선 제안 (역할별 응답을 명시적으로 검증)
 export const kakaoLoginResponseSchema = z.object({
   memberId: z.number(),
   name: z.string(),
-  role: z.enum(['ADMIN', 'USER']),
-  studentId: z.string(),
+  role: z.enum(['ADMIN', 'USER']),
+  studentId: z.string().nullable().optional(),
   email: z.string().nullable(),
   accessToken: z.string(),
 });

필요하면 더 엄격하게 z.discriminatedUnion('role', ...)으로 USER일 때만 studentId 필수로 강제하는 방식이 가장 안전합니다.

공식 문서: https://zod.dev/?id=optionals , https://zod.dev/?id=discriminated-unions

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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(),
});
export const kakaoLoginResponseSchema = z.object({
memberId: z.number(),
name: z.string(),
role: z.enum(['ADMIN', 'USER']),
studentId: z.string().nullable().optional(),
email: z.string().nullable(),
accessToken: z.string(),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/auth/model/schemas.ts` around lines 3 - 10,
kakaoLoginResponseSchema currently requires studentId for all roles causing
parse errors when ADMIN responses omit it; update the schema so role
discriminates responses: use z.discriminatedUnion('role', [...]) (or equivalent)
to define two branches where ROLE === 'USER' requires studentId: z.string(), and
ROLE === 'ADMIN' allows studentId to be z.string().nullable().optional() (or
omitted), then use this updated kakaoLoginResponseSchema in the kakaoLogin parse
call so ADMIN responses no longer throw.

2 changes: 1 addition & 1 deletion src/entities/auth/model/useUserStore.ts
Original file line number Diff line number Diff line change
@@ -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<UserType, 'guest'>;

Expand Down
19 changes: 11 additions & 8 deletions src/entities/course/api/courseApi.ts
Original file line number Diff line number Diff line change
@@ -1,15 +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';

export const getAllCourses = async (): Promise<DashboardCourseListResponse> => {
// 전체 강의 목록 조회 API
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);
};

export const deleteCourse = async (
courseId: number
): Promise<ApiResponse<string>> => {
// 강의 삭제 API
export const deleteCourse = async (courseId: number) => {
const response = await privateAxios.delete(`/courses/${courseId}`);
return response.data;
return apiResponseSchema(z.string()).parse(response.data);
};
9 changes: 9 additions & 0 deletions src/entities/course/api/courseMutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {deleteCourse} from './courseApi';

export const courseMutations = {
// 강의 삭제 뮤테이션 옵션
deleteCourse: {
mutationKey: ['deleteCourse'],
mutationFn: (courseId: number) => deleteCourse(courseId),
},
};
11 changes: 11 additions & 0 deletions src/entities/course/api/courseQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {getAllCourses} from './courseApi';
import {queryOptions} from '@tanstack/react-query';

export const courseQueries = {
// 전체 강의 조회 쿼리 옵션
getAllCourses: () =>
queryOptions({
queryKey: ['courses'],
queryFn: getAllCourses,
}),
};
9 changes: 0 additions & 9 deletions src/entities/course/api/courseQueryOptions.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/entities/course/index.ts

This file was deleted.

41 changes: 41 additions & 0 deletions src/entities/course/model/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {z} from 'zod';
import {semesterCodeSchema} from '@/shared/model/schemas';
import {
assignmentSchema,
assignmentScheduleSchema,
} from '@/entities/assignment/model/schemas';
import {unitSchema} from '@/entities/unit/model/schemas';

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 assignmentCourseSchema = 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};
Loading