package kr.itn.itnhub.contact; import kr.itn.itnhub.seed.SeedParseException; 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; import java.util.Map; /** * 회원정보 xlsx(Sheet1)를 읽는다. * * 시트 배치 (1행 헤더, 2행부터 데이터): * A=순번 B=구분 C=기관명 D=부서명 E=담당자명 F=직급/직함 G=유선전화 H=이메일 * * 담당자명(E)이 빈 첫 행에서 데이터가 끝난 것으로 보고 읽기를 멈춘다. 구분(B)이 * 알려진 값(주관기관/변호사/신청기관/수행기관)이 아니면 그 행은 건너뛰고 skipped로 * 센다 - 시트 끝은 아니므로 이어서 다음 행을 계속 읽는다. */ @Component public class MemberDirectoryParser { public static final String SHEET_NAME = "Sheet1"; private static final int DATA_START_ROW_INDEX = 1; // 2행 private static final int COL_CATEGORY = 1; // B private static final int COL_ORG_NAME = 2; // C private static final int COL_DEPT = 3; // D private static final int COL_NAME = 4; // E private static final int COL_TITLE = 5; // F private static final int COL_PHONE = 6; // G private static final int COL_EMAIL = 7; // H private static final Map CATEGORY_LABELS = Map.of( "주관기관", "MJ", "변호사", "LAWYER", "신청기관", "APPLICANT", "수행기관", "OPERATOR"); public MemberParseResult 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 MemberParseResult readRows(Sheet sheet) { List rows = new ArrayList<>(); int skipped = 0; for (int i = DATA_START_ROW_INDEX; i <= sheet.getLastRowNum(); i++) { Row row = sheet.getRow(i); String name = row == null ? "" : string(row, COL_NAME); if (name.isBlank()) { break; // 담당자명이 빈 첫 행에서 데이터 끝 } String category = CATEGORY_LABELS.get(string(row, COL_CATEGORY)); if (category == null) { skipped++; continue; } rows.add(new MemberRow( category, nullIfBlank(string(row, COL_ORG_NAME)), normalizeDept(string(row, COL_DEPT)), name, nullIfBlank(string(row, COL_TITLE)), nullIfBlank(string(row, COL_PHONE)), nullIfBlank(string(row, COL_EMAIL)))); } return new MemberParseResult(rows, skipped); } /** 부서명은 "-"와 공백 모두 값 없음으로 취급한다. */ private String normalizeDept(String value) { String v = nullIfBlank(value); return "-".equals(v) ? null : v; } private String string(Row row, int columnIndex) { Cell cell = row.getCell(columnIndex); if (cell == null) { return ""; } return cellValue(cell, cell.getCellType()); } /** SeedParser와 동일한 셀 값 변환 규칙(FORMULA는 캐시된 결과 타입으로 위임). */ private String cellValue(Cell cell, CellType cellType) { return switch (cellType) { case STRING -> cell.getStringCellValue().trim(); 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; } }