-
Notifications
You must be signed in to change notification settings - Fork 0
#62 chore: Zod 스키마 기반 API 응답 검증 및 타입 중복 제거 #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
b12d3b8
c17ba82
dd44942
c3fb10b
d590227
46d842f
b5495f5
c5b95d9
932b8d6
072672a
e228664
3c49ba1
2e6b2f4
92c4b51
d4428d1
004a045
df245ef
e7ea6cb
bbe3938
191e891
7bf990a
52b6f14
47eda8a
bd80094
6959037
628a411
f91092f
2607964
1dbdb31
1047e28
292ca07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }; |
| 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), | ||
| }, | ||
| }; |
| 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가 있을 때만 쿼리 실행 | ||
| }), | ||
| }; |
This file was deleted.
| 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(), | ||
| }) | ||
| ), | ||
| }); |
| 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>; |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 7의 개선 제안 (역할별 응답을 명시적으로 검증) 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(),
});필요하면 더 엄격하게 공식 문서: https://zod.dev/?id=optionals , https://zod.dev/?id=discriminated-unions 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| 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); | ||
| }; |
| 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), | ||
| }, | ||
| }; |
| 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, | ||
| }), | ||
| }; |
This file was deleted.
This file was deleted.
| 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}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
스키마로 옮겼으면 type.ts의 수동 타입 정의는 삭제하고 z.infer<>로 통일하면 될 것 같아요!