출석 서비스
이 페이지는 lumie-backend/modules/attendance를 다룹니다.
책임
attendance 모듈은 다음을 소유합니다.
- 날짜별 또는 class별 attendance 세션
- 학생별 attendance record
- 코드 기반 학생 체크인
- 수업, student, dashboard 출결 통계
- 출결 이력 CSV 내보내기
수업 모듈과 student 모듈과 달리, attendance는 현재 internal
libs/internal-api 계약을 노출하지 않습니다. 다른 모듈은 attendance 측
service API가 아니라 수업 조회와 공유 테넌트 범위 table을 통해 통합합니다.
소스 경로
| 경로 | 역할 |
|---|---|
lumie-backend/modules/attendance/src/main/java/com/lumie/attendance/adapter/in/web | 공개 HTTP 컨트롤러 |
lumie-backend/modules/attendance/src/main/java/com/lumie/attendance/application/service | 세션, record, statistics, 내보내기 service |
lumie-backend/modules/attendance/src/main/java/com/lumie/attendance/domain/entity | AttendanceSession, AttendanceRecord, StudentReadModel |
lumie-backend/modules/attendance/src/main/java/com/lumie/attendance/domain/repository | 세션, record, student read repository |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | 기준선 attendance_sessions, attendance_records table |
lumie-backend/app/src/main/resources/db/migration/public/V20__rename_qna_is_it_answered.sql | attendance table에 optimistic-lock version column 추가 |
공개 인터페이스
| 인터페이스 | 진입점 |
|---|---|
| Session CRUD | POST /v1/attendance/sessions, GET /v1/attendance/sessions, GET /v1/attendance/sessions/{id}, POST /v1/attendance/sessions/{id}/regenerate-code, DELETE /v1/attendance/sessions/{id} |
| Session records | GET /v1/attendance/sessions/{sessionId}/records, PATCH /v1/attendance/sessions/{sessionId}/records/{recordId}, POST /v1/attendance/sessions/{sessionId}/records/bulk-update |
| Class-oriented helpers | POST /v1/attendance/classes/{classId}/sessions/ensure, GET /v1/attendance/classes/{classId}/statistics, GET /v1/attendance/classes/{classId}/students |
| Student self check-in | POST /v1/attendance/check-in |
| Student history | GET /v1/attendance/students/{studentId}/records, GET /v1/attendance/students/{studentId}/statistics |
| 대시보드와 내보내기 | GET /v1/attendance/statistics/dashboard, GET /v1/attendance/records/export.csv |
집계와 읽기 모델
| 유형 | 테이블 | 참고 |
|---|---|---|
AttendanceSession | attendance_sessions | 세션 date, 선택적 수업 연결, attendance code, 지각 기준, record collection 저장 |
AttendanceRecord | attendance_records | (session_id, student_id) unique |
StudentReadModel | students | 변경 불가능한 읽기 전용 student projection |
StudentReadModel은 의도적인 cross-module read model입니다. attendance
모듈은 자체 student cache를 두는 대신, 불변에 가까운 이름과 user 연결을 위해
students table을 직접 읽습니다.
런타임 흐름
반 세션 보장 흐름
학생 체크인 흐름
핵심 동작
ensureSessionForClass(...)는(classId, KST 기준 오늘)에 대해 멱등이며, 이미 존재하면 기존 세션을 반환합니다.- class용 세션 생성은
ClassService.getEnrolledStudentIds(...)를 이용해 현재 등록 학생 수만큼 record를 미리 만듭니다. - class가 없는 단독 세션 생성은
StudentReadRepository.findByIsActiveTrue()의 모든 활성 학생을 사용합니다. - check-in은 payload의 student ID가 아니라 인증된
userId로 학생을 해석합니다. - dashboard 통계는 미결정 row가 출석률을 희석하지 않도록
PENDINGrecord를 제외합니다. - CSV export는 세션을 페이지 단위로 읽고, 각 페이지별로 record와 student 이름을 메모리에서 조합합니다.
대표 계약 예시
이 예시는 ClassAttendanceController, StudentCheckInController,
AttendanceSessionResponse, CheckInResponse,
AttendanceSessionCommandService, AttendanceRecordCommandService, 관련
컨트롤러/service test와 일치합니다.
오늘의 반 세션 보장
POST /v1/attendance/classes/9/sessions/ensure
{
"id": 77,
"name": "수학반 A",
"sessionDate": "2026-06-14",
"classId": 9,
"className": "수학반 A",
"attendanceCode": "654321",
"codeExpiresAt": null,
"lateThresholdMinutes": 10,
"createdAt": "2026-06-14T00:00:00Z",
"updatedAt": "2026-06-14T00:00:00Z",
"presentCount": 0,
"absentCount": 0,
"lateCount": 0,
"excusedCount": 0,
"totalStudents": 0
}
놓치기 쉬운 세부사항 두 가지:
codeExpiresAt은 응답 계약에는 있지만,AttendanceSession.create(...)와regenerateCode()가 값을 넣지 않기 때문에null로 남습니다.ensureSessionForClass(...)는fromWithoutRecords(...)를 반환하므로, enrolled student용 record가 생성되었어도 응답 카운트는 0으로 보입니다.
학생 셀프 체크인
POST /v1/attendance/check-in
{
"code": "654321"
}
{
"message": "지각으로 처리되었습니다.",
"status": "LATE"
}
CheckInRequest에는 6자리 code만 들어 있습니다. service는 인증된 user
context에서 student를 해석하고, 만료되었거나 알 수 없는 code는
CODE_EXPIRED 또는 INVALID_CODE로 거부합니다.
의존성과 경계
| 의존성 | 존재 이유 |
|---|---|
ClassService | 수업-bound 세션의 수업 이름과 등록 student ID 해석 |
students read model | check-in과 CSV export에 필요한 student 이름과 인증 user 연결 해석 |
attendance 모듈은 class_id, class_name, student_id를 저장하지만,
class와 student 도메인의 기준은 여전히 해당 모듈들입니다.
실패 모드
- 수업 세션 생성 또는 ensure는 수업 모듈이 대상 class를 해석하지 못하면
CLASS_NOT_FOUND로 실패합니다. - check-in은
STUDENT_NOT_FOUND,INVALID_CODE,CODE_EXPIRED,RECORD_NOT_FOUND로 실패합니다. - 세션 및 record 수정은
SESSION_NOT_FOUND또는RECORD_NOT_FOUND로 실패합니다. - record uniqueness는 table 수준의
(session_id, student_id)로 강제됩니다.
알려진 계약 드리프트
AttendanceSession은 codeExpiresAt을 노출하고,
AttendanceRecordCommandService.checkInByCode(...)는 만료 code를 거부하며,
AttendanceSessionResponse에도 그 field가 포함됩니다. 하지만 현재 create와
regenerate 경로는 codeExpiresAt을 할당하지 않으므로, 외부 프로세스가
column을 쓰지 않는 한 새 code는 만료되지 않습니다. 이 문서는 의도된 정책이
아니라 현재 코드 그 대로를 반영합니다.
관측성
- 세션 생성, ensure, 재생성, check-in, bulk update, export는 모두 애플리케이션 서비스를 통해 로그됩니다.
- 이 모듈에는 queue, retry loop, background sweeper가 없습니다.
검증
./gradlew :modules:attendance:test
./gradlew :app:test --tests '*Attendance*'
cd lumie-document/docusaurus && npm run build
예상 성공 신호:
- Gradle이
BUILD SUCCESSFUL로 끝납니다. AttendanceSessionCommandServiceTest,AttendanceRecordCommandServiceTest,StudentCheckInControllerTest가 실패 없이 통과합니다.- Docusaurus가
backend/attendance-svc에 대해 MDX 또는 broken-link 오류 없이 완료됩니다.