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.provision;
import kr.itn.itnhub.config.MattermostProperties;
import kr.itn.itnhub.mattermost.MattermostClient;
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<String> 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 (RuntimeException e) {
// MattermostException(서버 API 실패)뿐 아니라, 채널 생성/복구 직후 ID를 저장하는
// mapper 호출이 던지는 DataAccessException 등도 여기서 잡는다. Error나 checked
// exception은 의도적으로 넓히지 않는다 - 이 두 경우는 채널 하나만 FAILED로
// 강등시키고 메시지에 남긴 뒤, 나머지 채널은 계속 시도되게 해야 한다.
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)을 결정하는 유일한 지점.
* switch 식(expression)으로 작성해 ChannelKind에 값이 추가되고도 분기를
* 채우지 않으면 컴파일이 실패하도록 강제한다 - if/else였다면 새 값이
* 조용히 else(법률검토) 분기로 흘러 들어가는 것을 막지 못했을 것이다.
*/
private void applyChannelId(Long orgId, ChannelKind kind, String channelId) {
// 반환값(영향받은 행 수)은 쓰지 않지만, switch를 식으로 유지해야
// 컴파일러가 exhaustiveness를 검사한다 - 문(statement) 형태의
// switch/if-else는 이 검사를 강제하지 않는다.
var ignored = switch (kind) {
case MJ -> mapper.updateChannelIdMj(orgId, channelId);
case LAW -> mapper.updateChannelIdLaw(orgId, channelId);
};
}
}