feat: 기관 조회/수정/채널생성/시드 REST API 추가
@60c33c25051906d1b4e4cc8a5aff764b6e26d4d5
+++ src/main/java/kr/itn/itnhub/org/ContactRequest.java
... | ... | @@ -0,0 +1,12 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.Email; | |
| 4 | +import jakarta.validation.constraints.NotBlank; | |
| 5 | + | |
| 6 | +public record ContactRequest( | |
| 7 | + @NotBlank String deptName, | |
| 8 | + @NotBlank String managerName, | |
| 9 | + String managerTitle, | |
| 10 | + @NotBlank String managerPhone, | |
| 11 | + @NotBlank @Email String managerEmail) { | |
| 12 | +} |
+++ src/main/java/kr/itn/itnhub/org/OrgResponse.java
... | ... | @@ -0,0 +1,30 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +public record OrgResponse( | |
| 4 | + Long id, | |
| 5 | + String orgNo, | |
| 6 | + String orgName, | |
| 7 | + OrgStatus status, | |
| 8 | + String deptName, | |
| 9 | + String managerName, | |
| 10 | + String managerTitle, | |
| 11 | + String managerPhone, | |
| 12 | + String managerEmail, | |
| 13 | + String channelIdMj, | |
| 14 | + String channelIdLaw) { | |
| 15 | + | |
| 16 | + public static OrgResponse of(Organization org) { | |
| 17 | + return new OrgResponse( | |
| 18 | + org.getId(), | |
| 19 | + org.getOrgNo(), | |
| 20 | + org.getOrgName(), | |
| 21 | + org.getStatus(), | |
| 22 | + org.getDeptName(), | |
| 23 | + org.getManagerName(), | |
| 24 | + org.getManagerTitle(), | |
| 25 | + org.getManagerPhone(), | |
| 26 | + org.getManagerEmail(), | |
| 27 | + org.getChannelIdMj(), | |
| 28 | + org.getChannelIdLaw()); | |
| 29 | + } | |
| 30 | +} |
+++ src/main/java/kr/itn/itnhub/org/OrganizationController.java
... | ... | @@ -0,0 +1,48 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +import jakarta.validation.Valid; | |
| 4 | +import kr.itn.itnhub.provision.ChannelProvisionService; | |
| 5 | +import kr.itn.itnhub.provision.ProvisionResult; | |
| 6 | +import org.springframework.web.bind.annotation.GetMapping; | |
| 7 | +import org.springframework.web.bind.annotation.PathVariable; | |
| 8 | +import org.springframework.web.bind.annotation.PostMapping; | |
| 9 | +import org.springframework.web.bind.annotation.PutMapping; | |
| 10 | +import org.springframework.web.bind.annotation.RequestBody; | |
| 11 | +import org.springframework.web.bind.annotation.RequestMapping; | |
| 12 | +import org.springframework.web.bind.annotation.RestController; | |
| 13 | + | |
| 14 | +import java.util.List; | |
| 15 | + | |
| 16 | +@RestController | |
| 17 | +@RequestMapping("/api/orgs") | |
| 18 | +public class OrganizationController { | |
| 19 | + | |
| 20 | + private final OrganizationService orgService; | |
| 21 | + private final ChannelProvisionService provisionService; | |
| 22 | + | |
| 23 | + public OrganizationController(OrganizationService orgService, | |
| 24 | + ChannelProvisionService provisionService) { | |
| 25 | + this.orgService = orgService; | |
| 26 | + this.provisionService = provisionService; | |
| 27 | + } | |
| 28 | + | |
| 29 | + @GetMapping | |
| 30 | + public List<OrgResponse> list() { | |
| 31 | + return orgService.findAll(); | |
| 32 | + } | |
| 33 | + | |
| 34 | + @PutMapping("/{id}/contact") | |
| 35 | + public OrgResponse updateContact(@PathVariable Long id, | |
| 36 | + @Valid @RequestBody ContactRequest request) { | |
| 37 | + return orgService.updateContact(id, request); | |
| 38 | + } | |
| 39 | + | |
| 40 | + /** | |
| 41 | + * 채널 2개를 보장한다. 부분 실패도 200으로 돌려주고 본문의 mj/law로 판단하게 한다. | |
| 42 | + * 한쪽만 성공한 상태 자체가 정상적인 중간 상태이기 때문이다. | |
| 43 | + */ | |
| 44 | + @PostMapping("/{id}/channels") | |
| 45 | + public ProvisionResult provision(@PathVariable Long id) { | |
| 46 | + return provisionService.provision(id); | |
| 47 | + } | |
| 48 | +} |
+++ src/main/java/kr/itn/itnhub/org/OrganizationService.java
... | ... | @@ -0,0 +1,36 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +import org.springframework.stereotype.Service; | |
| 4 | +import org.springframework.transaction.annotation.Transactional; | |
| 5 | + | |
| 6 | +import java.util.List; | |
| 7 | + | |
| 8 | +@Service | |
| 9 | +public class OrganizationService { | |
| 10 | + | |
| 11 | + private final OrganizationMapper mapper; | |
| 12 | + | |
| 13 | + public OrganizationService(OrganizationMapper mapper) { | |
| 14 | + this.mapper = mapper; | |
| 15 | + } | |
| 16 | + | |
| 17 | + public List<OrgResponse> findAll() { | |
| 18 | + return mapper.findAll().stream().map(OrgResponse::of).toList(); | |
| 19 | + } | |
| 20 | + | |
| 21 | + @Transactional | |
| 22 | + public OrgResponse updateContact(Long id, ContactRequest request) { | |
| 23 | + Organization org = mapper.findById(id); | |
| 24 | + if (org == null) { | |
| 25 | + throw new IllegalArgumentException("기관을 찾을 수 없습니다: " + id); | |
| 26 | + } | |
| 27 | + org.setDeptName(request.deptName()); | |
| 28 | + org.setManagerName(request.managerName()); | |
| 29 | + org.setManagerTitle(request.managerTitle()); | |
| 30 | + org.setManagerPhone(request.managerPhone()); | |
| 31 | + org.setManagerEmail(request.managerEmail()); | |
| 32 | + mapper.updateContact(org); | |
| 33 | + | |
| 34 | + return OrgResponse.of(mapper.findById(id)); | |
| 35 | + } | |
| 36 | +} |
+++ src/main/java/kr/itn/itnhub/seed/SeedController.java
... | ... | @@ -0,0 +1,29 @@ |
| 1 | +package kr.itn.itnhub.seed; | |
| 2 | + | |
| 3 | +import org.springframework.web.bind.annotation.PostMapping; | |
| 4 | +import org.springframework.web.bind.annotation.RequestParam; | |
| 5 | +import org.springframework.web.bind.annotation.RestController; | |
| 6 | +import org.springframework.web.multipart.MultipartFile; | |
| 7 | + | |
| 8 | +import java.io.IOException; | |
| 9 | +import java.io.InputStream; | |
| 10 | + | |
| 11 | +@RestController | |
| 12 | +public class SeedController { | |
| 13 | + | |
| 14 | + private final SeedService seedService; | |
| 15 | + | |
| 16 | + public SeedController(SeedService seedService) { | |
| 17 | + this.seedService = seedService; | |
| 18 | + } | |
| 19 | + | |
| 20 | + @PostMapping("/api/seed") | |
| 21 | + public SeedReport upload(@RequestParam("file") MultipartFile file) throws IOException { | |
| 22 | + if (file.isEmpty()) { | |
| 23 | + throw new SeedParseException("업로드된 파일이 비어 있습니다."); | |
| 24 | + } | |
| 25 | + try (InputStream in = file.getInputStream()) { | |
| 26 | + return seedService.seed(in); | |
| 27 | + } | |
| 28 | + } | |
| 29 | +} |
+++ src/test/java/kr/itn/itnhub/org/OrganizationControllerTest.java
... | ... | @@ -0,0 +1,131 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +import kr.itn.itnhub.AbstractDbTest; | |
| 4 | +import kr.itn.itnhub.mattermost.MattermostClient; | |
| 5 | +import org.junit.jupiter.api.BeforeEach; | |
| 6 | +import org.junit.jupiter.api.Test; | |
| 7 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 8 | +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | |
| 9 | +import org.springframework.boot.test.mock.mockito.MockBean; | |
| 10 | +import org.springframework.http.MediaType; | |
| 11 | +import org.springframework.jdbc.core.JdbcTemplate; | |
| 12 | +import org.springframework.security.test.context.support.WithMockUser; | |
| 13 | +import org.springframework.test.web.servlet.MockMvc; | |
| 14 | + | |
| 15 | +import java.util.Optional; | |
| 16 | + | |
| 17 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 18 | +import static org.mockito.ArgumentMatchers.anyString; | |
| 19 | +import static org.mockito.Mockito.when; | |
| 20 | +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; | |
| 21 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | |
| 22 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | |
| 23 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; | |
| 24 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | |
| 25 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | |
| 26 | + | |
| 27 | +@AutoConfigureMockMvc | |
| 28 | +@WithMockUser(roles = "ADMIN") | |
| 29 | +class OrganizationControllerTest extends AbstractDbTest { | |
| 30 | + | |
| 31 | + @Autowired | |
| 32 | + MockMvc mvc; | |
| 33 | + | |
| 34 | + @Autowired | |
| 35 | + OrganizationMapper mapper; | |
| 36 | + | |
| 37 | + @Autowired | |
| 38 | + JdbcTemplate jdbc; | |
| 39 | + | |
| 40 | + @MockBean | |
| 41 | + MattermostClient mattermost; | |
| 42 | + | |
| 43 | + private Long orgId; | |
| 44 | + | |
| 45 | + @BeforeEach | |
| 46 | + void setUp() { | |
| 47 | + jdbc.update("delete from organization"); | |
| 48 | + | |
| 49 | + Organization org = new Organization(); | |
| 50 | + org.setOrgNo("001"); | |
| 51 | + org.setOrgName("국제방송교류재단"); | |
| 52 | + org.setChannelSlug("001"); | |
| 53 | + mapper.upsertBySeed(org); | |
| 54 | + | |
| 55 | + orgId = mapper.findAll().get(0).getId(); | |
| 56 | + } | |
| 57 | + | |
| 58 | + @Test | |
| 59 | + void 기관_목록을_상태와_함께_돌려준다() throws Exception { | |
| 60 | + mvc.perform(get("/api/orgs")) | |
| 61 | + .andExpect(status().isOk()) | |
| 62 | + .andExpect(jsonPath("$[0].orgNo").value("001")) | |
| 63 | + .andExpect(jsonPath("$[0].orgName").value("국제방송교류재단")) | |
| 64 | + .andExpect(jsonPath("$[0].status").value("INFO_PENDING")); | |
| 65 | + } | |
| 66 | + | |
| 67 | + @Test | |
| 68 | + void 담당자정보를_저장하면_생성가능_상태가_된다() throws Exception { | |
| 69 | + mvc.perform(put("/api/orgs/{id}/contact", orgId) | |
| 70 | + .with(csrf()) | |
| 71 | + .contentType(MediaType.APPLICATION_JSON) | |
| 72 | + .content(""" | |
| 73 | + { | |
| 74 | + "deptName": "데이터정보화팀", | |
| 75 | + "managerName": "송민지", | |
| 76 | + "managerTitle": "과장", | |
| 77 | + "managerPhone": "02-3475-5434", | |
| 78 | + "managerEmail": "ming@arirang.com" | |
| 79 | + } | |
| 80 | + """)) | |
| 81 | + .andExpect(status().isOk()) | |
| 82 | + .andExpect(jsonPath("$.status").value("READY")); | |
| 83 | + | |
| 84 | + assertThat(mapper.findById(orgId).getDeptName()).isEqualTo("데이터정보화팀"); | |
| 85 | + } | |
| 86 | + | |
| 87 | + @Test | |
| 88 | + void 필수값이_비면_400이다() throws Exception { | |
| 89 | + mvc.perform(put("/api/orgs/{id}/contact", orgId) | |
| 90 | + .with(csrf()) | |
| 91 | + .contentType(MediaType.APPLICATION_JSON) | |
| 92 | + .content(""" | |
| 93 | + { | |
| 94 | + "deptName": "", | |
| 95 | + "managerName": "송민지", | |
| 96 | + "managerPhone": "02-3475-5434", | |
| 97 | + "managerEmail": "ming@arirang.com" | |
| 98 | + } | |
| 99 | + """)) | |
| 100 | + .andExpect(status().isBadRequest()); | |
| 101 | + } | |
| 102 | + | |
| 103 | + @Test | |
| 104 | + void 채널생성을_요청하면_결과를_돌려준다() throws Exception { | |
| 105 | + Organization ready = mapper.findById(orgId); | |
| 106 | + ready.setDeptName("데이터정보화팀"); | |
| 107 | + ready.setManagerName("송민지"); | |
| 108 | + ready.setManagerTitle("과장"); | |
| 109 | + ready.setManagerPhone("02-3475-5434"); | |
| 110 | + ready.setManagerEmail("ming@arirang.com"); | |
| 111 | + mapper.updateContact(ready); | |
| 112 | + | |
| 113 | + when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); | |
| 114 | + when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); | |
| 115 | + when(mattermost.createPrivateChannel(anyString(), anyString())).thenReturn("chan"); | |
| 116 | + | |
| 117 | + mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf())) | |
| 118 | + .andExpect(status().isOk()) | |
| 119 | + .andExpect(jsonPath("$.mj").value("CREATED")) | |
| 120 | + .andExpect(jsonPath("$.law").value("CREATED")); | |
| 121 | + } | |
| 122 | + | |
| 123 | + @Test | |
| 124 | + void 담당자정보없이_채널생성하면_실패결과를_돌려준다() throws Exception { | |
| 125 | + mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf())) | |
| 126 | + .andExpect(status().isOk()) | |
| 127 | + .andExpect(jsonPath("$.mj").value("FAILED")) | |
| 128 | + .andExpect(jsonPath("$.message").value( | |
| 129 | + org.hamcrest.Matchers.containsString("담당자 정보"))); | |
| 130 | + } | |
| 131 | +} |
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?