이호영 이호영 7 days ago
fix: 권리확인 세부 지표 카드 DB 집계 연결
@08771738ac161e4315934e273dd3e09e4dc6f1b8
frontend/src/api/client.ts
--- frontend/src/api/client.ts
+++ frontend/src/api/client.ts
@@ -380,6 +380,9 @@
     processTypes: DistributionItem[]
     processStatuses: DistributionItem[]
     changedKoglCount: number
+    reviewFreeUseCount: number
+    reviewNeedsProcessingCount: number
+    reviewNoProcessingCount: number
   }
 }
 
frontend/src/components/RightsDashboard.test.tsx
--- frontend/src/components/RightsDashboard.test.tsx
+++ frontend/src/components/RightsDashboard.test.tsx
@@ -10,7 +10,7 @@
 
 const data = {
   summary: { applied: 2, docSubmitted: 0, reviewTotal: 120, reviewDone: 90, processTotal: 40, processDone: 10, unresolvedRe: 0, reportWriting: 0, completed: 0 },
-  rights: { reviewResults: [{ label: '신유형개방', count: 90 }], reviewSourceKoglTypes: [{ label: '1유형', count: 120 }], reviewJudgedKoglTypes: [{ label: '1유형', count: 90 }], processTypes: [{ label: '0유형', count: 30 }, { label: '1유형', count: 10 }], processStatuses: [{ label: '진행중', count: 30 }, { label: '처리완료', count: 10 }], changedKoglCount: 6 },
+  rights: { reviewResults: [{ label: '신유형개방', count: 90 }], reviewSourceKoglTypes: [{ label: '1유형', count: 120 }], reviewJudgedKoglTypes: [{ label: '1유형', count: 90 }], processTypes: [{ label: '0유형', count: 30 }, { label: '1유형', count: 10 }], processStatuses: [{ label: '진행중', count: 30 }, { label: '처리완료', count: 10 }], changedKoglCount: 6, reviewFreeUseCount: 80, reviewNeedsProcessingCount: 7, reviewNoProcessingCount: 3 },
   orgs: [
     { id: 1, orgNo: '001', orgName: '완료기관', stage: 7, hasChannel: true, lawyerName: null, mjName: null, lawyerAssignedDate: null, reviewTotal: 90, reviewDone: 90, processTotal: 10, processDone: 10, stageChangedAt: null, lastChangedAt: null, reCount: 0 },
     { id: 2, orgNo: '002', orgName: '대기기관', stage: 5, hasChannel: true, lawyerName: null, mjName: null, lawyerAssignedDate: null, reviewTotal: 30, reviewDone: 0, processTotal: 30, processDone: 0, stageChangedAt: null, lastChangedAt: null, reCount: 0 },
@@ -28,6 +28,9 @@
     expect(screen.getByText('권리확인 결과 유형별 현황')).toBeTruthy()
     expect(screen.getByText('검토 대상 분류 현황')).toBeTruthy()
     expect(screen.getByText('판정 공공누리유형 현황')).toBeTruthy()
+    expect(screen.getByText('자유이용 확정')).toBeTruthy()
+    expect(screen.getByText('처리필요 건수')).toBeTruthy()
+    expect(screen.getByText('권리처리 추진 미희망')).toBeTruthy()
   })
 
   it('기관 검색과 상태 필터가 동작한다', async () => {
frontend/src/components/RightsDashboard.tsx
--- frontend/src/components/RightsDashboard.tsx
+++ frontend/src/components/RightsDashboard.tsx
@@ -94,7 +94,6 @@
   const remaining = Math.max(total - done, 0)
   const rate = percent(done, total)
   const accent = isReview ? '#ea8a00' : '#078d2a'
-  const orgsWithWork = data.orgs.filter((row) => (isReview ? row.reviewTotal : row.processTotal) > 0).length
   const rights = data.rights
 
   return (
@@ -113,13 +112,16 @@
         ))}
       </section>
 
