package kr.itn.itnhub.mattermost; import com.fasterxml.jackson.databind.JsonNode; import kr.itn.itnhub.config.MattermostProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientException; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; @Component public class MattermostRestClient implements MattermostClient { private final RestClient rest; private final String teamId; @Autowired public MattermostRestClient(MattermostProperties props) { this(props, RestClient.builder()); } /** * 전송 계층(transport)을 직접 지정할 수 있는 생성자. 운영 코드에서는 사용하지 않으며, * 테스트 등 별도의 {@link RestClient.Builder} 설정이 필요한 호출자를 위한 확장 지점이다. */ MattermostRestClient(MattermostProperties props, RestClient.Builder builder) { this.teamId = props.teamId(); this.rest = builder .baseUrl(props.baseUrl()) .defaultHeader("Authorization", "Bearer " + props.token()) .build(); } @Override public Optional findChannelIdByInternalName(String internalName) { try { JsonNode body = rest.get() .uri("/api/v4/teams/{teamId}/channels/name/{name}", teamId, internalName) .retrieve() .onStatus(status -> status.value() == 404, (req, res) -> { throw new NotFound(); }) .body(JsonNode.class); return idOf(body); } catch (NotFound e) { return Optional.empty(); } catch (RestClientException e) { throw new MattermostException("채널 조회 실패: " + internalName, e); } } @Override public Optional findChannelIdByDisplayName(String displayName) { try { JsonNode channels = rest.get() .uri(uri -> uri.path("/api/v4/users/me/teams/{teamId}/channels") .build(teamId)) .retrieve() .body(JsonNode.class); if (channels == null || !channels.isArray()) { return Optional.empty(); } for (JsonNode channel : channels) { if (displayName.equals(channel.path("display_name").asText())) { return idOf(channel); } } return Optional.empty(); } catch (RestClientException e) { throw new MattermostException("채널 목록 조회 실패", e); } } @Override public String createPrivateChannel(String internalName, String displayName) { Map body = new LinkedHashMap<>(); body.put("team_id", teamId); body.put("name", internalName); body.put("display_name", displayName); body.put("type", "P"); body.put("purpose", ""); body.put("header", ""); try { JsonNode created = rest.post() .uri("/api/v4/channels") .contentType(MediaType.APPLICATION_JSON) .body(body) .retrieve() .body(JsonNode.class); return idOf(created).orElseThrow(() -> new MattermostException("채널 생성 응답에 id가 없습니다: " + displayName)); } catch (RestClientException e) { throw new MattermostException("채널 생성 실패: " + displayName, e); } } private Optional idOf(JsonNode node) { if (node == null) { return Optional.empty(); } String id = node.path("id").asText(""); return id.isBlank() ? Optional.empty() : Optional.of(id); } /** 404를 정상 흐름으로 되돌리기 위한 내부 신호. 밖으로 새지 않는다. */ private static final class NotFound extends RuntimeException { } }