Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
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
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
47 changes: 46 additions & 1 deletion src/entities/assignment/api/assignmentApi.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

지금

assignmentApi.ts
에서 서버 데이터를 가져올 때 TypeScript의 interface만 사용하고 있는데요,

추천하는 구조:

entities/assignment/model/schema.ts (신규 생성)

import { z } from 'zod';
// 서버 응답 스키마 정의
export const assignmentSchema = z.object({
  assignmentId: z.number(),
  title: z.string(),
});
// 스키마에서 타입을 자동으로 뽑아내기 (interface 대신 사용 가능!)
export type TAssignment = z.infer<typeof assignmentSchema>;

entities/assignment/api/assignmentApi.ts
 (수정)
// 기존 interface 대신 .parse() 사용
const raw = response.data.response;
const validated = z.array(assignmentSchema).parse(raw.assignments);
return {
  count: raw.count,
  assignments: validated.map(a => ({ id: a.assignmentId, title: a.title }))
};

이미 unit 도메인 쪽에서도 zod를 조금씩 쓰고 계시니까, API 응답 영역까지 확장해 보면 좋을 것 같아요!

Original file line number Diff line number Diff line change
@@ -1,8 +1,53 @@
import type {DashboardScheduleListResponse} from '@/entities/course/model/types';
import type {
AssignmentSelectResponse,
DashboardScheduleListResponse,
} from '@/entities/course/model/types';
import {privateAxios} from '@/shared/api/axiosInstance';
import type {ApiResponse} from '@/shared/model';
import type {AssignmentsResponse} from '../model/types';

// 과제 일정 조회 API
export const getAssignmentSchedules =
async (): Promise<DashboardScheduleListResponse> => {
const response = await privateAxios.get('/assignments/schedule');
return response.data;
};

// 전체 과제 목록 조회 API
export const getAllAssignments = async (): Promise<
ApiResponse<AssignmentsResponse>
> => {
const response = await privateAxios.get<
ApiResponse<{
count: number;
assignments: {assignmentId: number; title: string}[];
}>
>('/assignments/my');
const raw = response.data;
return {
...raw,
response: {
count: raw.response.count,
assignments: raw.response.assignments.map(({assignmentId, title}) => ({
id: assignmentId,
title,
})),
},
};
};

// 강의별 과제 목록 조회 API
export const getAssignmentsByCourse = async (
courseId: number
): Promise<AssignmentSelectResponse> => {
const response = await privateAxios.get(`/courses/${courseId}/assignments`);
return response.data;
};

// 과제 삭제 API
export const deleteAssignment = async (
assignmentId: number
): Promise<ApiResponse<string>> => {
const response = await privateAxios.delete(`/assignments/${assignmentId}`);
return 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'],
Copy link
Contributor

Choose a reason for hiding this comment

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

흠 이거 쿼리 키 그냥 따로 파일 만들고 얘도 그냥 상수로 여기에 쓸까하는데 어떠세요?
저번 회의 때 이렇게 하신 이유가 있었던 것 같은데 제가 기억이 안나서 다시 여쭤봅니다!

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.

5 changes: 5 additions & 0 deletions src/entities/assignment/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ export interface Assignment {
title: string;
submittedStatus?: SubmissionStatus;
}

export interface AssignmentsResponse {
count: number;
assignments: Assignment[];
}
2 changes: 2 additions & 0 deletions src/entities/course/api/courseApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type {ApiResponse} from '@/shared/model/common';
import type {DashboardCourseListResponse} from '@/entities/course/model/types';
import {privateAxios} from '@/shared/api/axiosInstance';

// 전체 강의 목록 조회 API
export const getAllCourses = async (): Promise<DashboardCourseListResponse> => {
const response = await privateAxios.get('/courses/my');
return response.data;
};

// 강의 삭제 API
export const deleteCourse = async (
courseId: number
): Promise<ApiResponse<string>> => {
Expand Down
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),
},
};
15 changes: 15 additions & 0 deletions src/entities/course/api/courseQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {getAllCourses} 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,
}),
}),
};
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.

11 changes: 11 additions & 0 deletions src/entities/unit/api/unitApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,14 @@ export const updateUnit = async (
const response = await privateAxios.put(`/units/${unitId}`, unit);
return response.data;
};

