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.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<String> 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);
}
}
/** 팀 채널 목록 조회 시 한 페이지에 요청하는 최대 개수. */
private static final int DISPLAY_NAME_PAGE_SIZE = 200;
/**
* 무한 루프 방지용 페이지 상한. VBA 원본의 안전장치를 그대로 옮긴 것으로,
* 정상적인 팀이라면 도달할 일이 없다.
*/
private static final int DISPLAY_NAME_MAX_PAGES = 100;
/**
* 표시명으로 채널을 찾는다. {@code /users/me/teams/{teamId}/channels}는 토큰 사용자가
* "가입한" 채널만 돌려주므로 쓰지 않는다 - 이 조회는 레거시 채널(현재 내부명 규칙 이전에
* 만들어져 내부명으로는 못 찾는 채널)을 복구하기 위한 것인데, 토큰 사용자가 그 채널에
* 가입해 있지 않으면 조회가 누락되어 중복 채널이 생성된다. 대신 팀 전체 채널을
* {@code /teams/{teamId}/channels}로 페이지네이션하며 훑는다 - 이 엔드포인트는 비공개
* 채널까지 보려면 PAT가 system-admin 권한이어야 한다(README 참고).
*/
@Override
public Optional<String> findChannelIdByDisplayName(String displayName) {
try {
for (int page = 0; page < DISPLAY_NAME_MAX_PAGES; page++) {
int currentPage = page;
JsonNode channels = rest.get()
.uri(uri -> uri.path("/api/v4/teams/{teamId}/channels")
.queryParam("page", currentPage)
.queryParam("per_page", DISPLAY_NAME_PAGE_SIZE)
.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);
}
}
if (channels.size() < DISPLAY_NAME_PAGE_SIZE) {
return Optional.empty();
}
}
throw new MattermostException(
"채널 목록 조회 페이지 한도(" + DISPLAY_NAME_MAX_PAGES + ") 초과: " + displayName);
} catch (RestClientException e) {
throw new MattermostException("채널 목록 조회 실패", e);
}
}
@Override
public String createPrivateChannel(String internalName, String displayName) {
Map<String, String> 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<String> 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 {
}
}