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");
static final int COL_SEQ = 0; // A
static final int COL_SITE_NAME = 1; // B
static final int COL_CATEGORY = 2; // C
static final int COL_BOARD_NAME = 3; // D
static final int COL_POST_TITLE = 4; // E
static final int COL_URL = 5; // F
static final int COL_DESCRIPTION = 6; // G
static final int COL_HAS_ATTACHMENT = 7; // H
static final int COL_PRIOR_KOGL_TYPE = 8; // I
static final int COL_CONTRACT_DOCS = 9; // J
static final int COL_PRODUCED_DATE = 10; // K
static final int COL_PUBLISHED_DATE = 11; // L
static final int COL_REVIEW_MAJOR = 12; // M
static final int COL_REVIEW_MINOR = 13; // N
static final int COL_REVIEW_RESULT = 14; // O
static final int COL_REVIEW_KOGL_TYPE = 15; // P
static final int COL_REVIEW_AI_TYPE = 16; // Q
static final int COL_REVIEW_OPINION = 17; // R
static final int COL_REVIEW_NOTE = 18; // S
// 실파일에서 헤더는 T열에 '기존 증빙자료'라고 적혀 있지만 실제 데이터는 U열에 있다
// (T열에는 엑셀 화면용 '수정' 링크 텍스트가 들어 있음 - 데이터 기준으로 U를 읽는다)
static final int COL_PRIOR_EVIDENCE = 20; // U
/** 헤더('기존 증빙자료')가 적힌 열. 이 시스템이 만드는 다운로드 파일은 여기에 값을 쓴다. */
static final int COL_PRIOR_EVIDENCE_HEADER = 19; // T
static final int COL_JUDGED_KOGL_TYPE = 21; // V
static final int COL_JUDGED_AI_TYPE = 22; // W
static final int COL_FINAL_OPINION = 23; // X
// 24 = Y, 빈 열, 건너뜀
static final int COL_JUDGMENT_BASIS = 25; // Z
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 = result.size() + 1;
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)),
priorEvidence(row),
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;
}
/**
* 기존 증빙자료는 파일마다 값이 있는 열이 다르다.
*
* <p>기관이 보내오는 원본은 헤더가 T열인데 데이터가 U열에 있고(T에는 엑셀 화면용 '수정'
* 링크 텍스트가 들어 있다), 이 시스템이 만들어 주는 다운로드 파일은 헤더 자리인 T열에
* 값을 쓴다. 한쪽만 읽으면 "내려받아 고쳐서 다시 올리는" 흐름에서 값이 조용히 사라지므로
* U를 먼저 보고, 비어 있으면 T를 본다.</p>
*/
private String priorEvidence(Row row) {
String fromDataColumn = nullIfBlank(string(row, COL_PRIOR_EVIDENCE));
return fromDataColumn != null
? fromDataColumn
: nullIfBlank(string(row, COL_PRIOR_EVIDENCE_HEADER));
}
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;
}
}