package kr.itn.itnhub.provision; import kr.itn.itnhub.config.MattermostProperties; import kr.itn.itnhub.mattermost.MattermostClient; import kr.itn.itnhub.mattermost.MattermostException; import kr.itn.itnhub.org.Organization; import kr.itn.itnhub.org.OrganizationMapper; 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; public ChannelProvisionService(MattermostClient mattermost, OrganizationMapper mapper, MattermostProperties props) { this.mattermost = mattermost; this.mapper = mapper; this.props = props; } public ProvisionResult provision(Long orgId) { Organization org = mapper.findById(orgId); if (org == null) { throw new IllegalArgumentException("기관을 찾을 수 없습니다: " + orgId); } if (!org.hasRequiredContact()) { 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); return new ProvisionResult(mj, law, messages.toString().trim()); } 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 recovered = mattermost.findChannelIdByInternalName(internalName); if (recovered.isEmpty()) { recovered = mattermost.findChannelIdByDisplayName(displayName); } if (recovered.isPresent()) { applyChannelId(org.getId(), kind, recovered.get()); return ProvisionOutcome.RECOVERED; } String created = mattermost.createPrivateChannel(internalName, displayName); applyChannelId(org.getId(), kind, created); return ProvisionOutcome.CREATED; } catch (MattermostException e) { log.warn("채널 처리 실패 org={} channel={}", org.getOrgName(), displayName, e); messages.append("[").append(label).append("] ") .append(e.getMessage()).append(System.lineSeparator()); return ProvisionOutcome.FAILED; } } /** * Channel ID를 저장할 컬럼(mj/law)을 결정하는 유일한 지점. * 문자열 키 기반 동적 분기 대신 정적 타입(ChannelKind)으로만 분기하여, * 잘못된 컬럼에 쓰는 실수를 컴파일 타임에 막는다. */ private void applyChannelId(Long orgId, ChannelKind kind, String channelId) { if (kind == ChannelKind.MJ) { mapper.updateChannelIdMj(orgId, channelId); } else { mapper.updateChannelIdLaw(orgId, channelId); } } }