Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit릴리스 노트
요약WalkthroughAPI 응답에 Zod 스키마 기반 런타임 검증을 도입하고, React Query 패턴을 재구성하며, 다중 도메인(할당, 코스, 단원, 학생)에 걸친 데이터 페칭 로직을 업데이트했습니다. 모의 데이터를 제거하고 타입을 Zod 추론 타입으로 마이그레이션했습니다. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 분 Possibly related PRs
Suggested labels
Suggested reviewers
상세 피드백💡 긍정적 측면스키마 기반 검증 도입 - Zod를 활용한 런타임 검증 계층이 타입 안전성을 크게 향상시킵니다. 이는 API 응답 형식의 변화를 조기에 감지할 수 있는 좋은 방식입니다. 📋 검토 포인트1. 스키마 정의의 일관성
2. 쿼리 select 변환의 정확성// assignmentQueries.ts 예시
getAssignmentSchedules: {
select: (data) => ({ scheduleCount: data.response.count, schedules: data.response.schedule })
}
3. UnitEditorPage의 모드 관리 단순화
4. 모의 데이터 제거 시 마이그레이션 경로
5. 타입 마이그레이션 완전성
6. AssignmentListContainer의 리팩토링// 변경 전
onSelect: (id: number) => void
// 변경 후
onSelect?: (item: Assignment) => void
7. CourseActionsBar의 동적 스타일링
✅ 추천 사항
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (22)
src/pages/course-overview/ui/CourseActionsBar.tsx (1)
17-18: 불필요한 템플릿 리터럴 사용 제거를 권장합니다.삼항 연산자가 이미 문자열을 반환하므로 템플릿 리터럴로 감쌀 필요가 없습니다. 더 간결한 코드를 위해 직접 값을 전달하세요.
♻️ 제안된 수정
<Link to='/admin/student' className={buttonStyles({ - color: `${isActiveCourse ? 'outlineWhite' : 'outlinePurple'}`, + color: isActiveCourse ? 'outlineWhite' : 'outlinePurple', })}> 학생 목록 </Link> <Link to={`/admin/units/${courseId}`} className={buttonStyles({ - color: `${isActiveCourse ? 'outlinePurple' : 'primary'}`, + color: isActiveCourse ? 'outlinePurple' : 'primary', })}> 단원 추가 </Link>Also applies to: 24-25
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/course-overview/ui/CourseActionsBar.tsx` around lines 17 - 18, Remove the unnecessary template literals when passing the color prop to buttonStyles in CourseActionsBar.tsx: where color is currently set as color: `${isActiveCourse ? 'outlineWhite' : 'outlinePurple'}` (and the similar usage at the other occurrence around lines 24-25), change it to pass the string directly using the ternary result (color: isActiveCourse ? 'outlineWhite' : 'outlinePurple') so buttonStyles receives a plain string instead of a template literal.src/entities/auth/api/authApi.ts (1)
16-16: 스키마 기반 검증 적용 - 에러 핸들링 고려
.parse()사용 시 스키마 불일치가 발생하면ZodError가 throw됩니다. 이는 API 계약 위반을 빠르게 감지하는 데 유용하지만, 호출하는 쪽에서 이 에러를 적절히 처리할 수 있는지 확인이 필요합니다.만약 더 유연한 에러 핸들링이 필요하다면
.safeParse()를 고려해볼 수 있습니다.💡 safeParse 사용 예시 (선택사항)
const result = kakaoLoginResponseSchema.safeParse(response.data.response); if (!result.success) { console.error('API 응답 검증 실패:', result.error); throw new Error('Invalid API response'); } const data = result.data;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/auth/api/authApi.ts` at line 16, Replace the direct zod parse call to avoid uncaught ZodError from crashing callers: in authApi.ts where kakaoLoginResponseSchema.parse(response.data.response) is used, switch to kakaoLoginResponseSchema.safeParse(...) (or wrap parse in a try/catch for ZodError) and handle validation failures by logging the validation error and throwing a controlled Error (or returning a structured failure) so callers of the function (the auth API method that consumes response.data.response) can handle the invalid-response case gracefully.src/entities/auth/model/schemas.ts (1)
3-10: Zod 스키마 기반 런타임 검증 도입 - 좋은 패턴입니다! 🛡️API 응답에 대한 런타임 검증을 추가하여 타입 안전성을 강화했습니다. 추가로, 이 스키마에서 타입을 추론하여 export하면 다른 곳에서도 재사용할 수 있습니다.
💡 타입 추론 export 제안
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(), }); + +export type KakaoLoginResponse = z.infer<typeof kakaoLoginResponseSchema>;참고: Zod 공식 문서 - Type inference
🤖 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, Export a TypeScript type inferred from the existing Zod schema so other modules can reuse the runtime-validated shape; add an exported type alias like "KakaoLoginResponse" using z.infer<typeof kakaoLoginResponseSchema> (referencing the kakaoLoginResponseSchema symbol) so callers can import the inferred type for static typing alongside the runtime schema.src/shared/ui/LabeledInput.tsx (1)
13-13:required기본값이true로 설정되어 있음 - 명시적 설정 권장컴포넌트가
React.InputHTMLAttributes를 확장하지만required속성의 기본값을true로 설정하는 것은 표준 HTML input 동작(기본값: 필수 아님)과 다릅니다. 현재 모든 사용처가 필수 필드이므로 문제없지만, 향후 선택적 필드가 필요한 경우 명시적으로required={false}를 추가해야 합니다.권장사항:
- 의도를 명확히 하기 위해 컴포넌트 JSDoc에 기본값 설명 추가
- 또는 기본값을
undefined로 변경하고 사용처에서 필요시required={true}명시 (더 직관적)// 현재: 모든 필드가 자동으로 필수 <LabeledInput label="제목" /> // 선택적 필드 필요 시 <LabeledInput label="소개" required={false} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/ui/LabeledInput.tsx` at line 13, The LabeledInput component currently sets the prop default required = true which deviates from standard HTML input default and forces callers to override; update the component by either (A) removing the default and setting required to undefined in the props/defaults so consumers must opt-in with required={true}, or (B) if keeping true is intentional, add a JSDoc comment on the LabeledInput component describing that required defaults to true; locate the default assignment (required = true) in LabeledInput.tsx and implement one of these two changes and adjust any callers if you choose option A.src/entities/student/model/schemas.ts (1)
18-25: 날짜 필드 검증 강화 고려
startDate,endDate가z.string()으로 정의되어 있는데, 잘못된 형식의 문자열도 통과됩니다. API 계약이 ISO 날짜 형식을 보장한다면, 추후z.string().datetime()또는 커스텀 정규식 검증을 추가하면 런타임 안정성이 높아집니다.// 예시: ISO 날짜 형식 검증 startDate: z.string().datetime(), // ISO 8601 형식 // 또는 커스텀 검증 startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),참고: Zod datetime 문서
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/student/model/schemas.ts` around lines 18 - 25, studentUnitSchema currently accepts any string for startDate and endDate; tighten validation by replacing the z.string() types for startDate and endDate with a datetime or regex validator (e.g., use z.string().datetime() for ISO‑8601 or z.string().regex(...) for a custom YYYY-MM-DD pattern) so the schema (studentUnitSchema) enforces proper date formatting for startDate and endDate and fails fast on invalid values.src/shared/model/schemas.ts (1)
10-14:errorResponseSchema는 현재 사용되지 않는 것으로 확인됨이 스키마가 향후 에러 응답 검증에 사용될 예정이라면,
additionalProp1,additionalProp2,additionalProp3는 Swagger/OpenAPI 자동 생성 예시명처럼 보이므로z.record(z.string(), z.string())로 개선하는 것이 좋습니다. 현재는 각 엔티티에서 특정 응답 스키마를 정의해서 사용 중입니다.♻️ 동적 에러 필드 스키마
export const errorResponseSchema = apiResponseSchema( z.object({ errorCode: z.string(), errorMessage: z.string(), - errors: z.object({ - additionalProp1: z.string(), - additionalProp2: z.string(), - additionalProp3: z.string(), - }), + errors: z.record(z.string(), z.string()), }) );참고: Zod record 문서
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/model/schemas.ts` around lines 10 - 14, errorResponseSchema에 정의된 errors 객체가 Swagger 예시 필드명(additionalProp1/2/3)을 사용하고 있어 일반적인 에러 키를 표현하지 못하므로, errors: z.object({...}) 대신 errors: z.record(z.string(), z.string())로 변경해 동적 키/값 형태의 에러 응답을 허용하도록 바꿔주세요; 관련 심볼은 errorResponseSchema과 errors 필드이며, 변경 시 기존 호출부에서 특정 엔티티별 스키마를 별도로 정의하는 방식에는 영향이 없도록 테스트 케이스나 타입 호환성을 확인해 주세요.src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx (2)
28-33: 접근성을 위해aria-label추가를 권장합니다.아이콘만 있는 버튼은 스크린 리더 사용자에게 기능이 전달되지 않습니다. 접근성 향상을 위해
aria-label을 추가하세요.♿ 접근성 개선 제안
- <button onClick={handleOnEdit}> + <button onClick={handleOnEdit} aria-label="과제 수정"> <EditIcon className='w-3.5 h-4' /> </button> - <button onClick={handleOnDelete}> + <button onClick={handleOnDelete} aria-label="과제 삭제"> <DeleteIcon className='w-3.5 h-4' /> </button>📚 참고: WAI-ARIA Authoring Practices - Button
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx` around lines 28 - 33, AssignmentManageActionsBar has icon-only buttons for edit/delete (buttons calling handleOnEdit and handleOnDelete with EditIcon/DeleteIcon) that lack accessible labels; update the JSX to add descriptive aria-label attributes (e.g., aria-label="Edit assignment" and aria-label="Delete assignment") on the respective buttons and ensure the icon SVGs remain aria-hidden (or role="img" with appropriate title) so screen readers read the button labels, not the icons.
16-22: 핸들러 네이밍을 간소화할 수 있습니다.
handleOnEdit→handleEdit,handleOnDelete→handleDelete로On을 생략하면 더 간결합니다. 선택적 개선 사항입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx` around lines 16 - 22, Rename the handler functions for brevity: change handleOnEdit to handleEdit and handleOnDelete to handleDelete, update their implementations to still call navigate(`/admin/assignments/${id}`) and onDelete(id) respectively, and replace all usages/references (e.g., JSX props or event bindings) that currently reference handleOnEdit or handleOnDelete to the new names so nothing breaks; ensure any tests or prop types referencing those function names are updated as well.src/pages/unit-editor/model/types.ts (1)
5-10:'idle'제거 후null사용에 대한 고려사항
Mode에서'idle'을 제거하고mode: Mode | null로 변경했는데,null보다'idle'이 의미를 더 명확히 전달합니다. 현재 설계가 의도적이라면 괜찮지만, 가독성 측면에서'idle' | 'creating' | 'editing'을 유지하는 것도 고려해 보세요.💡 대안 제안
-export type Mode = 'creating' | 'editing'; +export type Mode = 'idle' | 'creating' | 'editing'; export interface UnitFormProps { unit?: Unit; unitIndex: number; - mode: Mode | null; + mode: Mode;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/unit-editor/model/types.ts` around lines 5 - 10, The Mode type was changed to remove 'idle' and UnitFormProps.mode was made nullable, which reduces clarity; restore 'idle' by updating the Mode type to "type Mode = 'idle' | 'creating' | 'editing'" and change UnitFormProps.mode to use Mode (not Mode | null); then update any callers of UnitFormProps (e.g., components constructing UnitFormProps) to pass 'idle' as the default state instead of null (or handle the new non-nullable Mode accordingly) so the intent remains explicit; refer to the Mode type and UnitFormProps interface to locate and change the definitions and call sites.src/entities/assignment/api/assignmentQueries.ts (1)
11-18:queryKey를 더 구체적으로 지정하는 것을 권장합니다.
['schedules']는 다소 범용적입니다. 다른 도메인에서 스케줄 관련 쿼리가 추가될 경우 충돌 가능성이 있습니다.['assignments', 'schedules']와 같이 도메인을 포함하면 명확해집니다.♻️ queryKey 개선 제안
getAssignmentSchedules: () => queryOptions({ - queryKey: ['schedules'], + queryKey: ['assignments', 'schedules'], queryFn: getAssignmentSchedules,📚 참고: TanStack Query - Query Keys
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/assignment/api/assignmentQueries.ts` around lines 11 - 18, The queryKey used in the queryOptions call is too generic (['schedules']) and may collide with other schedule queries; update the queryOptions invocation that references queryKey and getAssignmentSchedules so the key is namespaced (for example ['assignments','schedules'] or similar) to include the domain, ensuring any select logic mapping scheduleCount/schedules remains unchanged.src/entities/unit/model/types.ts (1)
1-1:z는 타입 추론에만 사용되므로import type을 권장합니다.
z가z.infer로만 사용되고 있어 런타임에 필요하지 않습니다. 코딩 가이드라인에 따라 type-only import를 사용하세요.♻️ 수정 제안
-import {z} from 'zod'; +import type {z} from 'zod'; import type {unitFormSchema, unitSchema} from './schemas';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/unit/model/types.ts` at line 1, The file currently imports the zod runtime object with "z" but only uses it for type inference (z.infer); change the import to a type-only import and update usages accordingly: replace the value import of z with a type import (so TypeScript erases it at runtime), update any z.infer usages to reference the imported type alias (e.g., use the proper imported type helper or a type alias like Infer<typeof YourSchema>), and if any code actually needs the runtime z object leave a regular import there—refer to the symbol "z" and occurrences of "z.infer" to locate and modify the imports and type usages.src/entities/unit/api/unitQueries.ts (1)
21-24:skipToken패턴 적절히 사용되었습니다! ✨React Query v5의
skipToken을 활용하여 조건부 쿼리를 깔끔하게 처리했습니다.select함수는 한 줄로 단순화할 수 있습니다.♻️ select 함수 단순화
-select: (data) => { - return data.response; -}, +select: (data) => data.response,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/unit/api/unitQueries.ts` around lines 21 - 24, The select callback in the query configuration can be simplified to a single expression; update the select function used alongside queryFn (where queryFn: unitId ? () => getUnitById(unitId) : skipToken) to return data.response with a concise one-line arrow function instead of the current block body — locate the select option in the query definition and replace the multi-line function with a single-line expression.src/entities/assignment/model/schemas.ts (1)
2-2: Zod import 스타일 일관성 확인 필요
import z from 'zod'대신import {z} from 'zod'를 사용하면 다른 스키마 파일(예:src/entities/unit/model/schemas.ts)과 일관성을 유지할 수 있습니다.♻️ import 스타일 통일 제안
-import z from 'zod'; +import {z} from 'zod';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/assignment/model/schemas.ts` at line 2, The module currently imports Zod with a default import (`import z from 'zod'`); change this to the named import style used elsewhere (`import { z } from 'zod'`) so the schemas in this file use the same import pattern as other schema files (e.g., `src/entities/unit/model/schemas.ts`)—update the import statement at the top and ensure all uses of `z` remain unchanged.src/entities/unit/model/useUnitStore.ts (1)
24-29: 객체 속성 단축 구문 사용 가능동일한 이름의 변수를 객체에 할당할 때 단축 구문을 사용하면 더 간결해집니다.
♻️ 단축 구문 적용 제안
storeFormData: (title, releaseDate, dueDate) => set({ - title: title, - releaseDate: releaseDate, - dueDate: dueDate, + title, + releaseDate, + dueDate, }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/unit/model/useUnitStore.ts` around lines 24 - 29, In storeFormData, instead of creating an object with explicit key: value pairs, use the ES object property shorthand for title, releaseDate, and dueDate so the set call passes { title, releaseDate, dueDate } directly; update the storeFormData implementation that calls set(...) (function name: storeFormData, caller: set) to use the shortened property names.src/entities/unit/model/schemas.ts (1)
23-25: 날짜 문자열 비교 시 형식 의존성 주의문자열 비교(
data.releaseDate <= data.dueDate)는 ISO 8601 형식(YYYY-MM-DD)에서만 올바르게 동작합니다. 다른 형식의 날짜가 입력되면 예기치 않은 결과가 발생할 수 있습니다.♻️ Date 객체를 사용한 안전한 비교 제안
-.refine((data) => data.releaseDate <= data.dueDate, { +.refine((data) => new Date(data.releaseDate) <= new Date(data.dueDate), { message: '날짜 범위가 올바르지 않습니다.', path: ['dueDate'], });📚 참고: Zod refinements 공식 문서
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/unit/model/schemas.ts` around lines 23 - 25, The current Zod .refine on the unit schema compares releaseDate and dueDate as strings which is format-dependent; modify the refinement in schemas.ts to parse both fields into Date (or epoch numbers) inside the refine callback (e.g., new Date(data.releaseDate).getTime() and new Date(data.dueDate).getTime()), check for invalid dates (isNaN) and return false (or set a specific error) when parsing fails, then compare the numeric timestamps to ensure a correct chronological check for releaseDate <= dueDate; update the refine attached to the schema that references releaseDate and dueDate accordingly.src/features/assignment/filter-assignment/lib/useAssignmentList.ts (1)
12-17: 조건부 쿼리 실행 최적화 고려현재 구조에서 두 쿼리가 항상 등록됩니다.
enabled옵션이!!courseId로 설정되어 있어courseId = 0일 때 실행되지 않지만, 훅 자체는 항상 호출됩니다.명시적인 조건부 쿼리를 위해
enabled옵션을 직접 전달하는 방식도 고려해볼 수 있습니다.♻️ 대안 구조 제안
+import {skipToken} from '@tanstack/react-query'; + export const useAssignmentList = ( selectedCourseId: number | null ): Assignment[] => { - const {data: allAssignments} = useQuery( - assignmentQueries.getAllAssignments() - ); - const {data: assignments} = useQuery( - assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) - ); + const {data: allAssignments} = useQuery({ + ...assignmentQueries.getAllAssignments(), + enabled: !selectedCourseId, + }); + const {data: assignments} = useQuery({ + ...assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0), + enabled: !!selectedCourseId, + });📚 참고: TanStack Query Dependent Queries
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/assignment/filter-assignment/lib/useAssignmentList.ts` around lines 12 - 17, Both queries in the useAssignmentList hook are always registered; make them conditional by passing the `enabled` option to useQuery so they only run when appropriate: add `enabled: true/false` to the useQuery call that uses `assignmentQueries.getAllAssignments()` and to the call using `assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0)` (use `enabled: !!selectedCourseId` or `enabled: selectedCourseId !== 0` for the course-specific query, or `enabled: selectedCourseId == null` for the all-assignment query depending on desired behavior) so the queries are not executed unless the required `selectedCourseId` state is present; look for the `useQuery` calls and the `selectedCourseId` variable inside `useAssignmentList` to apply this change.src/pages/course-overview/CourseOverviewPage.tsx (1)
15-15: 불필요한 삼항 연산자 단순화 가능 👌
course.unitCount !== 0 ? true : false는 이미 boolean을 반환하는 표현식입니다.♻️ 단순화 제안
-const hasUnits = course.unitCount !== 0 ? true : false; +const hasUnits = course.unitCount !== 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/course-overview/CourseOverviewPage.tsx` at line 15, The hasUnits assignment uses an unnecessary ternary; replace the expression that sets hasUnits (currently using course.unitCount !== 0 ? true : false) with a direct boolean expression such as course.unitCount !== 0 or Boolean(course.unitCount) in CourseOverviewPage.tsx so hasUnits is computed without the redundant ternary.src/pages/select-assignment/ui/AssignmentListContainer.tsx (1)
5-5: 모듈 레벨에서T를 고정하면 제네릭의 의미가 사라집니다.현재 구조에서는
AssignmentListContainerProps<T>가 제네릭처럼 보이지만,T가 모듈 레벨에서 이미 고정되어 있어 실제로는 제네릭이 아닙니다. 진정한 제네릭이 필요 없다면 타입 별칭을 직접 사용하거나, 제네릭이 필요하다면 컴포넌트 정의 시점에 타입 파라미터를 받도록 수정하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx` at line 5, The module-level type alias T fixes the generic, so either remove the module-level alias and inline the concrete type where used (e.g., replace uses of T with AssignmentCourse['assignments'][number] and keep AssignmentListContainerProps non-generic), or make the component truly generic by removing the top-level T and adding a type parameter on the component declaration (e.g., declare AssignmentListContainer as a generic component using AssignmentListContainerProps<T>) and update all references to use that parameter; adjust imports/exports accordingly to ensure AssignmentListContainerProps and the component share the same generic parameter rather than relying on a module-scoped T.src/pages/manage-assignment/AssignmentManagePage.tsx (2)
31-48: 삭제 뮤테이션 구현이 잘 되어 있습니다!쿼리 무효화 패턴이 적절하게 구현되어 있네요. 다만 한 가지 개선 제안이 있습니다:
selectedCourseId가null일 때courseId: 0으로 쿼리를 무효화하게 되는데, 이는 실제로 존재하지 않는 쿼리를 무효화하려는 시도가 될 수 있습니다. 대신 조건부로 무효화하는 것을 고려해 보세요.♻️ 선택적 개선 제안
onSuccess: () => { queryClient.invalidateQueries({ queryKey: assignmentQueries.getAllAssignments().queryKey, }); - queryClient.invalidateQueries({ - queryKey: assignmentQueries.getAssignmentsByCourse( - selectedCourseId ?? 0 - ).queryKey, - }); + if (selectedCourseId) { + queryClient.invalidateQueries({ + queryKey: assignmentQueries.getAssignmentsByCourse(selectedCourseId).queryKey, + }); + } alert('문제가 성공적으로 삭제되었습니다.'); },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/manage-assignment/AssignmentManagePage.tsx` around lines 31 - 48, The current deleteAssignment mutation always invalidates assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) which uses 0 when selectedCourseId is null; change this to only call queryClient.invalidateQueries for assignmentQueries.getAssignmentsByCourse(selectedCourseId).queryKey when selectedCourseId is non-null/defined (e.g., guard on selectedCourseId !== null/undefined) so you don't attempt to invalidate a non-existent course query; keep the existing invalidateQueries for getAllAssignments and the onSuccess/onError handlers intact.
42-42:alert()를 커스텀 토스트/모달로 대체를 고려해 보세요.
alert()는 동작하지만 브라우저 기본 UI를 사용하므로 UX 일관성이 떨어질 수 있습니다. 향후 프로젝트의 디자인 시스템에 맞는 토스트 컴포넌트 도입을 권장합니다.Also applies to: 46-46
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/manage-assignment/AssignmentManagePage.tsx` at line 42, Replace the browser alert() calls in the AssignmentManagePage component with the app's toast/modal utilities to maintain consistent UX: locate the two alert('문제가 성공적으로 삭제되었습니다.') occurrences (the success callbacks after question/assignment deletion) and call the project's toast or modal API (e.g., showToast or openModal) instead, passing a localized success message and optional success variant/options; ensure you import and use the existing notification component used elsewhere in the app and remove the alert() usages.src/entities/unit/api/unitApi.ts (1)
10-17: 인라인 스키마를 별도 파일로 분리하는 것을 고려해 보세요.
getAllUnitsByCourseId의 응답 스키마가 인라인으로 정의되어 있습니다. 일관성을 위해src/entities/unit/model/schemas.ts에 별도로 정의하면 재사용성과 유지보수성이 향상됩니다.♻️ 스키마 분리 제안
// src/entities/unit/model/schemas.ts에 추가 export const unitListResponseSchema = z.object({ count: z.number(), units: z.array( unitSchema.pick({id: true, title: true, assignmentCount: true}) ), });// unitApi.ts +import {unitListResponseSchema} from '../model/schemas'; export const getAllUnitsByCourseId = async (courseId: number) => { const response = await privateAxios.get(`/courses/${courseId}/units`); - return apiResponseSchema( - z.object({ - count: z.number(), - units: z.array( - unitSchema.pick({id: true, title: true, assignmentCount: true}) - ), - }) - ).parse(response.data); + return apiResponseSchema(unitListResponseSchema).parse(response.data); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/entities/unit/api/unitApi.ts` around lines 10 - 17, The inline response schema in getAllUnitsByCourseId should be extracted to a reusable exported symbol: create unitListResponseSchema in src/entities/unit/model/schemas.ts using unitSchema.pick({id: true, title: true, assignmentCount: true}) wrapped with z.object({count: z.number(), units: z.array(...)}); then in src/entities/unit/api/unitApi.ts import unitListResponseSchema and pass it to apiResponseSchema(...).parse(response.data) instead of the inline z.object; update any imports/usages and tests that referenced the previous inline shape to use unitListResponseSchema.src/pages/select-assignment/AssignmentSelectPage.tsx (1)
70-79: 버튼 영역 구현이 깔끔합니다.취소/등록 버튼이 적절한 색상과 핸들러로 연결되어 있습니다. 다만
returnToPreviousPage()를 직접 전달하는 것도 고려해 보세요:♻️ 간소화 제안 (선택)
-<Button color='outlinePurple' onClick={() => returnToPreviousPage()}> +<Button color='outlinePurple' onClick={returnToPreviousPage}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/select-assignment/AssignmentSelectPage.tsx` around lines 70 - 79, The onClick prop for the cancel Button is using an unnecessary arrow wrapper; change the handler to pass the function reference directly by replacing the onClick={() => returnToPreviousPage()} usage with onClick={returnToPreviousPage} to simplify the code and avoid creating an extra closure (refer to returnToPreviousPage, handleConfirm, and the Button component in AssignmentSelectPage).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/pages/course-overview/CourseOverviewPage.tsx`:
- Around line 9-10: The code currently converts useParams()'s id to a number
without validation (const {id} = useParams(); const courseId = Number(id);)
which can produce NaN; update CourseOverviewPage to validate id before
conversion: ensure id is a defined string and parseInt/Number yields a finite
integer (e.g., typeof id === 'string' && !Number.isNaN(Number(id))); if invalid,
handle it (redirect to a 404/error page, show an error state, or bail out before
any API calls that use courseId). Place this check where courseId is
created/used (in the CourseOverviewPage component) so all subsequent API calls
and logic use a guaranteed valid courseId.
In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx`:
- Around line 5-12: The generic type T is constrained to
AssignmentCourse['assignments'][number] while onSelect and handleSelect expect
an Assignment (which may include submittedStatus), causing a type mismatch;
update the component props and handlers to be consistent by either broadening
the generic constraint to Assignment (e.g., make AssignmentListContainerProps<T
extends Assignment>) or by changing onSelect and handleSelect signatures to
accept T instead of Assignment; locate and update AssignmentListContainerProps,
the renderItem prop, and the handleSelect/onSelect usages so types align (choose
one approach and apply it across the component).
In `@src/pages/unit-editor/UnitEditorPage.tsx`:
- Around line 26-43: currentMode can be null and is being passed into UnitForm
causing unintended creation flows; change the render logic so UnitForm only
receives a concrete Mode ('creating' | 'editing') — use activeMode,
hasOngoingCreation and initialMode to derive a non-null mode before rendering or
simply conditionally render/return null while currentMode === null (or while
data/loading). Specifically, update the logic around currentMode/currentUnitId
(referencing currentMode, activeMode, hasOngoingCreation, initialMode,
selectedUnitId) so that UnitForm is not mounted with currentMode === null;
ensure any props passed to UnitForm are strictly 'creating' | 'editing' and
delay form rendering until a definite mode is determined.
- Around line 15-27: The code can call queries and create requests with an
invalid courseId (NaN); compute courseId as Number(id) and guard it with
Number.isFinite(courseId) before using it: create a boolean like isValidCourseId
= Number.isFinite(courseId) and pass enabled: isValidCourseId to
useQuery(unitQueries.getUnitList(courseId)); compute initialMode only when
isValidCourseId is true (otherwise keep null) and add the same isValidCourseId
guard inside onCreateUnit to early-return or block creation when invalid; update
references to useParams, courseId, useQuery, unitQueries.getUnitList,
initialMode, currentMode, onCreateUnit, and UnitForm accordingly so no requests
or create calls are made with NaN.
---
Nitpick comments:
In `@src/entities/assignment/api/assignmentQueries.ts`:
- Around line 11-18: The queryKey used in the queryOptions call is too generic
(['schedules']) and may collide with other schedule queries; update the
queryOptions invocation that references queryKey and getAssignmentSchedules so
the key is namespaced (for example ['assignments','schedules'] or similar) to
include the domain, ensuring any select logic mapping scheduleCount/schedules
remains unchanged.
In `@src/entities/assignment/model/schemas.ts`:
- Line 2: The module currently imports Zod with a default import (`import z from
'zod'`); change this to the named import style used elsewhere (`import { z }
from 'zod'`) so the schemas in this file use the same import pattern as other
schema files (e.g., `src/entities/unit/model/schemas.ts`)—update the import
statement at the top and ensure all uses of `z` remain unchanged.
In `@src/entities/auth/api/authApi.ts`:
- Line 16: Replace the direct zod parse call to avoid uncaught ZodError from
crashing callers: in authApi.ts where
kakaoLoginResponseSchema.parse(response.data.response) is used, switch to
kakaoLoginResponseSchema.safeParse(...) (or wrap parse in a try/catch for
ZodError) and handle validation failures by logging the validation error and
throwing a controlled Error (or returning a structured failure) so callers of
the function (the auth API method that consumes response.data.response) can
handle the invalid-response case gracefully.
In `@src/entities/auth/model/schemas.ts`:
- Around line 3-10: Export a TypeScript type inferred from the existing Zod
schema so other modules can reuse the runtime-validated shape; add an exported
type alias like "KakaoLoginResponse" using z.infer<typeof
kakaoLoginResponseSchema> (referencing the kakaoLoginResponseSchema symbol) so
callers can import the inferred type for static typing alongside the runtime
schema.
In `@src/entities/student/model/schemas.ts`:
- Around line 18-25: studentUnitSchema currently accepts any string for
startDate and endDate; tighten validation by replacing the z.string() types for
startDate and endDate with a datetime or regex validator (e.g., use
z.string().datetime() for ISO‑8601 or z.string().regex(...) for a custom
YYYY-MM-DD pattern) so the schema (studentUnitSchema) enforces proper date
formatting for startDate and endDate and fails fast on invalid values.
In `@src/entities/unit/api/unitApi.ts`:
- Around line 10-17: The inline response schema in getAllUnitsByCourseId should
be extracted to a reusable exported symbol: create unitListResponseSchema in
src/entities/unit/model/schemas.ts using unitSchema.pick({id: true, title: true,
assignmentCount: true}) wrapped with z.object({count: z.number(), units:
z.array(...)}); then in src/entities/unit/api/unitApi.ts import
unitListResponseSchema and pass it to
apiResponseSchema(...).parse(response.data) instead of the inline z.object;
update any imports/usages and tests that referenced the previous inline shape to
use unitListResponseSchema.
In `@src/entities/unit/api/unitQueries.ts`:
- Around line 21-24: The select callback in the query configuration can be
simplified to a single expression; update the select function used alongside
queryFn (where queryFn: unitId ? () => getUnitById(unitId) : skipToken) to
return data.response with a concise one-line arrow function instead of the
current block body — locate the select option in the query definition and
replace the multi-line function with a single-line expression.
In `@src/entities/unit/model/schemas.ts`:
- Around line 23-25: The current Zod .refine on the unit schema compares
releaseDate and dueDate as strings which is format-dependent; modify the
refinement in schemas.ts to parse both fields into Date (or epoch numbers)
inside the refine callback (e.g., new Date(data.releaseDate).getTime() and new
Date(data.dueDate).getTime()), check for invalid dates (isNaN) and return false
(or set a specific error) when parsing fails, then compare the numeric
timestamps to ensure a correct chronological check for releaseDate <= dueDate;
update the refine attached to the schema that references releaseDate and dueDate
accordingly.
In `@src/entities/unit/model/types.ts`:
- Line 1: The file currently imports the zod runtime object with "z" but only
uses it for type inference (z.infer); change the import to a type-only import
and update usages accordingly: replace the value import of z with a type import
(so TypeScript erases it at runtime), update any z.infer usages to reference the
imported type alias (e.g., use the proper imported type helper or a type alias
like Infer<typeof YourSchema>), and if any code actually needs the runtime z
object leave a regular import there—refer to the symbol "z" and occurrences of
"z.infer" to locate and modify the imports and type usages.
In `@src/entities/unit/model/useUnitStore.ts`:
- Around line 24-29: In storeFormData, instead of creating an object with
explicit key: value pairs, use the ES object property shorthand for title,
releaseDate, and dueDate so the set call passes { title, releaseDate, dueDate }
directly; update the storeFormData implementation that calls set(...) (function
name: storeFormData, caller: set) to use the shortened property names.
In `@src/features/assignment/filter-assignment/lib/useAssignmentList.ts`:
- Around line 12-17: Both queries in the useAssignmentList hook are always
registered; make them conditional by passing the `enabled` option to useQuery so
they only run when appropriate: add `enabled: true/false` to the useQuery call
that uses `assignmentQueries.getAllAssignments()` and to the call using
`assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0)` (use `enabled:
!!selectedCourseId` or `enabled: selectedCourseId !== 0` for the course-specific
query, or `enabled: selectedCourseId == null` for the all-assignment query
depending on desired behavior) so the queries are not executed unless the
required `selectedCourseId` state is present; look for the `useQuery` calls and
the `selectedCourseId` variable inside `useAssignmentList` to apply this change.
In `@src/pages/course-overview/CourseOverviewPage.tsx`:
- Line 15: The hasUnits assignment uses an unnecessary ternary; replace the
expression that sets hasUnits (currently using course.unitCount !== 0 ? true :
false) with a direct boolean expression such as course.unitCount !== 0 or
Boolean(course.unitCount) in CourseOverviewPage.tsx so hasUnits is computed
without the redundant ternary.
In `@src/pages/course-overview/ui/CourseActionsBar.tsx`:
- Around line 17-18: Remove the unnecessary template literals when passing the
color prop to buttonStyles in CourseActionsBar.tsx: where color is currently set
as color: `${isActiveCourse ? 'outlineWhite' : 'outlinePurple'}` (and the
similar usage at the other occurrence around lines 24-25), change it to pass the
string directly using the ternary result (color: isActiveCourse ? 'outlineWhite'
: 'outlinePurple') so buttonStyles receives a plain string instead of a template
literal.
In `@src/pages/manage-assignment/AssignmentManagePage.tsx`:
- Around line 31-48: The current deleteAssignment mutation always invalidates
assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0) which uses 0
when selectedCourseId is null; change this to only call
queryClient.invalidateQueries for
assignmentQueries.getAssignmentsByCourse(selectedCourseId).queryKey when
selectedCourseId is non-null/defined (e.g., guard on selectedCourseId !==
null/undefined) so you don't attempt to invalidate a non-existent course query;
keep the existing invalidateQueries for getAllAssignments and the
onSuccess/onError handlers intact.
- Line 42: Replace the browser alert() calls in the AssignmentManagePage
component with the app's toast/modal utilities to maintain consistent UX: locate
the two alert('문제가 성공적으로 삭제되었습니다.') occurrences (the success callbacks after
question/assignment deletion) and call the project's toast or modal API (e.g.,
showToast or openModal) instead, passing a localized success message and
optional success variant/options; ensure you import and use the existing
notification component used elsewhere in the app and remove the alert() usages.
In `@src/pages/manage-assignment/ui/AssignmentManageActionsBar.tsx`:
- Around line 28-33: AssignmentManageActionsBar has icon-only buttons for
edit/delete (buttons calling handleOnEdit and handleOnDelete with
EditIcon/DeleteIcon) that lack accessible labels; update the JSX to add
descriptive aria-label attributes (e.g., aria-label="Edit assignment" and
aria-label="Delete assignment") on the respective buttons and ensure the icon
SVGs remain aria-hidden (or role="img" with appropriate title) so screen readers
read the button labels, not the icons.
- Around line 16-22: Rename the handler functions for brevity: change
handleOnEdit to handleEdit and handleOnDelete to handleDelete, update their
implementations to still call navigate(`/admin/assignments/${id}`) and
onDelete(id) respectively, and replace all usages/references (e.g., JSX props or
event bindings) that currently reference handleOnEdit or handleOnDelete to the
new names so nothing breaks; ensure any tests or prop types referencing those
function names are updated as well.
In `@src/pages/select-assignment/AssignmentSelectPage.tsx`:
- Around line 70-79: The onClick prop for the cancel Button is using an
unnecessary arrow wrapper; change the handler to pass the function reference
directly by replacing the onClick={() => returnToPreviousPage()} usage with
onClick={returnToPreviousPage} to simplify the code and avoid creating an extra
closure (refer to returnToPreviousPage, handleConfirm, and the Button component
in AssignmentSelectPage).
In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx`:
- Line 5: The module-level type alias T fixes the generic, so either remove the
module-level alias and inline the concrete type where used (e.g., replace uses
of T with AssignmentCourse['assignments'][number] and keep
AssignmentListContainerProps non-generic), or make the component truly generic
by removing the top-level T and adding a type parameter on the component
declaration (e.g., declare AssignmentListContainer as a generic component using
AssignmentListContainerProps<T>) and update all references to use that
parameter; adjust imports/exports accordingly to ensure
AssignmentListContainerProps and the component share the same generic parameter
rather than relying on a module-scoped T.
In `@src/pages/unit-editor/model/types.ts`:
- Around line 5-10: The Mode type was changed to remove 'idle' and
UnitFormProps.mode was made nullable, which reduces clarity; restore 'idle' by
updating the Mode type to "type Mode = 'idle' | 'creating' | 'editing'" and
change UnitFormProps.mode to use Mode (not Mode | null); then update any callers
of UnitFormProps (e.g., components constructing UnitFormProps) to pass 'idle' as
the default state instead of null (or handle the new non-nullable Mode
accordingly) so the intent remains explicit; refer to the Mode type and
UnitFormProps interface to locate and change the definitions and call sites.
In `@src/shared/model/schemas.ts`:
- Around line 10-14: errorResponseSchema에 정의된 errors 객체가 Swagger 예시
필드명(additionalProp1/2/3)을 사용하고 있어 일반적인 에러 키를 표현하지 못하므로, errors: z.object({...})
대신 errors: z.record(z.string(), z.string())로 변경해 동적 키/값 형태의 에러 응답을 허용하도록 바꿔주세요;
관련 심볼은 errorResponseSchema과 errors 필드이며, 변경 시 기존 호출부에서 특정 엔티티별 스키마를 별도로 정의하는
방식에는 영향이 없도록 테스트 케이스나 타입 호환성을 확인해 주세요.
In `@src/shared/ui/LabeledInput.tsx`:
- Line 13: The LabeledInput component currently sets the prop default required =
true which deviates from standard HTML input default and forces callers to
override; update the component by either (A) removing the default and setting
required to undefined in the props/defaults so consumers must opt-in with
required={true}, or (B) if keeping true is intentional, add a JSDoc comment on
the LabeledInput component describing that required defaults to true; locate the
default assignment (required = true) in LabeledInput.tsx and implement one of
these two changes and adjust any callers if you choose option A.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (58)
src/App.tsxsrc/entities/assignment/api/assignmentApi.tssrc/entities/assignment/api/assignmentMutations.tssrc/entities/assignment/api/assignmentQueries.tssrc/entities/assignment/api/assignmentQueryOptions.tssrc/entities/assignment/model/schemas.tssrc/entities/assignment/model/types.tssrc/entities/auth/api/authApi.tssrc/entities/auth/model/schemas.tssrc/entities/auth/model/useUserStore.tssrc/entities/course/api/courseApi.tssrc/entities/course/api/courseMutations.tssrc/entities/course/api/courseQueries.tssrc/entities/course/api/courseQueryOptions.tssrc/entities/course/index.tssrc/entities/course/model/schemas.tssrc/entities/course/model/types.tssrc/entities/student/model/schemas.tssrc/entities/student/model/types.tssrc/entities/unit/api/unitApi.tssrc/entities/unit/api/unitMutations.tssrc/entities/unit/api/unitQueries.tssrc/entities/unit/model/schemas.tssrc/entities/unit/model/types.tssrc/entities/unit/model/useUnitStore.tssrc/features/assignment/filter-assignment/lib/useAssignmentList.tssrc/pages/course-overview/CourseOverviewPage.tsxsrc/pages/course-overview/ui/AssignmentList.tsxsrc/pages/course-overview/ui/CourseActionsBar.tsxsrc/pages/course-overview/ui/CourseHero.tsxsrc/pages/course-overview/ui/UnitItem.tsxsrc/pages/dashboard/Dashboard.tsxsrc/pages/manage-assignment/AssignmentManagePage.tsxsrc/pages/manage-assignment/ui/AssignmentManageActionsBar.tsxsrc/pages/select-assignment/AssignmentSelectPage.tsxsrc/pages/select-assignment/ui/AssignmentListContainer.tsxsrc/pages/unit-editor/UnitEditorPage.tsxsrc/pages/unit-editor/mock/unitMock.tssrc/pages/unit-editor/model/types.tssrc/pages/unit-editor/ui/UnitAssignmentList.tsxsrc/pages/unit-editor/ui/UnitForm.tsxsrc/pages/unit-editor/ui/UnitList.tsxsrc/shared/lib/course.tssrc/shared/mocks/assignmentDummyResponse.tssrc/shared/mocks/assignmentDummyTypes.tssrc/shared/mocks/assignmentSelectResponse.tssrc/shared/mocks/courseOverviewResponse.tssrc/shared/mocks/dashboardCourseList.tssrc/shared/mocks/dashboardScheduleList.tssrc/shared/model/index.tssrc/shared/model/schemas.tssrc/shared/model/type.tssrc/shared/ui/LabeledInput.tsxsrc/shared/ui/Layout.tsxsrc/shared/ui/button/Button.tsxsrc/shared/ui/list-row/ListRow.tsxsrc/shared/ui/list-row/list-row-styles.tssrc/widgets/assignment-page-layout/ui/AssignmentPageLayout.tsx
💤 Files with no reviewable changes (12)
- src/entities/course/index.ts
- src/shared/mocks/assignmentSelectResponse.ts
- src/entities/course/api/courseQueryOptions.ts
- src/entities/assignment/api/assignmentQueryOptions.ts
- src/pages/unit-editor/mock/unitMock.ts
- src/shared/mocks/assignmentDummyResponse.ts
- src/shared/mocks/assignmentDummyTypes.ts
- src/pages/unit-editor/ui/UnitAssignmentList.tsx
- src/shared/mocks/courseOverviewResponse.ts
- src/shared/mocks/dashboardCourseList.ts
- src/shared/mocks/dashboardScheduleList.ts
- src/shared/model/type.ts
| const {id} = useParams(); | ||
| const courseId = Number(id); |
There was a problem hiding this comment.
id 파라미터 유효성 검증이 필요합니다.
useParams()에서 반환되는 id가 undefined일 경우, Number(undefined)는 NaN을 반환하여 API 호출 시 예기치 않은 동작이 발생할 수 있습니다.
🛡️ 유효성 검증 추가 제안
const {id} = useParams();
const courseId = Number(id);
+
+if (isNaN(courseId)) {
+ throw new Error('Invalid course ID');
+}또는 React Router의 타입 안전성을 위해 loader에서 검증하는 방법도 고려해보세요.
📝 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.
| const {id} = useParams(); | |
| const courseId = Number(id); | |
| const {id} = useParams(); | |
| const courseId = Number(id); | |
| if (isNaN(courseId)) { | |
| throw new Error('Invalid course ID'); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/course-overview/CourseOverviewPage.tsx` around lines 9 - 10, The
code currently converts useParams()'s id to a number without validation (const
{id} = useParams(); const courseId = Number(id);) which can produce NaN; update
CourseOverviewPage to validate id before conversion: ensure id is a defined
string and parseInt/Number yields a finite integer (e.g., typeof id === 'string'
&& !Number.isNaN(Number(id))); if invalid, handle it (redirect to a 404/error
page, show an error state, or bail out before any API calls that use courseId).
Place this check where courseId is created/used (in the CourseOverviewPage
component) so all subsequent API calls and logic use a guaranteed valid
courseId.
| type T = AssignmentCourse['assignments'][number]; | ||
|
|
||
| interface AssignmentListContainerProps<T> { | ||
| items: T[]; | ||
| renderItem: (item: T) => React.ReactNode; | ||
| renderItem: (item: T) => ReactNode; | ||
| title: string; | ||
| onSelect: (id: number) => void; | ||
| onSelect?: (item: Assignment) => void; | ||
| } |
There was a problem hiding this comment.
제네릭 타입 T와 Assignment 타입 간의 불일치가 있습니다.
T가 AssignmentCourse['assignments'][number] (즉, {id: number, title: string})로 고정되어 있지만, onSelect와 handleSelect는 Assignment 타입을 기대합니다. Assignment는 submittedStatus 필드를 포함할 수 있어 타입이 정확히 일치하지 않습니다.
🔧 타입 일관성 개선 제안
-import type {Assignment} from '@/entities/assignment/model/types';
import type {AssignmentCourse} from '@/entities/course/model/types';
import type {MouseEvent, ReactNode} from 'react';
type T = AssignmentCourse['assignments'][number];
interface AssignmentListContainerProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
title: string;
- onSelect?: (item: Assignment) => void;
+ onSelect?: (item: T) => void;
}
const AssignmentListContainer = ({
onSelect,
items,
renderItem,
title,
}: AssignmentListContainerProps<T>) => {
- const handleSelect = (item: Assignment, event: MouseEvent) => {
+ const handleSelect = (item: T, event: MouseEvent) => {
event.stopPropagation();
onSelect?.(item);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/select-assignment/ui/AssignmentListContainer.tsx` around lines 5 -
12, The generic type T is constrained to AssignmentCourse['assignments'][number]
while onSelect and handleSelect expect an Assignment (which may include
submittedStatus), causing a type mismatch; update the component props and
handlers to be consistent by either broadening the generic constraint to
Assignment (e.g., make AssignmentListContainerProps<T extends Assignment>) or by
changing onSelect and handleSelect signatures to accept T instead of Assignment;
locate and update AssignmentListContainerProps, the renderItem prop, and the
handleSelect/onSelect usages so types align (choose one approach and apply it
across the component).
| const {id} = useParams(); // 강의 ID | ||
| const courseId = Number(id); | ||
| const [mode, setMode] = useState<Mode>('idle'); | ||
|
|
||
| const { | ||
| resetStore, | ||
| title: storedTitle, | ||
| assignments: storedAssignments, | ||
| releaseDate: storedReleaseDate, | ||
| dueDate: storedDueDate, | ||
| } = useUnitStore(); | ||
| const {data} = useQuery(unitQueries.getUnitList(courseId)); | ||
| const initialMode = | ||
| data === undefined ? null : data.unitCount === 0 ? 'creating' : 'editing'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/pages/unit-editor/UnitEditorPage.tsx | head -80Repository: 2025-snowCode/snowCode-Client
Length of output: 3267
🏁 Script executed:
cat -n src/pages/unit-editor/UnitEditorPage.tsx | sed -n '80,170p'Repository: 2025-snowCode/snowCode-Client
Length of output: 2918
🏁 Script executed:
cat -n src/pages/unit-editor/UnitEditorPage.tsx | sed -n '1,50p'Repository: 2025-snowCode/snowCode-Client
Length of output: 2236
🏁 Script executed:
rg -A5 "export.*getUnitList" src/entities/unit/api/ -B2Repository: 2025-snowCode/snowCode-Client
Length of output: 55
🏁 Script executed:
find src -name "*unitQueries*" -o -name "*unitMutations*"Repository: 2025-snowCode/snowCode-Client
Length of output: 148
🏁 Script executed:
cat src/entities/unit/api/unitQueries.tsRepository: 2025-snowCode/snowCode-Client
Length of output: 848
🏁 Script executed:
cat src/entities/unit/api/unitApi.tsRepository: 2025-snowCode/snowCode-Client
Length of output: 1786
🏁 Script executed:
cat src/entities/unit/api/unitMutations.tsRepository: 2025-snowCode/snowCode-Client
Length of output: 767
🏁 Script executed:
python3 << 'EOF'
# Verify the order of operations
operations = {
15: "const {id} = useParams()",
16: "const courseId = Number(id)",
25: "useQuery(unitQueries.getUnitList(courseId)) - CALLED HERE",
140: "if (!id) return <EmptyState>",
}
print("Order of operations:")
for line, desc in sorted(operations.items()):
print(f" Line {line}: {desc}")
print("\n❌ ISSUE: useQuery is called at line 25 with potentially NaN courseId")
print(" Guard check doesn't happen until line 140")
print(f"\n✓ When id is undefined:")
print(f" - Number(undefined) = NaN")
print(f" - API call: /courses/NaN/units")
EOFRepository: 2025-snowCode/snowCode-Client
Length of output: 487
🏁 Script executed:
rg "useQuery.*courseId|useQuery.*Number\(id\)" src/ -B3 -A2Repository: 2025-snowCode/snowCode-Client
Length of output: 567
🏁 Script executed:
cat src/pages/unit-editor/ui/UnitForm.tsx | head -50Repository: 2025-snowCode/snowCode-Client
Length of output: 1596
유효하지 않은 courseId로 쿼리와 생성 요청이 나갈 수 있습니다.
Line 16에서 Number(id)로 변환한 후 Line 25에서 바로 useQuery를 호출하는데, id가 없으면 courseId가 NaN이 됩니다. Line 140의 가드 체크는 렌더링만 방지할 뿐 이미 발동한 쿼리는 /courses/NaN/units 요청을 보내게 됩니다. Line 124의 onCreateUnit에서도 동일하게 NaN 값이 전달됩니다.
Line 26-27의 initialMode가 null이 될 수 있고 Line 39에서 currentMode도 null로 유지되어 Line 158에서 UnitForm에 전달되는데, 여기서 의도하지 않은 생성 모드 로직이 발동할 수 있습니다.
Number.isFinite(courseId) 검증 가드를 먼저 두고, 쿼리의 enabled 옵션으로 막아주세요.
🔧 제안 수정안
const {id} = useParams(); // 강의 ID
const courseId = Number(id);
+const hasValidCourseId = Number.isFinite(courseId);
-const {data} = useQuery(unitQueries.getUnitList(courseId));
+const {data} = useQuery({
+ ...unitQueries.getUnitList(courseId),
+ enabled: hasValidCourseId,
+});
const onCreateUnit = (unitForm: TUnitFormSchema) => {
+ if (!hasValidCourseId) return;
addUnit({courseId, unitForm});
};
-if (!id) {
+if (!hasValidCourseId) {
return <EmptyState>선택한 강의가 없습니다.</EmptyState>;
}참고: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/unit-editor/UnitEditorPage.tsx` around lines 15 - 27, The code can
call queries and create requests with an invalid courseId (NaN); compute
courseId as Number(id) and guard it with Number.isFinite(courseId) before using
it: create a boolean like isValidCourseId = Number.isFinite(courseId) and pass
enabled: isValidCourseId to useQuery(unitQueries.getUnitList(courseId)); compute
initialMode only when isValidCourseId is true (otherwise keep null) and add the
same isValidCourseId guard inside onCreateUnit to early-return or block creation
when invalid; update references to useParams, courseId, useQuery,
unitQueries.getUnitList, initialMode, currentMode, onCreateUnit, and UnitForm
accordingly so no requests or create calls are made with NaN.
| const initialMode = | ||
| data === undefined ? null : data.unitCount === 0 ? 'creating' : 'editing'; | ||
|
|
||
| const [activeMode, setActiveMode] = useState<Mode | null>(null); | ||
| const [selectedUnitId, setSelectedUnitId] = useState<number | null>(null); | ||
| const [currentIndex, setCurrentIndex] = useState<number>(1); | ||
| const {data: unitList} = useQuery(unitQueries.getUnitList(courseId)); | ||
| const {data: unit} = useQuery(unitQueries.getUnitDetails(selectedUnitId)); | ||
|
|
||
| useEffect(() => { | ||
| if (unitList && unitList?.response.count !== 0 && mode === 'idle') { | ||
| setSelectedUnitId(unitList.response.units[0].id); | ||
| setMode('editing'); // 편집 모드 | ||
| } else if (unitList?.response.count === 0 && mode === 'idle') { | ||
| setMode('creating'); // 생성 모드 | ||
|
|
||
| const hasOngoingCreation = | ||
| storedTitle !== '' || | ||
| storedReleaseDate !== '' || | ||
| storedDueDate !== '' || | ||
| storedAssignments.length > 0; | ||
|
|
||
| const currentMode: Mode | null = | ||
| activeMode ?? (hasOngoingCreation ? 'creating' : initialMode); | ||
| const currentUnitId = | ||
| currentMode === 'creating' | ||
| ? null | ||
| : (selectedUnitId ?? data?.firstUnitId ?? null); |
There was a problem hiding this comment.
currentMode의 null 상태가 폼 동작으로 전파됩니다.
Line 38-39에서 currentMode가 null일 수 있고, Line 161에서 그대로 UnitForm에 전달됩니다. 이 상태에서 저장 시 생성 분기로 흐를 수 있어 의도치 않은 단원 생성이 가능합니다. 로딩 중에는 폼 렌더를 보류하거나, UnitForm에는 확정된 모드(creating | editing)만 전달되도록 정리해 주세요.
🔧 제안 수정안
-const currentMode: Mode | null =
+const currentMode: Mode | null =
activeMode ?? (hasOngoingCreation ? 'creating' : initialMode);
+if (!hasOngoingCreation && currentMode === null) {
+ return <EmptyState>단원 정보를 불러오는 중입니다.</EmptyState>;
+}
<UnitForm
unit={unitDetail}
unitIndex={currentIndex}
- mode={currentMode}
+ mode={currentMode}
onCreateUnit={onCreateUnit}참고: https://react.dev/learn/conditional-rendering
Also applies to: 158-162
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/unit-editor/UnitEditorPage.tsx` around lines 26 - 43, currentMode
can be null and is being passed into UnitForm causing unintended creation flows;
change the render logic so UnitForm only receives a concrete Mode ('creating' |
'editing') — use activeMode, hasOngoingCreation and initialMode to derive a
non-null mode before rendering or simply conditionally render/return null while
currentMode === null (or while data/loading). Specifically, update the logic
around currentMode/currentUnitId (referencing currentMode, activeMode,
hasOngoingCreation, initialMode, selectedUnitId) so that UnitForm is not mounted
with currentMode === null; ensure any props passed to UnitForm are strictly
'creating' | 'editing' and delay form rendering until a definite mode is
determined.
|
|
||
| {isOpen ? ( | ||
| <Link to={`/assignments/${index}`} className='min-w-0'> | ||
| <Link to={`/admin/assignments/${index}`} className='min-w-0'> |
⚙️ Related ISSUE Number
Related #59
📄 Work Description
getCourseById,courseQueries.getCourseDetails)Link경로 업데이트📷 Screenshot
Screen.Recording.2026-02-28.at.22.26.53.mov
💬 To Reviewers
🔗 Reference