이호영 이호영 7 days ago
refactor: 기관 운영 상태 판정 규칙 통합
@5ac9b23b9421955f84fa563c8f329994f07e5e75
src/main/java/kr/itn/itnhub/dashboard/DashboardMapper.java
--- src/main/java/kr/itn/itnhub/dashboard/DashboardMapper.java
+++ src/main/java/kr/itn/itnhub/dashboard/DashboardMapper.java
@@ -14,7 +14,7 @@
      * @param doneStatus 권리처리 완료로 셀 상태값. 코드표에서 와야 하는데 SQL이 코드표를
      *                   참조할 방법이 없어 서비스가 읽어 넘긴다.
      */
-    List<DashboardOrgRow> findOrgRows(@Param("doneStatus") String doneStatus);
+    List<DashboardOrgProjection> findOrgRows(@Param("doneStatus") String doneStatus);
 
     List<DistributionItem> reviewResultDistribution();
     List<DistributionItem> reviewSourceKoglDistribution();
 
src/main/java/kr/itn/itnhub/dashboard/DashboardOrgProjection.java (added)
+++ src/main/java/kr/itn/itnhub/dashboard/DashboardOrgProjection.java
@@ -0,0 +1,32 @@
+package kr.itn.itnhub.dashboard;
+
+import kr.itn.itnhub.org.OrgOperationalState;
+
+/** DB 집계 결과를 API 응답 모델로 변환하는 내부 조회 모델. */
+public record DashboardOrgProjection(
+        Long id,
+        String orgNo,
+        String orgName,
+        Long applicantContactId,
+        String channelIdMj,
+        String channelIdLaw,
+        Integer stage,
+        String lawyerName,
+        String mjName,
+        String lawyerAssignedDate,
+        int reviewTotal,
+        int reviewDone,
+        int processTotal,
+        int processDone,
+        Long stageChangedAt,
+        Long lastChangedAt,
+        int reCount) {
+
+    DashboardOrgRow toRow() {
+        OrgOperationalState operationalState = OrgOperationalState.from(
+                applicantContactId, channelIdMj, channelIdLaw);
+        return new DashboardOrgRow(id, orgNo, orgName, operationalState.status(), stage,
+                operationalState.hasChannel(), lawyerName, mjName, lawyerAssignedDate,
+                reviewTotal, reviewDone, processTotal, processDone, stageChangedAt, lastChangedAt, reCount);
+    }
+}
src/main/java/kr/itn/itnhub/dashboard/DashboardService.java
--- src/main/java/kr/itn/itnhub/dashboard/DashboardService.java
+++ src/main/java/kr/itn/itnhub/dashboard/DashboardService.java
@@ -42,7 +42,9 @@
     }
 
     public DashboardResponse dashboard() {
-        List<DashboardOrgRow> rows = mapper.findOrgRows(doneStatus());
+        List<DashboardOrgRow> rows = mapper.findOrgRows(doneStatus()).stream()
+                .map(DashboardOrgProjection::toRow)
+                .toList();
         RightsDashboardStats rights = new RightsDashboardStats(
                 mapper.reviewResultDistribution(),
                 mapper.reviewSourceKoglDistribution(),
 
src/main/java/kr/itn/itnhub/org/OrgOperationalState.java (added)
+++ src/main/java/kr/itn/itnhub/org/OrgOperationalState.java
@@ -0,0 +1,30 @@
+package kr.itn.itnhub.org;
+
+/**
+ * 기관 담당자 및 채널 정보에서 파생되는 운영 상태.
+ * 상태 판정과 채널 보유 여부가 항상 같은 공백 처리 규칙을 사용하도록 한곳에서 계산한다.
+ */
+public record OrgOperationalState(OrgStatus status, boolean hasChannel) {
+
+    public static OrgOperationalState from(Long applicantContactId, String channelIdMj, String channelIdLaw) {
+        boolean mjActive = filled(channelIdMj);
+        boolean lawActive = filled(channelIdLaw);
+        OrgStatus status;
+
+        if (mjActive && lawActive) {
+            status = OrgStatus.ACTIVE;
+        } else if (mjActive || lawActive) {
+            status = OrgStatus.PARTIAL;
+        } else if (applicantContactId != null) {
+            status = OrgStatus.READY;
+        } else {
+            status = OrgStatus.INFO_PENDING;
+        }
+
+        return new OrgOperationalState(status, mjActive || lawActive);
+    }
+
+    private static boolean filled(String value) {
+        return value != null && !value.isBlank();
+    }
+}
src/main/java/kr/itn/itnhub/org/Organization.java
--- src/main/java/kr/itn/itnhub/org/Organization.java
+++ src/main/java/kr/itn/itnhub/org/Organization.java
@@ -28,25 +28,12 @@
      * 저장하면 채널ID와 상태가 어긋나는 순간이 생기고, 그 어긋남을 고칠 방법이 없다.
      */
     public OrgStatus getStatus() {
-        boolean mjActive = filled(channelIdMj);
-        boolean lawActive = filled(channelIdLaw);
-
-        if (mjActive && lawActive) {
-            return OrgStatus.ACTIVE;
-        }
-        if (mjActive || lawActive) {
-            return OrgStatus.PARTIAL;
-        }
-        return hasApplicantAssigned() ? OrgStatus.READY : OrgStatus.INFO_PENDING;
+        return OrgOperationalState.from(applicantContactId, channelIdMj, channelIdLaw).status();
     }
 
     /** 신청기관 담당자가 배정되어야 채널을 만들 수 있다(담당자관리에서 등록·배정된 담당자). */
     public boolean hasApplicantAssigned() {
         return applicantContactId != null;
-    }
-
-    private static boolean filled(String value) {
-        return value != null && !value.isBlank();
     }
 
     public Long getId() { return id; }
src/main/resources/mapper/DashboardMapper.xml
--- src/main/resources/mapper/DashboardMapper.xml
+++ src/main/resources/mapper/DashboardMapper.xml
@@ -5,16 +5,17 @@
 
   <!--
     record라서 constructor 매핑을 쓴다(ProcessItemMapper의 statsResultMap과 같은 방식).
-    arg 순서는 DashboardOrgRow의 컴포넌트 순서와 반드시 같아야 한다.
+    arg 순서는 DashboardOrgProjection의 컴포넌트 순서와 반드시 같아야 한다.
   -->
-  <resultMap id="orgRowResultMap" type="kr.itn.itnhub.dashboard.DashboardOrgRow">
+  <resultMap id="orgRowResultMap" type="kr.itn.itnhub.dashboard.DashboardOrgProjection">
     <constructor>
       <idArg column="id" javaType="java.lang.Long"/>
       <arg column="org_no" javaType="java.lang.String"/>
       <arg column="org_name" javaType="java.lang.String"/>
-      <arg column="org_status" javaType="kr.itn.itnhub.org.OrgStatus"/>
+      <arg column="applicant_contact_id" javaType="java.lang.Long"/>
+      <arg column="channel_id_mj" javaType="java.lang.String"/>
+      <arg column="channel_id_law" javaType="java.lang.String"/>
       <arg column="stage" javaType="java.lang.Integer"/>
-      <arg column="has_channel" javaType="_boolean"/>
       <arg column="lawyer_name" javaType="java.lang.String"/>
       <arg column="mj_name" javaType="java.lang.String"/>
       <arg column="lawyer_assigned_date" javaType="java.lang.String"/>
@@ -49,14 +50,10 @@
       o.id,
       o.org_no,
       o.org_name,
-      case
-        when nullif(trim(o.channel_id_mj), '') is not null and nullif(trim(o.channel_id_law), '') is not null then 'ACTIVE'
-        when nullif(trim(o.channel_id_mj), '') is not null or nullif(trim(o.channel_id_law), '') is not null then 'PARTIAL'
-        when o.applicant_contact_id is not null then 'READY'
-        else 'INFO_PENDING'
-      end as org_status,
+      o.applicant_contact_id,
+      o.channel_id_mj,
+      o.channel_id_law,
       o.stage,
-      (o.channel_id_mj is not null or o.channel_id_law is not null) as has_channel,
       lc.name as lawyer_name,
       mc.name as mj_name,
       o.lawyer_assigned_date,
src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java
--- src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java
+++ src/test/java/kr/itn/itnhub/org/OrganizationStatusTest.java
@@ -74,4 +74,14 @@
         org.setApplicantContactId(null);
         assertThat(org.hasApplicantAssigned()).isFalse();
     }
+
+    @Test
+    void 공백_채널ID는_등록된_채널로_보지_않는다() {
+        Organization org = withApplicant();
+        org.setChannelIdMj("  ");
+        org.setChannelIdLaw("\t");
+
+        assertThat(org.getStatus()).isEqualTo(OrgStatus.READY);
+        assertThat(OrgOperationalState.from(org.getApplicantContactId(), "  ", "\t").hasChannel()).isFalse();
+    }
 }
Add a comment
List