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 kr.itn.itnhub.provision.ChannelNaming;
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=이메일
*
* 축약 규칙:
* - 연번2가 "00"인 행이 대표기관이며 담당자 정보를 갖는다
* - 나머지 하위기관 행은 대표기관에 흡수한다 (008 경찰청의 서울청·부산청 등)
* - 단 경찰청_치안정책연구소만 예외로 별도 기관으로 남긴다
*/
@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 orgNo = orgNo(row);
String orgName = string(row, COL_ORG_NAME);
if (orgNo.isBlank() || orgName.isBlank()) {
continue;
}
boolean representative = "00".equals(subNo(row));
boolean special = ChannelNaming.SPECIAL_ORG_NAME.equalsIgnoreCase(orgName);
if (!representative && !special) {
continue; // 하위기관은 대표기관에 흡수된다
}
result.add(new SeedRow(
orgNo,
orgName,
special ? orgNo + "-policy" : 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;
}
/** 연번은 항상 3자리 문자열이다. 숫자로 저장된 셀도 앞자리 0을 복원해 준다. */
private String orgNo(Row row) {
String raw = string(row, COL_ORG_NO);
return raw.isBlank() ? "" : pad3(raw);
}
private String subNo(Row row) {
String raw = string(row, COL_SUB_NO);
return raw.isBlank() ? "" : (raw.length() == 1 ? "0" + raw : raw);
}
private String pad3(String value) {
String v = value.trim();
return v.length() >= 3 ? v : "0".repeat(3 - 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;
}
}