package kr.itn.itnhub.provision; import kr.itn.itnhub.config.MattermostProperties; import kr.itn.itnhub.mattermost.MattermostClient; import kr.itn.itnhub.org.OrgNotFoundException; import kr.itn.itnhub.org.Organization; import kr.itn.itnhub.org.OrganizationMapper; import kr.itn.itnhub.stage.StageHistoryMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.Optional; /** * 기관의 채널 2개(문정원/법률검토)를 보장한다. VBA M07의 이식. * * 판정 순서 (한쪽 채널당): * 1. DB에 Channel ID가 있으면 그대로 둔다 → CACHED * 2. 서버에 내부명으로 채널이 있으면 ID를 가져온다 → RECOVERED * 3. 표시명으로 있으면 ID를 가져온다 → RECOVERED * 4. 없을 때만 새로 만든다 → CREATED * * 트랜잭션을 쓰지 않는다. 외부 API 호출이라 롤백이 불가능하고, * 한쪽이 실패해도 성공한 쪽의 ID는 반드시 남아야 재시도가 안전해지기 때문이다. * 절대 @Transactional을 붙이지 말 것. */ @Service public class ChannelProvisionService { private static final Logger log = LoggerFactory.getLogger(ChannelProvisionService.class); private final MattermostClient mattermost; private final OrganizationMapper mapper; private final MattermostProperties props; private final StageHistoryMapper stageHistoryMapper; public ChannelProvisionService(MattermostClient mattermost, OrganizationMapper mapper, MattermostProperties props, StageHistoryMapper stageHistoryMapper) { this.mattermost = mattermost; this.mapper = mapper; this.props = props; this.stageHistoryMapper = stageHistoryMapper; } public ProvisionResult provision(Long orgId) { Organization org = mapper.findById(orgId); if (org == null) { throw new OrgNotFoundException("기관을 찾을 수 없습니다: " + orgId); } if (!org.hasApplicantAssigned()) { return new ProvisionResult( ProvisionOutcome.FAILED, ProvisionOutcome.FAILED, "신청기관 담당자가 지정되지 않아 채널을 만들 수 없습니다."); } StringBuilder messages = new StringBuilder(); ProvisionOutcome mj = ensure(org, ChannelKind.MJ, org.getChannelIdMj(), props.channelNameMj(), messages); ProvisionOutcome law = ensure(org, ChannelKind.LAW, org.getChannelIdLaw(), props.channelNameLaw(), messages); maybeStartStage(org.getId()); return new ProvisionResult(mj, law, messages.toString().trim()); } /** * 채널 2개가 이번 호출로 처음 모두 갖춰지고 아직 어떤 단계도 시작되지 않았다면 * 진행단계를 1(신청)로 자동 시작한다. 이미 단계가 있는 기관은 건드리지 않는다 * (재실행 안전성 - 사람이 수동으로 바꿔둔 단계를 채널 재확인이 되돌리면 안 된다). * *
이 훅은 채널 생성이 이미 끝난 뒤에 실행되므로, 실패해도 provision() 결과에는 * 영향을 주지 않고 경고 로그만 남긴다 - stage_history 문제로 채널 생성 자체를 * 실패로 보고하면 재시도만 반복하게 만든다.
*/ private void maybeStartStage(Long orgId) { try { Organization current = mapper.findById(orgId); if (current == null || current.getStage() != null) { return; } if (filled(current.getChannelIdMj()) && filled(current.getChannelIdLaw())) { mapper.updateStage(orgId, 1); stageHistoryMapper.insert(orgId, 1); } } catch (RuntimeException e) { log.warn("진행단계 자동 시작 실패 org={}", orgId, e); } } private static boolean filled(String value) { return value != null && !value.isBlank(); } private ProvisionOutcome ensure(Organization org, ChannelKind kind, String storedChannelId, String label, StringBuilder messages) { if (storedChannelId != null && !storedChannelId.isBlank()) { return ProvisionOutcome.CACHED; } String internalName = ChannelNaming.internalName( org.getOrgNo(), org.getOrgName(), kind); String displayName = ChannelNaming.displayName( org.getOrgNo(), org.getOrgName(), label); try { Optional