ITN Dev 07-22
feat: 기관 명부 도메인과 MyBatis 매퍼 추가
@39794fee974bfe8595ab4d56dab6b5fed1c56d98
 
src/main/java/kr/itn/itnhub/org/OrgStatus.java (added)
+++ src/main/java/kr/itn/itnhub/org/OrgStatus.java
@@ -0,0 +1,12 @@
+package kr.itn.itnhub.org;
+
+public enum OrgStatus {
+    /** 담당자 정보가 아직 없어 채널을 만들 수 없다. */
+    INFO_PENDING,
+    /** 담당자 정보가 다 찼고 채널 생성만 남았다. */
+    READY,
+    /** 채널 한쪽만 생성됐다. 재시도로 나머지를 만들어야 한다. */
+    PARTIAL,
+    /** 채널 2개가 모두 있다. */
+    ACTIVE
+}
 
src/main/java/kr/itn/itnhub/org/Organization.java (added)
+++ src/main/java/kr/itn/itnhub/org/Organization.java
@@ -0,0 +1,78 @@
+package kr.itn.itnhub.org;
+
+public class Organization {
+
+    private Long id;
+    private String orgNo;
+    private String orgName;
+    private String channelSlug;
+    private String deptName;
+    private String managerName;
+    private String managerTitle;
+    private String managerPhone;
+    private String managerEmail;
+    private String channelIdMj;
+    private String channelIdLaw;
+
+    /**
+     * 상태는 저장하지 않고 항상 파생한다.
+     * 저장하면 채널ID와 상태가 어긋나는 순간이 생기고, 그 어긋남을 고칠 방법이 없다.
+     */
+    public OrgStatus getStatus() {
+        boolean mj = filled(channelIdMj);
+        boolean law = filled(channelIdLaw);
+
+        if (mj && law) {
+            return OrgStatus.ACTIVE;
+        }
+        if (mj || law) {
+            return OrgStatus.PARTIAL;
+        }
+        return hasRequiredContact() ? OrgStatus.READY : OrgStatus.INFO_PENDING;
+    }
+
+    /** 직급/직함은 비어 있는 기관이 실제로 있으므로 필수에서 뺀다. */
+    public boolean hasRequiredContact() {
+        return filled(deptName)
+                && filled(managerName)
+                && filled(managerPhone)
+                && filled(managerEmail);
+    }
+
+    private static boolean filled(String value) {
+        return value != null && !value.isBlank();
+    }
+
+    public Long getId() { return id; }
+    public void setId(Long id) { this.id = id; }
+
+    public String getOrgNo() { return orgNo; }
+    public void setOrgNo(String orgNo) { this.orgNo = orgNo; }
+
+    public String getOrgName() { return orgName; }
+    public void setOrgName(String orgName) { this.orgName = orgName; }
+
+    public String getChannelSlug() { return channelSlug; }
+    public void setChannelSlug(String channelSlug) { this.channelSlug = channelSlug; }
+
+    public String getDeptName() { return deptName; }
+    public void setDeptName(String deptName) { this.deptName = deptName; }
+
+    public String getManagerName() { return managerName; }
+    public void setManagerName(String managerName) { this.managerName = managerName; }
+
+    public String getManagerTitle() { return managerTitle; }
+    public void setManagerTitle(String managerTitle) { this.managerTitle = managerTitle; }
+
+    public String getManagerPhone() { return managerPhone; }
+    public void setManagerPhone(String managerPhone) { this.managerPhone = managerPhone; }
+
+    public String getManagerEmail() { return managerEmail; }
+    public void setManagerEmail(String managerEmail) { this.managerEmail = managerEmail; }
+
+    public String getChannelIdMj() { return channelIdMj; }
+    public void setChannelIdMj(String channelIdMj) { this.channelIdMj = channelIdMj; }
+
+    public String getChannelIdLaw() { return channelIdLaw; }
+    public void setChannelIdLaw(String channelIdLaw) { this.channelIdLaw = channelIdLaw; }
+}
 
src/main/java/kr/itn/itnhub/org/OrganizationMapper.java (added)
+++ src/main/java/kr/itn/itnhub/org/OrganizationMapper.java
@@ -0,0 +1,23 @@
+package kr.itn.itnhub.org;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface OrganizationMapper {
+
+    List<Organization> findAll();
+
+    Organization findById(@Param("id") Long id);
+
+    /** 시드 전용. 비어 있는 칸만 채우고 채널ID·기입된 값은 그대로 둔다. */
+    int upsertBySeed(Organization org);
+
+    int updateContact(Organization org);
+
+    int updateChannelId(@Param("id") Long id,
+                        @Param("kind") String kind,
+                        @Param("channelId") String channelId);
+}
 
