Skip to main content

Assignment Service

This page covers lumie-backend/modules/assignment.

Responsibility

The assignment module owns:

  • tenant-scoped assignments with explicit class and student targets;
  • staff authoring workflows under /v1/assignments;
  • student dashboard reads and self-submission APIs under /v1/assignments/me;
  • exam-backed manual-answer submission storage for active assignments;
  • the assignment deadline, active/closed state, and correct-answer visibility toggle;
  • the internal lookup API used by other backend modules.

Source Paths

PathRole
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/web/AssignmentController.javaStaff and student assignment HTTP routes
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/web/AssignmentSubmissionController.javaStaff submission management routes
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentCommandService.javaAssignment creation, target replacement, self-submission, and grading rules
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentQueryService.javaStaff queries, student visibility queries, and student-facing submission/result reads
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/application/service/AssignmentLifecycleService.javaPersists overdue ACTIVE assignments as CLOSED
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/adapter/in/scheduling/AssignmentDeadlineScheduler.javaTenant-aware scheduled overdue-assignment close job protected by ShedLock
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/Assignment.javaAssignment aggregate, workflow flags, visibility flags, and compatibility class_id column
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/AssignmentTarget.javaCanonical audience rows for CLASS and STUDENT targeting
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/entity/AssignmentSubmission.javaSubmission aggregate for answer maps and exam-result linkage
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/repository/AssignmentRepository.javaAssignment list queries and bulk overdue close update
lumie-backend/modules/assignment/src/main/java/com/lumie/assignment/domain/repository/AssignmentTargetRepository.javaVisibility lookups and target replacement persistence
lumie-backend/libs/internal-api/src/main/java/com/lumie/assignment/api/AssignmentService.javaInternal monolith lookup contract
lumie-backend/libs/internal-api/src/main/java/com/lumie/exam/api/ExamService.javaInternal exam metadata and manual-grading dependency
lumie-backend/modules/exam/src/main/java/com/lumie/exam/application/service/ResultCommandService.javaInline manual-answer grading path consumed by EXAM_MANUAL assignments
lumie-backend/app/src/main/resources/db/migration/public/V81__assignment_redesign_expand.sqlAdds assignment workflow columns, assignment_targets, and exam-linked submission fields
lumie-backend/app/src/main/resources/db/migration/public/V82__assignment_redesign_tenant_safe_indexes.sqlAdds tenant-safe indexes and composite foreign keys for the redesign
lumie-backend/app/src/main/resources/db/migration/public/V83__drop_assignment_title_description.sqlDrops legacy assignment title and description columns
lumie-backend/app/src/main/resources/db/migration/public/V84__drop_assignment_type_and_draft.sqlDrops assignment type and removes the DRAFT status from active workflow
lumie-backend/app/src/main/resources/db/migration/public/V85__assignment_exam_only_active_guard.sqlCloses non-exam legacy rows and enforces active assignments as exam-backed
lumie-backend/app/src/main/resources/db/migration/public/V86__assignment_exam_fk_restrict.sqlChanges linked-exam deletion behavior to restrict active assignment breakage
lumie-backend/app/src/main/resources/db/migration/public/V89__require_assignment_due_date.sqlBackfills legacy null deadlines and requires assignments.due_date
lumie-backend/app/src/main/resources/db/migration/public/V90__assignment_overdue_close_index.sqlAdds the partial index used by overdue assignment close queries

Public Surface

Consumer surfaceEntrypoints
Staff authoringPOST /v1/assignments, GET /v1/assignments, GET /v1/assignments/{id}, PATCH /v1/assignments/{id}, DELETE /v1/assignments/{id}, GET /v1/assignments/class/{classId}
Student dashboardGET /v1/assignments/me, GET /v1/assignments/{id}/me, PUT /v1/assignments/{id}/me/submission
Staff submission managementGET /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}
Assignment statisticsGET /v1/assignments/statistics/dashboard

Internal Surface

AssignmentService still exports only lookup methods:

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

The redesign did not turn assignment grading into a separate service boundary. EXAM_MANUAL grading stays inside the modular monolith through the internal ExamService.

Aggregates And Tables