// 단원에 등록된 과제 삭제
export const deleteAssignmentFromUnit = async (
unitId: number,
assignmentId: number
): Promise<ApiResponse<string>> => {
const response = await privateAxios.delete(
`/units/${unitId}/assignments/${assignmentId}`
);
return response.data;
};
9 changes: 7 additions & 2 deletions src/entities/unit/api/unitMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ export const unitMutations = {
// 단원 추가 뮤테이션 옵션
createUnit: {
mutationKey: ['createUnit'],
mutationFn: ({courseId, unit}: {courseId: number; unit: TUnitFormSchema}) =>
createUnit(courseId, unit),
mutationFn: ({
courseId,
unitForm,
}: {
courseId: number;
unitForm: TUnitFormSchema;
}) => createUnit(courseId, unitForm),
},

// 단원 수정 뮤테이션 옵션
Expand Down
8 changes: 8 additions & 0 deletions src/entities/unit/api/unitQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export const unitQueries = {
queryOptions({
queryKey: ['units', courseId],
queryFn: () => getAllUnitsByCourseId(courseId),
select: (data) => ({
unitList: data.response.units,
unitCount: data.response.count,
firstUnitId: data.response.units[0]?.id ?? null,
}),
}),

// 단일 단원 조회 쿼리 옵션
Expand All @@ -15,5 +20,8 @@ export const unitQueries = {
queryKey: ['units', 'detail', unitId],
queryFn: () => getUnitById(unitId),
enabled: !!unitId,
select: (data) => {
return data.response;
},
}),
};
2 changes: 1 addition & 1 deletion src/entities/unit/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const unitFormSchema = z
})
.refine((data) => data.releaseDate <= data.dueDate, {
message: '날짜 범위가 올바르지 않습니다.',
path: ['dueDate'],
path: ['releaseDate'],
});

// 단원 생성/수정 폼 타입
Expand Down
51 changes: 51 additions & 0 deletions src/entities/unit/model/useUnitStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {Assignment} from '@/entities/assignment/model/types';
import {create} from 'zustand';
import {createJSONStorage, persist} from 'zustand/middleware';

interface UnitState {
title: string;
releaseDate: string;
dueDate: string;
assignments: Assignment[];
storeFormData: (title: string, releaseDate: string, dueDate: string) => void;
resetStore: () => void;
setAssignments: (assignments: Assignment[]) => void;
}

export const useUnitStore = create<UnitState>()(
persist(
(set) => ({
title: '',
releaseDate: '',
dueDate: '',
assignments: [],

// 단원 폼 임시 저장
storeFormData: (title, releaseDate, dueDate) =>
set({
title: title,
releaseDate: releaseDate,
dueDate: dueDate,
}),

// 선택된 과제 ID 저장
setAssignments: (assignments) => set({assignments}),

// 단원 폼 초기화
resetStore: () =>
set({title: '', releaseDate: '', dueDate: '', assignments: []}),
}),
{
name: 'unit-session-storage',
storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({
title: state.title,
releaseDate: state.releaseDate,
dueDate: state.dueDate,
assignments: state.assignments,
}),
}
)
);

export default useUnitStore;
22 changes: 22 additions & 0 deletions src/features/assignment/filter-assignment/lib/useAssignmentList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {useQuery} from '@tanstack/react-query';
import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries';
import type {Assignment} from '@/entities/assignment/model/types';
Comment on lines +1 to +3
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for the typo in folder name and find all references

# Find the folder with typo
echo "=== Searching for folder with typo 'filter-assignmnet' ==="
fd -t d "filter-assignmnet" src/ 2>/dev/null || echo "No 'filter-assignmnet' folder found"

# Find correct spelling folder
echo -e "\n=== Searching for correctly spelled 'filter-assignment' ==="
fd -t d "filter-assignment" src/ 2>/dev/null || echo "No 'filter-assignment' folder found"

# Find all imports referencing the typo path
echo -e "\n=== Finding all references to 'filter-assignmnet' ==="
rg "filter-assignmnet" src/ --type ts --type tsx 2>/dev/null | head -20

# Find all assignment-related folders
echo -e "\n=== Finding all assignment-related folders ==="
fd -t d "filter-assign" src/

# Check the actual file path
echo -e "\n=== Checking if file exists at mentioned path ==="
test -f "src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts" && echo "File exists at typo path" || echo "File NOT found at typo path"

Repository: 2025-snowCode/snowCode-Client

Length of output: 457


🏁 Script executed:

#!/bin/bash
# Check folder contents and verify if it's being used

echo "=== Contents of folder with typo ==="
find src/features/assignment/filter-assignmnet -type f | head -20

echo -e "\n=== Checking for any imports of files from this folder ==="
rg "from.*filter-assignmnet" src/ --type ts --type tsx 2>/dev/null || echo "No imports found"

echo -e "\n=== Checking for useAssignmentList imports (broader search) ==="
rg "useAssignmentList" src/ --type ts --type tsx 2>/dev/null || echo "No useAssignmentList imports found"

echo -e "\n=== Checking folder structure ==="
tree src/features/assignment/filter-assignmnet/ 2>/dev/null || find src/features/assignment/filter-assignmnet -type f -exec echo {} \;

Repository: 2025-snowCode/snowCode-Client

Length of output: 491


폴더명 오타: filter-assignmnetfilter-assignment로 수정 필요

