diff --git a/README.md b/README.md
index ee2a312..b652cd5 100644
--- a/README.md
+++ b/README.md
@@ -1,71 +1,347 @@
-# Getting Started with Create React App
-This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+# H-codeLab
+
+## 서비스 한눈에 보기
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 목차
+
+- [서비스 한눈에 보기](#서비스-한눈에-보기)
+- [프로젝트 소개](#프로젝트-소개)
+- [핵심 기능](#핵심-기능)
+- [개발 기간](#개발-기간)
+- [배포 주소](#배포-주소)
+- [개발자](#개발자)
+- [시작 가이드](#시작-가이드)
+- [기술 스택](#기술-스택)
+- [화면구성](#화면구성)
+- [아키텍처 및 디렉토리 구조](#아키텍처-및-디렉토리-구조)
-## Available Scripts
+# 프로젝트 소개
+ React, Spring Boot, DOMjudge 기반으로 개발한 웹 프로그래밍 실습 서비스이다. 학생은 별도 설치 없이 브라우저에서 코드를 작성하고 즉시 채점 결과를 확인할 수 있으며, 교수자는 과제 관리와 자동채점 기반 수업 운영이 가능하다. 실제 C 프로그래밍 수업에 적용하여 학습 효율과 문제 해결 집중도 향상을 확인하였다.
+
+## 핵심 기능
-In the project directory, you can run:
+- **학생**: 수업·과제·문제 풀이, 코딩 테스트 응시, 공지 확인, 커뮤니티 Q&A
+- **교수**: 수업·문제·과제·코딩 테스트 관리, 학생 진행 현황·채점·성적 관리
+- **자동 채점**: DOMjudge 연동으로 코드 제출 후 즉시 채점 결과 확인
+- **코드 에디터**: CodeMirror 기반 브라우저 IDE (C, C++, Java, Python 등)
+- **인증**: 이메일/비밀번호 및 소셜 로그인 (Google, GitHub)
-### `npm start`
+## 개발 기간
+- 2025.09 ~ 진행 중
-Runs the app in the development mode.\
-Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
+## 배포 주소
+- 서비스 주소: [`https://hcl.walab.info`](https://hcl.walab.info)
-The page will reload when you make changes.\
-You may also see any lint errors in the console.
+## 개발자
-### `npm test`
+
+
+
+
+
+
+
+ 우병희
+
+ Frontend, Backend
+
+
+
+
+
+
+ 곽서원
+
+ Frontend, Backend
+
+
+
+
+
+
+ 윤동혁
+
+ Infra, Backend
+
+
+
-Launches the test runner in the interactive watch mode.\
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+---
-### `npm run build`
+## 시작 가이드
-Builds the app for production to the `build` folder.\
-It correctly bundles React in production mode and optimizes the build for the best performance.
+### 사전 요구사항
-The build is minified and the filenames include the hashes.\
-Your app is ready to be deployed!
+- **Frontend**: Node.js 18+, npm
+- **Backend** (API 연동 시): Java 11, MariaDB, Redis, DOMjudge
-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+### Frontend 설치 및 실행
-### `npm run eject`
+```bash
+git clone https://github.com/walab-Capstone1/Handongjudge_FE.git
+cd Handongjudge_FE
+npm install
+npm start
+```
-**Note: this is a one-way operation. Once you `eject`, you can't go back!**
+개발 서버는 기본적으로 `http://localhost:3000` 에서 실행됩니다.
-If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+### 환경 변수
-Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
+프로젝트 루트에 `.env` 파일을 생성합니다.
-You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
+```bash
+REACT_APP_API_URL=http://localhost:8080/api
+```
-## Learn More
+환경 변수를 설정하지 않으면 기본값 `https://hcl.walab.info/api` 로 요청합니다.
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+### Backend 연동
-To learn React, check out the [React documentation](https://reactjs.org/).
+로컬에서 API·인증·채점 기능을 사용하려면 Backend를 함께 실행해야 합니다.
-### Code Splitting
+```bash
+git clone https://github.com/walab-Capstone1/Handongjudge_BE.git
+cd Handongjudge_BE
+# .env 파일 설정 (DB, Redis, DOMjudge, OAuth 등)
+./gradlew bootRun
+```
-This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
+- Backend 기본 포트: `8080`
+- Frontend 기본 포트: `3000`
+- `local` 프로파일에서는 `http://localhost:3000` CORS가 허용됩니다.
-### Analyzing the Bundle Size
+[Backend Repository](https://github.com/walab-Capstone1/Handongjudge_BE.git)
-This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
+### 빌드
-### Making a Progressive Web App
+```bash
+npm run build
+```
-This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
+빌드 결과물은 `build/` 디렉토리에 생성됩니다.
-### Advanced Configuration
+---
-This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
+# 기술 스택
-### Deployment
+### Environment
+
+
+
-This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
+### Language / Framework
+
+
+
-### `npm run build` fails to minify
+### State / Styling
+
+
+
+
+
+### Editor / Content
+
+
+
+
+
+
+
+### Interaction / UI
+
+
+
+
+### Test / Deploy
+
+
+
+
+
+---
+
+
+# 화면구성
+
+## 학생용
+
+### 메인 화면
+
+
+**서비스 소개와 시스템 공지/가이드를 한눈에 확인하고 강의실로 바로 이동할 수 있는 시작 페이지입니다.**
+
+### 로그인 / 회원가입
+
+**이메일/비밀번호 또는 소셜 로그인으로 인증을 진행하는 사용자 로그인 화면입니다.**
+
+****
+
+**신규 사용자가 기본 정보를 입력해 계정을 생성하는 회원가입 화면입니다.**
+
+****
+### 수업 / 과제 / 문제
+
+**내가 수강 중인 강의 목록과 강의별 진입 경로를 확인하는 페이지입니다.**
+
+****
+
+**선택한 수업의 과제 목록, 마감 기한, 진행 상태를 확인하는 페이지입니다.**
+
+****
+
+**과제의 문제 구성과 제출 현황, 세부 안내를 확인하는 상세 페이지입니다.**
+
+****
+
+**에디터에서 코드를 작성하고 제출/채점 결과를 확인하는 실습 화면입니다.**
+
+****
+
+**수업 공지사항과 중요 안내를 확인하는 공지 페이지입니다.**
+
+****
+### 커뮤니티
+
+**과제·문제별 질의응답 게시글을 검색하고 조회하는 커뮤니티 목록 화면입니다.**
+
+****
+
+**관련 과제/문제를 선택하고 질문을 작성하는 게시글 등록 화면입니다.**
+
+****
+
+**질문 본문, 댓글, 추천 및 해결 상태를 확인하는 게시글 상세 화면입니다.**
+
+****
+
+**코딩 테스트 목록과 응시 상태를 확인하고 테스트를 시작하는 화면입니다.**
+
+## 교수자용
+
+### 수업관리
+
+**담당 수업 목록을 조회하고 수업별 운영 설정으로 이동하는 관리 화면입니다.**
+
+****
+
+**학기/분반/기본 정보를 입력해 새로운 수업을 개설하는 페이지입니다.**
+
+### 문제관리
+****
+
+**등록된 문제를 조회·검색·수정·삭제하는 문제 관리 페이지입니다.**
+
+****
+
+**외부 또는 기존 문제 데이터를 가져와 수업 문제로 등록하는 화면입니다.**
+
+****
+
+**문제 설명, 입출력, 테스트케이스를 작성해 새 문제를 생성하는 페이지입니다.**
+
+### 과제 관리
+****
+
+**선택한 문제를 과제로 묶어 수업에 배포하고 마감/옵션을 설정하는 화면입니다.**
+
+
+****
+
+**기존 과제에 문제를 추가하거나 구성 순서를 조정하는 과제 편집 페이지입니다.**
+
+****
+
+**학생 과제 제출 현황을 반영하고 관리하는 화면입니다.**
+
+### 학생 관리
+****
+
+**수강생 목록, 등록 상태, 참여 정보를 확인·관리하는 페이지입니다.**
+
+### 코딩테스트
+****
+
+**코딩 테스트 생성, 배포, 문제 구성, 진행 상태를 관리하는 화면입니다.**
+
+****
+
+**코딩 테스트 기본 정보, 시작·종료 시간, 공개 상태를 확인하고 시험을 제어하는 메인 화면입니다.**
+
+****
+
+**코딩 테스트에 포함된 문제 목록과 제출·정답률 통계를 관리하는 화면입니다.**
+
+****
+
+**학생별 제출 기록과 채점 결과, 제출 코드를 조회하는 화면입니다.**
+
+****
+
+**학생별 진행 상태, 완료율, 문제별 풀이 현황을 한눈에 확인하는 화면입니다.**
+
+
+
+****
+### 성적 관리
+
+**과제/코딩테스트 성적 조회, 점수 입력, 배점 설정, 통계를 관리하는 성적 페이지입니다.**
+
+---
+
+# 아키텍처 및 디렉토리 구조
+
+```text
+📁 Handongjudge_FE
+├── 📁 public
+├── 📁 src
+│ ├── 📄 App.tsx
+│ ├── 📁 pages
+│ │ ├── 📁 Auth
+│ │ ├── 📁 Course
+│ │ ├── 📁 AssignmentPage
+│ │ ├── 📁 TutorPage
+│ │ └── 📁 SuperAdminPage
+│ ├── 📁 components
+│ ├── 📁 layouts
+│ ├── 📁 hooks
+│ ├── ⚛️ recoil
+│ ├── 🌐 services
+│ ├── 🎨 styles
+│ ├── 🏷️ types
+│ └── 🛠️ utils
+├── 🐳 Dockerfile-frontend
+├── ⚙️ .github/workflows/deploy-frontend.yml
+└── 📦 package.json
+```
+
+
+
+---
-This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
-c
\ No newline at end of file
diff --git a/docs/hcodelab-assignment-assign.png b/docs/hcodelab-assignment-assign.png
new file mode 100644
index 0000000..f85d8a2
Binary files /dev/null and b/docs/hcodelab-assignment-assign.png differ
diff --git a/docs/hcodelab-assignment-detail.png b/docs/hcodelab-assignment-detail.png
new file mode 100644
index 0000000..512eff6
Binary files /dev/null and b/docs/hcodelab-assignment-detail.png differ
diff --git a/docs/hcodelab-assignments.png b/docs/hcodelab-assignments.png
new file mode 100644
index 0000000..918b4b0
Binary files /dev/null and b/docs/hcodelab-assignments.png differ
diff --git a/docs/hcodelab-community-list.png b/docs/hcodelab-community-list.png
new file mode 100644
index 0000000..c913052
Binary files /dev/null and b/docs/hcodelab-community-list.png differ
diff --git a/docs/hcodelab-community-post.png b/docs/hcodelab-community-post.png
new file mode 100644
index 0000000..8a0f981
Binary files /dev/null and b/docs/hcodelab-community-post.png differ
diff --git a/docs/hcodelab-community-view.png b/docs/hcodelab-community-view.png
new file mode 100644
index 0000000..7906ef3
Binary files /dev/null and b/docs/hcodelab-community-view.png differ
diff --git a/docs/hcodelab-course-create.png b/docs/hcodelab-course-create.png
new file mode 100644
index 0000000..1792a54
Binary files /dev/null and b/docs/hcodelab-course-create.png differ
diff --git a/docs/hcodelab-course-manage.png b/docs/hcodelab-course-manage.png
new file mode 100644
index 0000000..cf43a08
Binary files /dev/null and b/docs/hcodelab-course-manage.png differ
diff --git a/docs/hcodelab-courses.png b/docs/hcodelab-courses.png
new file mode 100644
index 0000000..65df73c
Binary files /dev/null and b/docs/hcodelab-courses.png differ
diff --git a/docs/hcodelab-exam-manage.png b/docs/hcodelab-exam-manage.png
new file mode 100644
index 0000000..daafd3a
Binary files /dev/null and b/docs/hcodelab-exam-manage.png differ
diff --git a/docs/hcodelab-grading.png b/docs/hcodelab-grading.png
new file mode 100644
index 0000000..47f591f
Binary files /dev/null and b/docs/hcodelab-grading.png differ
diff --git a/docs/hcodelab-import.png b/docs/hcodelab-import.png
new file mode 100644
index 0000000..88fd635
Binary files /dev/null and b/docs/hcodelab-import.png differ
diff --git a/docs/hcodelab-index1.png b/docs/hcodelab-index1.png
new file mode 100644
index 0000000..66e6c8b
Binary files /dev/null and b/docs/hcodelab-index1.png differ
diff --git a/docs/hcodelab-index2.png b/docs/hcodelab-index2.png
new file mode 100644
index 0000000..5a79f73
Binary files /dev/null and b/docs/hcodelab-index2.png differ
diff --git a/docs/hcodelab-index3.png b/docs/hcodelab-index3.png
new file mode 100644
index 0000000..3d842c8
Binary files /dev/null and b/docs/hcodelab-index3.png differ
diff --git a/docs/hcodelab-index4.png b/docs/hcodelab-index4.png
new file mode 100644
index 0000000..b3fbc82
Binary files /dev/null and b/docs/hcodelab-index4.png differ
diff --git a/docs/hcodelab-index5.png b/docs/hcodelab-index5.png
new file mode 100644
index 0000000..9f43d0a
Binary files /dev/null and b/docs/hcodelab-index5.png differ
diff --git a/docs/hcodelab-index6.png b/docs/hcodelab-index6.png
new file mode 100644
index 0000000..96f0c78
Binary files /dev/null and b/docs/hcodelab-index6.png differ
diff --git a/docs/hcodelab-login.png b/docs/hcodelab-login.png
new file mode 100644
index 0000000..20b1284
Binary files /dev/null and b/docs/hcodelab-login.png differ
diff --git a/docs/hcodelab-main.png b/docs/hcodelab-main.png
new file mode 100644
index 0000000..c95f5a8
Binary files /dev/null and b/docs/hcodelab-main.png differ
diff --git a/docs/hcodelab-notice.png b/docs/hcodelab-notice.png
new file mode 100644
index 0000000..dfa122f
Binary files /dev/null and b/docs/hcodelab-notice.png differ
diff --git a/docs/hcodelab-problem-add.png b/docs/hcodelab-problem-add.png
new file mode 100644
index 0000000..469da49
Binary files /dev/null and b/docs/hcodelab-problem-add.png differ
diff --git a/docs/hcodelab-problem-create.png b/docs/hcodelab-problem-create.png
new file mode 100644
index 0000000..d328b50
Binary files /dev/null and b/docs/hcodelab-problem-create.png differ
diff --git a/docs/hcodelab-problem-manage.png b/docs/hcodelab-problem-manage.png
new file mode 100644
index 0000000..27e3a63
Binary files /dev/null and b/docs/hcodelab-problem-manage.png differ
diff --git a/docs/hcodelab-problem-solve.png b/docs/hcodelab-problem-solve.png
new file mode 100644
index 0000000..3f5b753
Binary files /dev/null and b/docs/hcodelab-problem-solve.png differ
diff --git a/docs/hcodelab-problem-testcase.png b/docs/hcodelab-problem-testcase.png
new file mode 100644
index 0000000..59d0920
Binary files /dev/null and b/docs/hcodelab-problem-testcase.png differ
diff --git a/docs/hcodelab-quiz-main.png b/docs/hcodelab-quiz-main.png
new file mode 100644
index 0000000..878c070
Binary files /dev/null and b/docs/hcodelab-quiz-main.png differ
diff --git a/docs/hcodelab-quiz-problemlists.png b/docs/hcodelab-quiz-problemlists.png
new file mode 100644
index 0000000..a664882
Binary files /dev/null and b/docs/hcodelab-quiz-problemlists.png differ
diff --git a/docs/hcodelab-quiz-student-progress.png b/docs/hcodelab-quiz-student-progress.png
new file mode 100644
index 0000000..7d72b25
Binary files /dev/null and b/docs/hcodelab-quiz-student-progress.png differ
diff --git a/docs/hcodelab-quiz-submit-detail.png b/docs/hcodelab-quiz-submit-detail.png
new file mode 100644
index 0000000..a5514bb
Binary files /dev/null and b/docs/hcodelab-quiz-submit-detail.png differ
diff --git a/docs/hcodelab-score-manage.png b/docs/hcodelab-score-manage.png
new file mode 100644
index 0000000..d8db08b
Binary files /dev/null and b/docs/hcodelab-score-manage.png differ
diff --git a/docs/hcodelab-signup.png b/docs/hcodelab-signup.png
new file mode 100644
index 0000000..8698e0e
Binary files /dev/null and b/docs/hcodelab-signup.png differ
diff --git a/docs/hcodelab-student-manage.png b/docs/hcodelab-student-manage.png
new file mode 100644
index 0000000..bc0fd17
Binary files /dev/null and b/docs/hcodelab-student-manage.png differ
diff --git a/docs/hcodelab-test.png b/docs/hcodelab-test.png
new file mode 100644
index 0000000..6cc2aef
Binary files /dev/null and b/docs/hcodelab-test.png differ
diff --git a/src/pages/AssignmentPage/ProblemSolvePage/hooks/useProblemSolve.ts b/src/pages/AssignmentPage/ProblemSolvePage/hooks/useProblemSolve.ts
index 0a695a0..4e139f0 100644
--- a/src/pages/AssignmentPage/ProblemSolvePage/hooks/useProblemSolve.ts
+++ b/src/pages/AssignmentPage/ProblemSolvePage/hooks/useProblemSolve.ts
@@ -505,6 +505,20 @@ export function useProblemSolve() {
return () => clearInterval(interval);
}, [saveToBackend]);
+ // 문제/과제 이동 시 이전 채점·테스트 결과 및 진행 중 폴링/SSE 정리
+ useEffect(() => {
+ void problemId;
+ void assignmentId;
+ submissionPollingCancelledRef.current = true;
+ sseAbortControllerRef.current?.abort();
+ sseAbortControllerRef.current = null;
+ testcaseOutputAccumRef.current = [];
+ setSubmissionResult(null);
+ setTestcaseResults(null);
+ setTotalTestcaseCount(null);
+ setIsSubmitting(false);
+ }, [problemId, assignmentId]);
+
// 언마운트 시 진행 중인 폴링/SSE 취소
useEffect(() => {
return () => {
diff --git a/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementCourseTable.tsx b/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementCourseTable.tsx
index bf8cc26..6feab6a 100644
--- a/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementCourseTable.tsx
+++ b/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementCourseTable.tsx
@@ -11,8 +11,9 @@ import {
GradeProblemCellDisplay,
GradeStatusLegendBar,
} from "./GradeProblemCellDisplay";
-import type { ProblemGrade } from "../types";
+import type { ProblemGrade, QuizSubmissionLogTarget } from "../types";
import * as GS from "../styles";
+import QuizSubmissionLogButton from "./QuizSubmissionLogButton";
export interface GradeManagementCourseTableProps {
courseLoading: boolean;
@@ -54,6 +55,7 @@ export interface GradeManagementCourseTableProps {
userId: number,
problemId: number,
) => void;
+ onOpenQuizSubmissionLog?: (ctx: QuizSubmissionLogTarget) => void;
onProblemDetail?: (problemId: number) => void;
onOpenAssignmentReview?: (ctx: {
assignmentId: number;
@@ -85,6 +87,7 @@ export default function GradeManagementCourseTable({
onViewCode,
onSaveGradeForQuiz,
onViewCodeForQuiz,
+ onOpenQuizSubmissionLog,
onProblemDetail,
onOpenAssignmentReview,
totalOnly = false,
@@ -371,6 +374,24 @@ export default function GradeManagementCourseTable({
fallbackPoints={problem.points ?? 1}
dueAt={item.dueAt}
showLateOnly={showLateOnly}
+ trailingActions={
+ onOpenQuizSubmissionLog ? (
+
+ ) : undefined
+ }
/>
);
diff --git a/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementQuizTable.tsx b/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementQuizTable.tsx
index 690eab5..52f6267 100644
--- a/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementQuizTable.tsx
+++ b/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementQuizTable.tsx
@@ -7,6 +7,8 @@ import {
GradeProblemCellDisplay,
GradeStatusLegendBar,
} from "./GradeProblemCellDisplay";
+import QuizSubmissionLogButton from "./QuizSubmissionLogButton";
+import type { QuizSubmissionLogTarget } from "../types";
export interface GradeManagementQuizTableProps {
grades: StudentGradeRow[];
@@ -27,6 +29,7 @@ export interface GradeManagementQuizTableProps {
comment: string,
) => void;
handleViewCode?: (userId: number, problemId: number) => void;
+ onOpenQuizSubmissionLog?: (ctx: QuizSubmissionLogTarget) => void;
onProblemDetail?: (problemId: number) => void;
totalOnly?: boolean;
onToggleTotalOnly?: (v: boolean) => void;
@@ -49,6 +52,7 @@ export default function GradeManagementQuizTable({
comments = {},
handleSaveGrade,
handleViewCode,
+ onOpenQuizSubmissionLog,
onProblemDetail,
totalOnly = false,
onToggleTotalOnly,
@@ -192,6 +196,25 @@ export default function GradeManagementQuizTable({
fallbackPoints={col.points ?? 1}
dueAt={quizDueAt}
showLateOnly={showLateOnly}
+ trailingActions={
+ onOpenQuizSubmissionLog && selectedQuiz ? (
+
+ ) : undefined
+ }
/>
);
diff --git a/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementView.tsx b/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementView.tsx
index 8a5ab86..c9ad185 100644
--- a/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementView.tsx
+++ b/src/pages/TutorPage/Grades/GradeManagement/components/GradeManagementView.tsx
@@ -14,6 +14,7 @@ import GradeStatsModal from "./GradeStatsModal";
import GradePointsModal from "./GradePointsModal";
import GradeProblemDetailModal from "./GradeProblemDetailModal";
import GradeAssignmentReviewModal from "./GradeAssignmentReviewModal";
+import GradeQuizSubmissionLogModal from "./GradeQuizSubmissionLogModal";
export default function GradeManagementView(d: GradeManagementHookReturn) {
const [totalOnly, setTotalOnly] = useState(false);
@@ -95,6 +96,15 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
openAssignmentReview,
closeAssignmentReview,
handleAssignmentReviewSaved,
+ quizLogTarget,
+ quizLogRecords,
+ quizLogSelectedId,
+ quizLogCode,
+ quizLogListLoading,
+ quizLogCodeLoading,
+ openQuizSubmissionLog,
+ closeQuizSubmissionLog,
+ selectQuizLogSubmission,
} = d;
// biome-ignore lint/correctness/useExhaustiveDependencies: 보기 모드·선택 과제/퀴즈 바뀔 때 헤더 문제 필터만 초기화
@@ -453,6 +463,7 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
onViewCode={handleViewCodeForAssignment}
onSaveGradeForQuiz={handleSaveGradeForQuizCourse}
onViewCodeForQuiz={handleViewCodeForQuiz}
+ onOpenQuizSubmissionLog={openQuizSubmissionLog}
onProblemDetail={openProblemDetail}
onOpenAssignmentReview={openAssignmentReview}
totalOnly={totalOnly}
@@ -497,6 +508,7 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
comments={comments}
onSaveGradeForQuiz={handleSaveGradeForQuizCourse}
onViewCodeForQuiz={handleViewCodeForQuiz}
+ onOpenQuizSubmissionLog={openQuizSubmissionLog}
onProblemDetail={openProblemDetail}
totalOnly={totalOnly}
onToggleTotalOnly={setTotalOnly}
@@ -524,6 +536,7 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
handleViewCodeForQuiz(selectedQuiz.id, userId, problemId)
: undefined
}
+ onOpenQuizSubmissionLog={openQuizSubmissionLog}
onProblemDetail={openProblemDetail}
totalOnly={totalOnly}
onToggleTotalOnly={setTotalOnly}
@@ -627,6 +640,18 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
onClose={closeProblemDetailModal}
/>
+
+
{assignmentReviewTarget && sectionId ? (
void;
+ onSelectSubmission: (submissionId: number) => void;
+}
+
+export default function GradeQuizSubmissionLogModal({
+ show,
+ target,
+ records,
+ selectedSubmissionId,
+ code,
+ listLoading,
+ codeLoading,
+ onClose,
+ onSelectSubmission,
+}: GradeQuizSubmissionLogModalProps) {
+ const selectedIndex = records.findIndex(
+ (r) => r.submissionId === selectedSubmissionId,
+ );
+
+ const moveSelection = useCallback(
+ (delta: number) => {
+ if (records.length === 0) return;
+ const base = selectedIndex >= 0 ? selectedIndex : 0;
+ const next = Math.max(0, Math.min(records.length - 1, base + delta));
+ onSelectSubmission(records[next].submissionId);
+ },
+ [records, selectedIndex, onSelectSubmission],
+ );
+
+ useEffect(() => {
+ if (!show) return;
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ onClose();
+ return;
+ }
+ if (e.key === "ArrowDown") {
+ e.preventDefault();
+ moveSelection(1);
+ }
+ if (e.key === "ArrowUp") {
+ e.preventDefault();
+ moveSelection(-1);
+ }
+ };
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [show, onClose, moveSelection]);
+
+ if (!show || !target) return null;
+
+ const title = `${target.studentName} · ${removeCopyLabel(target.problemTitle)}`;
+
+ return (
+ e.key === "Escape" && onClose()}
+ role="presentation"
+ >
+ e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ >
+
+
+
제출 로그
+
+ {title}
+ {target.quizTitle ? ` · ${target.quizTitle}` : ""}
+
+
+
+ ×
+
+
+
+ {listLoading ? (
+ 제출 기록을 불러오는 중...
+ ) : records.length === 0 ? (
+ 제출 기록이 없습니다.
+ ) : (
+
+
+
+ 제출 이력 ({records.length}건) · ↑↓ 선택
+
+
+ {records.map((rec, idx) => {
+ const active = rec.submissionId === selectedSubmissionId;
+ const resultColor = getQuizSubmissionResultColor(rec.result);
+ return (
+
+ onSelectSubmission(rec.submissionId)}
+ >
+
+ #{records.length - idx}{" "}
+ {formatQuizSubmissionDateTime(rec.submittedAt)}
+
+
+
+ {getQuizSubmissionResultLabel(rec.result)}
+
+ {rec.language ?? "-"}
+
+
+
+ );
+ })}
+
+
+
+ {codeLoading ? (
+
+ 코드를 불러오는 중...
+
+ ) : code ? (
+ <>
+
+
+ 제출
+ {formatQuizSubmissionDateTime(code.submittedAt)}
+
+
+ 결과
+ {getQuizSubmissionResultLabel(code.result)}
+
+
+ 언어
+ {code.language ?? "-"}
+
+
+
+ {code.code || "(빈 코드)"}
+
+ >
+ ) : (
+
+ 코드를 불러올 수 없습니다.
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/TutorPage/Grades/GradeManagement/components/QuizSubmissionLogButton.tsx b/src/pages/TutorPage/Grades/GradeManagement/components/QuizSubmissionLogButton.tsx
new file mode 100644
index 0000000..25f72e4
--- /dev/null
+++ b/src/pages/TutorPage/Grades/GradeManagement/components/QuizSubmissionLogButton.tsx
@@ -0,0 +1,25 @@
+import * as GS from "../styles";
+import type { QuizSubmissionLogTarget } from "../types";
+
+type Props = {
+ submitted?: boolean;
+ onOpen: (ctx: QuizSubmissionLogTarget) => void;
+ ctx: QuizSubmissionLogTarget;
+};
+
+export default function QuizSubmissionLogButton({
+ submitted,
+ onOpen,
+ ctx,
+}: Props) {
+ if (!submitted) return null;
+ return (
+ onOpen(ctx)}
+ >
+ log
+
+ );
+}
diff --git a/src/pages/TutorPage/Grades/GradeManagement/hooks/useGradeManagement.ts b/src/pages/TutorPage/Grades/GradeManagement/hooks/useGradeManagement.ts
index e05d1b0..68c2ce4 100644
--- a/src/pages/TutorPage/Grades/GradeManagement/hooks/useGradeManagement.ts
+++ b/src/pages/TutorPage/Grades/GradeManagement/hooks/useGradeManagement.ts
@@ -16,6 +16,9 @@ import type {
AllQuizProblemsEntry,
CodeResponse,
ProblemGrade,
+ QuizSubmissionLogCode,
+ QuizSubmissionLogTarget,
+ QuizSubmissionRecord,
} from "../types";
import { formatLateDurationForGradeCsv } from "../utils/gradeExportLateDuration";
@@ -92,6 +95,16 @@ export function useGradeManagement() {
displayScore?: number | null;
} | null>(null);
+ /** 코딩테스트: 학생·문제별 전체 제출 로그 모달 */
+ const [quizLogTarget, setQuizLogTarget] = useState(
+ null,
+ );
+ const [quizLogRecords, setQuizLogRecords] = useState([]);
+ const [quizLogSelectedId, setQuizLogSelectedId] = useState(null);
+ const [quizLogCode, setQuizLogCode] = useState(null);
+ const [quizLogListLoading, setQuizLogListLoading] = useState(false);
+ const [quizLogCodeLoading, setQuizLogCodeLoading] = useState(false);
+
const fetchQuizGrades = useCallback(async () => {
if (!selectedQuiz || !sectionId) return;
try {
@@ -628,6 +641,90 @@ export function useGradeManagement() {
[sectionId],
);
+ const loadQuizLogCode = useCallback(
+ async (quizId: number, submissionId: number) => {
+ if (!sectionId) return;
+ setQuizLogCodeLoading(true);
+ try {
+ const response = await APIService.getQuizSubmissionCode(
+ sectionId,
+ quizId,
+ submissionId,
+ );
+ const data = response?.data ?? response;
+ setQuizLogCode({
+ code: data?.code ?? "",
+ result: data?.result ?? "",
+ submittedAt: data?.submittedAt ?? "",
+ language: data?.language ?? "",
+ problemTitle: data?.problemTitle ?? "",
+ });
+ } catch (error) {
+ console.error("제출 코드 조회 실패:", error);
+ setQuizLogCode(null);
+ } finally {
+ setQuizLogCodeLoading(false);
+ }
+ },
+ [sectionId],
+ );
+
+ const openQuizSubmissionLog = useCallback(
+ async (ctx: QuizSubmissionLogTarget) => {
+ if (!sectionId) return;
+ setQuizLogTarget(ctx);
+ setQuizLogRecords([]);
+ setQuizLogSelectedId(null);
+ setQuizLogCode(null);
+ setQuizLogListLoading(true);
+ try {
+ const response = await APIService.getQuizSubmissions(
+ sectionId,
+ ctx.quizId,
+ {
+ userId: ctx.userId,
+ problemId: ctx.problemId,
+ page: 0,
+ size: 100,
+ },
+ );
+ const data = response?.data ?? response;
+ const content = (data?.content ?? []) as QuizSubmissionRecord[];
+ setQuizLogRecords(content);
+ if (content.length > 0) {
+ const firstId = content[0].submissionId;
+ setQuizLogSelectedId(firstId);
+ await loadQuizLogCode(ctx.quizId, firstId);
+ }
+ } catch (error) {
+ console.error("제출 로그 조회 실패:", error);
+ alert("제출 기록을 불러올 수 없습니다.");
+ setQuizLogTarget(null);
+ } finally {
+ setQuizLogListLoading(false);
+ }
+ },
+ [sectionId, loadQuizLogCode],
+ );
+
+ const closeQuizSubmissionLog = useCallback(() => {
+ setQuizLogTarget(null);
+ setQuizLogRecords([]);
+ setQuizLogSelectedId(null);
+ setQuizLogCode(null);
+ setQuizLogListLoading(false);
+ setQuizLogCodeLoading(false);
+ }, []);
+
+ const selectQuizLogSubmission = useCallback(
+ async (submissionId: number) => {
+ if (!quizLogTarget || submissionId === quizLogSelectedId) return;
+ setQuizLogSelectedId(submissionId);
+ await loadQuizLogCode(quizLogTarget.quizId, submissionId);
+ },
+ [quizLogTarget, quizLogSelectedId, loadQuizLogCode],
+ );
+
const openProblemDetail = useCallback(async (problemId: number) => {
try {
const response = await APIService.getProblemInfo(problemId);
@@ -2017,6 +2114,15 @@ export function useGradeManagement() {
openAssignmentReview,
closeAssignmentReview,
handleAssignmentReviewSaved,
+ quizLogTarget,
+ quizLogRecords,
+ quizLogSelectedId,
+ quizLogCode,
+ quizLogListLoading,
+ quizLogCodeLoading,
+ openQuizSubmissionLog,
+ closeQuizSubmissionLog,
+ selectQuizLogSubmission,
};
}
diff --git a/src/pages/TutorPage/Grades/GradeManagement/styles.ts b/src/pages/TutorPage/Grades/GradeManagement/styles.ts
index 72d0aaa..354a74c 100644
--- a/src/pages/TutorPage/Grades/GradeManagement/styles.ts
+++ b/src/pages/TutorPage/Grades/GradeManagement/styles.ts
@@ -921,6 +921,121 @@ export const BtnReviewCode = styled.button`
}
`;
+/** 코딩테스트 성적 셀: 제출 이력(log) 모달 */
+export const BtnSubmissionLog = styled.button`
+ flex-shrink: 0;
+ padding: 0.18rem 0.4rem;
+ border: 1px solid #bae6fd;
+ background: #f0f9ff;
+ border-radius: 5px;
+ font-size: 0.68rem;
+ font-weight: 700;
+ cursor: pointer;
+ color: #0369a1;
+ font-family: "Pretendard", -apple-system, BlinkMacSystemFont, system-ui, Roboto,
+ sans-serif;
+ letter-spacing: 0.02em;
+
+ &:hover {
+ background: #e0f2fe;
+ border-color: #7dd3fc;
+ color: #0c4a6e;
+ }
+`;
+
+export const QuizLogModalLayout = styled.div`
+ display: grid;
+ grid-template-columns: minmax(220px, 280px) 1fr;
+ gap: 1.25rem;
+ min-height: 360px;
+
+ @media (max-width: 768px) {
+ grid-template-columns: 1fr;
+ }
+`;
+
+export const QuizLogListPanel = styled.div`
+ display: flex;
+ flex-direction: column;
+ border: 1px solid #e2e8f0;
+ border-radius: 10px;
+ overflow: hidden;
+ background: #f8fafc;
+ min-height: 280px;
+ max-height: min(65vh, 520px);
+`;
+
+export const QuizLogListHeader = styled.div`
+ padding: 0.65rem 0.85rem;
+ font-size: 0.8rem;
+ font-weight: 700;
+ color: #475569;
+ border-bottom: 1px solid #e2e8f0;
+ background: #f1f5f9;
+`;
+
+export const QuizLogListScroll = styled.ul`
+ list-style: none;
+ margin: 0;
+ padding: 0.35rem;
+ overflow-y: auto;
+ flex: 1;
+`;
+
+export const QuizLogListItem = styled.li<{ $active?: boolean }>`
+ button {
+ width: 100%;
+ text-align: left;
+ padding: 0.55rem 0.65rem;
+ border: none;
+ border-radius: 8px;
+ background: ${(p) => (p.$active ? "#e0e7ff" : "transparent")};
+ cursor: pointer;
+ font-family: inherit;
+ transition: background 0.15s ease;
+
+ &:hover {
+ background: ${(p) => (p.$active ? "#e0e7ff" : "#f1f5f9")};
+ }
+ }
+`;
+
+export const QuizLogItemTime = styled.div`
+ font-size: 0.72rem;
+ font-weight: 600;
+ color: #334155;
+`;
+
+export const QuizLogItemMeta = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.35rem;
+ margin-top: 0.2rem;
+ font-size: 0.68rem;
+ color: #64748b;
+`;
+
+export const QuizLogDetailPanel = styled.div`
+ display: flex;
+ flex-direction: column;
+ min-height: 280px;
+ min-width: 0;
+`;
+
+export const QuizLogDetailMeta = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem 1.25rem;
+ margin-bottom: 0.85rem;
+ font-size: 0.85rem;
+ color: #475569;
+
+ strong {
+ color: #1e293b;
+ margin-right: 0.25rem;
+ }
+`;
+
export const SubmissionInfo = styled.div`
display: flex;
flex-direction: column;
diff --git a/src/pages/TutorPage/Grades/GradeManagement/types.ts b/src/pages/TutorPage/Grades/GradeManagement/types.ts
index 16fe5e2..d7557de 100644
--- a/src/pages/TutorPage/Grades/GradeManagement/types.ts
+++ b/src/pages/TutorPage/Grades/GradeManagement/types.ts
@@ -142,3 +142,33 @@ export interface CodeResponse {
code?: string;
codeString?: string;
}
+
+/** 코딩테스트 제출 로그 (튜터 submissions API) */
+export interface QuizSubmissionRecord {
+ submissionId: number;
+ userId: number;
+ studentId: string;
+ studentName: string;
+ problemId: number;
+ problemTitle: string;
+ submittedAt: string;
+ result: string;
+ language: string;
+}
+
+export interface QuizSubmissionLogTarget {
+ quizId: number;
+ quizTitle?: string;
+ userId: number;
+ problemId: number;
+ studentName: string;
+ problemTitle: string;
+}
+
+export interface QuizSubmissionLogCode {
+ code: string;
+ result: string;
+ submittedAt: string;
+ language?: string;
+ problemTitle?: string;
+}
diff --git a/src/pages/TutorPage/Grades/GradeManagement/utils/quizSubmissionResult.ts b/src/pages/TutorPage/Grades/GradeManagement/utils/quizSubmissionResult.ts
new file mode 100644
index 0000000..016aad5
--- /dev/null
+++ b/src/pages/TutorPage/Grades/GradeManagement/utils/quizSubmissionResult.ts
@@ -0,0 +1,44 @@
+export function getQuizSubmissionResultLabel(result: string): string {
+ const labels: Record = {
+ AC: "정답",
+ WA: "오답",
+ TLE: "시간초과",
+ RE: "런타임에러",
+ CE: "컴파일에러",
+ MLE: "메모리초과",
+ OLE: "출력초과",
+ };
+ return labels[result] ?? result ?? "-";
+}
+
+export function getQuizSubmissionResultColor(result: string): string {
+ switch (result) {
+ case "AC":
+ return "#10b981";
+ case "WA":
+ return "#ef4444";
+ case "TLE":
+ case "MLE":
+ case "OLE":
+ return "#f59e0b";
+ case "RE":
+ case "CE":
+ return "#8b5cf6";
+ default:
+ return "#64748b";
+ }
+}
+
+export function formatQuizSubmissionDateTime(iso: string | undefined): string {
+ if (!iso) return "-";
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return iso;
+ return d.toLocaleString("ko-KR", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ });
+}