ITN Dev 07-24
feat: 채널 생성 시 담당자 안내 최초 게시글 자동 발행·핀 고정
@aa1a63f108e14b1d2631fd20325f1f1cc33dacf7
src/main/java/kr/itn/itnhub/provision/ChannelProvisionService.java
--- src/main/java/kr/itn/itnhub/provision/ChannelProvisionService.java
+++ src/main/java/kr/itn/itnhub/provision/ChannelProvisionService.java
@@ -1,6 +1,7 @@
 package kr.itn.itnhub.provision;
 
 import kr.itn.itnhub.config.MattermostProperties;
+import kr.itn.itnhub.contact.Contact;
 import kr.itn.itnhub.mattermost.MattermostClient;
 import kr.itn.itnhub.org.OrgNotFoundException;
 import kr.itn.itnhub.org.Organization;
@@ -10,6 +11,7 @@
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Service;
 
+import java.util.List;
 import java.util.Optional;
 
 /**
@@ -127,6 +129,7 @@
 
             String created = mattermost.createPrivateChannel(internalName, displayName);
             applyChannelId(org.getId(), kind, created);
+            postIntro(org, kind, label, created, messages);
             return ProvisionOutcome.CREATED;
 
         } catch (RuntimeException e) {
@@ -142,6 +145,63 @@
     }
 
     /**
+     * 새로 생성된 채널에만 담당자 안내(최초 게시글)를 올리고 핀 고정한다. VBA M10의 이식.
+     * CACHED/RECOVERED 채널에는 올리지 않는다 - 재실행할 때마다 안내글이 쌓이면 안 되고,
+     * 기존 채널에는 이미 안내글이 있을 수 있기 때문이다.
+     *
+     * <p>실패해도 채널 생성 결과(CREATED)를 뒤집지 않는다 - 채널은 이미 만들어졌으므로
+     * 안내글만 나중에 수동으로 올리면 되는 문제다. 메시지에 경고만 남긴다.</p>
+     */
+    private void postIntro(Organization org, ChannelKind kind, String label,
+                           String channelId, StringBuilder messages) {
+        try {
+            String postId = mattermost.createPost(channelId, introMessage(org, kind), List.of());
+            if (postId != null && !postId.isBlank()) {
+                mattermost.pinPost(postId);
+            }
+        } catch (RuntimeException e) {
+            log.warn("최초 게시글 등록 실패 org={} channel={}", org.getOrgName(), channelId, e);
+            messages.append("[").append(label).append("] 채널은 생성됐지만 안내글 등록에 실패했습니다: ")
+                    .append(e.getMessage()).append(System.lineSeparator());
+        }
+    }
+
+    private String introMessage(Organization org, ChannelKind kind) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("안녕하세요.\n");
+        sb.append(org.getOrgName())
+                .append(kind == ChannelKind.MJ ? " 업무협의 채널입니다.\n" : " 법률검토 채널입니다.\n");
+
+        appendContactBlock(sb, "신청기관 담당자", "부서", org.getApplicant());
+        appendContactBlock(sb, "문정원 담당자", "부서", org.getMj());
+        appendContactBlock(sb, "㈜아이티앤 담당자", "부서", org.getItn());
+        appendContactBlock(sb, "담당 변호사", "소속", org.getLawyer());
+
+        return sb.toString().stripTrailing();
+    }
+
+    /** 배정 안 된 역할은 블록 자체를 생략한다. 빈 항목은 '-'로 채운다. */
+    private void appendContactBlock(StringBuilder sb, String title, String orgLabel, Contact contact) {
+        if (contact == null) {
+            return;
+        }
+        String orgValue = "소속".equals(orgLabel)
+                ? orDash(contact.getAffiliation())
+                : orDash(contact.getDeptName());
+        sb.append('\n')
+                .append("■ ").append(title).append('\n')
+                .append(orgLabel).append(" : ").append(orgValue).append('\n')
+                .append("담당자 : ").append(orDash(contact.getName())).append('\n')
+                .append("직위 : ").append(orDash(contact.getTitle())).append('\n')
+                .append("연락처 : ").append(orDash(contact.getPhone())).append('\n')
+                .append("이메일 : ").append(orDash(contact.getEmail())).append('\n');
+    }
+
+    private static String orDash(String value) {
+        return (value == null || value.isBlank()) ? "-" : value;
+    }
+
+    /**
      * Channel ID를 저장할 컬럼(mj/law)을 결정하는 유일한 지점.
      * switch 식(expression)으로 작성해 ChannelKind에 값이 추가되고도 분기를
      * 채우지 않으면 컴파일이 실패하도록 강제한다 - if/else였다면 새 값이
src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
--- src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
+++ src/test/java/kr/itn/itnhub/provision/ChannelProvisionServiceTest.java
@@ -16,6 +16,7 @@
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.jdbc.core.JdbcTemplate;
 
+import java.util.List;
 import java.util.Optional;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -24,6 +25,7 @@
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
 class ChannelProvisionServiceTest extends AbstractDbTest {
@@ -347,4 +349,59 @@
                 "select count(*) from stage_history where org_id = ?", Integer.class, orgId))
                 .isEqualTo(1);
     }
+    @Test
+    void 새로_생성된_채널에는_담당자_안내글을_올리고_핀_고정한다() {
+        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
+        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
+        when(mattermost.createPrivateChannel("org-001-mj", "001_국제방송교류재단 (문정원)"))
+                .thenReturn("id-mj");
+        when(mattermost.createPrivateChannel("org-001-law", "001_국제방송교류재단 (법률검토)"))
+                .thenReturn("id-law");
+        when(mattermost.createPost(eq("id-mj"), anyString(), eq(List.of()))).thenReturn("post-mj");
+        when(mattermost.createPost(eq("id-law"), anyString(), eq(List.of()))).thenReturn("post-law");
+
+        service.provision(orgId);
+
+        org.mockito.ArgumentCaptor<String> mjMsg = org.mockito.ArgumentCaptor.forClass(String.class);
+        verify(mattermost).createPost(eq("id-mj"), mjMsg.capture(), eq(List.of()));
+        assertThat(mjMsg.getValue())
+                .contains("국제방송교류재단 업무협의 채널입니다.")
+                .contains("■ 신청기관 담당자")
+                .contains("담당자 : 송민지");
+
+        org.mockito.ArgumentCaptor<String> lawMsg = org.mockito.ArgumentCaptor.forClass(String.class);
+        verify(mattermost).createPost(eq("id-law"), lawMsg.capture(), eq(List.of()));
+        assertThat(lawMsg.getValue()).contains("국제방송교류재단 법률검토 채널입니다.");
+
+        verify(mattermost).pinPost("post-mj");
+        verify(mattermost).pinPost("post-law");
+    }
+
+    @Test
+    void 이미_있는_채널에는_안내글을_올리지_않는다() {
+        mapper.updateChannelIdMj(orgId, "id-mj");
+        mapper.updateChannelIdLaw(orgId, "id-law");
+
+        service.provision(orgId);
+
+        verify(mattermost, never()).createPost(anyString(), anyString(), eq(List.of()));
+        verify(mattermost, never()).pinPost(anyString());
+    }
+
+    @Test
+    void 안내글_등록이_실패해도_채널_생성_결과는_유지된다() {
+        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
+        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
+        when(mattermost.createPrivateChannel(anyString(), anyString()))
+                .thenReturn("id-mj", "id-law");
+        when(mattermost.createPost(anyString(), anyString(), eq(List.of())))
+                .thenThrow(new MattermostException("게시글 실패"));
+
+        ProvisionResult result = service.provision(orgId);
+
+        assertThat(result.mj()).isEqualTo(ProvisionOutcome.CREATED);
+        assertThat(result.law()).isEqualTo(ProvisionOutcome.CREATED);
+        assertThat(result.message()).contains("안내글 등록에 실패");
+        verify(mattermost, times(2)).createPost(anyString(), anyString(), eq(List.of()));
+    }
 }
Add a comment
List