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.review;
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;
/**
* 기관별 권리확인 엑셀(시트명 {@code Sheet0}, 없으면 첫 시트로 대체)을 읽는다.
*
* 시트 배치 (1행 그룹제목, 2행 헤더, 3행부터 데이터, A열(순번)이 빈 첫 행에서 데이터 끝):
* A 순번 B 사이트명 C 범주 D 게시판 경로 E 게시판명 F 게시물제목 G URL주소
* H 게시글 등록 I 제작일 J 공표일 K 첨부파일여부 L 기존 공공누리부착 M 기존 공공누리유형
* N AI유형 O 비고(조사원) P 개방가능여부 Q 권리확인(대분류) R 권리확인(세부) S 처리결과
* T 공공누리유형(판정) U AI유형(판정) V 의견 W (빈 열) X 비고(변호사) Y 권리처리필요
*
* 시트의 선언된 dimension은 서식만 채워진 40만 행 안팎이라, lastRowNum까지 무조건 돌되
* A열이 비면 즉시 멈춘다(전체 dimension을 다 읽지 않는다).
*
* 제작일/공표일 셀은 날짜서식이 입혀진 숫자({@code 20230904})라도 절대 날짜로 읽지 않고
* 문자열 그대로 저장한다 - {@link #cellValue}가 NUMERIC을 항상 반올림 문자열로 바꾸고
* {@code getDateCellValue()}를 호출하지 않으므로 자동으로 이 요구사항을 만족한다.
* 수식 오류(#VALUE! 등) 셀은 캐시된 결과 타입이 ERROR가 되어 default 분기(빈 문자열 → null)로
* 처리된다.
*/
@Component
public class ReviewParser {
public static final String PRIMARY_SHEET_NAME = "Sheet0";
private static final int DATA_START_ROW_INDEX = 2; // 3행
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_PATH = 3; // D
private static final int COL_BOARD_NAME = 4; // E
private static final int COL_POST_TITLE = 5; // F
private static final int COL_URL = 6; // G
private static final int COL_POST_REGISTERED = 7; // H
private static final int COL_PRODUCED_DATE = 8; // I
private static final int COL_PUBLISHED_DATE = 9; // J
private static final int COL_HAS_ATTACHMENT = 10; // K
private static final int COL_KOGL_ATTACHED = 11; // L
private static final int COL_KOGL_TYPE = 12; // M
private static final int COL_AI_TYPE = 13; // N
private static final int COL_SURVEYOR_NOTE = 14; // O
private static final int COL_OPENABLE = 15; // P
private static final int COL_REVIEW_MAJOR = 16; // Q
private static final int COL_REVIEW_MINOR = 17; // R
private static final int COL_REVIEW_RESULT = 18; // S
private static final int COL_JUDGED_KOGL_TYPE = 19; // T
private static final int COL_JUDGED_AI_TYPE = 20; // U
private static final int COL_OPINION = 21; // V
// 22 = W, 빈 열
private static final int COL_LAWYER_NOTE = 23; // X
private static final int COL_NEEDS_PROCESSING = 24; // Y
public List<ReviewRow> 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<ReviewRow> readRows(Sheet sheet) {
List<ReviewRow> 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 ReviewRow(
seq,
nullIfBlank(string(row, COL_SITE_NAME)),
nullIfBlank(string(row, COL_CATEGORY)),
nullIfBlank(string(row, COL_BOARD_PATH)),
nullIfBlank(string(row, COL_BOARD_NAME)),
nullIfBlank(string(row, COL_POST_TITLE)),
nullIfBlank(string(row, COL_URL)),
nullIfBlank(string(row, COL_POST_REGISTERED)),
nullIfBlank(string(row, COL_PRODUCED_DATE)),
nullIfBlank(string(row, COL_PUBLISHED_DATE)),
nullIfBlank(string(row, COL_HAS_ATTACHMENT)),
nullIfBlank(string(row, COL_KOGL_ATTACHED)),
nullIfBlank(string(row, COL_KOGL_TYPE)),
nullIfBlank(string(row, COL_AI_TYPE)),
nullIfBlank(string(row, COL_SURVEYOR_NOTE)),
nullIfBlank(string(row, COL_OPENABLE)),
nullIfBlank(string(row, COL_REVIEW_MAJOR)),
nullIfBlank(string(row, COL_REVIEW_MINOR)),
nullIfBlank(string(row, COL_REVIEW_RESULT)),
nullIfBlank(string(row, COL_JUDGED_KOGL_TYPE)),
nullIfBlank(string(row, COL_JUDGED_AI_TYPE)),
nullIfBlank(string(row, COL_OPINION)),
nullIfBlank(string(row, COL_LAWYER_NOTE)),
nullIfBlank(string(row, COL_NEEDS_PROCESSING))));
}
return result;
}
private String string(Row row, int columnIndex) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
return "";
}
return cellValue(cell, cell.getCellType());
}
/** SeedParser/MemberDirectoryParser와 동일한 셀 값 변환 규칙. */
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;
}
}