Skip to main content

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

SurfaceSource path
Upload pagelumie-frontend/app/admin/students/import/page.tsx
Client formlumie-frontend/app/admin/students/import/_components/ImportStudentsClient.tsx
Bulk-import UIlumie-frontend/src/features/student-management/bulk-import/ui/*
Generated network calllumie-frontend/src/entities/student/api/generated.ts
Query wrapperlumie-frontend/src/entities/student/api/queries.ts
Backend endpointlumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/web/StudentController.java
Parserlumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.java
Import servicelumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java
Response contractlumie-backend/modules/student/src/main/java/com/lumie/student/application/dto/response/BulkImportResult.java

Symptoms

SymptomLikely cause
The UI rejects the file before uploadfile extension is not .xlsx or .csv
Backend returns a template errorfirst two headers are not exactly the expected Korean labels
CSV works locally but fails after export from Excelencoding or malformed quote handling
Retry creates confusionthe frontend generates an Idempotency-Key per selected file
Some rows import and some rows failvalidation is row-based and returns BulkImportResult.errors
Rows after a quota error all failservice marks remaining valid rows with a quota error after quota exhaustion

Template Contract

The frontend template uses these headers:

PositionHeaderRequired
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:

BoundValue
Maximum data rows1,000
Maximum CSV size5 MiB
Maximum cells per row32

Diagnostic Path

  1. Confirm the selected file extension. The UI accepts only .xlsx and .csv.
  2. Open the first row and verify the first two cells are exactly 학생 이름 and 학생 연락처.
  3. If the file is CSV, check whether the file is valid UTF-8, Windows-949, or EUC-KR and whether quoted cells are balanced.
  4. 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.
  5. Inspect the returned BulkImportResult.errors. rowNumber is 1-indexed and includes the header row, so the first data row is row 2.
  6. For duplicate phone failures, remember that phone numbers are normalized before duplicate checks.
  7. 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:

  1. parse and validate all rows, collecting row errors,
  2. 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-Key header 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 in StudentCommandService.
  • 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.

ConcernExpected behavior
File acceptanceUI accepts .xlsx and .csv; backend validates actual template shape.
Header validationFirst two cells must match exactly. Optional columns can be blank.
Duplicate handlingPhone numbers are normalized before duplicate checks.
Quota handlingValid remaining rows can be marked failed once quota is exhausted.
Retry behaviorSame 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

  1. Ask for the returned BulkImportResult, not only the spreadsheet.
  2. Verify the first two headers before investigating row data.
  3. For CSV files, check encoding and quote balance. Excel-exported CSVs are common suspects.
  4. Map row errors back to the spreadsheet using the 1-indexed row number that includes the header.
  5. If a retry behaves unexpectedly, confirm whether the user selected a new file or retried the same selected file.
  6. 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 > 0 as 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.