본문으로 건너뛰기

과제 서비스

이 페이지는 lumie-backend/modules/assignment를 다룹니다.

책임

assignment 모듈은 다음을 담당합니다.

  • 명시적인 수업 및 student 대상을 갖는 테넌트 범위 과제
  • /v1/assignments 아래의 staff 작성 워크플로
  • /v1/assignments/me 아래의 학생 대시보드 조회와 자기 제출 API
  • 활성 과제를 위한 시험 연계 수동 답안 제출 저장
  • 과제 마감일, active/closed 상태, 정답 공개 토글
  • 다른 백엔드 모듈이 사용하는 내부 조회 API

소스 경로

경로역할
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/web/AssignmentController.javastaff 및 student 과제 HTTP 라우트
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/web/AssignmentSubmissionController.javastaff 제출물 관리 라우트
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentCommandService.java과제 생성, 대상 교체, 자기 제출, 채점 규칙
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentQueryService.javastaff 조회, 학생 가시성 조회, 학생용 제출물/결과 조회
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentLifecycleService.java마감이 지난 ACTIVE 과제를 CLOSED로 영속화
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/scheduling/AssignmentDeadlineScheduler.javaShedLock으로 보호되는 테넌트-aware 마감 과제 종료 스케줄러
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/Assignment.java과제 aggregate, 워크플로 플래그, 가시성 플래그, 호환성용 class_id 컬럼
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/AssignmentTarget.javaCLASSSTUDENT 타기팅을 위한 표준 대상 행
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/AssignmentSubmission.java답안 맵과 시험 결과 연결을 담는 제출 aggregate
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/repository/AssignmentRepository.java과제 목록 조회와 마감 초과 과제 bulk close update
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/repository/AssignmentTargetRepository.java가시성 조회와 대상 교체 영속화
lumie-backend/libs/internal-api/src/main/java/com/lumie/assignment/api/AssignmentService.java모놀리스 내부 조회 계약
lumie-backend/libs/internal-api/src/main/java/com/lumie/exam/api/ExamService.java내부 시험 메타데이터 및 수동 채점 의존성
lumie-backend/modules/exam/src/main/java/com/lumie/exam/application/service/ResultCommandService.javaEXAM_MANUAL 과제가 소비하는 인라인 수동 답안 채점 경로
lumie-backend/app/src/main/resources/db/migration/public/V81__assignment_redesign_expand.sql과제 워크플로 컬럼, assignment_targets, 시험 연계 제출 필드를 추가
lumie-backend/app/src/main/resources/db/migration/public/V82__assignment_redesign_tenant_safe_indexes.sql리디자인용 테넌트-safe 인덱스와 복합 foreign key를 추가
lumie-backend/app/src/main/resources/db/migration/public/V83__drop_assignment_title_description.sql레거시 과제 title 및 description 컬럼 제거
lumie-backend/app/src/main/resources/db/migration/public/V84__drop_assignment_type_and_draft.sql과제 type과 DRAFT 상태 제거
lumie-backend/app/src/main/resources/db/migration/public/V85__assignment_exam_only_active_guard.sql비시험 레거시 행을 닫고 active 과제를 시험 연계 과제로 제한
lumie-backend/app/src/main/resources/db/migration/public/V86__assignment_exam_fk_restrict.sql과제가 참조하는 시험 삭제를 제한
lumie-backend/app/src/main/resources/db/migration/public/V89__require_assignment_due_date.sql레거시 null 마감일을 백필하고 assignments.due_date를 필수화
lumie-backend/app/src/main/resources/db/migration/public/V90__assignment_overdue_close_index.sql마감 과제 종료 쿼리가 사용하는 partial index 추가

공개 인터페이스

