package kr.itn.itnhub.review; 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; /** 기관관리 상세의 권리확인 탭이 쓰는 목록/상세/수정/삭제/다운로드 서비스. */ @Service public class ReviewService { /** 1행 그룹 제목 + 2행 컬럼 헤더. W열은 원본 시트의 빈 열이라 빈 문자열로 남겨둔다. */ private static final String[] COLUMN_HEADERS = { "순번", "사이트명", "범주", "게시판 경로", "게시판명", "게시물제목", "URL주소", "게시글 등록", "제작일", "공표일", "첨부파일여부", "기존 공공누리부착", "기존 공공누리유형", "AI유형", "비고(조사원)", "개방가능여부", "권리확인(대분류)", "권리확인(세부)", "처리결과", "공공누리유형(판정)", "AI유형(판정)", "의견", "", "비고(변호사)", "권리처리필요" }; private final ReviewItemMapper mapper; private final OrganizationMapper orgMapper; public ReviewService(ReviewItemMapper mapper, OrganizationMapper orgMapper) { this.mapper = mapper; this.orgMapper = orgMapper; } public ReviewPage list(Long orgId, String keyword, String site, String board, String title, int page, int size) { requireOrg(orgId); String kw = normalizeKeyword(keyword); String siteFilter = normalizeKeyword(site); String boardFilter = normalizeKeyword(board); String titleFilter = normalizeKeyword(title); int safePage = Math.max(page, 0); int safeSize = size <= 0 ? 30 : size; int offset = safePage * safeSize; List items = mapper.findPage(orgId, kw, siteFilter, boardFilter, titleFilter, offset, safeSize); int total = mapper.countByOrg(orgId, kw, siteFilter, boardFilter, titleFilter); int done = mapper.countDone(orgId); return new ReviewPage(items, total, done, safePage, safeSize); } /** * 목록에서 고른 여러 건에 같은 값을 한 번에 반영한다. 화면에서 비워 둔 항목은 null로 와서 * 기존 값이 유지된다 - "체크한 것 전부 0유형, 의견은 ooo" 식의 일괄 등록 요구를 위한 것이다. */ @Transactional public int updateBulk(Long orgId, BulkJudgmentRequest request) { requireOrg(orgId); if (request.ids() == null || request.ids().isEmpty()) { return 0; } return mapper.updateJudgmentBulk(orgId, request.ids(), blankToNull(request.reviewResult()), blankToNull(request.judgedKoglType()), blankToNull(request.opinion())); } /** 목록에서 고른 여러 건을 지운다. */ @Transactional public int deleteBulk(Long orgId, List ids) { requireOrg(orgId); if (ids == null || ids.isEmpty()) { return 0; } return mapper.deleteByIds(orgId, ids); } private String blankToNull(String value) { return value == null || value.isBlank() ? null : value; } public ReviewItem get(Long orgId, Long itemId) { requireOrg(orgId); return requireItem(orgId, itemId); } @Transactional public ReviewItem updateJudgment(Long orgId, Long itemId, JudgmentRequest request) { requireOrg(orgId); int updated = mapper.updateJudgment(orgId, itemId, request.hasAttachment(), request.koglAttached(), request.koglType(), request.aiType(), request.openable(), request.reviewMajor(), request.reviewMinor(), request.reviewResult(), request.judgedKoglType(), request.judgedAiType(), request.opinion(), request.lawyerNote(), request.needsProcessing()); if (updated == 0) { throw new ReviewNotFoundException("게시물을 찾을 수 없습니다: " + 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 ReviewNotFoundException("게시물을 찾을 수 없습니다: " + itemId); } } /** 원본 파일과 같은 헤더 배치(1행 그룹제목, 2행 컬럼헤더, 3행부터 데이터)로 xlsx를 만든다. */ public byte[] downloadWorkbook(Long orgId) { requireOrg(orgId); List items = mapper.findAllByOrg(orgId); try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { Sheet sheet = wb.createSheet(ReviewParser.PRIMARY_SHEET_NAME); Row groupRow = sheet.createRow(0); groupRow.createCell(0).setCellValue("게시물 정보(조사원)"); groupRow.createCell(15).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 (ReviewItem 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.getBoardPath()); setCell(row, 4, item.getBoardName()); setCell(row, 5, item.getPostTitle()); setCell(row, 6, item.getUrl()); setCell(row, 7, item.getPostRegistered()); setCell(row, 8, item.getProducedDate()); setCell(row, 9, item.getPublishedDate()); setCell(row, 10, item.getHasAttachment()); setCell(row, 11, item.getKoglAttached()); setCell(row, 12, item.getKoglType()); setCell(row, 13, item.getAiType()); setCell(row, 14, item.getSurveyorNote()); setCell(row, 15, item.getOpenable()); setCell(row, 16, item.getReviewMajor()); setCell(row, 17, item.getReviewMinor()); setCell(row, 18, item.getReviewResult()); setCell(row, 19, item.getJudgedKoglType()); setCell(row, 20, item.getJudgedAiType()); setCell(row, 21, item.getOpinion()); // 22 = W, 빈 열 setCell(row, 23, item.getLawyerNote()); setCell(row, 24, item.getNeedsProcessing()); } 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 String normalizeKeyword(String keyword) { return (keyword == null || keyword.isBlank()) ? null : keyword.trim(); } private void requireOrg(Long orgId) { if (orgMapper.findById(orgId) == null) { throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId); } } private ReviewItem requireItem(Long orgId, Long itemId) { ReviewItem item = mapper.findById(orgId, itemId); if (item == null) { throw new ReviewNotFoundException("게시물을 찾을 수 없습니다: " + itemId); } return item; } }