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.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와 같은 원칙) - 같은 사람이
* 다시 올라오면 새로 만들지 않고 기존 담당자를 찾아 재사용한다.
*
* <p>엑셀 파싱은 DB와 무관하므로 트랜잭션 밖에서 수행한다(kr.itn.itnhub.seed.SeedService와
* 동일한 이유 - 같은 빈의 @Transactional 메서드를 self-invocation으로 호출하면 프록시를
* 우회해 트랜잭션이 적용되지 않으므로 TransactionTemplate으로 감싼다).</p>
*/
@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;
}
}