본문으로 건너뛰기

학생 서비스

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

책임

student 모듈은 다음을 소유합니다.

  • auth user에 연결된 tenant 범위 student 레코드
  • activate, deactivate, delete, password reset, login ID change 같은 학생 lifecycle 작업
  • Excel 대량 import와 CSV export
  • 학생 검색과 통계
  • 모듈 간 후속 작업을 트리거하는 StudentRegisteredEvent

소스 경로

Path역할
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.javainternal monolith API 구현
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.javalifecycle, 삭제, 대량 import, event 발행
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentQueryService.java조회, export, enrollment trend, dropout summary
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.javaApache POI 기반 Excel parser
lumie-backend/modules/student/src/main/java/com/lumie/student/domain/entity/Student.javastudent aggregate
lumie-backend/libs/internal-api/src/main/java/com/lumie/student/api/StudentService.javainternal 조회 및 검증 계약
lumie-backend/libs/internal-api/src/main/java/com/lumie/student/api/StudentRegisteredEvent.javaafter-commit event 계약
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sqlbaseline students table

공개 인터페이스

SurfaceEntrypoints
Student CRUDPOST /v1/students, GET /v1/students, GET /v1/students/{id}, PATCH /v1/students/{id}, DELETE /v1/students/{id}
Lifecycle and credentialsPOST /v1/students/{id}/deactivate, POST /v1/students/{id}/reactivate, POST /v1/students/{id}/reset-password, PATCH /v1/students/{id}/login-id
Batch lifecyclePOST /v1/students/batch/deactivate, POST /v1/students/batch/reactivate, POST /v1/students/batch/delete
Bulk import and exportPOST /v1/students/bulk, GET /v1/students/export.csv
StatisticsGET /v1/students/statistics/enrollment-trend, GET /v1/students/statistics/dropout-summary

내부 인터페이스

StudentService는 다음을 export합니다.

  • student ID, phone, auth user ID 기준 조회
  • tenant 내부 student 검증
  • keyword 기반 join을 위한 user ID 검색
  • 다른 모듈이 쓰는 batch lookup helper

현재 가장 중요한 소비자는 exam 모듈입니다. exam 모듈은 student phone 조회와 StudentRegisteredEvent backfill 경로를 사용합니다.

집계와 데이터 구조

AggregateTable참고
Studentstudentsuser 연결, login ID mirror, 연락처, 학교, 출생 연도, 메모, active flag를 저장

Student의 중요한 불변식:

  • phone과 parent phone은 write 시 숫자만 남도록 정규화됩니다.
  • user_login_id는 빠른 조회를 위해 auth 측 login identity를 mirror합니다.
  • delete는 auth user를 삭제하는 방식으로 구현되며, DB cascade가 student row를 제거합니다.

런타임 흐름

등록 및 백필 흐름

이벤트 발행 지점의 형태

eventPublisher.publishEvent(
new StudentRegisteredEvent(saved.getId(), saved.getPhone(), tenantSlug)
);

이 event는 student row가 저장된 뒤 발행되며, downstream listener는 commit 후에 이를 소비합니다.

핵심 동작

  • 학생 등록과 재활성화는 모두 MetricType.STUDENTS를 통해 quota check를 수행합니다.
  • 전화번호 uniqueness는 phoneparent_phone 전반에서, 교차 필드 충돌을 포함해 강제됩니다.
  • 단건 삭제는 student가 비활성 상태여야 하며, ClassService.dropActiveEnrollmentsForStudent(...)로 활성 enrollment를 제거한 뒤 auth user를 삭제합니다.
  • 대량 import는 두 단계로 진행됩니다.
  • pass 1은 모든 row를 검증하고 모든 row 오류를 수집합니다.
  • pass 2는 유효한 row에 대해서만 auth user와 student row를 생성합니다.
  • Excel parser는 첫 두 header가 한국어 템플릿 라벨 학생 이름, 학생 연락처이기를 기대합니다.
  • dropout summary는 전용 deactivated_at field가 없기 때문에 비활성 row의 updated_at를 기반으로 한 근사치입니다.

대표 계약 예시

이 예시는 StudentExcelParser의 header 계약, StudentController의 inline POST /v1/students/bulk 동작, BulkImportResult의 partial-result 형태와 일치합니다.

템플릿 행 형태

첫 번째 worksheet는 다음 column으로 시작해야 합니다.

학생 이름,학생 연락처,학교명,출생 연도,학부모 연락처,메모
김철수,01012340001,한빛고,2008,01012340002,재원

StudentExcelParserTest는 header가 1행을 차지하므로 첫 데이터 행이 rowNumber = 2로 보고된다는 점을 확인합니다.

부분 성공 대량 import 응답

POST /v1/students/bulk는 일부 row가 실패해도 200 OK를 반환합니다. 이 import는 async job 모델로 전환하지 않고 inline으로 실행되며, row 수준 오류를 보고하기 때문입니다.

{
"totalRows": 2,
"successCount": 1,
"failureCount": 1,
"errors": [
{
"rowNumber": 3,
"column": "phone",
"value": "010-12",
"code": "STUDENT_502",
"message": "전화번호 형식이 올바르지 않습니다 (예: 010-1234-5678)"
}
]
}

rowNumber는 zero-based 배열 인덱스가 아니라 spreadsheet row label을 가리킵니다. failureCount가 0이 아니면, 호출자는 200 OK를 전체 성공으로 간주하지 말고 반드시 errors[]를 확인해야 합니다.

의존성과 경계

Dependency존재 이유
AuthServiceauth user 생성/삭제, 비밀번호 재설정, login ID 변경
BillingService학생 quota 검사
ClassService영구 삭제 전 활성 enrollment 제거
ContentServicestudent에게 활성 또는 예정 예약이 남아 있으면 삭제 차단

실패 모드

  • 등록과 수정은 AUTH_OP_FAILED, DUPLICATE_PHONE, quota 관련 오류로 실패할 수 있습니다.
  • 삭제는 활성 학생이거나 content 측의 미래 예약 의무가 남아 있으면 실패합니다.
  • 대량 import는 템플릿 형식 오류, 전화번호 형식 오류, 중복 전화번호, quota 소진, auth 생성 실패, 예상치 못한 insert 오류로 row별 실패할 수 있습니다.
  • 영구 삭제의 파괴적 단계는 auth 삭제이므로, auth 측 삭제가 실패하면 student row는 그대로 남습니다.

관측성과 할당량 동작

  • lifecycle과 batch 작업은 StudentCommandService에서 성공 수와 실패를 로그합니다.
  • 대량 import는 all-or-nothing rollback 대신 row 수준 오류가 담긴 partial success를 반환합니다.
  • student 모듈도 quota를 인식하지만, staff 모듈과 마찬가지로 현재 billing internal adapter에서 제한 없는 허용 결과를 받습니다.

검증

./gradlew :modules:student:test
./gradlew :app:test --tests '*Student*'
cd lumie-document/docusaurus && npm run build

예상 성공 신호:

  • Gradle이 BUILD SUCCESSFUL로 끝납니다.
  • StudentExcelParserTestStudentCommandServiceTest가 실패 없이 통과합니다.
  • Docusaurus가 backend/student-svc에 대해 MDX 또는 broken-link 오류 없이 완료됩니다.

관련 페이지