학생 서비스
이 페이지는 lumie-backend/modules/student를 다룹니다.
책임
student 모듈은 다음을 소유합니다.
- auth 사용자에 연결된 테넌트 범위 student 레코드
- activate, deactivate, delete, password reset, 로그인 ID change 같은 학생 수명주기 작업
- XLSX/CSV 대량 import와 CSV 내보내기
- 학생 검색과 통계
- 모듈 간 후속 작업을 트리거하는
StudentRegisteredEvent
소스 경로
| 경로 | 역할 |
|---|---|
lumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/web/StudentController.java | 공개 student HTTP API |
lumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/internal/StudentServiceAdapter.java | internal monolith API 구현 |
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java | 수명주기, 삭제, 대량 가져오기, event 발행 |
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentQueryService.java | 조회, 내보내기, enrollment trend, dropout summary |
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.java | Apache POI 기반 XLSX parser와 다중 인코딩 CSV parser |
lumie-backend/modules/student/src/test/java/com/lumie/student/application/service/StudentExcelParserTest.java | UTF-8, x-windows-949, CSV edge case parser coverage |
lumie-backend/modules/student/src/main/java/com/lumie/student/domain/entity/Student.java | student aggregate |
lumie-backend/libs/internal-api/src/main/java/com/lumie/student/api/StudentService.java | internal 조회 및 검증 계약 |
lumie-backend/libs/internal-api/src/main/java/com/lumie/student/api/StudentRegisteredEvent.java | after-커밋 event 계약 |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | 기준선 students table |
lumie-backend/app/src/main/resources/db/migration/public/V91__normalize_student_parent_phone_and_drop_unique_index.sql | 학부모 연락처 빈 값 정규화 및 보호자-number uniqueness 완화 |
공개 인터페이스
| 인터페이스 | 진입점 |
|---|---|
| Student CRUD | POST /v1/students, GET /v1/students, GET /v1/students/{id}, PATCH /v1/students/{id}, DELETE /v1/students/{id} |
| Lifecycle and credentials | POST /v1/students/{id}/deactivate, POST /v1/students/{id}/reactivate, POST /v1/students/{id}/reset-password, PATCH /v1/students/{id}/login-id |
| Batch 수명주기 | POST /v1/students/batch/deactivate, POST /v1/students/batch/reactivate, POST /v1/students/batch/delete |
| Bulk 가져오기 and 내보내기 | POST /v1/students/bulk, GET /v1/students/export.csv |
| Statistics | GET /v1/students/statistics/enrollment-trend, GET /v1/students/statistics/dropout-summary |
학생 목록 필터
GET /v1/students는 기존 isActive, search, 페이지네이션, 정렬 파라미터와
함께 다음 선택 필터를 받습니다.
| 파라미터 | 의미 |
|---|---|
classId | 해당 클래스에 활성 수강 등록이 있는 학생을 반환합니다. hasActiveEnrollment와 함께 전달되면 이 값이 우선합니다. |
hasActiveEnrollment=true | 활성 클래스 수강 등록이 하나 이상 있는 학생을 반환합니다. |
hasActiveEnrollment=false | 활성 클래스 수강 등록이 없는 학생을 반환하며, 미배정 보기에 사용합니다. |
student 조회 서비스는 class 모듈의 internal API로 수강 ID를 얻으며, student 모듈은 class 도메인 entity를 직접 import하지 않습니다.
내부 인터페이스
StudentService는 다음을 내보냅니다.
- student ID, phone, auth 사용자 ID 기준 조회
- 테넌트 내부 student 검증
- keyword 기반 join을 위한 user ID 검색
- 다른 모듈이 쓰는 batch 조회 helper
현재 가장 중요한 소비자는 exam 모듈입니다. exam 모듈은 student phone 조회와
StudentRegisteredEvent backfill 경로를 사용합니다.
집계와 데이터 구조
| 집계 | 테이블 | 참고 |
|---|---|---|
Student | students | user 연결, 로그인 ID mirror, 연락처, 학교, 출생 연도, 메모, active flag를 저장 |
Student의 중요한 불변식:
- phone과 parent phone은 write 시 숫자만 남도록 정규화되며, 빈
parent_phone값은NULL로 저장됩니다. user_login_id는 빠른 조회를 위해 auth 측 로그인 ID를 복제합니다.- delete는 auth 사용자를 삭제하는 방식으로 구현되며, DB cascade가 student row를 제거합니다.
런타임 흐름
등록 및 백필 흐름
이벤트 발행 지점의 형태
소스 앵커:
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java는
StudentCommandService.registerStudent에서 event를 발행합니다.
eventPublisher.publishEvent(
new StudentRegisteredEvent(
saved.getId(),
saved.getPhone(),
tenantSlug,
TenantContextHolder.getRequiredTenantId()
)
);
이 event는 student row가 저장된 뒤 발행되며, downstream listener는 커밋
후에 이를 소비합니다. event는 tenantSlug와 tenantId를 모두 담으므로
재생된 listener도 전체 RLS tenant context를 복원할 수 있습니다.
대량 import도 성공적으로 저장된 student row마다 같은 event를 한 번씩 발행합니다.
핵심 동작
- 학생 등록과 재활성화는 모두
MetricType.STUDENTS를 통해 quota check를 수행합니다. - 학생
phone은 값이 있을 때 unique하게 유지되며, 등록, 수정, 대량 import에서 숫자만 남긴 값이 11자리 모바일 번호여야 합니다.parent_phone은 선택 입력값이며 unique하지 않고 더 넓은 shared phone parser를 계속 사용하므로, 한 학부모 연락처가 여러 student row에 연결될 수 있습니다. - 단건 삭제는 student가 비활성 상태여야 하며,
ClassService.dropActiveEnrollmentsForStudent(...)로 활성 enrollment를 제거한 뒤 auth 사용자를 삭제합니다. - 대량 import는 두 단계로 진행됩니다.
- pass 1은 모든 row를 검증하고 모든 행 오류를 수집합니다.
- pass 2는 유효한 row에 대해서만 auth 사용자와 student row를 생성합니다.
- 가져오기 parser는
.xlsx업로드와 UTF-8,x-windows-949,EUC-KR순서로 decode하는.csv업로드를 받습니다.StudentExcelParser.parseCsvRecords(...)는 decode된 header가학생 이름,학생 연락처와 일치할 때까지 이 charset들을 시도하며, 닫히지 않은 CSV quote field, 1,000개를 초과하는 data 행, 32개를 초과하는 CSV column은 거부합니다. - 대량 import는 free-text column에서 spreadsheet formula로 해석될 수 있는 값을 거부합니다. CSV export도 저장된 값이 formula처럼 시작하면 cell 앞에 apostrophe를 붙여 내보냅니다.
POST /v1/students/bulk는Idempotency-Key를 요구합니다.StudentController는 byte size와 SHA-256 content hash로 upload fingerprint를 만든 뒤IdempotencyService.executeOnce(...)에 위임합니다.- 대량 import는 컨트롤러 boundary에서 최소
INSTRUCTOR권한을 요구합니다. - dropout summary는 전용
deactivated_atfield가 없기 때문에 비활성 row의updated_at를 기반으로 한 근사치입니다.
대표 계약 예시
이 대표 예시는 StudentExcelParser, StudentExcelParserTest,
StudentController, BulkImportResult에 근거합니다.
템플릿 행 형태
첫 번째 worksheet 또는 CSV 파일은 다음 column으로 시작해야 합니다.
학생 이름,학생 연락처,학교명,출생 연도,학부모 연락처,메모
김철수,01012340001,한빛고,2008,01012340002,재원
StudentExcelParserTest는 header가 1행을 차지하므로 첫 데이터 행이
rowNumber = 2로 보고된다는 점을 확인합니다. CSV 업로드도 같은 행 번호를
사용합니다.
소스 앵커:
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.javalumie-backend/modules/student/src/test/java/com/lumie/student/application/service/StudentExcelParserTest.java