Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 48 additions & 1 deletion src/entities/assignment/api/assignmentApi.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,55 @@
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`);

console.log('getAssignmentsByCourse API 응답:', response.data); // 응답 데이터 로깅
return response.data;
Comment on lines +45 to +46
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

API 응답 디버그 로그는 릴리스 전에 제거해 주세요.

Line 45의 console.log는 운영 환경에서 불필요한 데이터 노출과 콘솔 노이즈를 만듭니다. 필요하면 로컬 디버깅 플래그 기반으로 제한해 주세요.

🧼 제안 수정안
-  console.log('getAssignmentsByCourse API 응답:', response.data); // 응답 데이터 로깅
   return response.data;
📝 Committable suggestion

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

Suggested change
console.log('getAssignmentsByCourse API 응답:', response.data); // 응답 데이터 로깅
return response.data;
return response.data;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/entities/assignment/api/assignmentApi.ts` around lines 45 - 46, Remove
the development console.log in the getAssignmentsByCourse API function (the line
logging response.data) to avoid leaking data and noisy logs in production;
either delete that console.log entirely or guard it behind a debug flag/config
check (e.g., use a process.env.DEBUG or isLocalDebug boolean) so logging only
occurs in local/dev runs, and keep the function return value (response.data)
unchanged.

};

// 과제 삭제 API
export const deleteAssignment = async (
assignmentId: number
): Promise<ApiResponse<string>> => {
const response = await privateAxios.delete(`/assignments/${assignmentId}`);
return response.data;
};
30 changes: 30 additions & 0 deletions src/entities/assignment/api/assignmentQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {queryOptions} from '@tanstack/react-query';
import {
getAllAssignments,
getAssignmentsByCourse,
getAssignmentSchedules,
} from './assignmentApi';

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

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

// 강의별 과제 목록 조회 쿼리 옵션
getAssignmentsByCourse: (courseId: number) =>
queryOptions({
queryKey: ['courses', courseId, 'assignments'],
queryFn: () => getAssignmentsByCourse(courseId),
enabled: !!courseId, // courseId가 있을 때만 쿼리 실행
}),
};
9 changes: 0 additions & 9 deletions src/entities/assignment/api/assignmentQueryOptions.ts

This file was deleted.

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

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

This file was deleted.

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

This file was deleted.

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
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;
19 changes: 11 additions & 8 deletions src/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,38 @@ import AddIcon from '@/assets/svg/addIcon.svg?react';
import ScheduleList from './ui/ScheduleList';
import {Link} from 'react-router-dom';
import {useUserStore} from '@/entities/auth/model/useUserStore';
import courseQueryOptions from '@/entities/course/api/courseQueryOptions';
import {
useMutation,
useQueryClient,
useSuspenseQueries,
} from '@tanstack/react-query';
import assignmentQueryOptions from '@/entities/assignment/api/assignmentQueryOptions';
import {deleteCourse} from '@/entities/course';
import {EmptyState} from '@/shared/ui/EmptyState';
import {courseQueries} from '@/entities/course/api/courseQueries';
import {courseMutations} from '@/entities/course/api/courseMutations';
import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries';