-      <section className={`mt-3 grid gap-3 ${isReview ? 'lg:grid-cols-[1.2fr_1fr_1fr]' : 'lg:grid-cols-[1.1fr_1fr_1fr_1fr]'}`}>
+      <section className={`mt-3 grid gap-3 ${isReview ? 'lg:grid-cols-[1.15fr_repeat(4,1fr)]' : 'lg:grid-cols-[1.1fr_repeat(3,1fr)]'}`}>
         <article className="flex items-center gap-5 rounded-lg border border-gray-200 bg-white p-5 shadow-sm">
           <ProgressRing done={done} total={total} color={accent} />
           <div><p className="text-xs font-medium text-gray-500">전체 진행률</p><p className="mt-1 text-lg font-bold">{rate}%</p><p className="mt-2 text-xs text-gray-500">잔여 {format(remaining)}건</p></div>
         </article>
-        <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">대상 기관</p><p className="mt-2 text-2xl font-bold tabular-nums">{orgsWithWork}</p><p className="mt-2 text-xs text-gray-500">{title} 자료가 등록된 기관</p></article>
-        <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">진행 기관</p><p className="mt-2 text-2xl font-bold tabular-nums">{data.orgs.filter((row) => { const t=isReview?row.reviewTotal:row.processTotal; const d=isReview?row.reviewDone:row.processDone; return d>0&&d<t }).length}</p><p className="mt-2 text-xs text-gray-500">일부 처리가 완료된 기관</p></article>
+        <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">{title} 완료</p><p className="mt-2 text-2xl font-bold tabular-nums">{format(done)}</p><p className="mt-2 text-xs text-gray-500">전체 대상의 {rate}%</p><div className="mt-3 h-1 rounded-full bg-gray-200"><div className="h-full rounded-full" style={{width:`${rate}%`,backgroundColor:accent}} /></div></article>
+        {isReview && <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">자유이용 확정</p><p className="mt-2 text-2xl font-bold tabular-nums">{format(rights?.reviewFreeUseCount ?? 0)}</p><p className="mt-2 text-xs text-gray-500">신유형 개방 판정 건수</p><div className="mt-3 h-1 rounded-full bg-gray-200"><div className="h-full rounded-full bg-green-600" style={{width:`${done ? Math.min((rights?.reviewFreeUseCount ?? 0)/done*100,100):0}%`}} /></div></article>}
+        {isReview && <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">처리필요 건수</p><p className="mt-2 text-2xl font-bold tabular-nums">{format(rights?.reviewNeedsProcessingCount ?? 0)}</p><p className="mt-2 text-xs text-gray-500">권리처리필요 Y 판정 건수</p></article>}
+        {isReview && <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">권리처리 추진 미희망</p><p className="mt-2 text-2xl font-bold tabular-nums">{format(rights?.reviewNoProcessingCount ?? 0)}</p><p className="mt-2 text-xs text-gray-500">기존 공공누리 유형 유지 건수</p></article>}
+        {!isReview && <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">처리대상 건수(실제)</p><p className="mt-2 text-2xl font-bold tabular-nums">{format(total)}</p><p className="mt-2 text-xs text-gray-500">권리처리 자료 등록 건수</p></article>}
         {!isReview && <article className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm"><p className="text-xs font-medium text-gray-500">유형변경 건수</p><p className="mt-2 text-2xl font-bold tabular-nums text-pink-600">{format(rights?.changedKoglCount ?? 0)}</p><p className="mt-2 text-xs text-gray-500">기존 유형과 최종 판정 유형이 다른 건</p><div className="mt-3 h-1 rounded-full bg-gray-200"><div className="h-full rounded-full bg-pink-500" style={{width:`${total ? Math.min((rights?.changedKoglCount ?? 0)/total*100,100) : 0}%`}} /></div></article>}
       </section>
 
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
@@ -22,4 +22,7 @@
     List<DistributionItem> processTypeDistribution();
     List<DistributionItem> processStatusDistribution();
     int countChangedKoglTypes();
+    int countReviewFreeUse();
+    int countReviewNeedsProcessing();
+    int countReviewNoProcessing();
 }
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
@@ -49,7 +49,10 @@
                 mapper.reviewJudgedKoglDistribution(),
                 mapper.processTypeDistribution(),
                 mapper.processStatusDistribution(),
-                mapper.countChangedKoglTypes());
+                mapper.countChangedKoglTypes(),
+                mapper.countReviewFreeUse(),
+                mapper.countReviewNeedsProcessing(),
+                mapper.countReviewNoProcessing());
         return new DashboardResponse(summarize(rows), rows, rights);
     }
 
src/main/java/kr/itn/itnhub/dashboard/RightsDashboardStats.java
--- src/main/java/kr/itn/itnhub/dashboard/RightsDashboardStats.java
+++ src/main/java/kr/itn/itnhub/dashboard/RightsDashboardStats.java
@@ -8,5 +8,8 @@
         List<DistributionItem> reviewJudgedKoglTypes,
         List<DistributionItem> processTypes,
         List<DistributionItem> processStatuses,
-        int changedKoglCount) {
+        int changedKoglCount,
+        int reviewFreeUseCount,
+        int reviewNeedsProcessingCount,
+        int reviewNoProcessingCount) {
 }
