Student Bulk Import
Use this guide when the admin student import page fails to upload a file, returns row-level errors, imports only part of the file, or behaves unexpectedly after a retry.
The current workflow is intentionally synchronous. POST /v1/students/bulk returns 200 OK with the full BulkImportResult; it is not an async job like OMR batch confirm or report batch generation.
Source Surfaces
| Surface | Source path |
|---|---|
| Upload page | lumie-frontend/app/admin/students/import/page.tsx |
| Client form | lumie-frontend/app/admin/students/import/_components/ImportStudentsClient.tsx |
| Bulk-import UI | lumie-frontend/src/features/student-management/bulk-import/ui/* |
| Generated network call | lumie-frontend/src/entities/student/api/generated.ts |
| Query wrapper | lumie-frontend/src/entities/student/api/queries.ts |
| Backend endpoint | lumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/web/StudentController.java |
| Parser | lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.java |
| Import service | lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java |
| Response contract | lumie-backend/modules/student/src/main/java/com/lumie/student/application/dto/response/BulkImportResult.java |
Symptoms
| Symptom | Likely cause |
|---|---|
| The UI rejects the file before upload | file extension is not .xlsx or .csv |
| Backend returns a template error | first two headers are not exactly the expected Korean labels |
| CSV works locally but fails after export from Excel | encoding or malformed quote handling |
| Retry creates confusion | the frontend generates an Idempotency-Key per selected file |
| Some rows import and some rows fail | validation is row-based and returns BulkImportResult.errors |
| Rows after a quota error all fail | service marks remaining valid rows with a quota error after quota exhaustion |
Template Contract
The frontend template uses these headers:
| Position | Header | Required |
|---|---|---|
| 1 | 학생 이름 | yes |
| 2 | 학생 연락처 | yes |
| 3 | 학교명 | no |
| 4 | 출생 연도 | no |
| 5 | 학부모 연락처 | no |
| 6 | 메모 | no |
The backend parser checks the first two header cells exactly. For CSV, it supports UTF-8, Windows-949, and EUC-KR, and strips a UTF-8 BOM when present.
The parser also enforces practical bounds:
| Bound | Value |
|---|---|
| Maximum data rows | 1,000 |
| Maximum CSV size | 5 MiB |
| Maximum cells per row | 32 |
Diagnostic Path
- Confirm the selected file extension. The UI accepts only
.xlsxand.csv. - Open the first row and verify the first two cells are exactly
학생 이름and학생 연락처. - If the file is CSV, check whether the file is valid UTF-8, Windows-949, or EUC-KR and whether quoted cells are balanced.
- Check whether the same file selection is being retried. The UI keeps one idempotency key for the selected file and regenerates it when the file is changed or reset.
- Inspect the returned
BulkImportResult.errors.rowNumberis 1-indexed and includes the header row, so the first data row is row 2. - For duplicate phone failures, remember that phone numbers are normalized before duplicate checks.
- For partial imports, compare
successCount,failureCount, and row errors instead of assuming the whole file rolled back.
Failure Semantics
The backend runs in two passes:
- parse and validate all rows, collecting row errors,
- insert only rows that passed validation.
The response contract is defined by lumie-backend/modules/student/src/main/java/com/lumie/student/application/dto/response/BulkImportResult.java and mirrored by the generated frontend type in lumie-frontend/src/entities/student/api/generated.ts.
Representative shape:
type BulkImportResult = {
totalRows: number;
successCount: number;
failureCount: number;
errors: Array<{
rowNumber: number;
column: string | null;
value: string | null;
code: string;
message: string;
}>;
};
Simplified example:
{
"totalRows": 3,
"successCount": 2,
"failureCount": 1,
"errors": [
{
"rowNumber": 3,
"column": "phone",
"value": "010-1234-5678",
"code": "BULK_IMPORT_DUPLICATE_PHONE",
"message": "이미 등록된 연락처입니다."
}
]
}
This is a partial-success contract. Operators should fix the reported rows and upload the corrected file again rather than manually editing server state.
Prevention
- Keep the downloadable CSV template in sync with
StudentExcelParser. - Preserve the
Idempotency-Keyheader in the frontend query wrapper. - Do not convert this endpoint to an async job unless real uploads exceed the request-timeout budget.
- Keep parser-specific file-format logic inside
StudentExcelParser; business validation belongs inStudentCommandService. - Keep spreadsheet-injection checks on user-controlled text cells before expanding accepted columns.
Operational Detail
This workflow is intentionally a partial-success import, not an all-or-nothing transaction from the operator's point of view. The response is the recovery surface.
| Concern | Expected behavior |
|---|---|
| File acceptance | UI accepts .xlsx and .csv; backend validates actual template shape. |
| Header validation | First two cells must match exactly. Optional columns can be blank. |
| Duplicate handling | Phone numbers are normalized before duplicate checks. |
| Quota handling | Valid remaining rows can be marked failed once quota is exhausted. |
| Retry behavior | Same selected file keeps the same Idempotency-Key; reset or new file changes it. |
Operators should not manually insert missing successful rows after a partial result. The safer path is to correct the reported rows and upload a clean file.
Recovery Playbook
- Ask for the returned
BulkImportResult, not only the spreadsheet. - Verify the first two headers before investigating row data.
- For CSV files, check encoding and quote balance. Excel-exported CSVs are common suspects.
- Map row errors back to the spreadsheet using the 1-indexed row number that includes the header.
- If a retry behaves unexpectedly, confirm whether the user selected a new file or retried the same selected file.
- If quota errors appear, check whether earlier successful rows consumed the remaining capacity.
Verification Evidence
Before closing:
- a corrected sample file imports with expected
successCount, - row-level errors point to the intended spreadsheet rows,
- frontend still sends
Idempotency-Key, - generated frontend types still match
BulkImportResult, - the downloadable template matches parser expectations.
Anti-Patterns
- Treating any
failureCount > 0as a full rollback. - Editing database rows manually to compensate for a partial import.
- Changing the frontend template without updating
StudentExcelParser. - Expanding accepted columns without adding validation and spreadsheet-injection checks.