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 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 readRows(Sheet sheet) { List 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; } }