src/features/assignment/ 하위의 filter-assignmnet 폴더명에 철자 오류가 있습니다. 현재 이 모듈은 아직 프로젝트 내에서 사용되지 않고 있으므로, 지금 수정하면 영향도가 없을 것으로 보입니다.

수정사항:

  • 폴더명: filter-assignmnet/filter-assignment/
  • 이에 따라 import 경로도 함께 업데이트 필요

coding guidelines 에서 규정한 kebab-case 명명 규칙을 따르되, 정확한 철자 사용도 중요합니다. 초기 단계에서 수정하는 것이 좋습니다! ✨

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/assignment/filter-assignmnet/lib/useAssignmentList.ts` around
lines 1 - 3, The folder name contains a typo: rename the directory from
"filter-assignmnet" to "filter-assignment" and update all imports referencing
that module (e.g., the module that exports useAssignmentList in
useAssignmentList.ts and any import lines like import {useQuery}... from
'@/features/assignment/filter-assignmnet/...') to the corrected kebab-case path;
ensure your VCS commit records the folder rename (not a delete/add) so imports
(and any path aliases) are updated consistently across the codebase.


// 중복 제거
const unique = (list: Assignment[]) =>
Array.from(new Map(list.map((a) => [a.id, a])).values());

export const useAssignmentList = (
selectedCourseId: number | null
): Assignment[] => {
const {data: allAssignments} = useQuery(
assignmentQueries.getAllAssignments()
);
const {data: assignments} = useQuery(
assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0)
);

return unique(
selectedCourseId ? (assignments ?? []) : (allAssignments ?? [])
);
Comment on lines +12 to +21
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

🧩 Analysis chain

🏁 Script executed:

cat -n src/features/assignment/filter-assignment/lib/useAssignmentList.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 917


🏁 Script executed:

# assignmentQueries 정의 확인
find . -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "assignmentQueries" | head -5

Repository: 2025-snowCode/snowCode-Client

Length of output: 283


🏁 Script executed:

# useQuery 사용 패턴 확인
rg "useQuery" --type ts --type tsx -A 3 | head -50

Repository: 2025-snowCode/snowCode-Client

Length of output: 100


🏁 Script executed:

cat -n src/entities/assignment/api/assignmentQueries.ts

Repository: 2025-snowCode/snowCode-Client

Length of output: 1178


🏁 Script executed:

cat -n package.json | grep -A 2 -B 2 "react-query\|@tanstack"

Repository: 2025-snowCode/snowCode-Client

Length of output: 493


🏁 Script executed:

# Check how assignmentQueries returns query config
rg "getAllAssignments|getAssignmentsByCourse" --type ts -A 5

Repository: 2025-snowCode/snowCode-Client

Length of output: 4462


🏁 Script executed:

# Verify that queryOptions can be spread and extended
rg "useQuery.*\.\.\." --type ts -A 2 | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 55


🏁 Script executed:

# Check if there are other examples of spreading queryOptions in the codebase
rg "queryOptions" --type ts -A 3 | grep -E "enabled|spread|\.\.\." | head -20

Repository: 2025-snowCode/snowCode-Client

Length of output: 210


getAllAssignments 쿼리가 항상 실행되어 불필요한 네트워크 비용이 발생합니다

현재 구현에서 getAllAssignments는 과제 필터 선택 여부와 관계없이 항상 활성화됩니다. getAssignmentsByCourse에는 이미 enabled 옵션이 있지만, getAllAssignments에는 없어서 필터가 선택될 때 두 쿼리가 동시에 실행됩니다.

queryOptions 객체를 spread하여 enabled 조건을 명시적으로 제어하고, Line 20의 truthy 체크를 selectedCourseId !== null로 변경해 주세요:

🔧 제안 diff
 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 === null,
+  });
+  const {data: assignments} = useQuery({
+    ...assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0),
+    enabled: selectedCourseId !== null,
+  });
 
   return unique(
-    selectedCourseId ? (assignments ?? []) : (allAssignments ?? [])
+    selectedCourseId !== null ? (assignments ?? []) : (allAssignments ?? [])
   );
 };

참고: React Query - Disabling 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 - 21, The getAllAssignments query is always running and causing extra
network traffic; update the useQuery call that uses
assignmentQueries.getAllAssignments() to accept the shared queryOptions object
(spread into the second arg) and include an enabled flag that is true only when
selectedCourseId === null (i.e., enabled: selectedCourseId === null), mirror the
existing enabled pattern used for getAssignmentsByCourse, and change the return
selector so the conditional uses selectedCourseId !== null to decide between
(assignments ?? []) and (allAssignments ?? []) before passing into unique;
reference the useQuery calls, getAllAssignments, getAssignmentsByCourse,
selectedCourseId, and unique when making the change.

};
Loading
Loading