feat: 채널 생성 시 담당자 안내 최초 게시글 자동 발행·핀 고정
@aa1a63f108e14b1d2631fd20325f1f1cc33dacf7
--- src/main/java/kr/itn/itnhub/provision/ChannelProvisionService.java
+++ src/main/java/kr/itn/itnhub/provision/ChannelProvisionService.java
... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 |
package kr.itn.itnhub.provision; |
| 2 | 2 |
|
| 3 | 3 |
import kr.itn.itnhub.config.MattermostProperties; |
| 4 |
+import kr.itn.itnhub.contact.Contact; |
|
| 4 | 5 |
import kr.itn.itnhub.mattermost.MattermostClient; |
| 5 | 6 |
import kr.itn.itnhub.org.OrgNotFoundException; |
| 6 | 7 |
import kr.itn.itnhub.org.Organization; |
... | ... | @@ -10,6 +11,7 @@ |
| 10 | 11 |
import org.slf4j.LoggerFactory; |
| 11 | 12 |
import org.springframework.stereotype.Service; |
| 12 | 13 |
|
| 14 |
+import java.util.List; |
|
| 13 | 15 |
import java.util.Optional; |
| 14 | 16 |
|
| 15 | 17 |
/** |
... | ... | @@ -127,6 +129,7 @@ |
| 127 | 129 |
|
| 128 | 130 |
String created = mattermost.createPrivateChannel(internalName, displayName); |
| 129 | 131 |
applyChannelId(org.getId(), kind, created); |
| 132 |
+ postIntro(org, kind, label, created, messages); |
|
| 130 | 133 |
return ProvisionOutcome.CREATED; |
| 131 | 134 |
|
| 132 | 135 |
} catch (RuntimeException e) {
|
... | ... | @@ -142,6 +145,63 @@ |
| 142 | 145 |
} |
| 143 | 146 |
|
| 144 | 147 |
/** |
| 148 |
+ * 새로 생성된 채널에만 담당자 안내(최초 게시글)를 올리고 핀 고정한다. VBA M10의 이식. |
|
| 149 |
+ * CACHED/RECOVERED 채널에는 올리지 않는다 - 재실행할 때마다 안내글이 쌓이면 안 되고, |
|
| 150 |
+ * 기존 채널에는 이미 안내글이 있을 수 있기 때문이다. |
|
| 151 |
+ * |
|
| 152 |
+ * <p>실패해도 채널 생성 결과(CREATED)를 뒤집지 않는다 - 채널은 이미 만들어졌으므로 |
|
| 153 |
+ * 안내글만 나중에 수동으로 올리면 되는 문제다. 메시지에 경고만 남긴다.</p> |
|
| 154 |
+ */ |
|
| 155 |
+ private void postIntro(Organization org, ChannelKind kind, String label, |
|
| 156 |
+ String channelId, StringBuilder messages) {
|
|
| 157 |
+ try {
|
|
| 158 |
+ String postId = mattermost.createPost(channelId, introMessage(org, kind), List.of()); |
|
| 159 |
+ if (postId != null && !postId.isBlank()) {
|
|
| 160 |
+ mattermost.pinPost(postId); |
|
| 161 |
+ } |
|
| 162 |
+ } catch (RuntimeException e) {
|
|
| 163 |
+ log.warn("최초 게시글 등록 실패 org={} channel={}", org.getOrgName(), channelId, e);
|
|
| 164 |
+ messages.append("[").append(label).append("] 채널은 생성됐지만 안내글 등록에 실패했습니다: ")
|
|
| 165 |
+ .append(e.getMessage()).append(System.lineSeparator()); |
|
| 166 |
+ } |
|
| 167 |
+ } |
|
| 168 |
+ |
|
| 169 |
+ private String introMessage(Organization org, ChannelKind kind) {
|
|
| 170 |
+ StringBuilder sb = new StringBuilder(); |
|
| 171 |
+ sb.append("안녕하세요.\n");
|
|
| 172 |
+ sb.append(org.getOrgName()) |
|
| 173 |
+ .append(kind == ChannelKind.MJ ? " 업무협의 채널입니다.\n" : " 법률검토 채널입니다.\n"); |
|
| 174 |
+ |
|
| 175 |
+ appendContactBlock(sb, "신청기관 담당자", "부서", org.getApplicant()); |
|
| 176 |
+ appendContactBlock(sb, "문정원 담당자", "부서", org.getMj()); |
|
| 177 |
+ appendContactBlock(sb, "㈜아이티앤 담당자", "부서", org.getItn()); |
|
| 178 |
+ appendContactBlock(sb, "담당 변호사", "소속", org.getLawyer()); |
|
| 179 |
+ |
|
| 180 |
+ return sb.toString().stripTrailing(); |
|
| 181 |
+ } |
|
| 182 |
+ |
|
| 183 |
+ /** 배정 안 된 역할은 블록 자체를 생략한다. 빈 항목은 '-'로 채운다. */ |
|
| 184 |
+ private void appendContactBlock(StringBuilder sb, String title, String orgLabel, Contact contact) {
|
|
| 185 |
+ if (contact == null) {
|
|
| 186 |
+ return; |
|
| 187 |
+ } |
|
| 188 |
+ String orgValue = "소속".equals(orgLabel) |
|
| 189 |
+ ? orDash(contact.getAffiliation()) |
|
| 190 |
+ : orDash(contact.getDeptName()); |
|
| 191 |
+ sb.append('\n')
|
|
| 192 |
+ .append("■ ").append(title).append('\n')
|
|
| 193 |
+ .append(orgLabel).append(" : ").append(orgValue).append('\n')
|
|
| 194 |
+ .append("담당자 : ").append(orDash(contact.getName())).append('\n')
|
|
| 195 |
+ .append("직위 : ").append(orDash(contact.getTitle())).append('\n')
|
|
| 196 |
+ .append("연락처 : ").append(orDash(contact.getPhone())).append('\n')
|
|
| 197 |
+ .append("이메일 : ").append(orDash(contact.getEmail())).append('\n');
|
|
| 198 |
+ } |
|
| 199 |
+ |
|
| 200 |
+ private static String orDash(String value) {
|
|
| 201 |
+ return (value == null || value.isBlank()) ? "-" : value; |
|
| 202 |
+ } |
|
| 203 |
+ |
|
| 204 |
+ /** |
|
| 145 | 205 |
* Channel ID를 저장할 컬럼(mj/law)을 결정하는 유일한 지점. |
| 146 | 206 |
* switch 식(expression)으로 작성해 ChannelKind에 값이 추가되고도 분기를 |
| 147 | 207 |
* 채우지 않으면 컴파일이 실패하도록 강제한다 - if/else였다면 새 값이 |
--- src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
+++ src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
... | ... | @@ -16,6 +16,7 @@ |
| 16 | 16 |
import org.springframework.dao.DataIntegrityViolationException; |
| 17 | 17 |
import org.springframework.jdbc.core.JdbcTemplate; |
| 18 | 18 |
|
| 19 |
+import java.util.List; |
|
| 19 | 20 |
import java.util.Optional; |
| 20 | 21 |
|
| 21 | 22 |
import static org.assertj.core.api.Assertions.assertThat; |
... | ... | @@ -24,6 +25,7 @@ |
| 24 | 25 |
import static org.mockito.Mockito.doThrow; |
| 25 | 26 |
import static org.mockito.Mockito.never; |
| 26 | 27 |
import static org.mockito.Mockito.verify; |
| 28 |
+import static org.mockito.Mockito.times; |
|
| 27 | 29 |
import static org.mockito.Mockito.when; |
| 28 | 30 |
|
| 29 | 31 |
class ChannelProvisionServiceTest extends AbstractDbTest {
|
... | ... | @@ -347,4 +349,59 @@ |
| 347 | 349 |
"select count(*) from stage_history where org_id = ?", Integer.class, orgId)) |
| 348 | 350 |
.isEqualTo(1); |
| 349 | 351 |
} |
| 352 |
+ @Test |
|
| 353 |
+ void 새로_생성된_채널에는_담당자_안내글을_올리고_핀_고정한다() {
|
|
| 354 |
+ when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); |
|
| 355 |
+ when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); |
|
| 356 |
+ when(mattermost.createPrivateChannel("org-001-mj", "001_국제방송교류재단 (문정원)"))
|
|
| 357 |
+ .thenReturn("id-mj");
|
|
| 358 |
+ when(mattermost.createPrivateChannel("org-001-law", "001_국제방송교류재단 (법률검토)"))
|
|
| 359 |
+ .thenReturn("id-law");
|
|
| 360 |
+ when(mattermost.createPost(eq("id-mj"), anyString(), eq(List.of()))).thenReturn("post-mj");
|
|
| 361 |
+ when(mattermost.createPost(eq("id-law"), anyString(), eq(List.of()))).thenReturn("post-law");
|
|
| 362 |
+ |
|
| 363 |
+ service.provision(orgId); |
|
| 364 |
+ |
|
| 365 |
+ org.mockito.ArgumentCaptor<String> mjMsg = org.mockito.ArgumentCaptor.forClass(String.class); |
|
| 366 |
+ verify(mattermost).createPost(eq("id-mj"), mjMsg.capture(), eq(List.of()));
|
|
| 367 |
+ assertThat(mjMsg.getValue()) |
|
| 368 |
+ .contains("국제방송교류재단 업무협의 채널입니다.")
|
|
| 369 |
+ .contains("■ 신청기관 담당자")
|
|
| 370 |
+ .contains("담당자 : 송민지");
|
|
| 371 |
+ |
|
| 372 |
+ org.mockito.ArgumentCaptor<String> lawMsg = org.mockito.ArgumentCaptor.forClass(String.class); |
|
| 373 |
+ verify(mattermost).createPost(eq("id-law"), lawMsg.capture(), eq(List.of()));
|
|
| 374 |
+ assertThat(lawMsg.getValue()).contains("국제방송교류재단 법률검토 채널입니다.");
|
|
| 375 |
+ |
|
| 376 |
+ verify(mattermost).pinPost("post-mj");
|
|
| 377 |
+ verify(mattermost).pinPost("post-law");
|
|
| 378 |
+ } |
|
| 379 |
+ |
|
| 380 |
+ @Test |
|
| 381 |
+ void 이미_있는_채널에는_안내글을_올리지_않는다() {
|
|
| 382 |
+ mapper.updateChannelIdMj(orgId, "id-mj"); |
|
| 383 |
+ mapper.updateChannelIdLaw(orgId, "id-law"); |
|
| 384 |
+ |
|
| 385 |
+ service.provision(orgId); |
|
| 386 |
+ |
|
| 387 |
+ verify(mattermost, never()).createPost(anyString(), anyString(), eq(List.of())); |
|
| 388 |
+ verify(mattermost, never()).pinPost(anyString()); |
|
| 389 |
+ } |
|
| 390 |
+ |
|
| 391 |
+ @Test |
|
| 392 |
+ void 안내글_등록이_실패해도_채널_생성_결과는_유지된다() {
|
|
| 393 |
+ when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); |
|
| 394 |
+ when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); |
|
| 395 |
+ when(mattermost.createPrivateChannel(anyString(), anyString())) |
|
| 396 |
+ .thenReturn("id-mj", "id-law");
|
|
| 397 |
+ when(mattermost.createPost(anyString(), anyString(), eq(List.of()))) |
|
| 398 |
+ .thenThrow(new MattermostException("게시글 실패"));
|
|
| 399 |
+ |
|
| 400 |
+ ProvisionResult result = service.provision(orgId); |
|
| 401 |
+ |
|
| 402 |
+ assertThat(result.mj()).isEqualTo(ProvisionOutcome.CREATED); |
|
| 403 |
+ assertThat(result.law()).isEqualTo(ProvisionOutcome.CREATED); |
|
| 404 |
+ assertThat(result.message()).contains("안내글 등록에 실패");
|
|
| 405 |
+ verify(mattermost, times(2)).createPost(anyString(), anyString(), eq(List.of())); |
|
| 406 |
+ } |
|
| 350 | 407 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?