--- frontend/src/api/client.ts
+++ frontend/src/api/client.ts
... | ... | @@ -353,6 +353,7 @@ |
| 353 | 353 |
id: number |
| 354 | 354 |
orgNo: string |
| 355 | 355 |
orgName: string |
| 356 |
+ status: OrgStatus |
|
| 356 | 357 |
stage: number | null |
| 357 | 358 |
/** false면 칸반의 "단계 미지정" 컬럼으로 간다. 드래그로 단계를 옮길 수 없다. */ |
| 358 | 359 |
hasChannel: boolean |
--- frontend/src/components/Dashboard.test.tsx
+++ frontend/src/components/Dashboard.test.tsx
... | ... | @@ -22,6 +22,7 @@ |
| 22 | 22 |
id: 1, |
| 23 | 23 |
orgNo: '001', |
| 24 | 24 |
orgName: '한국출판문화산업진흥원', |
| 25 |
+ status: 'ACTIVE', |
|
| 25 | 26 |
stage: 5, |
| 26 | 27 |
hasChannel: true, |
| 27 | 28 |
lawyerName: '전세준', |
--- frontend/src/components/ProjectsPage.test.tsx
+++ frontend/src/components/ProjectsPage.test.tsx
... | ... | @@ -25,6 +25,7 @@ |
| 25 | 25 |
id: 1, |
| 26 | 26 |
orgNo: '001_00', |
| 27 | 27 |
orgName: '국제방송교류재단', |
| 28 |
+ status: 'ACTIVE', |
|
| 28 | 29 |
stage: 5, |
| 29 | 30 |
hasChannel: true, |
| 30 | 31 |
lawyerName: '권홍철', |
--- frontend/src/components/RightsDashboard.test.tsx
+++ frontend/src/components/RightsDashboard.test.tsx
... | ... | @@ -12,8 +12,8 @@ |
| 12 | 12 |
summary: { applied: 2, docSubmitted: 0, reviewTotal: 120, reviewDone: 90, processTotal: 40, processDone: 10, unresolvedRe: 0, reportWriting: 0, completed: 0 },
|
| 13 | 13 |
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 },
|
| 14 | 14 |
orgs: [ |
| 15 |
- { 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 },
|
|
| 16 |
- { 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 },
|
|
| 15 |
+ { id: 1, orgNo: '001', orgName: '완료기관', status: 'ACTIVE' as const, stage: 7, hasChannel: true, lawyerName: null, mjName: null, lawyerAssignedDate: null, reviewTotal: 90, reviewDone: 90, processTotal: 10, processDone: 10, stageChangedAt: null, lastChangedAt: null, reCount: 0 },
|
|
| 16 |
+ { id: 2, orgNo: '002', orgName: '대기기관', status: 'READY' as const, stage: 5, hasChannel: true, lawyerName: null, mjName: null, lawyerAssignedDate: null, reviewTotal: 30, reviewDone: 0, processTotal: 30, processDone: 0, stageChangedAt: null, lastChangedAt: null, reCount: 0 },
|
|
| 17 | 17 |
], |
| 18 | 18 |
} |
| 19 | 19 |
|
... | ... | @@ -31,6 +31,8 @@ |
| 31 | 31 |
expect(screen.getByText('자유이용 확정')).toBeTruthy()
|
| 32 | 32 |
expect(screen.getByText('처리필요 건수')).toBeTruthy()
|
| 33 | 33 |
expect(screen.getByText('권리처리 추진 미희망')).toBeTruthy()
|
| 34 |
+ expect(screen.getByText('운영 중')).toBeTruthy()
|
|
| 35 |
+ expect(screen.getByText('생성 가능')).toBeTruthy()
|
|
| 34 | 36 |
}) |
| 35 | 37 |
|
| 36 | 38 |
it('기관 검색과 상태 필터가 동작한다', async () => {
|
--- frontend/src/components/RightsDashboard.tsx
+++ frontend/src/components/RightsDashboard.tsx
... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 |
import { useEffect, useMemo, useState } from 'react'
|
| 2 | 2 |
import { getDashboard, type DashboardData, type DashboardOrgRow, type DistributionItem } from '../api/client'
|
| 3 |
+import StatusBadge from './StatusBadge' |
|
| 3 | 4 |
|
| 4 | 5 |
type Mode = 'review' | 'process' |
| 5 | 6 |
|
... | ... | @@ -150,13 +151,13 @@ |
| 150 | 151 |
</div> |
| 151 | 152 |
<div className="overflow-x-auto"> |
| 152 | 153 |
<table className="w-full min-w-[680px] text-xs"> |
| 153 |
- <thead className="bg-gray-50 text-gray-500"><tr><th className="px-4 py-2.5 text-left font-medium">기관명</th><th className="px-4 py-2.5 text-right font-medium">대상</th><th className="px-4 py-2.5 text-right font-medium">완료</th><th className="px-4 py-2.5 text-right font-medium">잔여</th><th className="w-64 px-4 py-2.5 text-left font-medium">진척률</th></tr></thead> |
|
| 154 |
+ <thead className="bg-gray-50 text-gray-500"><tr><th className="px-4 py-2.5 text-left font-medium">기관명</th><th className="px-4 py-2.5 text-left font-medium">기관 상태</th><th className="px-4 py-2.5 text-right font-medium">대상</th><th className="px-4 py-2.5 text-right font-medium">완료</th><th className="px-4 py-2.5 text-right font-medium">잔여</th><th className="w-64 px-4 py-2.5 text-left font-medium">진척률</th></tr></thead> |
|
| 154 | 155 |
<tbody className="divide-y divide-gray-100"> |
| 155 | 156 |
{rows.map((row: DashboardOrgRow) => { const rowTotal=isReview?row.reviewTotal:row.processTotal; const rowDone=isReview?row.reviewDone:row.processDone; const rowRate=percent(rowDone,rowTotal); return (
|
| 156 | 157 |
<tr key={row.id} className="cursor-pointer hover:bg-gray-50" tabIndex={0} onClick={() => onOpenOrg(row.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpenOrg(row.id) } }}>
|
| 157 |
- <td className="px-4 py-3 font-medium text-gray-900"><span className="mr-2 text-gray-400">{row.orgNo}</span>{row.orgName}</td><td className="px-4 py-3 text-right tabular-nums">{format(rowTotal)}</td><td className="px-4 py-3 text-right font-medium tabular-nums" style={{ color: accent }}>{format(rowDone)}</td><td className="px-4 py-3 text-right tabular-nums">{format(Math.max(rowTotal-rowDone,0))}</td><td className="px-4 py-3"><div className="flex items-center gap-2"><div className="h-1.5 flex-1 rounded-full bg-gray-200"><div className="h-full rounded-full" style={{ width: `${rowRate}%`, backgroundColor: accent }} /></div><b className="w-10 text-right tabular-nums">{rowRate}%</b></div></td>
|
|
| 158 |
+ <td className="px-4 py-3 font-medium text-gray-900"><span className="mr-2 text-gray-400">{row.orgNo}</span>{row.orgName}</td><td className="px-4 py-3"><StatusBadge status={row.status} /></td><td className="px-4 py-3 text-right tabular-nums">{format(rowTotal)}</td><td className="px-4 py-3 text-right font-medium tabular-nums" style={{ color: accent }}>{format(rowDone)}</td><td className="px-4 py-3 text-right tabular-nums">{format(Math.max(rowTotal-rowDone,0))}</td><td className="px-4 py-3"><div className="flex items-center gap-2"><div className="h-1.5 flex-1 rounded-full bg-gray-200"><div className="h-full rounded-full" style={{ width: `${rowRate}%`, backgroundColor: accent }} /></div><b className="w-10 text-right tabular-nums">{rowRate}%</b></div></td>
|
|
| 158 | 159 |
</tr>) })} |
| 159 |
- {!rows.length && <tr><td colSpan={5} className="px-4 py-12 text-center text-gray-500">조건에 맞는 기관이 없습니다.</td></tr>}
|
|
| 160 |
+ {!rows.length && <tr><td colSpan={6} className="px-4 py-12 text-center text-gray-500">조건에 맞는 기관이 없습니다.</td></tr>}
|
|
| 160 | 161 |
</tbody> |
| 161 | 162 |
</table> |
| 162 | 163 |
</div> |
--- src/main/java/kr/itn/itnhub/dashboard/DashboardOrgRow.java
+++ src/main/java/kr/itn/itnhub/dashboard/DashboardOrgRow.java
... | ... | @@ -1,5 +1,7 @@ |
| 1 | 1 |
package kr.itn.itnhub.dashboard; |
| 2 | 2 |
|
| 3 |
+import kr.itn.itnhub.org.OrgStatus; |
|
| 4 |
+ |
|
| 3 | 5 |
/** |
| 4 | 6 |
* 대시보드가 기관 1곳을 그리는 데 필요한 값을 한 줄로 모은 것. 칸반 카드와 "최근 진행 기관" |
| 5 | 7 |
* 표가 같은 행을 쓴다 - 두 곳이 같은 숫자를 보여야 하고, 기관 47곳을 그릴 때마다 기관별로 |
... | ... | @@ -17,6 +19,7 @@ |
| 17 | 19 |
Long id, |
| 18 | 20 |
String orgNo, |
| 19 | 21 |
String orgName, |
| 22 |
+ OrgStatus status, |
|
| 20 | 23 |
Integer stage, |
| 21 | 24 |
boolean hasChannel, |
| 22 | 25 |
String lawyerName, |
--- src/main/resources/mapper/DashboardMapper.xml
+++ src/main/resources/mapper/DashboardMapper.xml
... | ... | @@ -12,6 +12,7 @@ |
| 12 | 12 |
<idArg column="id" javaType="java.lang.Long"/> |
| 13 | 13 |
<arg column="org_no" javaType="java.lang.String"/> |
| 14 | 14 |
<arg column="org_name" javaType="java.lang.String"/> |
| 15 |
+ <arg column="org_status" javaType="kr.itn.itnhub.org.OrgStatus"/> |
|
| 15 | 16 |
<arg column="stage" javaType="java.lang.Integer"/> |
| 16 | 17 |
<arg column="has_channel" javaType="_boolean"/> |
| 17 | 18 |
<arg column="lawyer_name" javaType="java.lang.String"/> |
... | ... | @@ -48,6 +49,12 @@ |
| 48 | 49 |
o.id, |
| 49 | 50 |
o.org_no, |
| 50 | 51 |
o.org_name, |
| 52 |
+ case |
|
| 53 |
+ when nullif(trim(o.channel_id_mj), '') is not null and nullif(trim(o.channel_id_law), '') is not null then 'ACTIVE' |
|
| 54 |
+ when nullif(trim(o.channel_id_mj), '') is not null or nullif(trim(o.channel_id_law), '') is not null then 'PARTIAL' |
|
| 55 |
+ when o.applicant_contact_id is not null then 'READY' |
|
| 56 |
+ else 'INFO_PENDING' |
|
| 57 |
+ end as org_status, |
|
| 51 | 58 |
o.stage, |
| 52 | 59 |
(o.channel_id_mj is not null or o.channel_id_law is not null) as has_channel, |
| 53 | 60 |
lc.name as lawyer_name, |
--- src/test/java/kr/itn/itnhub/dashboard/DashboardControllerTest.java
+++ src/test/java/kr/itn/itnhub/dashboard/DashboardControllerTest.java
... | ... | @@ -157,10 +157,12 @@ |
| 157 | 157 |
.andExpect(status().isOk()) |
| 158 | 158 |
.andExpect(jsonPath("$.orgs.length()").value(3))
|
| 159 | 159 |
.andExpect(jsonPath("$.orgs[0].orgNo").value("001"))
|
| 160 |
+ .andExpect(jsonPath("$.orgs[0].status").value("INFO_PENDING"))
|
|
| 160 | 161 |
.andExpect(jsonPath("$.orgs[0].hasChannel").value(false))
|
| 161 | 162 |
.andExpect(jsonPath("$.orgs[0].stage").doesNotExist())
|
| 162 | 163 |
.andExpect(jsonPath("$.orgs[1].orgNo").value("002"))
|
| 163 | 164 |
.andExpect(jsonPath("$.orgs[1].hasChannel").value(true))
|
| 165 |
+ .andExpect(jsonPath("$.orgs[1].status").value("PARTIAL"))
|
|
| 164 | 166 |
.andExpect(jsonPath("$.orgs[1].stage").value(2));
|
| 165 | 167 |
} |
| 166 | 168 |
|
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?