ITN Dev 07-22
feat: 기관 조회/수정/채널생성/시드 REST API 추가
@60c33c25051906d1b4e4cc8a5aff764b6e26d4d5
 
src/main/java/kr/itn/itnhub/org/ContactRequest.java (added)
+++ src/main/java/kr/itn/itnhub/org/ContactRequest.java
@@ -0,0 +1,12 @@
+package kr.itn.itnhub.org;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+
+public record ContactRequest(
+        @NotBlank String deptName,
+        @NotBlank String managerName,
+        String managerTitle,
+        @NotBlank String managerPhone,
+        @NotBlank @Email String managerEmail) {
+}
 
src/main/java/kr/itn/itnhub/org/OrgResponse.java (added)
+++ src/main/java/kr/itn/itnhub/org/OrgResponse.java
@@ -0,0 +1,30 @@
+package kr.itn.itnhub.org;
+
+public record OrgResponse(
+        Long id,
+        String orgNo,
+        String orgName,
+        OrgStatus status,
+        String deptName,
+        String managerName,
+        String managerTitle,
+        String managerPhone,
+        String managerEmail,
+        String channelIdMj,
+        String channelIdLaw) {
+
+    public static OrgResponse of(Organization org) {
+        return new OrgResponse(
+                org.getId(),
+                org.getOrgNo(),
+                org.getOrgName(),
+                org.getStatus(),
+                org.getDeptName(),
+                org.getManagerName(),
+                org.getManagerTitle(),
+                org.getManagerPhone(),
+                org.getManagerEmail(),
+                org.getChannelIdMj(),
+                org.getChannelIdLaw());
+    }
+}
 
src/main/java/kr/itn/itnhub/org/OrganizationController.java (added)
+++ src/main/java/kr/itn/itnhub/org/OrganizationController.java
@@ -0,0 +1,48 @@
+package kr.itn.itnhub.org;
+
+import jakarta.validation.Valid;
+import kr.itn.itnhub.provision.ChannelProvisionService;
+import kr.itn.itnhub.provision.ProvisionResult;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/orgs")
+public class OrganizationController {
+
+    private final OrganizationService orgService;
+    private final ChannelProvisionService provisionService;
+
+    public OrganizationController(OrganizationService orgService,
+                                  ChannelProvisionService provisionService) {
+        this.orgService = orgService;
+        this.provisionService = provisionService;
+    }
+
+    @GetMapping
+    public List<OrgResponse> list() {
+        return orgService.findAll();
+    }
+
+    @PutMapping("/{id}/contact")
+    public OrgResponse updateContact(@PathVariable Long id,
+                                     @Valid @RequestBody ContactRequest request) {
+        return orgService.updateContact(id, request);
+    }
+
+    /**
+     * 채널 2개를 보장한다. 부분 실패도 200으로 돌려주고 본문의 mj/law로 판단하게 한다.
+     * 한쪽만 성공한 상태 자체가 정상적인 중간 상태이기 때문이다.
+     */
+    @PostMapping("/{id}/channels")
+    public ProvisionResult provision(@PathVariable Long id) {
+        return provisionService.provision(id);
+    }
+}
 
src/main/java/kr/itn/itnhub/org/OrganizationService.java (added)
+++ src/main/java/kr/itn/itnhub/org/OrganizationService.java
@@ -0,0 +1,36 @@
+package kr.itn.itnhub.org;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+@Service
+public class OrganizationService {
+
+    private final OrganizationMapper mapper;
+
+    public OrganizationService(OrganizationMapper mapper) {
+        this.mapper = mapper;
+    }
+
+    public List<OrgResponse> findAll() {
+        return mapper.findAll().stream().map(OrgResponse::of).toList();
+    }
+
+    @Transactional
+    public OrgResponse updateContact(Long id, ContactRequest request) {
+        Organization org = mapper.findById(id);
+        if (org == null) {
+            throw new IllegalArgumentException("기관을 찾을 수 없습니다: " + id);
+        }
+        org.setDeptName(request.deptName());
+        org.setManagerName(request.managerName());
+        org.setManagerTitle(request.managerTitle());
+        org.setManagerPhone(request.managerPhone());
+        org.setManagerEmail(request.managerEmail());
+        mapper.updateContact(org);
+
+        return OrgResponse.of(mapper.findById(id));
+    }
+}
 
