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
354 changes: 315 additions & 39 deletions README.md

Large diffs are not rendered by default.

Binary file added docs/hcodelab-assignment-assign.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-assignment-detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-assignments.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-community-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-community-post.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-community-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-course-create.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-course-manage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-courses.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-exam-manage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-grading.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-import.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-index1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-index2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-index3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-index4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-index5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-index6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-notice.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-problem-add.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-problem-create.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-problem-manage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-problem-solve.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/hcodelab-problem-testcase.png
Binary file added docs/hcodelab-quiz-main.png
Binary file added docs/hcodelab-quiz-problemlists.png
Binary file added docs/hcodelab-quiz-student-progress.png
Binary file added docs/hcodelab-quiz-submit-detail.png
Binary file added docs/hcodelab-score-manage.png
Binary file added docs/hcodelab-signup.png
Binary file added docs/hcodelab-student-manage.png
Binary file added docs/hcodelab-test.png
14 changes: 14 additions & 0 deletions src/pages/AssignmentPage/ProblemSolvePage/hooks/useProblemSolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +55,7 @@ export interface GradeManagementCourseTableProps {
userId: number,
problemId: number,
) => void;
onOpenQuizSubmissionLog?: (ctx: QuizSubmissionLogTarget) => void;
onProblemDetail?: (problemId: number) => void;
onOpenAssignmentReview?: (ctx: {
assignmentId: number;
Expand Down Expand Up @@ -85,6 +87,7 @@ export default function GradeManagementCourseTable({
onViewCode,
onSaveGradeForQuiz,
onViewCodeForQuiz,
onOpenQuizSubmissionLog,
onProblemDetail,
onOpenAssignmentReview,
totalOnly = false,
Expand Down Expand Up @@ -371,6 +374,24 @@ export default function GradeManagementCourseTable({
fallbackPoints={problem.points ?? 1}
dueAt={item.dueAt}
showLateOnly={showLateOnly}
trailingActions={
onOpenQuizSubmissionLog ? (
<QuizSubmissionLogButton
submitted={problemGrade?.submitted}
onOpen={onOpenQuizSubmissionLog}
ctx={{
quizId: item.id,
quizTitle: item.title,
userId: student.userId,
problemId: problem.problemId,
studentName:
student.studentName ?? "",
problemTitle:
problem.problemTitle ?? "",
}}
/>
) : undefined
}
/>
</S.TdCourseProblemCell>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
GradeProblemCellDisplay,
GradeStatusLegendBar,
} from "./GradeProblemCellDisplay";
import QuizSubmissionLogButton from "./QuizSubmissionLogButton";
import type { QuizSubmissionLogTarget } from "../types";

export interface GradeManagementQuizTableProps {
grades: StudentGradeRow[];
Expand All @@ -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;
Expand All @@ -49,6 +52,7 @@ export default function GradeManagementQuizTable({
comments = {},
handleSaveGrade,
handleViewCode,
onOpenQuizSubmissionLog,
onProblemDetail,
totalOnly = false,
onToggleTotalOnly,
Expand Down Expand Up @@ -192,6 +196,25 @@ export default function GradeManagementQuizTable({
fallbackPoints={col.points ?? 1}
dueAt={quizDueAt}
showLateOnly={showLateOnly}
trailingActions={
onOpenQuizSubmissionLog && selectedQuiz ? (
<QuizSubmissionLogButton
submitted={problem?.submitted}
onOpen={onOpenQuizSubmissionLog}
ctx={{
quizId: selectedQuiz.id,
quizTitle: selectedQuiz.title,
userId: student.userId,
problemId: col.problemId,
studentName: student.studentName ?? "",
problemTitle:
col.problemTitle ??
problem?.problemTitle ??
"",
}}
/>
) : undefined
}
/>
</S.TdCourseProblemCell>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: 보기 모드·선택 과제/퀴즈 바뀔 때 헤더 문제 필터만 초기화
Expand Down Expand Up @@ -453,6 +463,7 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
onViewCode={handleViewCodeForAssignment}
onSaveGradeForQuiz={handleSaveGradeForQuizCourse}
onViewCodeForQuiz={handleViewCodeForQuiz}
onOpenQuizSubmissionLog={openQuizSubmissionLog}
onProblemDetail={openProblemDetail}
onOpenAssignmentReview={openAssignmentReview}
totalOnly={totalOnly}
Expand Down Expand Up @@ -497,6 +508,7 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
comments={comments}
onSaveGradeForQuiz={handleSaveGradeForQuizCourse}
onViewCodeForQuiz={handleViewCodeForQuiz}
onOpenQuizSubmissionLog={openQuizSubmissionLog}
onProblemDetail={openProblemDetail}
totalOnly={totalOnly}
onToggleTotalOnly={setTotalOnly}
Expand Down Expand Up @@ -524,6 +536,7 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
handleViewCodeForQuiz(selectedQuiz.id, userId, problemId)
: undefined
}
onOpenQuizSubmissionLog={openQuizSubmissionLog}
onProblemDetail={openProblemDetail}
totalOnly={totalOnly}
onToggleTotalOnly={setTotalOnly}
Expand Down Expand Up @@ -627,6 +640,18 @@ export default function GradeManagementView(d: GradeManagementHookReturn) {
onClose={closeProblemDetailModal}
/>

<GradeQuizSubmissionLogModal
show={quizLogTarget != null}
target={quizLogTarget}
records={quizLogRecords}
selectedSubmissionId={quizLogSelectedId}
code={quizLogCode}
listLoading={quizLogListLoading}
codeLoading={quizLogCodeLoading}
onClose={closeQuizSubmissionLog}
onSelectSubmission={selectQuizLogSubmission}
/>

{assignmentReviewTarget && sectionId ? (
<GradeAssignmentReviewModal
show
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useCallback, useEffect } from "react";
import { removeCopyLabel } from "../../../../../utils/problemUtils";
import * as S from "../styles";
import type {
QuizSubmissionLogCode,
QuizSubmissionLogTarget,
QuizSubmissionRecord,
} from "../types";
import {
formatQuizSubmissionDateTime,
getQuizSubmissionResultColor,
getQuizSubmissionResultLabel,
} from "../utils/quizSubmissionResult";

export interface GradeQuizSubmissionLogModalProps {
show: boolean;
target: QuizSubmissionLogTarget | null;
records: QuizSubmissionRecord[];
selectedSubmissionId: number | null;
code: QuizSubmissionLogCode | null;
listLoading: boolean;
codeLoading: boolean;
onClose: () => 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 (
<S.ModalOverlay
onClick={onClose}
onKeyDown={(e) => e.key === "Escape" && onClose()}
role="presentation"
>
<S.ModalContent
$large
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<S.ModalHeader>
<div>
<h2 id="quiz-submission-log-title">제출 로그</h2>
<p
style={{
margin: "0.35rem 0 0",
fontSize: "0.9rem",
fontWeight: 500,
opacity: 0.92,
}}
>
{title}
{target.quizTitle ? ` · ${target.quizTitle}` : ""}
</p>
</div>
<S.ModalClose type="button" onClick={onClose} aria-label="닫기">
×
</S.ModalClose>
</S.ModalHeader>
<S.ModalBody>
{listLoading ? (
<p style={{ color: "#64748b", margin: 0 }}>제출 기록을 불러오는 중...</p>
) : records.length === 0 ? (
<p style={{ color: "#64748b", margin: 0 }}>제출 기록이 없습니다.</p>
) : (
<S.QuizLogModalLayout>
<S.QuizLogListPanel>
<S.QuizLogListHeader>
제출 이력 ({records.length}건) · ↑↓ 선택
</S.QuizLogListHeader>
<S.QuizLogListScroll>
{records.map((rec, idx) => {
const active = rec.submissionId === selectedSubmissionId;
const resultColor = getQuizSubmissionResultColor(rec.result);
return (
<S.QuizLogListItem key={rec.submissionId} $active={active}>
<button
type="button"
onClick={() => onSelectSubmission(rec.submissionId)}
>
<S.QuizLogItemTime>
#{records.length - idx}{" "}
{formatQuizSubmissionDateTime(rec.submittedAt)}
</S.QuizLogItemTime>
<S.QuizLogItemMeta>
<span
style={{
display: "inline-block",
padding: "0.1rem 0.35rem",
borderRadius: "4px",
fontWeight: 600,
background: `${resultColor}20`,
color: resultColor,
}}
>
{getQuizSubmissionResultLabel(rec.result)}
</span>
<span>{rec.language ?? "-"}</span>
</S.QuizLogItemMeta>
</button>
</S.QuizLogListItem>
);
})}
</S.QuizLogListScroll>
</S.QuizLogListPanel>
<S.QuizLogDetailPanel>
{codeLoading ? (
<p style={{ color: "#64748b", margin: 0 }}>
코드를 불러오는 중...
</p>
) : code ? (
<>
<S.QuizLogDetailMeta>
<span>
<strong>제출</strong>
{formatQuizSubmissionDateTime(code.submittedAt)}
</span>
<span>
<strong>결과</strong>
{getQuizSubmissionResultLabel(code.result)}
</span>
<span>
<strong>언어</strong>
{code.language ?? "-"}
</span>
</S.QuizLogDetailMeta>
<S.CodeDisplay style={{ flex: 1, maxHeight: "min(55vh, 480px)" }}>
<code>{code.code || "(빈 코드)"}</code>
</S.CodeDisplay>
</>
) : (
<p style={{ color: "#64748b", margin: 0 }}>
코드를 불러올 수 없습니다.
</p>
)}
</S.QuizLogDetailPanel>
</S.QuizLogModalLayout>
)}
</S.ModalBody>
</S.ModalContent>
</S.ModalOverlay>
);
}
Loading