Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
15eb502
#52 fix: 코드래빗 리뷰 반영
suminb99 Feb 28, 2026
d3ddd54
#52 refactor: 데이터 변환 로직을 쿼리 레이어로 이동
suminb99 Feb 28, 2026
e06d73e
#52 fix: 빌드 에러 수정
suminb99 Feb 28, 2026
6550556
#52 refactor: AssignmentPageLayout 버튼 인터페이스를 render prop으로 변경
suminb99 Feb 28, 2026
15b8a7a
#52 refactor: 편집 모드 시 문제 연결 버튼 렌더링 x
suminb99 Feb 28, 2026
1597f15
#52 fix: isSubmitting을 isPending으로 교체하여 이중 제출 방지
suminb99 Feb 28, 2026
9f3fcc6
#52 fix: 불필요한 fallback 제거
suminb99 Feb 28, 2026
0d94e1f
#52 fix: 불필요한 래퍼 함수 및 코드 정리
suminb99 Feb 28, 2026
4f94358
#52 feat: LabeledInput에 errorMessage, required prop 추가
suminb99 Feb 28, 2026
8c1f4f7
#52 refactor: UnitForm 에러 메세지를 LabeledInput prop으로 이동
suminb99 Feb 28, 2026
6888d2b
#52 fix: 날짜 범위 유효성 에러 path를 releaseDate로 수정
suminb99 Feb 28, 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
c2454fa
Merge branch 'feat/52-manage-assignment-ui-api' into feat/59-course-o…
suminb99 Feb 28, 2026
1047e28
#62 fix: Unit 타입 import 경로 변경
suminb99 Feb 28, 2026
292ca07
#62 refactor: 불필요한 네이밍 접두사 제거 및 Unit 타입 직접 참조로 변경
suminb99 Feb 28, 2026
a20e8a1
Merge branch 'chore/62-zod-schema' into feat/59-course-overview-api
suminb99 Feb 28, 2026
7ce440d
#59 fix: 시멘틱 개선 및 헤더가 보이게 layout header 스타일 속성 변경
suminb99 Feb 28, 2026
83783f4
#59 feat: 단일 강의 조회 API 및 쿼리 옵션 추가
suminb99 Feb 28, 2026
308f316
#59 feat: 강의상세페이지 api 연동
suminb99 Feb 28, 2026
c0d8fad
#59 feat: 타입 수정 및 props 추가
suminb99 Feb 28, 2026
ce66839
#59 fix: Link 경로 업데이트
suminb99 Feb 28, 2026
df8758b
chore: 불필요한 mock 파일 제거
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),
},
};
37 changes: 37 additions & 0 deletions src/entities/assignment/api/assignmentQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {queryOptions} from '@tanstack/react-query';
import {
getAllAssignments,
getAssignmentsByCourse,
getAssignmentSchedules,
} from './assignmentApi';

export const assignmentQueries = {
// 과제 일정 조회 쿼리 옵션
getAssignmentSchedules: () =>
queryOptions({
queryKey: ['schedules'],
queryFn: getAssignmentSchedules,
select: (data) => ({
scheduleCount: data.response.count,
schedules: data.response.schedule,
}),
}),

// 전체 과제 목록 조회 쿼리 옵션
getAllAssignments: () =>
queryOptions({
queryKey: ['assignments'],
queryFn: getAllAssignments,
select: (data) => data.response.assignments,
}),

// 강의별 과제 목록 조회 쿼리 옵션
getAssignmentsByCourse: (courseId: number) =>
queryOptions({
queryKey: ['courses', courseId, 'assignments'],
queryFn: () => getAssignmentsByCourse(courseId),
enabled: !!courseId,
select: (data) =>
data.response.courses.flatMap((course) => course.assignments),
}),
};
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
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(),
});
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
25 changes: 17 additions & 8 deletions src/entities/course/api/courseApi.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
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 {courseOverviewSchema, 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 getCourseById = async (courseId: number) => {
const response = await privateAxios.get(`/courses/${courseId}`);
return apiResponseSchema(courseOverviewSchema).parse(response.data);
};

// 강의 삭제 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),
},
};
23 changes: 23 additions & 0 deletions src/entities/course/api/courseQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {getAllCourses, getCourseById} from './courseApi';
import {queryOptions} from '@tanstack/react-query';

export const courseQueries = {
// 전체 강의 조회 쿼리 옵션
getAllCourses: () =>
queryOptions({
queryKey: ['courses'],
queryFn: getAllCourses,
select: (data) => ({
courseCount: data.response.count,
courses: data.response.courses,
}),
}),

// 단일 강의 조회 쿼리 옵션
getCourseDetails: (courseId: number) =>
queryOptions({
queryKey: ['courses', 'detail', courseId],
queryFn: () => getCourseById(courseId),
select: (data) => data.response,
}),
};
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