src/main/resources/mapper/DashboardMapper.xml
--- src/main/resources/mapper/DashboardMapper.xml
+++ src/main/resources/mapper/DashboardMapper.xml
@@ -133,4 +133,16 @@
       and coalesce(trim(prior_kogl_type), '') &lt;&gt; trim(judged_kogl_type)
   </select>
 
+  <select id="countReviewFreeUse" resultType="int">
+    select count(*)::int from review_item where review_result = '신유형 개방'
+  </select>
+
+  <select id="countReviewNeedsProcessing" resultType="int">
+    select count(*)::int from review_item where upper(trim(coalesce(needs_processing, ''))) = 'Y'
+  </select>
+
+  <select id="countReviewNoProcessing" resultType="int">
+    select count(*)::int from review_item where review_result = '권리처리 추진 미희망'
+  </select>
+
 </mapper>
src/test/java/kr/itn/itnhub/dashboard/DashboardControllerTest.java
--- src/test/java/kr/itn/itnhub/dashboard/DashboardControllerTest.java
+++ src/test/java/kr/itn/itnhub/dashboard/DashboardControllerTest.java
@@ -127,17 +127,20 @@
 
     @Test
     void 권리관리_그래프는_DB의_분류값을_집계한다() throws Exception {
-        jdbc.update("update review_item set kogl_type = '1유형', judged_kogl_type = '2유형' where org_id = ? and seq = 1", stage2Id);
+        jdbc.update("update review_item set kogl_type = '1유형', judged_kogl_type = '2유형', review_result = '신유형 개방', needs_processing = 'Y' where org_id = ? and seq = 1", stage2Id);
+        jdbc.update("update review_item set review_result = '권리처리 추진 미희망' where org_id = ? and seq = 2", stage2Id);
         jdbc.update("update process_item set judged_kogl_type = '1유형', prior_kogl_type = '0유형' where org_id = ? and seq = 1", stage2Id);
 
         mvc.perform(get("/api/dashboard"))
                 .andExpect(status().isOk())
-                .andExpect(jsonPath("$.rights.reviewResults[0].label").value("미확인"))
                 .andExpect(jsonPath("$.rights.reviewSourceKoglTypes[0].label").value("미부착"))
                 .andExpect(jsonPath("$.rights.reviewJudgedKoglTypes[0].label").value("2유형"))
                 .andExpect(jsonPath("$.rights.processTypes.length()").value(2))
                 .andExpect(jsonPath("$.rights.processStatuses.length()").value(2))
-                .andExpect(jsonPath("$.rights.changedKoglCount").value(1));
+                .andExpect(jsonPath("$.rights.changedKoglCount").value(1))
+                .andExpect(jsonPath("$.rights.reviewFreeUseCount").value(1))
+                .andExpect(jsonPath("$.rights.reviewNeedsProcessingCount").value(1))
+                .andExpect(jsonPath("$.rights.reviewNoProcessingCount").value(1));
     }
 
     @Test
Add a comment
List