사용 표면엔트리포인트
Staff 작성POST /v1/assignments, GET /v1/assignments, GET /v1/assignments/{id}, PATCH /v1/assignments/{id}, DELETE /v1/assignments/{id}, GET /v1/assignments/class/{classId}
학생 대시보드GET /v1/assignments/me, GET /v1/assignments/{id}/me, PUT /v1/assignments/{id}/me/submission
Staff 제출물 관리GET /v1/assignments/{assignmentId}/submissions, GET /v1/assignments/{assignmentId}/submissions/status, POST /v1/assignments/{assignmentId}/submissions, PATCH /v1/assignments/{assignmentId}/submissions/{submissionId}, GET /v1/assignments/{assignmentId}/submissions/student/{studentId}
과제 통계GET /v1/assignments/statistics/dashboard

내부 인터페이스

AssignmentService는 여전히 조회 메서드만 제공합니다.

  • getAssignment(...)
  • getAssignmentsByClass(...)

이번 리디자인은 과제 채점을 별도의 서비스 경계로 분리하지 않았습니다. EXAM_MANUAL 채점은 내부 ExamService를 통해 모듈러 모놀리스 안에 머뭅니다.

Aggregate와 테이블

집계테이블참고
Assignmentassignments시험 연계 워크플로 모드, 필수 마감일, 상태, 정답 공개 설정, active 행의 필수 linked_exam_id, 그리고 새 대상 행과 함께 호환성용 class_id를 저장
AssignmentTargetassignment_targets표준 대상 행입니다. target_type=CLASStarget_type=STUDENT는 모두 테넌트 RLS와 함께 target_id를 사용합니다
AssignmentSubmissionassignment_submissions(assignment_id, student_id)에 대해 unique이며, 객관식 답안 맵, exam_result_id, 점수/합격 필드, 피드백 metadata를 저장합니다

AssignmentResponse는 호환성용 classId와 새로운 targetClassIds / targetStudentIds 목록을 모두 노출합니다. 호환성 컬럼은 오래된 수업 중심 조회를 위해 남아 있지만, 학생 가시성 계약은 이제 assignment_targets에서 옵니다.

대상 타기팅과 가시성

  • 최소 한 개의 대상이 필요합니다. class와 student 대상 목록이 모두 비어 있으면 TARGET_REQUIRED로 실패합니다.
  • CreateAssignmentRequest.classId는 여전히 허용되며, 이후 targetClassIds에 병합됩니다.
  • AssignmentCommandService.legacyClassId(...)는 첫 번째 수업 대상을 선택하거나, 개별 타기팅된 학생의 첫 등록 class를 추론해 assignments.class_id를 채운 상태로 유지합니다.
  • GET /v1/assignments/me는 auth 사용자 ID에서 현재 학생을 해석하고, 직접 student 대상과 등록 수업 대상을 합친 뒤, 제출 결과를 계속 볼 수 있도록 종료된 행을 포함한 가시 과제를 반환합니다.
  • GET /v1/assignments/{id}/me는 비대상 과제를 ASSIGNMENT_NOT_VISIBLE 뒤로 숨깁니다. 종료된 과제는 여전히 읽을 수 있지만 canSubmitfalse가 됩니다.
  • GET /v1/assignments/{assignmentId}/submissions/status는 대상 학생을 모두 펼친 다음, 저장된 최신 제출물이 있으면 이를 덧씌웁니다. 저장된 행이 없는 학생은 PENDING으로 보고됩니다.

워크플로 모드와 채점 규칙

  • 생성과 수정에는 유효한 linkedExamId가 필요합니다.
  • 활성 과제는 submissionMode=EXAM_MANUALevaluationMode=EXAM_AUTO를 사용합니다. AssignmentCommandService.requireExamManualWorkflow(...)는 그 외 active 워크플로를 INVALID_STATUS로 거부합니다.
  • CreateAssignmentRequest.dueDate는 필수입니다. 공개 POST 요청에 이 값이 없으면 request-body validation에서 실패합니다. 내부 create/update 경로와 도메인 aggregate는 null 마감일을 DUE_DATE_REQUIRED로 거부합니다.
  • 마감이 지난 ACTIVE 과제는 CLOSED로 영속화됩니다. 스케줄러가 주기적으로 종료 처리하고, 단건 과제 상세 및 제출 경로도 과제 row를 읽기 전에 assignment-scoped close guard를 실행하므로 UI가 스케줄러 간격에 의존하지 않습니다.
  • 목록 응답도 dueDate로 effective status를 계산하므로, 스케줄러가 상태를 영속화하기 전에도 마감 초과 행은 목록에서 CLOSED로 표시됩니다.
  • 학생 및 staff 제출 payload는 문항 번호 문자열을 키로 하는 맵인 answers를 사용합니다.
  • 채점은 내부 시험 모듈을 통해 인라인으로 실행되며, 같은 요청 경로 안에 assignment_submissions 행과 exam_results 행을 모두 저장합니다.
  • 과제가 EXAM_AUTO를 사용할 때는 PATCH /submissions/{submissionId}로 하는 Staff 채점이 차단됩니다.
  • Staff 작성 UI는 시험 관리와 같은 시험지 생성 필드를 사용해 새 시험지를 만들고, 과제 row에는 과제 전용 마감일, 대상, 상태, 정답 공개 설정만 저장합니다.