src/main/java/kr/itn/itnhub/seed/SeedController.java (added)
+++ src/main/java/kr/itn/itnhub/seed/SeedController.java
@@ -0,0 +1,29 @@
+package kr.itn.itnhub.seed;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@RestController
+public class SeedController {
+
+    private final SeedService seedService;
+
+    public SeedController(SeedService seedService) {
+        this.seedService = seedService;
+    }
+
+    @PostMapping("/api/seed")
+    public SeedReport upload(@RequestParam("file") MultipartFile file) throws IOException {
+        if (file.isEmpty()) {
+            throw new SeedParseException("업로드된 파일이 비어 있습니다.");
+        }
+        try (InputStream in = file.getInputStream()) {
+            return seedService.seed(in);
+        }
+    }
+}
 
src/test/java/kr/itn/itnhub/org/OrganizationControllerTest.java (added)
+++ src/test/java/kr/itn/itnhub/org/OrganizationControllerTest.java
@@ -0,0 +1,131 @@
+package kr.itn.itnhub.org;
+
+import kr.itn.itnhub.AbstractDbTest;
+import kr.itn.itnhub.mattermost.MattermostClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@AutoConfigureMockMvc
+@WithMockUser(roles = "ADMIN")
+class OrganizationControllerTest extends AbstractDbTest {
+
+    @Autowired
+    MockMvc mvc;
+
+    @Autowired
+    OrganizationMapper mapper;
+
+    @Autowired
+    JdbcTemplate jdbc;
+
+    @MockBean
+    MattermostClient mattermost;
+
+    private Long orgId;
+
+    @BeforeEach
+    void setUp() {
+        jdbc.update("delete from organization");
+
+        Organization org = new Organization();
+        org.setOrgNo("001");
+        org.setOrgName("국제방송교류재단");
+        org.setChannelSlug("001");
+        mapper.upsertBySeed(org);
+
+        orgId = mapper.findAll().get(0).getId();
+    }
+
+    @Test
+    void 기관_목록을_상태와_함께_돌려준다() throws Exception {
+        mvc.perform(get("/api/orgs"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$[0].orgNo").value("001"))
+                .andExpect(jsonPath("$[0].orgName").value("국제방송교류재단"))
+                .andExpect(jsonPath("$[0].status").value("INFO_PENDING"));
+    }
+
+    @Test
+    void 담당자정보를_저장하면_생성가능_상태가_된다() throws Exception {
+        mvc.perform(put("/api/orgs/{id}/contact", orgId)
+                        .with(csrf())
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
+                                {
+                                  "deptName": "데이터정보화팀",
+                                  "managerName": "송민지",
+                                  "managerTitle": "과장",
+                                  "managerPhone": "02-3475-5434",
+                                  "managerEmail": "ming@arirang.com"
+                                }
+                                """))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.status").value("READY"));
+
+        assertThat(mapper.findById(orgId).getDeptName()).isEqualTo("데이터정보화팀");
+    }
+
+    @Test
+    void 필수값이_비면_400이다() throws Exception {
+        mvc.perform(put("/api/orgs/{id}/contact", orgId)
+                        .with(csrf())
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
+                                {
+                                  "deptName": "",
+                                  "managerName": "송민지",
+                                  "managerPhone": "02-3475-5434",
+                                  "managerEmail": "ming@arirang.com"
+                                }
+                                """))
+                .andExpect(status().isBadRequest());
+    }
+
+    @Test
+    void 채널생성을_요청하면_결과를_돌려준다() throws Exception {
+        Organization ready = mapper.findById(orgId);
+        ready.setDeptName("데이터정보화팀");
+        ready.setManagerName("송민지");
+        ready.setManagerTitle("과장");
+        ready.setManagerPhone("02-3475-5434");
+        ready.setManagerEmail("ming@arirang.com");
+        mapper.updateContact(ready);
+
+        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
+        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
+        when(mattermost.createPrivateChannel(anyString(), anyString())).thenReturn("chan");
+
+        mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf()))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.mj").value("CREATED"))
+                .andExpect(jsonPath("$.law").value("CREATED"));
+    }
+
+    @Test
+    void 담당자정보없이_채널생성하면_실패결과를_돌려준다() throws Exception {
+        mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf()))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.mj").value("FAILED"))
+                .andExpect(jsonPath("$.message").value(
+                        org.hamcrest.Matchers.containsString("담당자 정보")));
+    }
+}
Add a comment
List