학생 대량 가져오기
관리자 학생 가져오기 페이지가 파일 업로드에 실패하거나, 행 수준 오류를 반환하거나, 파일의 일부만 가져오거나, 재시도 후 예기치 않게 동작하는 경우 이 가이드를 사용하세요.
현재 워크플로는 의도적으로 동기식입니다. POST /v1/students/bulk는 전체 BulkImportResult와 함께 200 OK를 반환합니다. OMR 일괄 확인이나 보고서 일괄 생성과 같은 비동기 작업이 아닙니다.
소스 경로
| 영역 | 소스 경로 |
|---|---|
| 페이지 업로드 | lumie-frontend/app/admin/students/import/page.tsx |
| 클라이언트 폼 | lumie-frontend/app/admin/students/import/_components/ImportStudentsClient.tsx |
| 대량 가져오기 UI | lumie-frontend/src/features/student-management/bulk-import/ui/* |
| 생성된 네트워크 호출 | lumie-frontend/src/entities/student/api/generated.ts |
| 쿼리 래퍼 | lumie-frontend/src/entities/student/api/queries.ts |
| 백엔드 엔드포인트 | lumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/web/StudentController.java |
| 파서 | lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.java |
| 가져오기 서비스 | lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java |
| 응답 계약 | lumie-backend/modules/student/src/main/java/com/lumie/student/application/dto/response/BulkImportResult.java |
증상
| 증상 | 가능한 원인 |
|---|---|
| UI가 업로드 전에 파일을 거부합니다. | 파일 확장자가 .xlsx 또는 .csv가 아닙니다 |
| 백엔드가 템플릿 오류를 반환함 | 처음 두 헤더가 정확히 예상되는 한국어 레이블이 아닙니다 |
| CSV는 로컬에서 작동하지만 Excel에서 내보낸 후 실패 | 인코딩 또는 잘못된 인용문 처리 |
| 재시 도는 혼란을 야기합니다 | 프런트엔드는 선택한 파일별로 Idempotency-Key를 생성합니다. |
| 일부 행은 가져오고 일부 행은 실패 | 유효성 검사는 행 단위이며 BulkImportResult.errors를 반환합니다. |
| 할당량 오류 이후의 행은 모두 실패 | 서비스는 할당량이 소진된 후 남은 유효한 행을 할당량 오류로 표시합니다. |
템플릿 계약
프런트엔드 템플릿은 다음 헤더를 사용합니다.
| 위치 | 헤더 | 필수 |
|---|---|---|
| 1 | 학생 이름 | 예 |
| 2 | 학생 연락처 | 예 |
| 3 | 학교명 | 아니 |
| 4 | 출생 연도 | 아니 |
| 5 | 학부모 연락처 | 아니 |
| 6 | 메모 | 아니 |
백엔드 파서는 처음 두 헤더 셀을 정확하게 확인합니다. CSV의 경우 UTF-8, Windows-949 및 EUC-KR을 지원하고 UTF-8 BOM이 있는 경우 이를 제거합니다.
파서는 또한 실제 경계를 적용합니다.
| 제한 | 값 |
|---|---|
| 최대 데이터 행 | 1,000 |
| 최대 CSV 크기 | 5MiB |
| 행당 최대 셀 수 | 32 |
진단 경로
- 선택한 파일 확장자를 확인하세요. UI는
.xlsx및.csv만 허용합니다. - 첫 번째 행을 열고 처음 두 셀이 정확히
학생 이름및학생 연락처인지 확인합니다. - 파일이 CSV인 경우 파일이 유효한 UTF-8, Windows-949 또는 EUC-KR인지 확인하고 인용된 셀이 균형을 이루고 있는지 확인하세요.
- 동일한 파일 선택이 재시도되는지 확인하세요. UI는 선택한 파일에 대해 하나의 멱등성 키를 유지하고 파일이 변경되거나 재설정될 때 이를 다시 생성합니다.
- 반환된
BulkImportResult.errors를 검사합니다.rowNumber는 1-인덱싱되고 헤더 행을 포함하므로 첫 번째 데이터 행은 행 2입니다. - 중복 전화 실패의 경우 중복 확인 전에 전화번호가 표준화되어 있다는 점을 기억하세요.
- 부분 가져오기의 경우 전체 파일이 롤백되었다고 가정하는 대신
successCount,failureCount및 행 오류를 비교합니다.
실패 처리 방식
백엔드는 두 가지 패스로 실행됩니다.
- 모든 행을 구문 분석 및 검증하고 행 오류를 수집합니다.
- 유효성 검사를 통과한 행만 삽입합니다.
응답 계약은 lumie-backend/modules/student/src/main/java/com/lumie/student/application/dto/response/BulkImportResult.java에 정의되어 있으며, lumie-frontend/src/entities/student/api/generated.ts에 생성된 프런트엔드 타입이 이를 반영합니다.
대표적인 형태:
type BulkImportResult = {
totalRows: number;
successCount: number;
failureCount: number;
errors: Array<{
rowNumber: number;
column: string | null;
value: string | null;
code: string;
message: string;
}>;
};
단순화된 예:
{
"totalRows": 3,
"successCount": 2,
"failureCount": 1,
"errors": [
{
"rowNumber": 3,
"column": "phone",
"value": "010-1234-5678",
"code": "BULK_IMPORT_DUPLICATE_PHONE",
"message": "이미 등록된 연락처입니다."
}
]
}
이것은 부분 성공 계약입니다. 운영자는 서버 상태를 직접 편집하는 대신 보고된 행을 수정하고 수정된 파일을 다시 업로드해야 합니다.
예방
- 다운로드 가능한 CSV 템플릿을
StudentExcelParser와 동기화 상태로 유지하세요. - 프런트엔드 쿼리 래퍼에
Idempotency-Key헤더를 유지합니다. - 실제 업로드가 요청 제한 시간 예산을 초과하지 않는 한 이 끝점을 비동기 작업으로 변환하지 마십시오.
StudentExcelParser내부에 파서별 파일 형식 논리를 유지합니다. 비즈니스 검증은StudentCommandService에 속합니다.- 허용되는 열을 확장하기 전에 사용자가 제어하는 텍스트 셀에 대한 스프레드시트 삽입 검사를 유지합니다.
운영 세부사항
이 workflow는 operator 관점에서 all-or-nothing transaction이 아니라 partial-success import입니다. 응답 자체가 복구 표면입니다.
| 관심사 | 기대 동작 |
|---|---|
| File acceptance | UI는 .xlsx와 .csv를 허용하고 backend는 실제 template shape를 검증합니다. |
| Header validation | 처음 두 cell은 정확히 일치해야 합니다. optional column은 비어 있을 수 있습니다. |
| Duplicate handling | duplicate check 전에 phone number를 normalize합니다. |
| Quota handling | quota가 소진되면 남은 valid row도 실패로 표시될 수 있습니다. |
| Retry behavior | 같은 selected file은 같은 Idempotency-Key를 유지하고 reset 또는 새 file은 key를 바꿉니다. |
operator는 partial result 후 누락된 성공 row를 수동 insert하지 않습니다. 더 안전한 경로는 보고된 row를 고친 뒤 깨끗한 file을 다시 upload하는 것입니다.
복구 절차
- spreadsheet만 받지 말고 반환된
BulkImportResult를 요청합니다. - row data를 조사하기 전에 처음 두 header를 검증합니다.
- CSV라면 encoding과 quote balance를 확인합니다. Excel-exported CSV가 흔한 원인입니다.
- header를 포함하는 1-indexed row number를 사용해 row error를 spreadsheet와 매핑합니다.
- retry가 이상하게 보이면 사용자가 새 file을 선택했는지 같은 selected file을 retry했는지 확인합니다.
- quota error가 보이면 앞선 successful row가 남은 capacity를 소비했는지 확인합니다.
검증 증거
닫기 전에 다음을 확인합니다.
- corrected sample file이 기대한
successCount로 import됩니다. - row-level error가 의도한 spreadsheet row를 가리킵니다.
- frontend가 여전히
Idempotency-Key를 보냅니다. - generated frontend type이
BulkImportResult와 일치합니다. - downloadable template이 parser expectation과 일치합니다.
피해야 할 패턴
failureCount > 0을 full rollback으로 취급하는 것.- partial import를 보정하려고 database row를 수동 편집하는 것.
StudentExcelParser를 갱신하지 않고 frontend template만 바꾸는 것.- validation과 spreadsheet-injection check 없이 accepted column을 늘리는 것.