소스 앵커: lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentCommandService.java#submitCurrentStudent

ExamService.ManualGradeResult result = examService.gradeManualAnswers(
TenantContextHolder.getRequiredTenant(),
assignment.getLinkedExamId(),
studentId,
request.answers());
submission.submitExam(request.answers(), result.examResultId(), result.totalScore(),
result.passed(), result.grade() != null ? String.valueOf(result.grade()) : null);

이 경로는 동기식이며 프로세스 내부에서 끝납니다. RabbitMQ event를 발행하거나, omr_grading_jobs를 생성하거나, OMR 이미지를 업로드하거나, lumie-workergrading-svc를 호출하지 않습니다.

결과 가시성 계약

활성 과제 계약은 점수, pass/fail, 문항별 유형과 정오를 항상 공개합니다. 등급은 과제에서 사용하지 않으므로 showGrade=false입니다. 학생 대상 결과에서 설정 가능한 유일한 토글은 showCorrectAnswers입니다.

AssignmentQueryService.studentSubmissionResponse(...)는 학생 대상 조회에서 점수와 pass/fail을 노출합니다. AssignmentCommandService.filteredQuestionResults(...)는 문항별 유형, 정오, 점수를 포함하고, showCorrectAnswers=true일 때만 correctAnswer를 포함합니다.

즉 다음이 성립합니다.

  • GET /v1/assignments/{id}/me는 현재 학생의 최신 과제 제출과 과제-scoped 문항 결과를 반환할 수 있습니다.
  • PUT /v1/assignments/{id}/me/submission은 저장된 제출물과 함께 같은 가시성 boolean을 반환하므로, 대시보드는 어떤 필드를 즉시 공개할지 결정할 수 있습니다.
  • 피드백은 이 플래그로 마스킹되지 않습니다. 값이 있으면 그대로 노출됩니다.

런타임 흐름

Staff 생성 또는 수정 흐름

  1. 요청에서 상태, 대상 ID, 마감일, 정답 공개 설정, nested 시험 payload를 파싱합니다.
  2. 각 수업 대상은 ClassService로, 각 student 대상은 StudentService로 검증합니다.
  3. ExamService를 통해 연결 시험을 생성하거나 검증합니다.
  4. 요청된 ACTIVE 상태가 이미 마감 시간을 지났다면 과제를 즉시 종료합니다.
  5. 호환성용 class_id를 포함한 시험 연계 assignments 행을 저장합니다.
  6. 과제의 기존 assignment_targets 행을 삭제하고 교체 집합을 삽입합니다.

학생 자기 제출 흐름

  1. 대상 과제가 마감이 지났고 아직 ACTIVE이면 CLOSED로 영속화합니다.
  2. 테넌트 slug와 auth 사용자 ID에서 현재 학생 ID를 해석합니다.
  3. ACTIVE가 아닌 과제는 ASSIGNMENT_CLOSED로 거부합니다.
  4. 비대상이거나 숨겨진 과제는 ASSIGNMENT_NOT_VISIBLE로 거부합니다.
  5. 기존 (assignment_id, student_id) 제출 행이 있으면 재사용하고, 없으면 생성합니다.
  6. ExamService.gradeManualAnswers(...)를 호출하고, answersexam_result_id를 저장하며, 과제-scoped questionResults를 구성합니다.
  7. 저장된 제출물과 활성 가시성 플래그를 담은 AssignmentSubmissionResultResponse를 반환합니다.

