package kr.itn.itnhub.contact; import kr.itn.itnhub.org.Organization; 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; /** * 회원정보 xlsx를 읽어 담당자를 등록/갱신하고, 신청기관 담당자는 같은 이름의 기관에 * 자동 배정한다. 재업로드해도 안전해야 한다(SeedService와 같은 원칙) - 같은 사람이 * 다시 올라오면 새로 만들지 않고 기존 담당자를 찾아 재사용한다. * *
엑셀 파싱은 DB와 무관하므로 트랜잭션 밖에서 수행한다(kr.itn.itnhub.seed.SeedService와 * 동일한 이유 - 같은 빈의 @Transactional 메서드를 self-invocation으로 호출하면 프록시를 * 우회해 트랜잭션이 적용되지 않으므로 TransactionTemplate으로 감싼다).
*/ @Service public class MemberImportService { private final MemberDirectoryParser parser; private final ContactMapper contactMapper; private final OrganizationMapper orgMapper; private final TransactionTemplate transactionTemplate; public MemberImportService(MemberDirectoryParser parser, ContactMapper contactMapper, OrganizationMapper orgMapper, PlatformTransactionManager transactionManager) { this.parser = parser; this.contactMapper = contactMapper; this.orgMapper = orgMapper; this.transactionTemplate = new TransactionTemplate(transactionManager); } public MemberImportReport importFile(InputStream xlsx) { MemberParseResult parsed = parser.parse(xlsx); return transactionTemplate.execute(status -> importRows(parsed)); } private MemberImportReport importRows(MemberParseResult parsed) { int created = 0; int updated = 0; int assigned = 0; for (MemberRow row : parsed.rows()) { Contact existing = contactMapper.findMatching( row.category(), row.name(), row.phone(), row.email()); Contact contact; if (existing == null) { contact = new Contact(); contact.setCategory(row.category()); contact.setName(row.name()); contact.setAffiliation(row.affiliation()); contact.setDeptName(row.deptName()); contact.setTitle(row.title()); contact.setPhone(row.phone()); contact.setEmail(row.email()); contactMapper.insert(contact); created++; } else { fillMissingFields(existing, row); contactMapper.update(existing); updated++; contact = existing; } if ("APPLICANT".equals(row.category()) && tryAssignApplicant(row, contact)) { assigned++; } } return new MemberImportReport(created, updated, parsed.skipped(), assigned); } /** 기존 담당자의 소속/부서/직급은 이미 값이 있으면 건드리지 않고, 비어 있을 때만 채운다. */ private void fillMissingFields(Contact existing, MemberRow row) { if (existing.getAffiliation() == null && row.affiliation() != null) { existing.setAffiliation(row.affiliation()); } if (existing.getDeptName() == null && row.deptName() != null) { existing.setDeptName(row.deptName()); } if (existing.getTitle() == null && row.title() != null) { existing.setTitle(row.title()); } } /** 기관명이 정확히 일치하고 아직 신청기관 담당자가 배정되지 않은 기관에만 배정한다. */ private boolean tryAssignApplicant(MemberRow row, Contact contact) { if (row.affiliation() == null) { return false; } Organization org = orgMapper.findByOrgName(row.affiliation()); if (org == null || org.getApplicantContactId() != null) { return false; } orgMapper.updateAssignments(org.getId(), contact.getId(), org.getMjContactId(), org.getLawyerContactId(), org.getLawyerAssignedDate()); return true; } }