package kr.itn.itnhub.review; import kr.itn.itnhub.org.OrgNotFoundException; import kr.itn.itnhub.org.OrganizationMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; import java.io.InputStream; import java.util.HashSet; import java.util.List; import java.util.Set; @Service public class ReviewImportService { private final ReviewParser parser; private final ReviewItemMapper mapper; private final OrganizationMapper orgMapper; private final kr.itn.itnhub.stage.StageService stageService; private final kr.itn.itnhub.code.CodeService codeService; private final TransactionTemplate transactionTemplate; public ReviewImportService(ReviewParser parser, ReviewItemMapper mapper, OrganizationMapper orgMapper, kr.itn.itnhub.stage.StageService stageService, kr.itn.itnhub.code.CodeService codeService, PlatformTransactionManager transactionManager) { this.parser = parser; this.mapper = mapper; this.orgMapper = orgMapper; this.stageService = stageService; this.codeService = codeService; this.transactionTemplate = new TransactionTemplate(transactionManager); } /** 공공누리 유형의 옛 표기를 표준 표기로 맞춘다(제4유형 → 4유형). */ private String canonicalKogl(String value) { return codeService.canonical(kr.itn.itnhub.code.Codes.KOGL_TYPE, value); } /** * 엑셀 파싱은 DB와 무관하므로 트랜잭션 밖에서 수행한다(SeedService와 동일한 이유). */ public ReviewImportReport importFile(Long orgId, InputStream xlsx) { if (orgMapper.findById(orgId) == null) { throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId); } List rows = parser.parse(xlsx); ReviewImportReport report = transactionTemplate.execute(status -> importRows(orgId, rows)); // 요구사항 [18]: 권리확인 자료가 올라오면 ⑤법률검토(권리확인)로 저절로 넘어간다. // 이미 그보다 앞서 있으면 아무 일도 하지 않는다. if (report != null && report.total() > 0) { stageService.autoAdvance(orgId, kr.itn.itnhub.stage.AutoStage.REVIEW_STARTED, kr.itn.itnhub.stage.AutoStage.REASON_REVIEW_STARTED); } return report; } private ReviewImportReport importRows(Long orgId, List rows) { // 이미 DB에 있는 순번 + 이번 파일에서 먼저 처리된 순번을 모두 "이미 존재함"으로 취급한다 // (SeedService.seedRows와 동일한 관례 - 같은 순번이 파일에 중복돼 있어도 두 번째는 갱신으로 집계). Set seen = new HashSet<>(mapper.findSeqsByOrg(orgId)); int created = 0; int updated = 0; for (ReviewRow row : rows) { if (seen.add(row.seq())) { created++; } else { updated++; } mapper.upsertFromFile(toReviewItem(orgId, row)); } return new ReviewImportReport(created, updated, rows.size()); } private ReviewItem toReviewItem(Long orgId, ReviewRow row) { ReviewItem item = new ReviewItem(); item.setOrgId(orgId); item.setSeq(row.seq()); item.setSiteName(row.siteName()); item.setCategory(row.category()); item.setBoardPath(row.boardPath()); item.setBoardName(row.boardName()); item.setPostTitle(row.postTitle()); item.setUrl(row.url()); item.setPostRegistered(row.postRegistered()); item.setProducedDate(row.producedDate()); item.setPublishedDate(row.publishedDate()); item.setHasAttachment(row.hasAttachment()); item.setKoglAttached(row.koglAttached()); // 옛 표기(제4유형)로 적혀 온 값은 표준 표기(4유형)로 맞춰 넣는다. item.setKoglType(canonicalKogl(row.koglType())); item.setAiType(row.aiType()); item.setSurveyorNote(row.surveyorNote()); item.setOpenable(row.openable()); item.setReviewMajor(row.reviewMajor()); item.setReviewMinor(row.reviewMinor()); item.setReviewResult(row.reviewResult()); item.setJudgedKoglType(canonicalKogl(row.judgedKoglType())); item.setJudgedAiType(row.judgedAiType()); item.setOpinion(row.opinion()); item.setLawyerNote(row.lawyerNote()); item.setNeedsProcessing(row.needsProcessing()); return item; } }