학생용 PUT /v1/assignments/{id}/me/submission 경로는 현재 학생의 첫 제출만 받습니다. (assignment_id, student_id) 행이 이미 있으면 같은 경로는 DUPLICATE_SUBMISSION을 발생시키며, 프론트엔드는 제출 form을 제거하고 채점 결과를 표시합니다.

대표 계약 예시

이 요청 형태는 CreateAssignmentRequest, AssignmentResponse, 그리고 lumie-frontend/src/features/assignment-management/create-assignment/ui/CreateAssignmentForm.tsx의 관리자 과제 폼과 일치합니다.

Staff가 EXAM_MANUAL 과제를 생성하는 경우

POST /v1/assignments

{
"classId": 12,
"status": "ACTIVE",
"dueDate": "2026-07-15T14:59:00Z",
"submissionMode": "EXAM_MANUAL",
"evaluationMode": "EXAM_AUTO",
"exam": {
"name": "중간고사",
"category": "GRADED",
"gradingType": "GRADED",
"gradeScale": "NINE_GRADE",
"totalQuestions": 2,
"correctAnswers": {
"1": "2",
"2": "4"
},
"questionScores": {
"1": 50,
"2": 50
},
"questionTypes": {
"1": "객관식",
"2": "객관식"
}
},
"targetClassIds": [12],
"targetStudentIds": [301],
"showScore": true,
"showPassFail": true,
"showGrade": false,
"showQuestionResults": true,
"showCorrectAnswers": false
}
{
"id": 88,
"classId": 12,
"dueDate": "2026-07-15T14:59:00Z",
"maxScore": null,
"attachments": null,
"status": "ACTIVE",
"submissionMode": "EXAM_MANUAL",
"evaluationMode": "EXAM_AUTO",
"linkedExamId": 44,
"targetClassIds": [12],
"targetStudentIds": [301],
"showScore": true,
"showPassFail": true,
"showGrade": false,
"showQuestionResults": true,
"showCorrectAnswers": false,
"createdAt": "2026-07-10T03:00:00Z",
"updatedAt": "2026-07-10T03:00:00Z"
}

학생이 EXAM_MANUAL 과제에 답을 제출하는 경우

이 응답 형태는 SubmitAssignmentRequest, AssignmentSubmissionResultResponse, 그리고 StudentAssignmentSubmissionResultSchema와 일치합니다.

PUT /v1/assignments/88/me/submission

{
"answers": {
"1": "2",
"2": "4"
}
}
{
"submission": {
"id": 410,
"assignmentId": 88,
"studentId": 301,
"content": null,
"attachments": null,
"answers": {
"1": "2",
"2": "4"
},
"examResultId": 902,
"score": 95,
"passed": true,
"gradeValue": null,
"feedback": null,
"submittedAt": "2026-07-10T03:12:00Z",
"gradedAt": "2026-07-10T03:12:00Z",
"status": "GRADED",
"createdAt": "2026-07-10T03:12:00Z",
"updatedAt": "2026-07-10T03:12:00Z"
},
"showScore": true,
"showPassFail": true,
"showGrade": false,
"showQuestionResults": true,
"showCorrectAnswers": false,
"questionResults": [
{
"questionNumber": 1,
"selectedChoice": "2",
"correct": true,
"questionType": "객관식",
"score": 50,
"correctAnswer": null
}
]
}

showCorrectAnswers=false이면 questionResults[].correctAnswernull입니다. true이면 같은 과제-scoped 결과 목록에 정답 키가 포함됩니다.

의존성과 경계

의존성존재 이유
ClassService수업 대상을 검증하고, 등록 class를 조회하며, 호환성용 class_id를 도출
StudentService대상 student를 검증하고 auth 사용자 ID에서 현재 학생을 해석
ExamService연결된 시험을 검증하고, 학생 상세 라우트에 시험 메타데이터를 노출하며, 수동 답안을 인라인으로 채점