AggregateTableNotes
AssignmentassignmentsStores exam-backed workflow mode, required deadline, status, correct-answer visibility, required linked_exam_id for active rows, and a compatibility class_id alongside the new target rows
AssignmentTargetassignment_targetsCanonical audience rows. target_type=CLASS and target_type=STUDENT both use target_id plus tenant RLS
AssignmentSubmissionassignment_submissionsUnique on (assignment_id, student_id) and stores objective-answer maps, exam_result_id, score/pass fields, and feedback metadata

AssignmentResponse exposes both the compatibility classId and the new targetClassIds / targetStudentIds lists. The compatibility column remains for older class-centric queries, but the student visibility contract now comes from assignment_targets.

Audience Targeting And Visibility

  • At least one target is required. Empty class and student target lists fail with TARGET_REQUIRED.
  • CreateAssignmentRequest.classId is still accepted, then merged into targetClassIds.
  • AssignmentCommandService.legacyClassId(...) keeps assignments.class_id populated by choosing the first class target, or by deriving the first enrolled class for individually targeted students.
  • GET /v1/assignments/me resolves the current student from auth user ID, unions direct-student targets with enrolled-class targets, and returns visible assignments, including closed rows so submitted results remain accessible.
  • GET /v1/assignments/{id}/me hides untargeted assignments behind ASSIGNMENT_NOT_VISIBLE. Closed assignments can still be read, but canSubmit becomes false.
  • GET /v1/assignments/{assignmentId}/submissions/status expands every targeted student, then overlays the latest stored submission if one exists. Students without a stored row are reported as PENDING.

Workflow Mode And Grading Rules

  • Create and update require a valid linkedExamId.
  • Active assignments use submissionMode=EXAM_MANUAL and evaluationMode=EXAM_AUTO. AssignmentCommandService.requireExamManualWorkflow(...) rejects any other active workflow with INVALID_STATUS.
  • CreateAssignmentRequest.dueDate is required. Public POST requests without it fail request-body validation. Internal create/update paths and the domain aggregate reject null deadlines with DUE_DATE_REQUIRED.
  • Overdue ACTIVE assignments are persisted as CLOSED. The scheduler closes them periodically, and single-assignment detail plus submission paths run an assignment-scoped close guard before loading the row so the UI is not dependent on the scheduler interval.
  • List responses also compute the effective status from dueDate, so overdue rows appear as CLOSED in lists even before the scheduler persists the status.
  • Student and staff submission payloads use answers, a map keyed by question number as a string.
  • Grading runs inline through the internal exam module and stores both an assignment_submissions row and an exam_results row in the same request path.
  • Staff grading by PATCH /submissions/{submissionId} is blocked when the assignment uses EXAM_AUTO.
  • The staff authoring UI creates a new exam sheet using the same exam-builder fields as exam management, then stores only assignment-specific deadline, target, status, and correct-answer visibility data on the assignment row.

Source anchor: 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);

That path is synchronous and in-process. It does not publish RabbitMQ events, create omr_grading_jobs, upload OMR images, or call lumie-worker's grading-svc.

Result Visibility Contract

The active assignment contract always exposes score, pass/fail, per-question type, and per-question correctness. Grade is not used by assignments, so showGrade=false. The only student-facing result toggle is showCorrectAnswers.

AssignmentQueryService.studentSubmissionResponse(...) keeps score and pass/fail visible on student-facing reads. AssignmentCommandService.filteredQuestionResults(...) includes per-item type, correctness, and score, and only includes correctAnswer when showCorrectAnswers=true.

This means:

  • GET /v1/assignments/{id}/me returns the latest assignment-scoped submission and question-result view for the current student when one exists.
  • PUT /v1/assignments/{id}/me/submission returns the same visibility booleans alongside the saved submission so the dashboard can decide which fields to reveal immediately.
  • Feedback is not masked by these flags. It stays visible when present.

Runtime Flows

Staff create or update flow

  1. Parse status, target IDs, deadline, correct-answer visibility, and the nested exam payload from the request.
  2. Validate each class target through ClassService and each student target through StudentService.
  3. Create or validate the linked exam through ExamService.
  4. Close the assignment immediately if the requested ACTIVE status is already past its deadline.
  5. Save the exam-backed assignments row, including the compatibility class_id.
  6. Delete existing assignment_targets rows for the assignment and insert the replacement set.

