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.org.OrgNotFoundException;
import kr.itn.itnhub.org.OrganizationMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Set;
/** 기관관리 상세의 권리처리 탭이 쓰는 목록/상세/수정/삭제/다운로드 서비스. */
@Service
public class ProcessService {
private static final Set<String> VALID_PROCESS_STATUSES = Set.of("미처리", "처리완료");
/**
* 1행 그룹 제목(A=게시물 정보(조사원), M=권리확인(변호사), U=권리처리(변호사)) + 2행 컬럼 헤더.
* U열(수정 링크)과 Y열은 원본 시트의 건너뛰는 열이라 빈 문자열로 남겨둔다.
*/
private static final String[] COLUMN_HEADERS = {
"순번", "사이트명", "범주", "게시판명", "게시물제목", "URL주소", "설명",
"첨부파일여부", "기존 공공누리유형(+AI)", "계약서 유무", "제작일", "공표일",
"권리확인(대분류)", "권리확인(세부)", "처리 구분", "공공누리 유형", "AI유형", "의견", "비고", "기존 증빙자료",
"", "공공누리유형(판정)", "AI유형(판정)", "최종의견", "", "판단근거", "처리상태"
};
private final ProcessItemMapper mapper;
private final OrganizationMapper orgMapper;
public ProcessService(ProcessItemMapper mapper, OrganizationMapper orgMapper) {
this.mapper = mapper;
this.orgMapper = orgMapper;
}
public ProcessPage list(Long orgId, String keyword, String status, int page, int size) {
requireOrg(orgId);
String kw = normalizeKeyword(keyword);
String st = normalizeStatus(status);
int safePage = Math.max(page, 0);
int safeSize = size <= 0 ? 30 : size;
int offset = safePage * safeSize;
List<ProcessItem> items = mapper.findPage(orgId, kw, st, offset, safeSize);
// total/done은 검색어·상태 필터와 무관한 기관 전체 진행률이다(헤더/통계 카드용 - ProcessPage 참고).
ProcessStats stats = mapper.stats(orgId);
return new ProcessPage(items, stats.total(), stats.done(), safePage, safeSize);
}
public ProcessItem get(Long orgId, Long itemId) {
requireOrg(orgId);
return requireItem(orgId, itemId);
}
@Transactional
public ProcessItem updateProcessing(Long orgId, Long itemId, ProcessingRequest request) {
requireOrg(orgId);
validateProcessStatus(request.processStatus());
int updated = mapper.updateProcessing(orgId, itemId,
request.contractDocs(), request.judgedKoglType(), request.judgedAiType(),
request.finalOpinion(), request.judgmentBasis(), request.processStatus());
if (updated == 0) {
throw new ProcessNotFoundException("게시물을 찾을 수 없습니다: " + itemId);
}
return mapper.findById(orgId, itemId);
}
@Transactional
public void delete(Long orgId, Long itemId) {
requireOrg(orgId);
int deleted = mapper.deleteById(orgId, itemId);
if (deleted == 0) {
throw new ProcessNotFoundException("게시물을 찾을 수 없습니다: " + itemId);
}
}
/** 원본 파일과 같은 헤더 배치(1행 그룹제목, 2행 컬럼헤더, 3행부터 데이터)로 xlsx를 만든다. */
public byte[] downloadWorkbook(Long orgId) {
requireOrg(orgId);
List<ProcessItem> items = mapper.findAllByOrg(orgId);
try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
Sheet sheet = wb.createSheet(ProcessParser.PRIMARY_SHEET_NAME);
Row groupRow = sheet.createRow(0);
groupRow.createCell(0).setCellValue("게시물 정보(조사원)");
groupRow.createCell(12).setCellValue("권리확인(변호사)");
groupRow.createCell(20).setCellValue("권리처리(변호사)");
Row headerRow = sheet.createRow(1);
for (int i = 0; i < COLUMN_HEADERS.length; i++) {
headerRow.createCell(i).setCellValue(COLUMN_HEADERS[i]);
}
int rowNum = 2;
for (ProcessItem item : items) {
Row row = sheet.createRow(rowNum++);
setCell(row, 0, String.valueOf(item.getSeq()));
setCell(row, 1, item.getSiteName());
setCell(row, 2, item.getCategory());
setCell(row, 3, item.getBoardName());
setCell(row, 4, item.getPostTitle());
setCell(row, 5, item.getUrl());
setCell(row, 6, item.getDescription());
setCell(row, 7, item.getHasAttachment());
setCell(row, 8, item.getPriorKoglType());
setCell(row, 9, item.getContractDocs());
setCell(row, 10, item.getProducedDate());
setCell(row, 11, item.getPublishedDate());
setCell(row, 12, item.getReviewMajor());
setCell(row, 13, item.getReviewMinor());
setCell(row, 14, item.getReviewResult());
setCell(row, 15, item.getReviewKoglType());
setCell(row, 16, item.getReviewAiType());
setCell(row, 17, item.getReviewOpinion());
setCell(row, 18, item.getReviewNote());
setCell(row, 19, item.getPriorEvidence());
// 20 = U, 수정 링크 열, 건너뜀
setCell(row, 21, item.getJudgedKoglType());
setCell(row, 22, item.getJudgedAiType());
setCell(row, 23, item.getFinalOpinion());
// 24 = Y, 빈 열
setCell(row, 25, item.getJudgmentBasis());
setCell(row, 26, item.getProcessStatus());
}
wb.write(out);
return out.toByteArray();
} catch (IOException e) {
throw new UncheckedIOException("권리처리 엑셀을 만들지 못했습니다.", e);
}
}
private void setCell(Row row, int columnIndex, String value) {
if (value == null) {
return;
}
Cell cell = row.createCell(columnIndex);
cell.setCellValue(value);
}
private void validateProcessStatus(String processStatus) {
if (processStatus != null && !VALID_PROCESS_STATUSES.contains(processStatus)) {
throw new InvalidProcessStatusException("처리상태는 미처리 또는 처리완료여야 합니다.");
}
}
private String normalizeKeyword(String keyword) {
return (keyword == null || keyword.isBlank()) ? null : keyword.trim();
}
private String normalizeStatus(String status) {
return (status == null || status.isBlank()) ? null : status.trim();
}
private void requireOrg(Long orgId) {
if (orgMapper.findById(orgId) == null) {
throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId);
}
}
private ProcessItem requireItem(Long orgId, Long itemId) {
ProcessItem item = mapper.findById(orgId, itemId);
if (item == null) {
throw new ProcessNotFoundException("게시물을 찾을 수 없습니다: " + itemId);
}
return item;
}
}