assignment 모듈은 여전히 모듈러 모놀리스 내부의 백엔드 모듈입니다. 이 모듈은 워커 messaging, 외부 OMR HTTP 호출, 백그라운드 채점 job을 소유하지 않습니다.

실패 모드

  • 대상 ID가 유효하지 않으면 CLASS_NOT_FOUND 또는 STUDENT_NOT_FOUND
  • 정규화 후 수업 또는 student 대상이 하나도 남지 않으면 TARGET_REQUIRED
  • EXAM_MANUAL 생성 또는 수정 시 유효한 연결 시험이 없으면 EXAM_NOT_FOUND
  • 학생이 비대상 과제를 읽거나 제출하면 ASSIGNMENT_NOT_VISIBLE
  • 학생이 ACTIVE가 아닌 과제에 제출하면 ASSIGNMENT_CLOSED
  • 필요한 답안이 없거나 staff가 exam-auto 과제를 수동 채점하려 하면 INVALID_SUBMISSION
  • DUPLICATE_SUBMISSION은 staff 또는 student 제출 경로가 같은 (assignment_id, student_id)에 대해 두 번째 제출을 받으면 발생
  • 내부 create/update 경로나 도메인 메서드가 null 마감일을 저장하려 하면 DUE_DATE_REQUIRED. 공개 POST 요청에서 dueDate가 없으면 command 처리 전에 request validation으로 실패하고, PATCH 요청에서 dueDate를 생략하면 기존 마감일을 유지합니다.
  • 요청 enum이 과제, 제출, 모드, 평가 enum에 매핑되지 않으면 INVALID_STATUS
  • JSONB 첨부나 답안 변환에 실패하면 JSON_SERIALIZATION_FAILED

관측성과 재시도 동작

  • AssignmentCommandService는 과제 생성, 수정, 삭제, 제출 생성, 제출 갱신 이벤트를 로그로 남깁니다.
  • AssignmentLifecycleService는 마감 과제 종료 이벤트를 로그로 남기고, AssignmentDeadlineScheduler는 스케줄 실행으로 row가 변경되면 테넌트 slug와 종료한 과제 수를 로그로 남깁니다.
  • 학생용 PUT /me/submission 흐름에는 queue, 워커 handoff, broker retry가 없습니다.
  • EXAM_MANUAL 채점은 요청 트랜잭션 경계 안에 포함됩니다. 이 표면에서의 유일한 재시도 메커니즘은 클라이언트 측 HTTP 재시도입니다.
  • 학생 라우트는 성공한 제출 이후 retry-idempotent하지 않습니다. 같은 과제와 학생에 대한 두 번째 제출은 채점이나 저장 전에 거부됩니다.

검증

./gradlew :modules:assignment:test
./gradlew :modules:exam:test --tests '*ResultCommandService*'
rg -n "assignment_targets|submission_mode|evaluation_mode|linked_exam_id|show_score|exam_result_id|due_date" \
lumie-backend/app/src/main/resources/db/migration/public/V81__assignment_redesign_expand.sql \
lumie-backend/app/src/main/resources/db/migration/public/V82__assignment_redesign_tenant_safe_indexes.sql \
lumie-backend/app/src/main/resources/db/migration/public/V85__assignment_exam_only_active_guard.sql \
lumie-backend/app/src/main/resources/db/migration/public/V89__require_assignment_due_date.sql \
lumie-backend/app/src/main/resources/db/migration/public/V90__assignment_overdue_close_index.sql
cd lumie-document/docusaurus && npm run build

예상 성공 신호:

  • Gradle이 BUILD SUCCESSFUL로 끝납니다.
  • assignment 모듈 테스트와 ResultCommandService 테스트가 실패 없이 통과합니다.
  • rgV81의 리디자인 컬럼과 테이블, V82의 테넌트-safe FK/인덱스 이름을 찾습니다.
  • Docusaurus가 backend/assignment-svc에 대해 MDX 또는 broken-link 오류 없이 완료됩니다.

관련 페이지