const Dashboard = () => {
const userType = useUserStore((state) => state.userType);
const queryClient = useQueryClient();

// 강의 및 스케쥴 데이터 패칭
const [{data: courses}, {data: schedules}] = useSuspenseQueries({
queries: [courseQueryOptions(), assignmentQueryOptions()],
queries: [
courseQueries.getAllCourses(),
assignmentQueries.getAssignmentSchedules(),
],
});

// 강의 삭제 뮤테이션
// 강의 삭제
const {mutate} = useMutation({
mutationFn: (courseId: number) => deleteCourse(courseId),
...courseMutations.deleteCourse,
onSuccess: () => {
// 강의 목록 및 스케쥴 목록 갱신
queryClient.invalidateQueries({
queryKey: courseQueryOptions().queryKey,
queryKey: courseQueries.getAllCourses().queryKey,
});
queryClient.invalidateQueries({
queryKey: assignmentQueryOptions().queryKey,
queryKey: assignmentQueries.getAssignmentSchedules().queryKey,
});
alert('강의가 성공적으로 삭제되었습니다.');
},
Expand Down
71 changes: 53 additions & 18 deletions src/pages/select-assignment/AssignmentSelectPage.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,68 @@
import AssignmentListContainer from './ui/AssignmentListContainer';
import {useState} from 'react';
import {
response,
courseOptionsResponse,
} from '@/shared/mocks/assignmentSelectResponse';
import {useCourseFilter} from '@/features/course/filter-course/lib/useCourseFilter';
import {AssignmentPageLayout} from '@/widgets/assignment-page-layout';
import ListRow from '@/shared/ui/list-row/ListRow';
import {useQuery} from '@tanstack/react-query';
import {courseQueries} from '@/entities/course/api/courseQueries';
import {assignmentQueries} from '@/entities/assignment/api/assignmentQueries';
import useUnitStore from '@/entities/unit/model/useUnitStore';
import {useLocation, useNavigate} from 'react-router-dom';
import type {Assignment} from '@/entities/assignment/model/types';

const AssignmentSelectPage = () => {
const {courses} = courseOptionsResponse.response; // /courses/my API 응답 모킹
const [selectedAssignments, setSelectedAssignments] = useState<number[]>([]); // 선택된 문제 ID 목록

const {courseOptions, handleCourseSelect} = useCourseFilter(courses);
const navigate = useNavigate();
const location = useLocation();
const {data: courseList} = useQuery(courseQueries.getAllCourses());
const {setAssignments, assignments: currentSelectedAssignments} =
useUnitStore();
const [selectedAssignments, setSelectedAssignments] = useState<Assignment[]>(
currentSelectedAssignments ?? []
);
const {courseOptions, handleCourseSelect, selectedCourseId} = useCourseFilter(
courseList?.response.courses ?? []
);
const {data: allAssignments} = useQuery(
assignmentQueries.getAllAssignments()
);
const {data: assignments} = useQuery(
assignmentQueries.getAssignmentsByCourse(selectedCourseId ?? 0)
);

// 문제 목록 /courses/{courseId}/assignments API 응답 모킹
const assignmentList = response.response.courses.flatMap(
const filteredAssignments = assignments?.response.courses.flatMap(
(course) => course.assignments
);

// 선택된 강의에 따라 보여줄 과제 목록 결정
const assignmentList = selectedCourseId
? (filteredAssignments ?? [])
: (allAssignments?.response.assignments ?? []);

// 문제 선택 핸들러
const handleAssignmentSelect = (assignmentId: number) => {
const handleAssignmentSelect = (assignment: Assignment) => {
setSelectedAssignments((prev) => {
if (prev.includes(assignmentId)) {
return prev.filter((id) => id !== assignmentId); // 선택 해제
} else {
return [...prev, assignmentId]; // 선택 추가
if (prev.some((a) => a.id === assignment.id)) {
return prev.filter((a) => a.id !== assignment.id);
}
return [...prev, assignment];
});
};

const returnToPreviousPage = () => {
navigate(location.state?.backPath ?? -1, {
state: {
mode: location.state?.mode,
unitId: location.state?.unitId,
currentIndex: location.state?.currentIndex,
},
});
Comment on lines +52 to +58
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:

cd /tmp && git ls-files | grep -i "AssignmentSelectPage" | head -5

Repository: 2025-snowCode/snowCode-Client

Length of output: 141


🌐 Web query:

React Router v7 useNavigate delta navigation state parameter behavior

💡 Result:

In React Router v7, useNavigate() has two distinct call forms: navigate(to, options?) and navigate(delta) (history stack movement). When you use the delta form (e.g. navigate(-1)), you are effectively calling the underlying history “go” operation, which does not create a new location entry—so there’s nowhere to attach a new state payload. As a result, any “state parameter” you might expect to pass is not applied; you simply land on the existing entry and get whatever location.state was stored on that entry. [1], [2]

If you need to send new state, use path navigation instead: navigate("/path", { state }) (or the v7-supported navigate({ pathname, state })). [1]

Sources:
[1] React Router v7 useNavigate API reference (api.reactrouter.com)
[2] React Router Navigator interface (go(delta) vs push/replace(..., state)) (api.reactrouter.com)

Citations:


🏁 Script executed:

fd "AssignmentSelectPage.tsx" --type f

Repository: 2025-snowCode/snowCode-Client

Length of output: 125


🏁 Script executed:

cat -n src/pages/select-assignment/AssignmentSelectPage.tsx 2>/dev/null || find . -name "AssignmentSelectPage.tsx" -type f -exec cat -n {} \;

Repository: 2025-snowCode/snowCode-Client

Length of output: 3720


Delta 이동 시 state 파라미터가 적용되지 않습니다.

52줄의 navigate(location.state?.backPath ?? -1, { state }) 호출은 backPath가 없으면 navigate(-1, { state })가 되는데, React Router v7에서 delta 이동(navigate(-1))은 브라우저의 history.go() 동작을 수행하므로 state 파라미터가 무시됩니다. 따라서 이전 페이지로 돌아갈 때 모드, unitId, currentIndex 정보가 손실됩니다.

해결 방법: path와 delta를 분리하여 처리하세요. backPath가 있으면 path 이동으로 state를 함께 전달하고, 없으면 delta 이동만 수행하세요.

🔧 수정 제안
  const returnToPreviousPage = () => {
+   const backPath = location.state?.backPath;
+   const nextState = {
+     mode: location.state?.mode,
+     unitId: location.state?.unitId,
+     currentIndex: location.state?.currentIndex,
+   };
+
+   if (backPath) {
+     navigate(backPath, { state: nextState });
+     return;
+   }
+   navigate(-1);
-   navigate(location.state?.backPath ?? -1, {
-     state: {
-       mode: location.state?.mode,
-       unitId: location.state?.unitId,
-       currentIndex: location.state?.currentIndex,
-     },
-   });
  };

참고: React Router v7 useNavigate API 문서

🤖 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 52 - 58,
The navigate call currently passes state even when using delta navigation
(navigate(-1)), but React Router v7 ignores state for delta moves; update the
logic around the navigate(location.state?.backPath ?? -1, { state: ... }) call
in AssignmentSelectPage so that if location.state?.backPath exists you call
navigate(backPath, { state: { mode: location.state?.mode, unitId:
location.state?.unitId, currentIndex: location.state?.currentIndex } }),
otherwise call navigate(-1) (no state) to perform a history delta move; adjust
the branch around navigate to separate path navigation from delta navigation and
use the same state object only for path-based navigation.

};

const handleConfirm = () => {
setAssignments(selectedAssignments);
returnToPreviousPage();
};

return (
<AssignmentPageLayout
title='문제 선택'
Expand All @@ -43,13 +76,15 @@ const AssignmentSelectPage = () => {
renderItem={(assignment) => (
<ListRow
title={assignment.title}
selected={selectedAssignments.includes(assignment.id)}
selected={selectedAssignments.some((a) => a.id === assignment.id)}
/>
)}
/>
}
onCancel={() => {}}
onConfirm={() => {}}
onCancel={() => {
returnToPreviousPage();
}}
onConfirm={handleConfirm}
/>
);
};
Expand Down
Loading
Loading