File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
package kr.itn.itnhub.seed;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
/**
* 신유형 신청목록 시트의 모든 행을 개별 기관으로 읽는다.
*
* 시트 배치 (1행 그룹제목, 2행 헤더, 3행부터 데이터):
* B=연번1 C=연번2 F=기관명 G=부서명 H=담당자명 I=직급/직함 J=유선전화 K=이메일
*
* 기관 코드는 {연번1}_{연번2} 합성 코드다(예: 001_00, 008_02). 연번1+연번2+기관명이
* 모두 있는 행은 전부 별도 기관이 된다 - 예전처럼 연번2="00" 대표기관에 하위기관을
* 흡수하지 않는다(경찰청_치안정책연구소 같은 예외도 더 이상 필요 없다 - 연번 자체가
* 하위기관까지 구분하기 때문). 담당자 정보가 없는 행은 담당자 필드만 null이 되고
* 행 자체는 그대로 만들어진다.
*/
@Component
public class SeedParser {
public static final String SHEET_NAME = "신유형 신청목록";
private static final int DATA_START_ROW_INDEX = 2; // 3행
private static final int COL_ORG_NO = 1; // B
private static final int COL_SUB_NO = 2; // C
private static final int COL_ORG_NAME = 5; // F
private static final int COL_DEPT = 6; // G
private static final int COL_MANAGER = 7; // H
private static final int COL_TITLE = 8; // I
private static final int COL_PHONE = 9; // J
private static final int COL_EMAIL = 10; // K
public List<SeedRow> parse(InputStream xlsx) {
try (Workbook wb = WorkbookFactory.create(xlsx)) {
Sheet sheet = wb.getSheet(SHEET_NAME);
if (sheet == null) {
throw new SeedParseException(
"통합문서에 '" + SHEET_NAME + "' 시트가 없습니다.");
}
return readRows(sheet);
} catch (IOException e) {
throw new SeedParseException("엑셀 파일을 읽지 못했습니다.", e);
}
}
private List<SeedRow> readRows(Sheet sheet) {
List<SeedRow> result = new ArrayList<>();
for (int i = DATA_START_ROW_INDEX; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
continue;
}
String orgNo1 = orgNo1(row);
String subNo = subNo(row);
String orgName = string(row, COL_ORG_NAME);
if (orgNo1.isBlank() || subNo.isBlank() || orgName.isBlank()) {
continue;
}
String orgNo = orgNo1 + "_" + subNo;
result.add(new SeedRow(
orgNo,
orgName,
orgNo,
nullIfBlank(string(row, COL_DEPT)),
nullIfBlank(string(row, COL_MANAGER)),
nullIfBlank(string(row, COL_TITLE)),
nullIfBlank(string(row, COL_PHONE)),
nullIfBlank(string(row, COL_EMAIL))));
}
return result;
}
/** 연번1은 항상 3자리 문자열이다. 숫자로 저장된 셀도 앞자리 0을 복원해 준다. */
private String orgNo1(Row row) {
String raw = string(row, COL_ORG_NO);
return raw.isBlank() ? "" : pad(raw, 3);
}
/** 연번2도 항상 2자리 문자열이다("0" → "00", "1" → "01"). */
private String subNo(Row row) {
String raw = string(row, COL_SUB_NO);
return raw.isBlank() ? "" : pad(raw, 2);
}
private String pad(String value, int width) {
String v = value.trim();
return v.length() >= width ? v : "0".repeat(width - v.length()) + v;
}
private String string(Row row, int columnIndex) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
return "";
}
return cellValue(cell, cell.getCellType());
}
/**
* cellType으로 셀 값을 문자열로 변환한다. FORMULA는 수식을 평가하지 않고
* 파일에 저장된 캐시 결과 타입({@link Cell#getCachedFormulaResultType()})으로 재귀 위임해
* STRING/NUMERIC/BOOLEAN 처리 로직을 그대로 재사용한다.
*/
private String cellValue(Cell cell, CellType cellType) {
return switch (cellType) {
case STRING -> cell.getStringCellValue().trim();
// 반올림: 부동소수점 오차로 6.999999999999998처럼 저장된 값이 절삭되어
// 한 자리 작은 값("006")으로 읽히는 것을 방지한다.
case NUMERIC -> String.valueOf(Math.round(cell.getNumericCellValue()));
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
case FORMULA -> cellValue(cell, cell.getCachedFormulaResultType());
default -> "";
};
}
private String nullIfBlank(String value) {
return (value == null || value.isBlank()) ? null : value;
}
}