src/main/resources/mapper/OrganizationMapper.xml (added)
+++ src/main/resources/mapper/OrganizationMapper.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="kr.itn.itnhub.org.OrganizationMapper">
+
+  <sql id="columns">
+    id, org_no, org_name, channel_slug,
+    dept_name, manager_name, manager_title, manager_phone, manager_email,
+    channel_id_mj, channel_id_law
+  </sql>
+
+  <select id="findAll" resultType="kr.itn.itnhub.org.Organization">
+    select <include refid="columns"/>
+    from organization
+    order by org_no asc, org_name asc
+  </select>
+
+  <select id="findById" resultType="kr.itn.itnhub.org.Organization">
+    select <include refid="columns"/>
+    from organization
+    where id = #{id}
+  </select>
+
+  <!--
+    시드 upsert. coalesce(기존값, 새값) 순서가 핵심이다.
+    기존값이 있으면 그대로 두고 비어 있을 때만 시트 값으로 채운다.
+    반대로 쓰면 사람이 웹에서 고친 값이 시트 값으로 되돌아간다.
+  -->
+  <insert id="upsertBySeed" parameterType="kr.itn.itnhub.org.Organization">
+    insert into organization
+      (org_no, org_name, channel_slug,
+       dept_name, manager_name, manager_title, manager_phone, manager_email)
+    values
+      (#{orgNo}, #{orgName}, #{channelSlug},
+       #{deptName}, #{managerName}, #{managerTitle}, #{managerPhone}, #{managerEmail})
+    on conflict (org_no, org_name) do update set
+      channel_slug  = excluded.channel_slug,
+      dept_name     = coalesce(organization.dept_name,     excluded.dept_name),
+      manager_name  = coalesce(organization.manager_name,  excluded.manager_name),
+      manager_title = coalesce(organization.manager_title, excluded.manager_title),
+      manager_phone = coalesce(organization.manager_phone, excluded.manager_phone),
+      manager_email = coalesce(organization.manager_email, excluded.manager_email),
+      updated_at    = now()
+  </insert>
+
+  <update id="updateContact" parameterType="kr.itn.itnhub.org.Organization">
+    update organization set
+      dept_name     = #{deptName},
+      manager_name  = #{managerName},
+      manager_title = #{managerTitle},
+      manager_phone = #{managerPhone},
+      manager_email = #{managerEmail},
+      updated_at    = now()
+    where id = #{id}
+  </update>
+
+  <update id="updateChannelId">
+    update organization set
+    <choose>
+      <when test="kind == 'mj'">channel_id_mj = #{channelId},</when>
+      <otherwise>channel_id_law = #{channelId},</otherwise>
+    </choose>
+      updated_at = now()
+    where id = #{id}
+  </update>
+
+</mapper>
 
src/test/java/kr/itn/itnhub/org/OrganizationMapperTest.java (added)
+++ src/test/java/kr/itn/itnhub/org/OrganizationMapperTest.java
@@ -0,0 +1,126 @@
+package kr.itn.itnhub.org;
+
+import kr.itn.itnhub.AbstractDbTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OrganizationMapperTest extends AbstractDbTest {
+
+    @Autowired
+    OrganizationMapper mapper;
+
+    @Autowired
+    JdbcTemplate jdbc;
+
+    @BeforeEach
+    void clean() {
+        jdbc.update("delete from organization");
+    }
+
+    private Organization seedRow(String orgNo, String orgName, String dept) {
+        Organization org = new Organization();
+        org.setOrgNo(orgNo);
+        org.setOrgName(orgName);
+        org.setChannelSlug(orgNo);
+        org.setDeptName(dept);
+        org.setManagerName("담당자");
+        org.setManagerTitle("과장");
+        org.setManagerPhone("02-0000-0000");
+        org.setManagerEmail("a@b.kr");
+        return org;
+    }
+
+    @Test
+    void 신규_기관을_넣고_다시_읽는다() {
+        mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
+
+        List<Organization> all = mapper.findAll();
+
+        assertThat(all).hasSize(1);
+        assertThat(all.get(0).getOrgNo()).isEqualTo("001");
+        assertThat(all.get(0).getOrgName()).isEqualTo("국제방송교류재단");
+        assertThat(all.get(0).getStatus()).isEqualTo(OrgStatus.READY);
+    }
+
+    @Test
+    void 연번이_같아도_기관명이_다르면_별도_행이다() {
+        mapper.upsertBySeed(seedRow("008", "경찰청", "데이터정책계"));
+        mapper.upsertBySeed(seedRow("008", "경찰청_치안정책연구소", "연구지원과"));
+
+        assertThat(mapper.findAll()).hasSize(2);
+    }
+
+    @Test
+    void 시드_재실행은_기존_채널ID를_지우지_않는다() {
+        mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
+        Long id = mapper.findAll().get(0).getId();
+        mapper.updateChannelId(id, "mj", "chan-mj");
+        mapper.updateChannelId(id, "law", "chan-law");
+
+        mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
+
+        Organization after = mapper.findById(id);
+        assertThat(after.getChannelIdMj()).isEqualTo("chan-mj");
+        assertThat(after.getChannelIdLaw()).isEqualTo("chan-law");
+        assertThat(after.getStatus()).isEqualTo(OrgStatus.ACTIVE);
+    }
+
+    @Test
+    void 시드_재실행은_사람이_입력한_값을_덮어쓰지_않는다() {
+        mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
+        Organization org = mapper.findAll().get(0);
+        org.setDeptName("사람이 고친 부서");
+        mapper.updateContact(org);
+
+        Organization reseeded = seedRow("001", "국제방송교류재단", "시트에 있던 부서");
+        mapper.upsertBySeed(reseeded);
+
+        assertThat(mapper.findById(org.getId()).getDeptName())
+                .isEqualTo("사람이 고친 부서");
+    }
+
+    @Test
+    void 시드_재실행은_비어_있던_칸만_채운다() {
+        Organization blank = new Organization();
+        blank.setOrgNo("009");
+        blank.setOrgName("서울도서관");
+        blank.setChannelSlug("009");
+        mapper.upsertBySeed(blank);
+
+        mapper.upsertBySeed(seedRow("009", "서울도서관", "정보서비스과"));
+
+        Organization after = mapper.findAll().get(0);
+        assertThat(after.getDeptName()).isEqualTo("정보서비스과");
+        assertThat(after.getManagerEmail()).isEqualTo("a@b.kr");
+    }
+
+    @Test
+    void 채널ID를_한쪽만_저장할_수_있다() {
+        mapper.upsertBySeed(seedRow("002", "세종학당재단", "콘텐츠개발팀"));
+        Long id = mapper.findAll().get(0).getId();
+
+        mapper.updateChannelId(id, "mj", "only-mj");
+
+        Organization after = mapper.findById(id);
+        assertThat(after.getChannelIdMj()).isEqualTo("only-mj");
+        assertThat(after.getChannelIdLaw()).isNull();
+        assertThat(after.getStatus()).isEqualTo(OrgStatus.PARTIAL);
+    }
+
+    @Test
+    void 목록은_연번_오름차순이다() {
+        mapper.upsertBySeed(seedRow("010", "국립아시아문화전당", "기획운영과"));
+        mapper.upsertBySeed(seedRow("002", "세종학당재단", "콘텐츠개발팀"));
+        mapper.upsertBySeed(seedRow("001", "국제방송교류재단", "데이터정보화팀"));
+
+        assertThat(mapper.findAll())
+                .extracting(Organization::getOrgNo)
+                .containsExactly("001", "002", "010");
+    }
+}
 
src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java (added)
+++ src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java
@@ -0,0 +1,78 @@
+package kr.itn.itnhub.org;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OrganizationStatusTest {
+
+    private Organization withContact() {
+        Organization org = new Organization();
+        org.setOrgNo("001");
+        org.setOrgName("국제방송교류재단");
+        org.setChannelSlug("001");
+        org.setDeptName("데이터정보화팀");
+        org.setManagerName("송민지");
+        org.setManagerPhone("02-3475-5434");
+        org.setManagerEmail("ming@arirang.com");
+        return org;
+    }
+
+    @Test
+    void 담당자정보가_없으면_정보대기다() {
+        Organization org = new Organization();
+        org.setOrgNo("008");
+        org.setOrgName("경찰청_치안정책연구소");
+
+        assertThat(org.getStatus()).isEqualTo(OrgStatus.INFO_PENDING);
+    }
+
+    @Test
+    void 담당자정보가_다_차면_생성가능이다() {
+        assertThat(withContact().getStatus()).isEqualTo(OrgStatus.READY);
+    }
+
+    @Test
+    void 직급은_없어도_생성가능이다() {
+        Organization org = withContact();
+        org.setManagerTitle(null);
+
+        assertThat(org.getStatus()).isEqualTo(OrgStatus.READY);
+    }
+
+    @Test
+    void 채널이_한쪽만_있으면_부분생성이다() {
+        Organization org = withContact();
+        org.setChannelIdMj("chan-mj");
+
+        assertThat(org.getStatus()).isEqualTo(OrgStatus.PARTIAL);
+    }
+
+    @Test
+    void 채널이_둘_다_있으면_운영중이다() {
+        Organization org = withContact();
+        org.setChannelIdMj("chan-mj");
+        org.setChannelIdLaw("chan-law");
+
+        assertThat(org.getStatus()).isEqualTo(OrgStatus.ACTIVE);
+    }
+
+    @Test
+    void 담당자정보가_비어도_채널이_있으면_운영중이다() {
+        Organization org = new Organization();
+        org.setOrgNo("008");
+        org.setOrgName("경찰청_치안정책연구소");
+        org.setChannelIdMj("chan-mj");
+        org.setChannelIdLaw("chan-law");
+
+        assertThat(org.getStatus()).isEqualTo(OrgStatus.ACTIVE);
+    }
+
+    @Test
+    void 공백만_있는_값은_없는_것으로_본다() {
+        Organization org = withContact();
+        org.setManagerEmail("   ");
+
+        assertThat(org.getStatus()).isEqualTo(OrgStatus.INFO_PENDING);
+    }
+}
Add a comment
List