feat: 기관 명부 도메인과 MyBatis 매퍼 추가
@39794fee974bfe8595ab4d56dab6b5fed1c56d98
+++ src/main/java/kr/itn/itnhub/org/OrgStatus.java
... | ... | @@ -0,0 +1,12 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +public enum OrgStatus { | |
| 4 | + /** 담당자 정보가 아직 없어 채널을 만들 수 없다. */ | |
| 5 | + INFO_PENDING, | |
| 6 | + /** 담당자 정보가 다 찼고 채널 생성만 남았다. */ | |
| 7 | + READY, | |
| 8 | + /** 채널 한쪽만 생성됐다. 재시도로 나머지를 만들어야 한다. */ | |
| 9 | + PARTIAL, | |
| 10 | + /** 채널 2개가 모두 있다. */ | |
| 11 | + ACTIVE | |
| 12 | +} |
+++ src/main/java/kr/itn/itnhub/org/Organization.java
... | ... | @@ -0,0 +1,78 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +public class Organization { | |
| 4 | + | |
| 5 | + private Long id; | |
| 6 | + private String orgNo; | |
| 7 | + private String orgName; | |
| 8 | + private String channelSlug; | |
| 9 | + private String deptName; | |
| 10 | + private String managerName; | |
| 11 | + private String managerTitle; | |
| 12 | + private String managerPhone; | |
| 13 | + private String managerEmail; | |
| 14 | + private String channelIdMj; | |
| 15 | + private String channelIdLaw; | |
| 16 | + | |
| 17 | + /** | |
| 18 | + * 상태는 저장하지 않고 항상 파생한다. | |
| 19 | + * 저장하면 채널ID와 상태가 어긋나는 순간이 생기고, 그 어긋남을 고칠 방법이 없다. | |
| 20 | + */ | |
| 21 | + public OrgStatus getStatus() { | |
| 22 | + boolean mj = filled(channelIdMj); | |
| 23 | + boolean law = filled(channelIdLaw); | |
| 24 | + | |
| 25 | + if (mj && law) { | |
| 26 | + return OrgStatus.ACTIVE; | |
| 27 | + } | |
| 28 | + if (mj || law) { | |
| 29 | + return OrgStatus.PARTIAL; | |
| 30 | + } | |
| 31 | + return hasRequiredContact() ? OrgStatus.READY : OrgStatus.INFO_PENDING; | |
| 32 | + } | |
| 33 | + | |
| 34 | + /** 직급/직함은 비어 있는 기관이 실제로 있으므로 필수에서 뺀다. */ | |
| 35 | + public boolean hasRequiredContact() { | |
| 36 | + return filled(deptName) | |
| 37 | + && filled(managerName) | |
| 38 | + && filled(managerPhone) | |
| 39 | + && filled(managerEmail); | |
| 40 | + } | |
| 41 | + | |
| 42 | + private static boolean filled(String value) { | |
| 43 | + return value != null && !value.isBlank(); | |
| 44 | + } | |
| 45 | + | |
| 46 | + public Long getId() { return id; } | |
| 47 | + public void setId(Long id) { this.id = id; } | |
| 48 | + | |
| 49 | + public String getOrgNo() { return orgNo; } | |
| 50 | + public void setOrgNo(String orgNo) { this.orgNo = orgNo; } | |
| 51 | + | |
| 52 | + public String getOrgName() { return orgName; } | |
| 53 | + public void setOrgName(String orgName) { this.orgName = orgName; } | |
| 54 | + | |
| 55 | + public String getChannelSlug() { return channelSlug; } | |
| 56 | + public void setChannelSlug(String channelSlug) { this.channelSlug = channelSlug; } | |
| 57 | + | |
| 58 | + public String getDeptName() { return deptName; } | |
| 59 | + public void setDeptName(String deptName) { this.deptName = deptName; } | |
| 60 | + | |
| 61 | + public String getManagerName() { return managerName; } | |
| 62 | + public void setManagerName(String managerName) { this.managerName = managerName; } | |
| 63 | + | |
| 64 | + public String getManagerTitle() { return managerTitle; } | |
| 65 | + public void setManagerTitle(String managerTitle) { this.managerTitle = managerTitle; } | |
| 66 | + | |
| 67 | + public String getManagerPhone() { return managerPhone; } | |
| 68 | + public void setManagerPhone(String managerPhone) { this.managerPhone = managerPhone; } | |
| 69 | + | |
| 70 | + public String getManagerEmail() { return managerEmail; } | |
| 71 | + public void setManagerEmail(String managerEmail) { this.managerEmail = managerEmail; } | |
| 72 | + | |
| 73 | + public String getChannelIdMj() { return channelIdMj; } | |
| 74 | + public void setChannelIdMj(String channelIdMj) { this.channelIdMj = channelIdMj; } | |
| 75 | + | |
| 76 | + public String getChannelIdLaw() { return channelIdLaw; } | |
| 77 | + public void setChannelIdLaw(String channelIdLaw) { this.channelIdLaw = channelIdLaw; } | |
| 78 | +} |
+++ src/main/java/kr/itn/itnhub/org/OrganizationMapper.java
... | ... | @@ -0,0 +1,23 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +import org.apache.ibatis.annotations.Mapper; | |
| 4 | +import org.apache.ibatis.annotations.Param; | |
| 5 | + | |
| 6 | +import java.util.List; | |
| 7 | + | |
| 8 | +@Mapper | |
| 9 | +public interface OrganizationMapper { | |
| 10 | + | |
| 11 | + List<Organization> findAll(); | |
| 12 | + | |
| 13 | + Organization findById(@Param("id") Long id); | |
| 14 | + | |
| 15 | + /** 시드 전용. 비어 있는 칸만 채우고 채널ID·기입된 값은 그대로 둔다. */ | |
| 16 | + int upsertBySeed(Organization org); | |
| 17 | + | |
| 18 | + int updateContact(Organization org); | |
| 19 | + | |
| 20 | + int updateChannelId(@Param("id") Long id, | |
| 21 | + @Param("kind") String kind, | |
| 22 | + @Param("channelId") String channelId); | |
| 23 | +} |
+++ src/main/resources/mapper/OrganizationMapper.xml
... | ... | @@ -0,0 +1,67 @@ |
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" | |
| 3 | + "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |
| 4 | +<mapper namespace="kr.itn.itnhub.org.OrganizationMapper"> | |
| 5 | + | |
| 6 | + <sql id="columns"> | |
| 7 | + id, org_no, org_name, channel_slug, | |
| 8 | + dept_name, manager_name, manager_title, manager_phone, manager_email, | |
| 9 | + channel_id_mj, channel_id_law | |
| 10 | + </sql> | |
| 11 | + | |
| 12 | + <select id="findAll" resultType="kr.itn.itnhub.org.Organization"> | |
| 13 | + select <include refid="columns"/> | |
| 14 | + from organization | |
| 15 | + order by org_no asc, org_name asc | |
| 16 | + </select> | |
| 17 | + | |
| 18 | + <select id="findById" resultType="kr.itn.itnhub.org.Organization"> | |
| 19 | + select <include refid="columns"/> | |
| 20 | + from organization | |
| 21 | + where id = #{id} | |
| 22 | + </select> | |
| 23 | + | |
| 24 | + <!-- | |
| 25 | + 시드 upsert. coalesce(기존값, 새값) 순서가 핵심이다. | |
| 26 | + 기존값이 있으면 그대로 두고 비어 있을 때만 시트 값으로 채운다. | |
| 27 | + 반대로 쓰면 사람이 웹에서 고친 값이 시트 값으로 되돌아간다. | |
| 28 | + --> | |
| 29 | + <insert id="upsertBySeed" parameterType="kr.itn.itnhub.org.Organization"> | |
| 30 | + insert into organization | |
| 31 | + (org_no, org_name, channel_slug, | |
| 32 | + dept_name, manager_name, manager_title, manager_phone, manager_email) | |
| 33 | + values | |
| 34 | + (#{orgNo}, #{orgName}, #{channelSlug}, | |
| 35 | + #{deptName}, #{managerName}, #{managerTitle}, #{managerPhone}, #{managerEmail}) | |
| 36 | + on conflict (org_no, org_name) do update set | |
| 37 | + channel_slug = excluded.channel_slug, | |
| 38 | + dept_name = coalesce(organization.dept_name, excluded.dept_name), | |
| 39 | + manager_name = coalesce(organization.manager_name, excluded.manager_name), | |
| 40 | + manager_title = coalesce(organization.manager_title, excluded.manager_title), | |
| 41 | + manager_phone = coalesce(organization.manager_phone, excluded.manager_phone), | |
| 42 | + manager_email = coalesce(organization.manager_email, excluded.manager_email), | |
| 43 | + updated_at = now() | |
| 44 | + </insert> | |
| 45 | + | |
| 46 | + <update id="updateContact" parameterType="kr.itn.itnhub.org.Organization"> | |
| 47 | + update organization set | |
| 48 | + dept_name = #{deptName}, | |
| 49 | + manager_name = #{managerName}, | |
| 50 | + manager_title = #{managerTitle}, | |
| 51 | + manager_phone = #{managerPhone}, | |
| 52 | + manager_email = #{managerEmail}, | |
| 53 | + updated_at = now() | |
| 54 | + where id = #{id} | |
| 55 | + </update> | |
| 56 | + | |
| 57 | + <update id="updateChannelId"> | |
| 58 | + update organization set | |
| 59 | + <choose> | |
| 60 | + <when test="kind == 'mj'">channel_id_mj = #{channelId},</when> | |
| 61 | + <otherwise>channel_id_law = #{channelId},</otherwise> | |
| 62 | + </choose> | |
| 63 | + updated_at = now() | |
| 64 | + where id = #{id} | |
| 65 | + </update> | |
| 66 | + | |
| 67 | +</mapper> |
+++ src/test/java/kr/itn/itnhub/org/OrganizationMapperTest.java
... | ... | @@ -0,0 +1,126 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +import kr.itn.itnhub.AbstractDbTest; | |
| 4 | +import org.junit.jupiter.api.BeforeEach; | |
| 5 | +import org.junit.jupiter.api.Test; | |
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 7 | +import org.springframework.jdbc.core.JdbcTemplate; | |
| 8 | + | |
| 9 | +import java.util.List; | |
| 10 | + | |
| 11 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 12 | + | |
| 13 | +class OrganizationMapperTest extends AbstractDbTest { | |
| 14 | + | |
| 15 | + @Autowired | |
| 16 | + OrganizationMapper mapper; | |
| 17 | + | |
| 18 | + @Autowired | |
| 19 | + JdbcTemplate jdbc; | |
| 20 | + | |
| 21 | + @BeforeEach | |
| 22 | + void clean() { | |
| 23 | + jdbc.update("delete from organization"); | |
| 24 | + } | |
| 25 | + | |
| 26 | + private Organization seedRow(String orgNo, String orgName, String dept) { | |
| 27 | + Organization org = new Organization(); | |
| 28 | + org.setOrgNo(orgNo); | |
| 29 | + org.setOrgName(orgName); | |
| 30 | + org.setChannelSlug(orgNo); | |
| 31 | + org.setDeptName(dept); | |
| 32 | + org.setManagerName("담당자"); | |
| 33 | + org.setManagerTitle("과장"); | |
| 34 | + org.setManagerPhone("02-0000-0000"); | |
| 35 | + org.setManagerEmail("a@b.kr"); | |
| 36 | + return org; | |
| 37 | + } | |
| 38 | + | |
| 39 | + @Test | |
| 40 | + void 신규_기관을_넣고_다시_읽는다() { | |
| 41 | + mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀")); | |
| 42 | + | |
| 43 | + List<Organization> all = mapper.findAll(); | |
| 44 | + | |
| 45 | + assertThat(all).hasSize(1); | |
| 46 | + assertThat(all.get(0).getOrgNo()).isEqualTo("001"); | |
| 47 | + assertThat(all.get(0).getOrgName()).isEqualTo("국제방송교류재단"); | |
| 48 | + assertThat(all.get(0).getStatus()).isEqualTo(OrgStatus.READY); | |
| 49 | + } | |
| 50 | + | |
| 51 | + @Test | |
| 52 | + void 연번이_같아도_기관명이_다르면_별도_행이다() { | |
| 53 | + mapper.upsertBySeed(seedRow("008", "경찰청", "데이터정책계")); | |
| 54 | + mapper.upsertBySeed(seedRow("008", "경찰청_치안정책연구소", "연구지원과")); | |
| 55 | + | |
| 56 | + assertThat(mapper.findAll()).hasSize(2); | |
| 57 | + } | |
| 58 | + | |
| 59 | + @Test | |
| 60 | + void 시드_재실행은_기존_채널ID를_지우지_않는다() { | |
| 61 | + mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀")); | |
| 62 | + Long id = mapper.findAll().get(0).getId(); | |
| 63 | + mapper.updateChannelId(id, "mj", "chan-mj"); | |
| 64 | + mapper.updateChannelId(id, "law", "chan-law"); | |
| 65 | + | |
| 66 | + mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀")); | |
| 67 | + | |
| 68 | + Organization after = mapper.findById(id); | |
| 69 | + assertThat(after.getChannelIdMj()).isEqualTo("chan-mj"); | |
| 70 | + assertThat(after.getChannelIdLaw()).isEqualTo("chan-law"); | |
| 71 | + assertThat(after.getStatus()).isEqualTo(OrgStatus.ACTIVE); | |
| 72 | + } | |
| 73 | + | |
| 74 | + @Test | |
| 75 | + void 시드_재실행은_사람이_입력한_값을_덮어쓰지_않는다() { | |
| 76 | + mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀")); | |
| 77 | + Organization org = mapper.findAll().get(0); | |
| 78 | + org.setDeptName("사람이 고친 부서"); | |
| 79 | + mapper.updateContact(org); | |
| 80 | + | |
| 81 | + Organization reseeded = seedRow("001", "국제방송교류재단", "시트에 있던 부서"); | |
| 82 | + mapper.upsertBySeed(reseeded); | |
| 83 | + | |
| 84 | + assertThat(mapper.findById(org.getId()).getDeptName()) | |
| 85 | + .isEqualTo("사람이 고친 부서"); | |
| 86 | + } | |
| 87 | + | |
| 88 | + @Test | |
| 89 | + void 시드_재실행은_비어_있던_칸만_채운다() { | |
| 90 | + Organization blank = new Organization(); | |
| 91 | + blank.setOrgNo("009"); | |
| 92 | + blank.setOrgName("서울도서관"); | |
| 93 | + blank.setChannelSlug("009"); | |
| 94 | + mapper.upsertBySeed(blank); | |
| 95 | + | |
| 96 | + mapper.upsertBySeed(seedRow("009", "서울도서관", "정보서비스과")); | |
| 97 | + | |
| 98 | + Organization after = mapper.findAll().get(0); | |
| 99 | + assertThat(after.getDeptName()).isEqualTo("정보서비스과"); | |
| 100 | + assertThat(after.getManagerEmail()).isEqualTo("a@b.kr"); | |
| 101 | + } | |
| 102 | + | |
| 103 | + @Test | |
| 104 | + void 채널ID를_한쪽만_저장할_수_있다() { | |
| 105 | + mapper.upsertBySeed(seedRow("002", "세종학당재단", "콘텐츠개발팀")); | |
| 106 | + Long id = mapper.findAll().get(0).getId(); | |
| 107 | + | |
| 108 | + mapper.updateChannelId(id, "mj", "only-mj"); | |
| 109 | + | |
| 110 | + Organization after = mapper.findById(id); | |
| 111 | + assertThat(after.getChannelIdMj()).isEqualTo("only-mj"); | |
| 112 | + assertThat(after.getChannelIdLaw()).isNull(); | |
| 113 | + assertThat(after.getStatus()).isEqualTo(OrgStatus.PARTIAL); | |
| 114 | + } | |
| 115 | + | |
| 116 | + @Test | |
| 117 | + void 목록은_연번_오름차순이다() { | |
| 118 | + mapper.upsertBySeed(seedRow("010", "국립아시아문화전당", "기획운영과")); | |
| 119 | + mapper.upsertBySeed(seedRow("002", "세종학당재단", "콘텐츠개발팀")); | |
| 120 | + mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀")); | |
| 121 | + | |
| 122 | + assertThat(mapper.findAll()) | |
| 123 | + .extracting(Organization::getOrgNo) | |
| 124 | + .containsExactly("001", "002", "010"); | |
| 125 | + } | |
| 126 | +} |
+++ src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java
... | ... | @@ -0,0 +1,78 @@ |
| 1 | +package kr.itn.itnhub.org; | |
| 2 | + | |
| 3 | +import org.junit.jupiter.api.Test; | |
| 4 | + | |
| 5 | +import static org.assertj.core.api.Assertions.assertThat; | |
| 6 | + | |
| 7 | +class OrganizationStatusTest { | |
| 8 | + | |
| 9 | + private Organization withContact() { | |
| 10 | + Organization org = new Organization(); | |
| 11 | + org.setOrgNo("001"); | |
| 12 | + org.setOrgName("국제방송교류재단"); | |
| 13 | + org.setChannelSlug("001"); | |
| 14 | + org.setDeptName("데이터정보화팀"); | |
| 15 | + org.setManagerName("송민지"); | |
| 16 | + org.setManagerPhone("02-3475-5434"); | |
| 17 | + org.setManagerEmail("ming@arirang.com"); | |
| 18 | + return org; | |
| 19 | + } | |
| 20 | + | |
| 21 | + @Test | |
| 22 | + void 담당자정보가_없으면_정보대기다() { | |
| 23 | + Organization org = new Organization(); | |
| 24 | + org.setOrgNo("008"); | |
| 25 | + org.setOrgName("경찰청_치안정책연구소"); | |
| 26 | + | |
| 27 | + assertThat(org.getStatus()).isEqualTo(OrgStatus.INFO_PENDING); | |
| 28 | + } | |
| 29 | + | |
| 30 | + @Test | |
| 31 | + void 담당자정보가_다_차면_생성가능이다() { | |
| 32 | + assertThat(withContact().getStatus()).isEqualTo(OrgStatus.READY); | |
| 33 | + } | |
| 34 | + | |
| 35 | + @Test | |
| 36 | + void 직급은_없어도_생성가능이다() { | |
| 37 | + Organization org = withContact(); | |
| 38 | + org.setManagerTitle(null); | |
| 39 | + | |
| 40 | + assertThat(org.getStatus()).isEqualTo(OrgStatus.READY); | |
| 41 | + } | |
| 42 | + | |
| 43 | + @Test | |
| 44 | + void 채널이_한쪽만_있으면_부분생성이다() { | |
| 45 | + Organization org = withContact(); | |
| 46 | + org.setChannelIdMj("chan-mj"); | |
| 47 | + | |
| 48 | + assertThat(org.getStatus()).isEqualTo(OrgStatus.PARTIAL); | |
| 49 | + } | |
| 50 | + | |
| 51 | + @Test | |
| 52 | + void 채널이_둘_다_있으면_운영중이다() { | |
| 53 | + Organization org = withContact(); | |
| 54 | + org.setChannelIdMj("chan-mj"); | |
| 55 | + org.setChannelIdLaw("chan-law"); | |
| 56 | + | |
| 57 | + assertThat(org.getStatus()).isEqualTo(OrgStatus.ACTIVE); | |
| 58 | + } | |
| 59 | + | |
| 60 | + @Test | |
| 61 | + void 담당자정보가_비어도_채널이_있으면_운영중이다() { | |
| 62 | + Organization org = new Organization(); | |
| 63 | + org.setOrgNo("008"); | |
| 64 | + org.setOrgName("경찰청_치안정책연구소"); | |
| 65 | + org.setChannelIdMj("chan-mj"); | |
| 66 | + org.setChannelIdLaw("chan-law"); | |
| 67 | + | |
| 68 | + assertThat(org.getStatus()).isEqualTo(OrgStatus.ACTIVE); | |
| 69 | + } | |
| 70 | + | |
| 71 | + @Test | |
| 72 | + void 공백만_있는_값은_없는_것으로_본다() { | |
| 73 | + Organization org = withContact(); | |
| 74 | + org.setManagerEmail(" "); | |
| 75 | + | |
| 76 | + assertThat(org.getStatus()).isEqualTo(OrgStatus.INFO_PENDING); | |
| 77 | + } | |
| 78 | +} |
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?