--- frontend/src/App.tsx
+++ frontend/src/App.tsx
... | ... | @@ -7,6 +7,7 @@ |
| 7 | 7 |
import OrgList from './components/OrgList' |
| 8 | 8 |
import OrgOverview from './components/OrgOverview' |
| 9 | 9 |
import ProjectsPage from './components/ProjectsPage' |
| 10 |
+import RightsDashboard from './components/RightsDashboard' |
|
| 10 | 11 |
import SeedUpload from './components/SeedUpload' |
| 11 | 12 |
import SharedFiles from './components/SharedFiles' |
| 12 | 13 |
import Sidebar from './components/Sidebar' |
... | ... | @@ -17,7 +18,7 @@ |
| 17 | 18 |
// 화면 4개: Dashboard(전체 기관 현황 - 좌측 기관 목록 없이 전폭으로 쓴다), |
| 18 | 19 |
// 기관관리(시안 레이아웃의 기관 상세), 채널관리(명부 시드 + 담당자 배정 + 채널 생성), |
| 19 | 20 |
// 담당자관리(담당자 3종 CRUD - 담당자 정보가 입력되는 유일한 화면). |
| 20 |
-type Page = 'dashboard' | 'orgs' | 'channels' | 'contacts' | 'projects' | 'files' | 'system' |
|
| 21 |
+type Page = 'dashboard' | 'orgs' | 'channels' | 'contacts' | 'projects' | 'rights-check' | 'rights-process' | 'files' | 'system' |
|
| 21 | 22 |
|
| 22 | 23 |
const PAGE_TITLE: Record<Page, string> = {
|
| 23 | 24 |
dashboard: 'Dashboard', |
... | ... | @@ -25,6 +26,8 @@ |
| 25 | 26 |
channels: '채널관리', |
| 26 | 27 |
contacts: '담당자관리', |
| 27 | 28 |
projects: '사업관리', |
| 29 |
+ 'rights-check': '권리확인', |
|
| 30 |
+ 'rights-process': '권리처리', |
|
| 28 | 31 |
files: '자료실', |
| 29 | 32 |
system: '시스템관리', |
| 30 | 33 |
} |
... | ... | @@ -44,6 +47,7 @@ |
| 44 | 47 |
const [auth, setAuth] = useState<AuthState>('checking')
|
| 45 | 48 |
const [authErrorMessage, setAuthErrorMessage] = useState<string | null>(null) |
| 46 | 49 |
const [page, setPage] = useState<Page>('dashboard')
|
| 50 |
+ const [orgInitialTab, setOrgInitialTab] = useState<'권리확인' | '권리처리' | undefined>() |
|
| 47 | 51 |
// 창이 좁아지면 자동으로 접힌다. 사용자의 접기/펼치기 선택은 저장된다. |
| 48 | 52 |
const [menuCollapsed, toggleMenu] = useCollapsiblePanel('itnhub.menu', '(max-width: 1023px)')
|
| 49 | 53 |
const [listCollapsed, toggleList] = useCollapsiblePanel('itnhub.orglist', '(max-width: 1279px)')
|
... | ... | @@ -119,9 +123,12 @@ |
| 119 | 123 |
key === 'channels' || |
| 120 | 124 |
key === 'contacts' || |
| 121 | 125 |
key === 'projects' || |
| 126 |
+ key === 'rights-check' || |
|
| 127 |
+ key === 'rights-process' || |
|
| 122 | 128 |
key === 'files' || |
| 123 | 129 |
key === 'system' |
| 124 | 130 |
) {
|
| 131 |
+ if (key === 'orgs') setOrgInitialTab(undefined) |
|
| 125 | 132 |
setPage(key) |
| 126 | 133 |
} |
| 127 | 134 |
}} |
... | ... | @@ -144,7 +151,7 @@ |
| 144 | 151 |
|
| 145 | 152 |
<div className="flex min-h-0 flex-1"> |
| 146 | 153 |
{page !== 'contacts' && page !== 'dashboard' && page !== 'system'
|
| 147 |
- && page !== 'projects' && page !== 'files' && ( |
|
| 154 |
+ && page !== 'projects' && page !== 'rights-check' && page !== 'rights-process' && page !== 'files' && ( |
|
| 148 | 155 |
<div |
| 149 | 156 |
className={`flex shrink-0 flex-col border-r border-gray-200 transition-all duration-200 ${
|
| 150 | 157 |
listCollapsed ? 'w-14' : 'w-80' |
... | ... | @@ -168,6 +175,7 @@ |
| 168 | 175 |
<Dashboard |
| 169 | 176 |
onOpenOrg={(orgId) => {
|
| 170 | 177 |
setSelectedId(orgId) |
| 178 |
+ setOrgInitialTab(undefined) |
|
| 171 | 179 |
setPage('orgs')
|
| 172 | 180 |
}} |
| 173 | 181 |
// 요청자 요구: 대시보드 하단에서 전체를 볼 때는 사업관리로 넘어간다 |
... | ... | @@ -177,6 +185,16 @@ |
| 177 | 185 |
<ProjectsPage |
| 178 | 186 |
onOpenOrg={(orgId) => {
|
| 179 | 187 |
setSelectedId(orgId) |
| 188 |
+ setOrgInitialTab(undefined) |
|
| 189 |
+ setPage('orgs')
|
|
| 190 |
+ }} |
|
| 191 |
+ /> |
|
| 192 |
+ ) : page === 'rights-check' || page === 'rights-process' ? ( |
|
| 193 |
+ <RightsDashboard |
|
| 194 |
+ mode={page === 'rights-check' ? 'review' : 'process'}
|
|
| 195 |
+ onOpenOrg={(orgId) => {
|
|
| 196 |
+ setSelectedId(orgId) |
|
| 197 |
+ setOrgInitialTab(page === 'rights-check' ? '권리확인' : '권리처리') |
|
| 180 | 198 |
setPage('orgs')
|
| 181 | 199 |
}} |
| 182 | 200 |
/> |
... | ... | @@ -190,7 +208,7 @@ |
| 190 | 208 |
page === 'channels' ? ( |
| 191 | 209 |
<OrgDetail org={selected} onChanged={() => void reload()} />
|
| 192 | 210 |
) : ( |
| 193 |
- <OrgOverview org={selected} onChanged={() => void reload()} />
|
|
| 211 |
+ <OrgOverview org={selected} onChanged={() => void reload()} initialTab={orgInitialTab} />
|
|
| 194 | 212 |
) |
| 195 | 213 |
) : ( |
| 196 | 214 |
<p className="p-8 text-sm text-gray-500">왼쪽에서 기관을 선택하세요.</p> |
--- frontend/src/components/OrgOverview.tsx
+++ frontend/src/components/OrgOverview.tsx
... | ... | @@ -133,12 +133,18 @@ |
| 133 | 133 |
export default function OrgOverview({
|
| 134 | 134 |
org, |
| 135 | 135 |
onChanged, |
| 136 |
+ initialTab, |
|
| 136 | 137 |
}: {
|
| 137 | 138 |
org: Org |
| 138 | 139 |
onChanged?: () => void |
| 140 |
+ initialTab?: '권리확인' | '권리처리' |
|
| 139 | 141 |
}) {
|
| 140 | 142 |
const stages = useStages() |
| 141 |
- const [activeTab, setActiveTab] = useState(TABS[0]) |
|
| 143 |
+ const [activeTab, setActiveTab] = useState(initialTab ?? TABS[0]) |
|
| 144 |
+ |
|
| 145 |
+ useEffect(() => {
|
|
| 146 |
+ if (initialTab) setActiveTab(initialTab) |
|
| 147 |
+ }, [initialTab, org.id]) |
|
| 142 | 148 |
const [pickerRole, setPickerRole] = useState<'applicant' | 'mj' | 'itn' | 'lawyer' | null>(null) |
| 143 | 149 |
const [assignBusy, setAssignBusy] = useState(false) |
| 144 | 150 |
const [assignError, setAssignError] = useState<string | null>(null) |
+++ frontend/src/components/RightsDashboard.test.tsx
... | ... | @@ -0,0 +1,46 @@ |
| 1 | +import { fireEvent, render, screen, within } from '@testing-library/react' | |
| 2 | +import { beforeEach, describe, expect, it, vi } from 'vitest' | |
| 3 | +import RightsDashboard from './RightsDashboard' | |
| 4 | + | |
| 5 | +const mocks = vi.hoisted(() => ({ getDashboard: vi.fn() })) | |
| 6 | +vi.mock('../api/client', async (importOriginal) => ({ | |
| 7 | + ...(await importOriginal<typeof import('../api/client')>()), | |
| 8 | + getDashboard: mocks.getDashboard, | |
| 9 | +})) | |
| 10 | + | |
| 11 | +const data = { | |
| 12 | + summary: { applied: 2, docSubmitted: 0, reviewTotal: 120, reviewDone: 90, processTotal: 40, processDone: 10, unresolvedRe: 0, reportWriting: 0, completed: 0 }, | |
| 13 | + orgs: [ | |
| 14 | + { 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 }, | |
| 15 | + { 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 }, | |
| 16 | + ], | |
| 17 | +} | |
| 18 | + | |
| 19 | +describe('RightsDashboard', () => { | |
| 20 | + beforeEach(() => mocks.getDashboard.mockResolvedValue(data)) | |
| 21 | + | |
| 22 | + it('DB 집계 응답으로 권리확인 현황을 표시한다', async () => { | |
| 23 | + render(<RightsDashboard mode="review" onOpenOrg={() => {}} />) | |
| 24 | + expect(await screen.findByText('90 / 120')).toBeTruthy() | |
| 25 | + expect(screen.getAllByText('75%')).toHaveLength(3) | |
| 26 | + expect(screen.getByText('완료기관')).toBeTruthy() | |
| 27 | + }) | |
| 28 | + | |
| 29 | + it('기관 검색과 상태 필터가 동작한다', async () => { | |
| 30 | + render(<RightsDashboard mode="process" onOpenOrg={() => {}} />) | |
| 31 | + await screen.findByText('완료기관') | |
| 32 | + fireEvent.change(screen.getByLabelText('기관명 검색'), { target: { value: '대기' } }) | |
| 33 | + expect(screen.queryByText('완료기관')).toBeNull() | |
| 34 | + expect(screen.getByText('대기기관')).toBeTruthy() | |
| 35 | + fireEvent.change(screen.getByLabelText('진행 상태'), { target: { value: 'complete' } }) | |
| 36 | + expect(screen.getByText('조건에 맞는 기관이 없습니다.')).toBeTruthy() | |
| 37 | + }) | |
| 38 | + | |
| 39 | + it('기관 행을 선택하면 상세 화면 콜백을 호출한다', async () => { | |
| 40 | + const onOpenOrg = vi.fn() | |
| 41 | + render(<RightsDashboard mode="review" onOpenOrg={onOpenOrg} />) | |
| 42 | + const cell = await screen.findByText('완료기관') | |
| 43 | + fireEvent.click(within(cell.closest('tr')!).getByText('완료기관')) | |
| 44 | + expect(onOpenOrg).toHaveBeenCalledWith(1) | |
| 45 | + }) | |
| 46 | +}) |
+++ frontend/src/components/RightsDashboard.tsx
... | ... | @@ -0,0 +1,124 @@ |
| 1 | +import { useEffect, useMemo, useState } from 'react' | |
| 2 | +import { getDashboard, type DashboardData, type DashboardOrgRow } from '../api/client' | |
| 3 | + | |
| 4 | +type Mode = 'review' | 'process' | |
| 5 | + | |
| 6 | +interface Props { | |
| 7 | + mode: Mode | |
| 8 | + onOpenOrg: (orgId: number) => void | |
| 9 | +} | |
| 10 | + | |
| 11 | +const format = (value: number) => value.toLocaleString('ko-KR') | |
| 12 | +const percent = (done: number, total: number) => total ? Math.round((done / total) * 1000) / 10 : 0 | |
| 13 | + | |
| 14 | +function ProgressRing({ done, total, color }: { done: number; total: number; color: string }) { | |
| 15 | + const rate = percent(done, total) | |
| 16 | + return ( | |
| 17 | + <div | |
| 18 | + className="relative flex h-24 w-24 shrink-0 items-center justify-center rounded-full" | |
| 19 | + style={{ background: `conic-gradient(${color} ${rate}%, #e5e7eb 0)` }} | |
| 20 | + role="img" | |
| 21 | + aria-label={`${rate}% 완료`} | |
| 22 | + > | |
| 23 | + <div className="absolute inset-3 rounded-full bg-white" /> | |
| 24 | + <div className="relative text-center"> | |
| 25 | + <strong className="block text-xl font-bold">{rate}%</strong> | |
| 26 | + <span className="text-[10px] text-gray-500">{format(done)} / {format(total)}</span> | |
| 27 | + </div> | |
| 28 | + </div> | |
| 29 | + ) | |
| 30 | +} | |
| 31 | + | |
| 32 | +export default function RightsDashboard({ mode, onOpenOrg }: Props) { | |
| 33 | + const [data, setData] = useState<DashboardData | null>(null) | |
| 34 | + const [error, setError] = useState<string | null>(null) | |
| 35 | + const [keyword, setKeyword] = useState('') | |
| 36 | + const [status, setStatus] = useState('all') | |
| 37 | + | |
| 38 | + useEffect(() => { | |
| 39 | + let active = true | |
| 40 | + setData(null) | |
| 41 | + setError(null) | |
| 42 | + void getDashboard() | |
| 43 | + .then((result) => active && setData(result)) | |
| 44 | + .catch((cause: Error) => active && setError(cause.message)) | |
| 45 | + return () => { active = false } | |
| 46 | + }, [mode]) | |
| 47 | + | |
| 48 | + const rows = useMemo(() => { | |
| 49 | + if (!data) return [] | |
| 50 | + return data.orgs.filter((row) => { | |
| 51 | + const total = mode === 'review' ? row.reviewTotal : row.processTotal | |
| 52 | + const done = mode === 'review' ? row.reviewDone : row.processDone | |
| 53 | + const matchesKeyword = row.orgName.includes(keyword.trim()) || row.orgNo.includes(keyword.trim()) | |
| 54 | + const matchesStatus = status === 'all' | |
| 55 | + || (status === 'complete' && total > 0 && done === total) | |
| 56 | + || (status === 'progress' && done > 0 && done < total) | |
| 57 | + || (status === 'waiting' && total > 0 && done === 0) | |
| 58 | + return matchesKeyword && matchesStatus | |
| 59 | + }) | |
| 60 | + }, [data, keyword, mode, status]) | |
| 61 | + | |
| 62 | + if (error) return <div role="alert" className="m-6 rounded-md border border-red-200 bg-red-50 p-4 text-sm text-red-700">{error}</div> | |
| 63 | + if (!data) return <p className="p-8 text-sm text-gray-500">권리관리 현황을 불러오는 중…</p> | |
| 64 | + | |
| 65 | + const isReview = mode === 'review' | |
| 66 | + const title = isReview ? '권리확인' : '권리처리' | |
| 67 | + const total = isReview ? data.summary.reviewTotal : data.summary.processTotal | |
| 68 | + const done = isReview ? data.summary.reviewDone : data.summary.processDone | |
| 69 | + const remaining = Math.max(total - done, 0) | |
| 70 | + const rate = percent(done, total) | |
| 71 | + const accent = isReview ? '#ea8a00' : '#078d2a' | |
| 72 | + const orgsWithWork = data.orgs.filter((row) => (isReview ? row.reviewTotal : row.processTotal) > 0).length | |
| 73 | + | |
| 74 | + return ( | |
| 75 | + <div className="p-5 lg:p-7"> | |
| 76 | + <div className="mb-4"> | |
| 77 | + <h1 className="text-xl font-bold text-gray-900">{title}</h1> | |
| 78 | + <p className="mt-1 text-xs text-gray-500">{isReview ? '검토 대상 게시물의 권리 확인 및 처리 방향 결정' : '권리확인 완료 건의 처리 진행 상황 관리'}</p> | |
| 79 | + </div> | |
| 80 | + | |
| 81 | + <section className="grid overflow-hidden rounded-lg text-white shadow-sm sm:grid-cols-2 lg:grid-cols-4" style={{ backgroundColor: accent }}> | |
| 82 | + {[[`${rate}%`, `${title} 진척률`], [format(done), `${title} 완료`], [format(total), '전체 대상'], [format(remaining), '잔여 건수']].map(([value, label]) => ( | |
| 83 | + <div key={label} className="border-b border-white/20 px-5 py-4 sm:border-r lg:border-b-0"> | |
| 84 | + <strong className="block text-2xl font-bold tabular-nums">{value}</strong> | |
| 85 | + <span className="text-[11px] font-medium text-white/85">{label}</span> | |
| 86 | + </div> | |
| 87 | + ))} | |
| 88 | + </section> | |
| 89 | + | |
| 90 | + <section className="mt-3 grid gap-3 lg:grid-cols-[1.2fr_1fr_1fr]"> | |
| 91 | + <article className="flex items-center gap-5 rounded-lg border border-gray-200 bg-white p-5 shadow-sm"> | |
| 92 | + <ProgressRing done={done} total={total} color={accent} /> | |
| 93 | + <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> | |
| 94 | + </article> | |
| 95 | + <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> | |
| 96 | + <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> | |
| 97 | + </section> | |
| 98 | + | |
| 99 | + <section className="mt-3 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm"> | |
| 100 | + <div className="flex flex-col gap-3 border-b border-gray-200 p-4 sm:flex-row sm:items-end sm:justify-between"> | |
| 101 | + <div><h2 className="text-sm font-semibold">기관별 {title} 현황</h2><p className="mt-1 text-xs text-gray-500">기관을 선택하면 상세 {title} 화면으로 이동합니다.</p></div> | |
| 102 | + <div className="flex gap-2"> | |
| 103 | + <label className="sr-only" htmlFor={`${mode}-search`}>기관명 검색</label> | |
| 104 | + <input id={`${mode}-search`} value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="기관명 검색…" className="h-9 w-44 rounded-md border border-gray-300 px-3 text-xs focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100" /> | |
| 105 | + <label className="sr-only" htmlFor={`${mode}-status`}>진행 상태</label> | |
| 106 | + <select id={`${mode}-status`} value={status} onChange={(e) => setStatus(e.target.value)} className="h-9 rounded-md border border-gray-300 bg-white px-2 text-xs focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100"><option value="all">상태 전체</option><option value="complete">완료</option><option value="progress">진행 중</option><option value="waiting">대기</option></select> | |
| 107 | + </div> | |
| 108 | + </div> | |
| 109 | + <div className="overflow-x-auto"> | |
| 110 | + <table className="w-full min-w-[680px] text-xs"> | |
| 111 | + <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> | |
| 112 | + <tbody className="divide-y divide-gray-100"> | |
| 113 | + {rows.map((row: DashboardOrgRow) => { const rowTotal=isReview?row.reviewTotal:row.processTotal; const rowDone=isReview?row.reviewDone:row.processDone; const rowRate=percent(rowDone,rowTotal); return ( | |
| 114 | + <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) } }}> | |
| 115 | + <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> | |
| 116 | + </tr>) })} | |
| 117 | + {!rows.length && <tr><td colSpan={5} className="px-4 py-12 text-center text-gray-500">조건에 맞는 기관이 없습니다.</td></tr>} | |
| 118 | + </tbody> | |
| 119 | + </table> | |
| 120 | + </div> | |
| 121 | + </section> | |
| 122 | + </div> | |
| 123 | + ) | |
| 124 | +} |
--- frontend/src/components/Sidebar.test.tsx
+++ frontend/src/components/Sidebar.test.tsx
... | ... | @@ -40,7 +40,7 @@ |
| 40 | 40 |
expect(onSelect).toHaveBeenCalledWith('channels')
|
| 41 | 41 |
}) |
| 42 | 42 |
|
| 43 |
- // 자료실은 요구사항 [26]으로 열렸다. 아직 준비 중인 메뉴는 권리확인·권리처리·Mattermost다. |
|
| 43 |
+ // 권리확인·권리처리는 통합 현황 화면으로 열렸다. 아직 준비 중인 메뉴는 Mattermost다. |
|
| 44 | 44 |
it('미구현 메뉴는 비활성으로 표시되고 링크가 아니다', () => {
|
| 45 | 45 |
render(<Sidebar />) |
| 46 | 46 |
|
... | ... | @@ -50,6 +50,17 @@ |
| 50 | 50 |
expect(screen.queryByRole('link', { name: 'Mattermost' })).toBeNull()
|
| 51 | 51 |
}) |
| 52 | 52 |
|
| 53 |
+ it('권리관리 메뉴는 구현되어 링크로 열린다', () => {
|
|
| 54 |
+ const onSelect = vi.fn() |
|
| 55 |
+ render(<Sidebar onSelect={onSelect} />)
|
|
| 56 |
+ |
|
| 57 |
+ fireEvent.click(screen.getByRole('link', { name: '권리확인' }))
|
|
| 58 |
+ fireEvent.click(screen.getByRole('link', { name: '권리처리' }))
|
|
| 59 |
+ |
|
| 60 |
+ expect(onSelect).toHaveBeenNthCalledWith(1, 'rights-check') |
|
| 61 |
+ expect(onSelect).toHaveBeenNthCalledWith(2, 'rights-process') |
|
| 62 |
+ }) |
|
| 63 |
+ |
|
| 53 | 64 |
it('자료실은 구현되어 링크로 열린다', () => {
|
| 54 | 65 |
const onSelect = vi.fn() |
| 55 | 66 |
render(<Sidebar onSelect={onSelect} />)
|
--- frontend/src/components/Sidebar.tsx
+++ frontend/src/components/Sidebar.tsx
... | ... | @@ -68,7 +68,7 @@ |
| 68 | 68 |
{
|
| 69 | 69 |
key: 'rights-check', |
| 70 | 70 |
label: '권리확인', |
| 71 |
- enabled: false, |
|
| 71 |
+ enabled: true, |
|
| 72 | 72 |
icon: ( |
| 73 | 73 |
<svg className={ICON_CLASS} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 74 | 74 |
<path d="M12 3l8 3v6c0 4.5-3.2 7.7-8 9-4.8-1.3-8-4.5-8-9V6l8-3z" /> |
... | ... | @@ -79,7 +79,7 @@ |
| 79 | 79 |
{
|
| 80 | 80 |
key: 'rights-process', |
| 81 | 81 |
label: '권리처리', |
| 82 |
- enabled: false, |
|
| 82 |
+ enabled: true, |
|
| 83 | 83 |
icon: ( |
| 84 | 84 |
<svg className={ICON_CLASS} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 85 | 85 |
<path d="M7 3h7l5 5v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z" /> |
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?