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.process;
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.DateUtil;
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.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/**
* 기관별 권리처리 엑셀(시트명 {@code 권리처리}, 없으면 첫 시트로 대체)을 읽는다.
*
* 시트 배치 (1행 그룹제목, 2행 헤더, 3행부터 데이터, A열(순번)이 빈 첫 행에서 데이터 끝):
* A 순번 B 사이트명 C 범주 D 게시판명 E 게시물제목 F URL주소 G 설명
* H 첨부파일여부 I 기존 공공누리유형(+AI) J 계약서 유무 K 제작일 L 공표일
* M 권리확인(대분류) N 권리확인(세부) O 처리 구분 P 공공누리 유형 Q AI유형
* R 의견 S 비고 T 기존 증빙자료 U (수정 링크 열, 건너뜀)
* V 공공누리유형(판정) W AI유형(판정) X 최종의견 Y (빈 열, 건너뜀)
* Z 판단근거 AA 처리상태
*
* ReviewParser와 마찬가지로 lastRowNum까지 돌되 A열이 비면 즉시 멈춘다.
*
* 제작일/공표일(K, L열)은 실제 날짜서식이 입혀진 값이라 {@code yyyy-MM-dd} 문자열로
* 정규화한다({@link DateUtil#isCellDateFormatted}로 판별). 그 외 숫자 셀은 ReviewParser와
* 동일하게 반올림 문자열로 바꾼다. 수식 오류(#VALUE! 등) 셀은 캐시된 결과 타입이 ERROR가
* 되어 default 분기(빈 문자열 → null)로 처리된다.
*/
@Component
public class ProcessParser {
public static final String PRIMARY_SHEET_NAME = "권리처리";
private static final int DATA_START_ROW_INDEX = 2; // 3행
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final int COL_SEQ = 0; // A
private static final int COL_SITE_NAME = 1; // B
private static final int COL_CATEGORY = 2; // C
private static final int COL_BOARD_NAME = 3; // D
private static final int COL_POST_TITLE = 4; // E
private static final int COL_URL = 5; // F
private static final int COL_DESCRIPTION = 6; // G
private static final int COL_HAS_ATTACHMENT = 7; // H
private static final int COL_PRIOR_KOGL_TYPE = 8; // I
private static final int COL_CONTRACT_DOCS = 9; // J
private static final int COL_PRODUCED_DATE = 10; // K
private static final int COL_PUBLISHED_DATE = 11; // L
private static final int COL_REVIEW_MAJOR = 12; // M
private static final int COL_REVIEW_MINOR = 13; // N
private static final int COL_REVIEW_RESULT = 14; // O
private static final int COL_REVIEW_KOGL_TYPE = 15; // P
private static final int COL_REVIEW_AI_TYPE = 16; // Q
private static final int COL_REVIEW_OPINION = 17; // R
private static final int COL_REVIEW_NOTE = 18; // S
private static final int COL_PRIOR_EVIDENCE = 19; // T
// 20 = U, 수정 링크 열, 건너뜀
private static final int COL_JUDGED_KOGL_TYPE = 21; // V
private static final int COL_JUDGED_AI_TYPE = 22; // W
private static final int COL_FINAL_OPINION = 23; // X
// 24 = Y, 빈 열, 건너뜀
private static final int COL_JUDGMENT_BASIS = 25; // Z
private static final int COL_PROCESS_STATUS = 26; // AA
public List<ProcessRow> parse(InputStream xlsx) {
try (Workbook wb = WorkbookFactory.create(xlsx)) {
Sheet sheet = wb.getSheet(PRIMARY_SHEET_NAME);
if (sheet == null && wb.getNumberOfSheets() > 0) {
sheet = wb.getSheetAt(0);
}
if (sheet == null) {
throw new SeedParseException("통합문서에 읽을 수 있는 시트가 없습니다.");
}
return readRows(sheet);
} catch (IOException e) {
throw new SeedParseException("엑셀 파일을 읽지 못했습니다.", e);
}
}
private List<ProcessRow> readRows(Sheet sheet) {
List<ProcessRow> result = new ArrayList<>();
for (int i = DATA_START_ROW_INDEX; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
break; // 데이터 영역은 연속적이다 - 빈 행이 나오면 그게 끝이다
}
String seqRaw = string(row, COL_SEQ);
if (seqRaw.isBlank()) {
break;
}
int seq;
try {
seq = Integer.parseInt(seqRaw.trim());
} catch (NumberFormatException e) {
break;
}
result.add(new ProcessRow(
seq,
nullIfBlank(string(row, COL_SITE_NAME)),
nullIfBlank(string(row, COL_CATEGORY)),
nullIfBlank(string(row, COL_BOARD_NAME)),
nullIfBlank(string(row, COL_POST_TITLE)),
nullIfBlank(string(row, COL_URL)),
nullIfBlank(string(row, COL_DESCRIPTION)),
nullIfBlank(string(row, COL_HAS_ATTACHMENT)),
nullIfBlank(string(row, COL_PRIOR_KOGL_TYPE)),
nullIfBlank(string(row, COL_CONTRACT_DOCS)),
nullIfBlank(string(row, COL_PRODUCED_DATE)),
nullIfBlank(string(row, COL_PUBLISHED_DATE)),
nullIfBlank(string(row, COL_REVIEW_MAJOR)),
nullIfBlank(string(row, COL_REVIEW_MINOR)),
nullIfBlank(string(row, COL_REVIEW_RESULT)),
nullIfBlank(string(row, COL_REVIEW_KOGL_TYPE)),
nullIfBlank(string(row, COL_REVIEW_AI_TYPE)),
nullIfBlank(string(row, COL_REVIEW_OPINION)),
nullIfBlank(string(row, COL_REVIEW_NOTE)),
nullIfBlank(string(row, COL_PRIOR_EVIDENCE)),
nullIfBlank(string(row, COL_JUDGED_KOGL_TYPE)),
nullIfBlank(string(row, COL_JUDGED_AI_TYPE)),
nullIfBlank(string(row, COL_FINAL_OPINION)),
nullIfBlank(string(row, COL_JUDGMENT_BASIS)),
nullIfBlank(string(row, COL_PROCESS_STATUS))));
}
return result;
}
private String string(Row row, int columnIndex) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
return "";
}
return cellValue(cell, cell.getCellType());
}
/** ReviewParser와 동일한 셀 값 변환 규칙에, 날짜서식 숫자 셀을 yyyy-MM-dd로 정규화하는 분기를 더한다. */
private String cellValue(Cell cell, CellType cellType) {
return switch (cellType) {
case STRING -> cell.getStringCellValue().trim();
case NUMERIC -> DateUtil.isCellDateFormatted(cell)
? cell.getLocalDateTimeCellValue().format(DATE_FORMAT)
: 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;
}
}