Student self-submission flow

  1. Persist the target assignment as CLOSED when it is overdue and still ACTIVE.
  2. Resolve the current student ID from tenant slug plus auth user ID.
  3. Reject non-ACTIVE assignments with ASSIGNMENT_CLOSED.
  4. Reject untargeted or hidden assignments with ASSIGNMENT_NOT_VISIBLE.
  5. Reuse the existing (assignment_id, student_id) submission row when present; otherwise create it.
  6. Call ExamService.gradeManualAnswers(...), persist answers plus exam_result_id, and build assignment-scoped questionResults.
  7. Return AssignmentSubmissionResultResponse with the saved submission and the active visibility flags.

The student PUT /v1/assignments/{id}/me/submission path accepts only the first submission for the current student. Once an (assignment_id, student_id) row exists, the same route raises DUPLICATE_SUBMISSION; the frontend removes the submission form and shows the graded result.

Representative Contract Examples

This request shape matches CreateAssignmentRequest, AssignmentResponse, and the admin assignment form in lumie-frontend/src/features/assignment-management/create-assignment/ui/CreateAssignmentForm.tsx.

Staff creates an EXAM_MANUAL assignment

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"
}

Student submits answers for an EXAM_MANUAL assignment

This response shape matches SubmitAssignmentRequest, AssignmentSubmissionResultResponse, and 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
}
]
}

When showCorrectAnswers=false, questionResults[].correctAnswer is null. When it is true, the same assignment-scoped result list includes the answer key.

Dependencies And Boundaries

DependencyWhy it exists
ClassServiceValidate class targets, list enrolled classes, and derive a compatibility class_id
StudentServiceValidate targeted students and resolve the current student from auth user ID
ExamServiceValidate linked exams, expose exam metadata to the student detail route, and grade manual answers inline

The assignment module remains a backend module inside the modular monolith. It does not own worker messaging, external OMR HTTP calls, or background grading jobs.

Failure Modes

  • CLASS_NOT_FOUND or STUDENT_NOT_FOUND when any target ID is invalid.
  • TARGET_REQUIRED when no class or student target survives normalization.
  • EXAM_NOT_FOUND when EXAM_MANUAL is created or updated without a valid linked exam.
  • ASSIGNMENT_NOT_VISIBLE when a student reads or submits an untargeted assignment.
  • ASSIGNMENT_CLOSED when a student submits to a non-ACTIVE assignment.
  • INVALID_SUBMISSION when required answers are missing or when staff attempts manual grading for an exam-auto assignment.
  • DUPLICATE_SUBMISSION when a staff or student submission path receives a second submission for the same (assignment_id, student_id).
  • DUE_DATE_REQUIRED when an internal create/update path or domain method tries to persist an assignment with a null deadline. Public POST requests without dueDate fail request validation before command handling, and PATCH requests that omit dueDate preserve the existing deadline.
  • INVALID_STATUS when request enums do not map to assignment, submission, mode, or evaluation enums.
  • JSON_SERIALIZATION_FAILED when JSONB attachment or answer conversion fails.

Observability And Retry Behavior

  • AssignmentCommandService logs assignment create, update, delete, submission create, and submission update events.
  • AssignmentLifecycleService logs overdue close events, and AssignmentDeadlineScheduler logs the tenant slug plus number of rows changed when a scheduled pass closes overdue assignments.
  • The student PUT /me/submission flow has no queue, worker handoff, or broker retry.
  • EXAM_MANUAL grading is part of the request transaction boundary. Client-side HTTP retry is the only retry mechanism on this surface.
  • The student route is not retry-idempotent after a successful submission. A second submit for the same assignment and student is rejected before grading or saving.

Verification

./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

Expected success signals:

  • Gradle finishes with BUILD SUCCESSFUL.
  • Assignment module tests and ResultCommandService tests pass without failures.
  • rg hits the redesign columns and tables in V81 and the tenant-safe FK/index names in V82.
  • Docusaurus finishes without MDX or broken-link errors for backend/assignment-svc.