refactor: 권리확인 화면을 코드표에 연결
이 파일 상단에 옵션 배열과 판정 규칙이 90줄 가까이 박혀 있었다. 전부 코드표로 옮겼다. 대분류/세부 추천값 이름을 직접 비교하던 if 사슬을 suggest 속성 조회로 바꿈 세부별 허용 처리결과 ALLOWED_RESULTS_BY_MINOR 맵을 allowed 속성으로 세부 표시명·부연설명 REVIEW_MINOR_DISPLAY를 label·note로 처리결과 뱃지색 Tailwind 클래스 맵을 tone 토큰으로 공공누리 유형 3중 정의(7개/5개/6개)를 scope 하나로 통합 판정 매트릭스는 종이 양식과 같은 자리에 칸을 놓아야 해서 목록을 훑지 않고 majors[0], minors[1] 처럼 자리로 집는다. 배치는 그대로고 값의 출처만 바뀌었다. 값이 바뀔 때 폼을 채우는 동작(기존 유형 복사·비고 문구 덧붙임)은 코드에 남겼다. 데이터로 옮기면 코드표 안에 규칙 언어를 만드는 꼴이 된다. 문구는 messages.ts로 뺐다. 덤으로 실제 버그가 하나 고쳐진다. 기존 공공누리 유형 드롭다운이 0~4유형만 갖고 있어서, 운영 DB에 '유형 확인 필요'(1,858건)·'제4유형'(68건)으로 저장된 1,926건이 상세 화면에서 선택 안 됨으로 보이고 있었다. 이제 prior scope에 그 값들이 들어간다. 프론트 208건 전부 통과. Co-Authored-By: Claude Opus 5 (1M context)
@089f17b05443793692885f18d9651924becfdfa7
+++ frontend/src/codes/messages.ts
... | ... | @@ -0,0 +1,7 @@ |
| 1 | +/** | |
| 2 | + * 화면이 자동으로 채워 넣는 안내 문구. 코드값이 아니라 문장이라 코드표에 넣지 않고 | |
| 3 | + * 여기 모아 둔다 - 코드표에 문장을 넣으면 선택지 목록에 섞여 관리가 어려워진다. | |
| 4 | + */ | |
| 5 | + | |
| 6 | +/** 권리처리를 추진하지 않아 기존 공공누리 유형을 그대로 쓸 때 비고에 남기는 문구. */ | |
| 7 | +export const NOTE_NO_PROCESSING = '권리처리 추진 미희망에 따른 기존 공공누리 부착' |
--- frontend/src/components/ReviewBoard.test.tsx
+++ frontend/src/components/ReviewBoard.test.tsx
... | ... | @@ -1,6 +1,7 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import ReviewBoard from './ReviewBoard' |
| 4 |
+import { withCodes } from '../codes/fixtures'
|
|
| 4 | 5 |
import type { Org, ReviewItem, ReviewPage } from '../api/client'
|
| 5 | 6 |
|
| 6 | 7 |
const mocks = vi.hoisted(() => ({
|
... | ... | @@ -102,7 +103,7 @@ |
| 102 | 103 |
it('목록을 표로 보여주고 처리결과 배지가 나온다', async () => {
|
| 103 | 104 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 104 | 105 |
|
| 105 |
- render(<ReviewBoard org={org()} />)
|
|
| 106 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 106 | 107 |
|
| 107 | 108 |
await waitFor(() => {
|
| 108 | 109 |
expect(screen.getByText('2026년 사업 공고')).toBeTruthy()
|
... | ... | @@ -116,7 +117,7 @@ |
| 116 | 117 |
it('자료가 없으면 안내 문구를 보여준다', async () => {
|
| 117 | 118 |
mocks.getReviewPage.mockResolvedValue(page({ items: [], total: 0 }))
|
| 118 | 119 |
|
| 119 |
- render(<ReviewBoard org={org()} />)
|
|
| 120 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 120 | 121 |
|
| 121 | 122 |
await waitFor(() => {
|
| 122 | 123 |
expect( |
... | ... | @@ -128,7 +129,7 @@ |
| 128 | 129 |
it('검색어를 입력하고 검색을 누르면 keyword로 다시 조회한다', async () => {
|
| 129 | 130 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 130 | 131 |
|
| 131 |
- render(<ReviewBoard org={org()} />)
|
|
| 132 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 132 | 133 |
|
| 133 | 134 |
await waitFor(() => |
| 134 | 135 |
expect(mocks.getReviewPage).toHaveBeenCalledWith(1, { keyword: undefined, page: 0, size: 30 }),
|
... | ... | @@ -146,7 +147,7 @@ |
| 146 | 147 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 147 | 148 |
mocks.importReview.mockResolvedValue({ created: 3, updated: 2, total: 5 })
|
| 148 | 149 |
|
| 149 |
- const { container } = render(<ReviewBoard org={org()} />)
|
|
| 150 |
+ const { container } = render(withCodes(<ReviewBoard org={org()} />))
|
|
| 150 | 151 |
|
| 151 | 152 |
await waitFor(() => expect(mocks.getReviewPage).toHaveBeenCalledTimes(1)) |
| 152 | 153 |
|
... | ... | @@ -167,7 +168,7 @@ |
| 167 | 168 |
mocks.bulkDeleteReview.mockResolvedValue({ deleted: 1 })
|
| 168 | 169 |
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) |
| 169 | 170 |
|
| 170 |
- render(<ReviewBoard org={org()} />)
|
|
| 171 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 171 | 172 |
|
| 172 | 173 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 173 | 174 |
|
... | ... | @@ -186,7 +187,7 @@ |
| 186 | 187 |
it('전체 선택 체크박스로 페이지의 모든 행을 고른다', async () => {
|
| 187 | 188 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 188 | 189 |
|
| 189 |
- render(<ReviewBoard org={org()} />)
|
|
| 190 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 190 | 191 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 191 | 192 |
|
| 192 | 193 |
fireEvent.click(screen.getByLabelText('전체 선택'))
|
... | ... | @@ -201,7 +202,7 @@ |
| 201 | 202 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 202 | 203 |
mocks.bulkUpdateReview.mockResolvedValue({ updated: 1 })
|
| 203 | 204 |
|
| 204 |
- render(<ReviewBoard org={org()} />)
|
|
| 205 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 205 | 206 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 206 | 207 |
|
| 207 | 208 |
fireEvent.click(screen.getByLabelText('1번 선택'))
|
... | ... | @@ -223,7 +224,7 @@ |
| 223 | 224 |
it('아무 값도 채우지 않고 일괄 등록하면 안내만 하고 호출하지 않는다', async () => {
|
| 224 | 225 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 225 | 226 |
|
| 226 |
- render(<ReviewBoard org={org()} />)
|
|
| 227 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 227 | 228 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 228 | 229 |
|
| 229 | 230 |
fireEvent.click(screen.getByLabelText('1번 선택'))
|
... | ... | @@ -236,7 +237,7 @@ |
| 236 | 237 |
it('상세검색 3칸은 열별 조건으로 함께 넘어간다', async () => {
|
| 237 | 238 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 238 | 239 |
|
| 239 |
- render(<ReviewBoard org={org()} />)
|
|
| 240 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 240 | 241 |
await waitFor(() => expect(mocks.getReviewPage).toHaveBeenCalled()) |
| 241 | 242 |
|
| 242 | 243 |
fireEvent.click(screen.getByRole('button', { name: '상세검색' }))
|
... | ... | @@ -260,7 +261,7 @@ |
| 260 | 261 |
it('다음 페이지 버튼을 누르면 page+1로 다시 조회한다', async () => {
|
| 261 | 262 |
mocks.getReviewPage.mockResolvedValue(page({ total: 31 }))
|
| 262 | 263 |
|
| 263 |
- render(<ReviewBoard org={org()} />)
|
|
| 264 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 264 | 265 |
|
| 265 | 266 |
await waitFor(() => |
| 266 | 267 |
expect(mocks.getReviewPage).toHaveBeenCalledWith(1, { keyword: undefined, page: 0, size: 30 }),
|
... | ... | @@ -281,7 +282,7 @@ |
| 281 | 282 |
item({ reviewMajor: '만료 저작물', opinion: '검토 완료' }),
|
| 282 | 283 |
) |
| 283 | 284 |
|
| 284 |
- render(<ReviewBoard org={org()} />)
|
|
| 285 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 285 | 286 |
|
| 286 | 287 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 287 | 288 |
fireEvent.click(screen.getByRole('button', { name: '수정' }))
|
... | ... | @@ -297,7 +298,7 @@ |
| 297 | 298 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 298 | 299 |
mocks.getReviewItem.mockResolvedValue(item({ reviewMajor: null, reviewResult: null }))
|
| 299 | 300 |
|
| 300 |
- render(<ReviewBoard org={org()} />)
|
|
| 301 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 301 | 302 |
|
| 302 | 303 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 303 | 304 |
fireEvent.click(screen.getByRole('button', { name: '수정' }))
|
... | ... | @@ -322,7 +323,7 @@ |
| 322 | 323 |
mocks.getReviewItem.mockResolvedValue( |
| 323 | 324 |
item({ reviewMajor: null, reviewMinor: null, reviewResult: null, judgedKoglType: null, ...itemOverrides }),
|
| 324 | 325 |
) |
| 325 |
- render(<ReviewBoard org={org()} />)
|
|
| 326 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 326 | 327 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 327 | 328 |
fireEvent.click(screen.getByRole('button', { name: '수정' }))
|
| 328 | 329 |
await waitFor(() => expect(mocks.getReviewItem).toHaveBeenCalled()) |
... | ... | @@ -400,7 +401,7 @@ |
| 400 | 401 |
mocks.getReviewItem.mockResolvedValue(item({ reviewMajor: null, reviewResult: null }))
|
| 401 | 402 |
mocks.updateReviewJudgment.mockResolvedValue(item({ reviewResult: '신유형 개방' }))
|
| 402 | 403 |
|
| 403 |
- render(<ReviewBoard org={org()} />)
|
|
| 404 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 404 | 405 |
|
| 405 | 406 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 406 | 407 |
fireEvent.click(screen.getByRole('button', { name: '수정' }))
|
... | ... | @@ -436,7 +437,7 @@ |
| 436 | 437 |
mocks.getReviewPage.mockResolvedValue(page()) |
| 437 | 438 |
mocks.getReviewItem.mockResolvedValue(item()) |
| 438 | 439 |
|
| 439 |
- render(<ReviewBoard org={org()} />)
|
|
| 440 |
+ render(withCodes(<ReviewBoard org={org()} />))
|
|
| 440 | 441 |
|
| 441 | 442 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 442 | 443 |
fireEvent.click(screen.getByRole('button', { name: '수정' }))
|
--- frontend/src/components/ReviewBoard.tsx
+++ frontend/src/components/ReviewBoard.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 1 |
+import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 2 | 2 |
import {
|
| 3 | 3 |
bulkDeleteReview, |
| 4 | 4 |
bulkUpdateReview, |
... | ... | @@ -10,102 +10,50 @@ |
| 10 | 10 |
type JudgmentRequest, |
| 11 | 11 |
type Org, |
| 12 | 12 |
type ReviewImportReport, |
| 13 |
+ type Code, |
|
| 13 | 14 |
type ReviewItem, |
| 14 | 15 |
} from '../api/client' |
| 16 |
+import { NOTE_NO_PROCESSING } from '../codes/messages'
|
|
| 17 |
+import { badgeClass } from '../codes/tone'
|
|
| 18 |
+import { attr, useCodes, useScopedCodes, useSplitByFirstRow } from '../codes/useCodes'
|
|
| 15 | 19 |
|
| 16 | 20 |
const PAGE_SIZE = 30 |
| 17 | 21 |
|
| 18 |
-const REVIEW_MAJOR_OPTIONS = ['만료 저작물', '업무상 저작물', '제3자 권리', '개인정보 포함'] |
|
| 22 |
+/** 공공누리 유형이 쓰이는 자리. 변호사 판정과 기존 부착 유형은 고를 수 있는 목록이 다르다. */ |
|
| 23 |
+const SCOPE_REVIEW = 'review' |
|
| 24 |
+const SCOPE_PRIOR = 'prior' |
|
| 19 | 25 |
|
| 20 |
-const REVIEW_MINOR_OPTIONS = [ |
|
| 21 |
- '1. 원시적 권리 전부 보유', |
|
| 22 |
- '2. 후천적 권리 전부 보유(계약서 확인 필요)', |
|
| 23 |
- '3. 권리 일부(공동) 보유', |
|
| 24 |
- '4. 권리 미보유', |
|
| 25 |
- '초상권 포함', |
|
| 26 |
-] |
|
| 26 |
+// 대분류·세부·처리결과·공공누리 유형은 모두 코드표에서 온다. |
|
| 27 |
+// 판정 매트릭스는 종이 양식과 같은 자리에 칸을 놓아야 해서 목록을 순서대로 훑지 않고 |
|
| 28 |
+// majors[0], minors[1] 처럼 자리로 집는다. 순서는 코드표의 정렬순서가 정한다. |
|
| 27 | 29 |
|
| 28 | 30 |
/** |
| 29 |
- * 처리결과 선택지. 앞 4개의 순서는 판정 매트릭스의 행 배치와 맞물려 있으므로 바꾸지 말고, |
|
| 30 |
- * 새 항목은 뒤에 붙인다. '권리처리 추진 미희망'은 제3자 권리 정책변경으로 추가된 항목이다. |
|
| 31 |
+ * 대분류/세부 선택에 따른 처리결과 추천값. 매칭되는 조합이 없으면 null(현재 값 유지). |
|
| 32 |
+ * |
|
| 33 |
+ * 예전에는 대분류·세부 이름을 직접 비교하는 if 사슬이었다. 지금은 코드의 suggest 속성만 |
|
| 34 |
+ * 읽는다 - 추천값이 바뀌어도 코드표만 고치면 된다. |
|
| 31 | 35 |
*/ |
| 32 |
-const REVIEW_RESULT_OPTIONS = [ |
|
| 33 |
- '신유형 개방', |
|
| 34 |
- '계약서 등 재확인', |
|
| 35 |
- '권리처리 추진', |
|
| 36 |
- '개방불가', |
|
| 37 |
- '권리처리 추진 미희망', |
|
| 38 |
-] |
|
| 39 |
- |
|
| 40 |
-const RESULT_PROCESSING = '권리처리 추진' |
|
| 41 |
-const RESULT_NO_PROCESSING = '권리처리 추진 미희망' |
|
| 42 |
-const RESULT_BLOCKED = '개방불가' |
|
| 43 |
- |
|
| 44 |
-/** 권리처리를 추진하는 동안에는 공공누리 유형을 확정하지 않고 '보류'로 둔다(정책변경으로 신설). */ |
|
| 45 |
-const KOGL_ON_HOLD = '보류' |
|
| 46 |
- |
|
| 47 |
-/** 권리처리를 추진하지 않아 기존 공공누리 유형을 그대로 쓸 때 비고에 남기는 문구. */ |
|
| 48 |
-const NOTE_NO_PROCESSING = '권리처리 추진 미희망에 따른 기존 공공누리 부착' |
|
| 49 |
- |
|
| 50 |
-const JUDGED_KOGL_TYPE_OPTIONS = ['0유형', '1유형', '2유형', '3유형', '4유형', '개방불가', KOGL_ON_HOLD] |
|
| 51 |
- |
|
| 52 |
-/** 공공누리 유형 라디오 첫 줄에 놓는 항목(나머지는 둘째 줄). */ |
|
| 53 |
-const KOGL_FIRST_ROW = ['0유형', RESULT_BLOCKED, KOGL_ON_HOLD] |
|
| 54 |
- |
|
| 55 |
-/** |
|
| 56 |
- * 제3자 권리 세부항목별로 고를 수 있는 처리결과. 정책변경(2026-07-20 검토안)에 따라 |
|
| 57 |
- * 3·4번은 추진/미희망 택일, 초상권은 개방불가까지 3택이다. 여기 없는 세부항목은 제한이 없다. |
|
| 58 |
- */ |
|
| 59 |
-const ALLOWED_RESULTS_BY_MINOR: Record<string, string[]> = {
|
|
| 60 |
- '3. 권리 일부(공동) 보유': [RESULT_PROCESSING, RESULT_NO_PROCESSING], |
|
| 61 |
- '4. 권리 미보유': [RESULT_PROCESSING, RESULT_NO_PROCESSING], |
|
| 62 |
- '초상권 포함': [RESULT_PROCESSING, RESULT_NO_PROCESSING, RESULT_BLOCKED], |
|
| 36 |
+function suggestReviewResult( |
|
| 37 |
+ major: string, |
|
| 38 |
+ minor: string, |
|
| 39 |
+ majors: Code[], |
|
| 40 |
+ minors: Code[], |
|
| 41 |
+): string | null {
|
|
| 42 |
+ const majorCode = majors.find((candidate) => candidate.code === major) |
|
| 43 |
+ if (!majorCode) {
|
|
| 44 |
+ return null |
|
| 45 |
+ } |
|
| 46 |
+ // 세부항목을 쓰는 대분류(제3자 권리)는 세부에서 추천값이 나온다. |
|
| 47 |
+ if (majorCode.attrs.hasMinor === true) {
|
|
| 48 |
+ return attr(minors.find((candidate) => candidate.code === minor), 'suggest') |
|
| 49 |
+ } |
|
| 50 |
+ return attr(majorCode, 'suggest') |
|
| 63 | 51 |
} |
| 64 | 52 |
|
| 65 |
-const REVIEW_RESULT_BADGE_STYLES: Record<string, string> = {
|
|
| 66 |
- '신유형 개방': 'bg-emerald-50 text-emerald-700', |
|
| 67 |
- '계약서 등 재확인': 'bg-amber-50 text-amber-700', |
|
| 68 |
- '권리처리 추진': 'bg-blue-50 text-blue-700', |
|
| 69 |
- '권리처리 추진 미희망': 'bg-slate-100 text-slate-700', |
|
| 70 |
- 개방불가: 'bg-red-50 text-red-700', |
|
| 53 |
+/** 세부항목의 회색 부연설명. 없으면 undefined. */ |
|
| 54 |
+function noteOf(code: Code | undefined): string | undefined {
|
|
| 55 |
+ return attr(code, 'note') ?? undefined |
|
| 71 | 56 |
} |
| 72 |
- |
|
| 73 |
-/** 대분류/세부 선택에 따른 처리결과 추천값. 매칭되는 조합이 없으면 null(현재 값 유지). */ |
|
| 74 |
-function suggestReviewResult(major: string, minor: string): string | null {
|
|
| 75 |
- if (major === '만료 저작물' || major === '업무상 저작물') {
|
|
| 76 |
- return '신유형 개방' |
|
| 77 |
- } |
|
| 78 |
- if (major === '개인정보 포함') {
|
|
| 79 |
- return '개방불가' |
|
| 80 |
- } |
|
| 81 |
- if (major === '제3자 권리') {
|
|
| 82 |
- if (minor === '1. 원시적 권리 전부 보유') {
|
|
| 83 |
- return '신유형 개방' |
|
| 84 |
- } |
|
| 85 |
- if (minor === '2. 후천적 권리 전부 보유(계약서 확인 필요)') {
|
|
| 86 |
- return '계약서 등 재확인' |
|
| 87 |
- } |
|
| 88 |
- if (ALLOWED_RESULTS_BY_MINOR[minor]) {
|
|
| 89 |
- // 정책변경: 추진/미희망(초상권은 개방불가까지) 중 택일이며 기본값은 '권리처리 추진'이다. |
|
| 90 |
- return RESULT_PROCESSING |
|
| 91 |
- } |
|
| 92 |
- } |
|
| 93 |
- return null |
|
| 94 |
-} |
|
| 95 |
- |
|
| 96 |
-/** 세부(review-minor) 라디오의 실제 저장값은 REVIEW_MINOR_OPTIONS 그대로 유지하고, |
|
| 97 |
- * 참조 양식과 같은 화면 표기(줄바꿈된 부연설명)만 별도로 매핑한다. */ |
|
| 98 |
-const REVIEW_MINOR_DISPLAY: { value: string; label: string; note?: string }[] = [
|
|
| 99 |
- { value: REVIEW_MINOR_OPTIONS[0], label: '1. 원시적 권리 전부 보유' },
|
|
| 100 |
- { value: REVIEW_MINOR_OPTIONS[1], label: '2. 후천적 권리 전부 보유', note: '(계약에 의한 전부 양수)' },
|
|
| 101 |
- {
|
|
| 102 |
- value: REVIEW_MINOR_OPTIONS[2], |
|
| 103 |
- label: '3. 권리 일부(공동) 보유', |
|
| 104 |
- note: '(보도자료·제3자저작물, 계약에 의한 전부/공동 보유 등)', |
|
| 105 |
- }, |
|
| 106 |
- { value: REVIEW_MINOR_OPTIONS[3], label: '4. 권리 미보유', note: '(공모전 수상작 등)' },
|
|
| 107 |
- { value: REVIEW_MINOR_OPTIONS[4], label: '초상권 포함' },
|
|
| 108 |
-] |
|
| 109 | 57 |
|
| 110 | 58 |
/** 소제목: 작은 파란 사각 불릿 + 굵은 제목 + 회색 부연설명. */ |
| 111 | 59 |
function SectionHeader({ title, note }: { title: string; note?: string }) {
|
... | ... | @@ -212,22 +160,25 @@ |
| 212 | 160 |
} |
| 213 | 161 |
|
| 214 | 162 |
function ReviewResultBadge({ value }: { value: string | null }) {
|
| 163 |
+ // 뱃지 색은 코드의 tone에서 온다. 코드표에 없는 값이면 badgeClass가 회색으로 떨어뜨린다. |
|
| 164 |
+ const results = useCodes('REVIEW_RESULT')
|
|
| 215 | 165 |
if (!value) {
|
| 216 | 166 |
return ( |
| 217 | 167 |
<span className="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-500">미판정</span> |
| 218 | 168 |
) |
| 219 | 169 |
} |
| 170 |
+ const tone = attr(results.find((candidate) => candidate.code === value), 'tone') |
|
| 220 | 171 |
return ( |
| 221 |
- <span |
|
| 222 |
- className={`rounded-full px-2 py-0.5 text-xs font-medium ${REVIEW_RESULT_BADGE_STYLES[value] ?? 'bg-gray-100 text-gray-600'}`}
|
|
| 223 |
- > |
|
| 224 |
- {value}
|
|
| 225 |
- </span> |
|
| 172 |
+ <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${badgeClass(tone)}`}>{value}</span>
|
|
| 226 | 173 |
) |
| 227 | 174 |
} |
| 228 | 175 |
|
| 229 | 176 |
/** 기관관리 상세의 권리확인 탭. 목록↔상세 두 화면을 이 컴포넌트 하나에서 전환한다. */ |
| 230 | 177 |
export default function ReviewBoard({ org }: { org: Org }) {
|
| 178 |
+ // 일괄 등록 드롭다운이 쓰는 선택지. 변호사가 고르는 자리라 review scope다. |
|
| 179 |
+ const results = useCodes('REVIEW_RESULT')
|
|
| 180 |
+ const koglReview = useScopedCodes('KOGL_TYPE', SCOPE_REVIEW)
|
|
| 181 |
+ |
|
| 231 | 182 |
const [mode, setMode] = useState<'list' | number>('list')
|
| 232 | 183 |
|
| 233 | 184 |
const [items, setItems] = useState<ReviewItem[]>([]) |
... | ... | @@ -533,8 +484,8 @@ |
| 533 | 484 |
className="rounded-md border border-gray-300 px-2 py-1.5 text-sm" |
| 534 | 485 |
> |
| 535 | 486 |
<option value="">처리결과 그대로</option> |
| 536 |
- {REVIEW_RESULT_OPTIONS.map((option) => (
|
|
| 537 |
- <option key={option} value={option}>{option}</option>
|
|
| 487 |
+ {results.map((option) => (
|
|
| 488 |
+ <option key={option.code} value={option.code}>{option.label}</option>
|
|
| 538 | 489 |
))} |
| 539 | 490 |
</select> |
| 540 | 491 |
<select |
... | ... | @@ -544,8 +495,8 @@ |
| 544 | 495 |
className="rounded-md border border-gray-300 px-2 py-1.5 text-sm" |
| 545 | 496 |
> |
| 546 | 497 |
<option value="">공공누리 유형 그대로</option> |
| 547 |
- {JUDGED_KOGL_TYPE_OPTIONS.map((option) => (
|
|
| 548 |
- <option key={option} value={option}>{option}</option>
|
|
| 498 |
+ {koglReview.map((option) => (
|
|
| 499 |
+ <option key={option.code} value={option.code}>{option.label}</option>
|
|
| 549 | 500 |
))} |
| 550 | 501 |
</select> |
| 551 | 502 |
<input |
... | ... | @@ -725,6 +676,16 @@ |
| 725 | 676 |
onBack: () => void |
| 726 | 677 |
onSaved: () => void |
| 727 | 678 |
}) {
|
| 679 |
+ // 판정 매트릭스가 쓰는 선택지. 자리(majors[0] 등)로 집으므로 코드표의 정렬순서가 곧 배치다. |
|
| 680 |
+ const majors = useCodes('REVIEW_MAJOR')
|
|
| 681 |
+ const minors = useCodes('REVIEW_MINOR')
|
|
| 682 |
+ const results = useCodes('REVIEW_RESULT')
|
|
| 683 |
+ // 변호사 판정값과 기존 부착 유형은 고를 수 있는 목록이 다르다. |
|
| 684 |
+ const kogl = useSplitByFirstRow('KOGL_TYPE', SCOPE_REVIEW)
|
|
| 685 |
+ const koglPrior = useScopedCodes('KOGL_TYPE', SCOPE_PRIOR)
|
|
| 686 |
+ const attachmentYn = useCodes('ATTACHMENT_YN')
|
|
| 687 |
+ const koglAttachedOptions = useCodes('KOGL_ATTACHED')
|
|
| 688 |
+ |
|
| 728 | 689 |
const [item, setItem] = useState<ReviewItem | null>(null) |
| 729 | 690 |
const [loading, setLoading] = useState(true) |
| 730 | 691 |
const [loadError, setLoadError] = useState<string | null>(null) |
... | ... | @@ -791,7 +752,7 @@ |
| 791 | 752 |
if (resultManuallySet) {
|
| 792 | 753 |
return |
| 793 | 754 |
} |
| 794 |
- const suggestion = suggestReviewResult(major, minor) |
|
| 755 |
+ const suggestion = suggestReviewResult(major, minor, majors, minors) |
|
| 795 | 756 |
if (suggestion) {
|
| 796 | 757 |
setReviewResult(suggestion) |
| 797 | 758 |
applyResultDerivations(suggestion) |
... | ... | @@ -808,15 +769,24 @@ |
| 808 | 769 |
* - 개방불가 → 공공누리 유형 '개방불가' |
| 809 | 770 |
* |
| 810 | 771 |
* 사용자가 이 값들을 다시 손으로 바꿀 수 있게, 잠그지는 않고 값만 채워 준다. |
| 772 |
+ * |
|
| 773 |
+ * 어떤 값을 채울지는 코드표(needsProcessing·koglSet·koglCopiesPrior)에서 오고, |
|
| 774 |
+ * "기존 유형을 복사하고 비고에 문구를 덧붙인다"는 동작만 여기 남는다. 동작까지 데이터로 |
|
| 775 |
+ * 옮기면 코드표 안에 규칙 언어를 새로 만드는 꼴이 된다. |
|
| 811 | 776 |
*/ |
| 812 | 777 |
function applyResultDerivations(result: string) {
|
| 813 |
- if (result === RESULT_PROCESSING) {
|
|
| 814 |
- setJudgedKoglType(KOGL_ON_HOLD) |
|
| 815 |
- setNeedsProcessing(true) |
|
| 778 |
+ const resultCode = results.find((candidate) => candidate.code === result) |
|
| 779 |
+ if (!resultCode) {
|
|
| 816 | 780 |
return |
| 817 | 781 |
} |
| 818 |
- if (result === RESULT_NO_PROCESSING) {
|
|
| 819 |
- setNeedsProcessing(false) |
|
| 782 |
+ |
|
| 783 |
+ const needs = resultCode.attrs.needsProcessing |
|
| 784 |
+ if (typeof needs === 'boolean') {
|
|
| 785 |
+ setNeedsProcessing(needs) |
|
| 786 |
+ } |
|
| 787 |
+ |
|
| 788 |
+ if (resultCode.attrs.koglCopiesPrior === true) {
|
|
| 789 |
+ // 기존에 부착돼 있던 유형을 그대로 쓰고, 그 사유를 비고에 남긴다. |
|
| 820 | 790 |
setJudgedKoglType(koglType || '') |
| 821 | 791 |
if (koglType) {
|
| 822 | 792 |
setLawyerNote((prev) => |
... | ... | @@ -827,21 +797,25 @@ |
| 827 | 797 |
} |
| 828 | 798 |
return |
| 829 | 799 |
} |
| 830 |
- if (result === RESULT_BLOCKED) {
|
|
| 831 |
- setJudgedKoglType(RESULT_BLOCKED) |
|
| 832 |
- setNeedsProcessing(false) |
|
| 800 |
+ |
|
| 801 |
+ const koglSet = attr(resultCode, 'koglSet') |
|
| 802 |
+ if (koglSet) {
|
|
| 803 |
+ setJudgedKoglType(koglSet) |
|
| 833 | 804 |
} |
| 834 | 805 |
} |
| 835 | 806 |
|
| 836 | 807 |
/** 지금 고른 세부항목에서 그 처리결과를 고를 수 있는지. 제한이 없는 세부항목은 전부 허용이다. */ |
| 837 | 808 |
function resultAllowed(option: string): boolean {
|
| 838 |
- const allowed = ALLOWED_RESULTS_BY_MINOR[reviewMinor] |
|
| 839 |
- return !allowed || allowed.includes(option) |
|
| 809 |
+ const minorCode = minors.find((candidate) => candidate.code === reviewMinor) |
|
| 810 |
+ const allowed = minorCode?.attrs.allowed |
|
| 811 |
+ return !Array.isArray(allowed) || (allowed as string[]).includes(option) |
|
| 840 | 812 |
} |
| 841 | 813 |
|
| 842 | 814 |
function pickMajor(major: string) {
|
| 843 | 815 |
setReviewMajor(major) |
| 844 |
- const minor = major === '제3자 권리' ? reviewMinor : '' |
|
| 816 |
+ // 세부항목을 쓰지 않는 대분류로 옮기면 세부 선택을 비운다. |
|
| 817 |
+ const majorCode = majors.find((candidate) => candidate.code === major) |
|
| 818 |
+ const minor = majorCode?.attrs.hasMinor === true ? reviewMinor : '' |
|
| 845 | 819 |
setReviewMinor(minor) |
| 846 | 820 |
applySuggestion(major, minor) |
| 847 | 821 |
} |
... | ... | @@ -936,34 +910,34 @@ |
| 936 | 910 |
/> |
| 937 | 911 |
<InfoRow label="첨부파일 여부"> |
| 938 | 912 |
<div className="flex items-center gap-5 text-sm"> |
| 939 |
- {['있음', '없음'].map((option) => (
|
|
| 940 |
- <label key={option} className="flex cursor-pointer items-center gap-1.5">
|
|
| 913 |
+ {attachmentYn.map((option) => (
|
|
| 914 |
+ <label key={option.code} className="flex cursor-pointer items-center gap-1.5">
|
|
| 941 | 915 |
<input |
| 942 | 916 |
type="radio" |
| 943 | 917 |
name="has-attachment" |
| 944 |
- aria-label={`첨부파일 ${option}`}
|
|
| 945 |
- checked={hasAttachment === option}
|
|
| 946 |
- onChange={() => setHasAttachment(option)}
|
|
| 918 |
+ aria-label={`첨부파일 ${option.code}`}
|
|
| 919 |
+ checked={hasAttachment === option.code}
|
|
| 920 |
+ onChange={() => setHasAttachment(option.code)}
|
|
| 947 | 921 |
className="h-3.5 w-3.5 accent-blue-600" |
| 948 | 922 |
/> |
| 949 |
- {option}
|
|
| 923 |
+ {option.label}
|
|
| 950 | 924 |
</label> |
| 951 | 925 |
))} |
| 952 | 926 |
</div> |
| 953 | 927 |
</InfoRow> |
| 954 | 928 |
<InfoRow label="기존 공공누리"> |
| 955 | 929 |
<div className="flex flex-wrap items-center gap-5 text-sm"> |
| 956 |
- {['부착', '미부착'].map((option) => (
|
|
| 957 |
- <label key={option} className="flex cursor-pointer items-center gap-1.5">
|
|
| 930 |
+ {koglAttachedOptions.map((option) => (
|
|
| 931 |
+ <label key={option.code} className="flex cursor-pointer items-center gap-1.5">
|
|
| 958 | 932 |
<input |
| 959 | 933 |
type="radio" |
| 960 | 934 |
name="kogl-attached" |
| 961 |
- aria-label={`기존 공공누리 ${option}`}
|
|
| 962 |
- checked={koglAttached === option}
|
|
| 963 |
- onChange={() => setKoglAttached(option)}
|
|
| 935 |
+ aria-label={`기존 공공누리 ${option.code}`}
|
|
| 936 |
+ checked={koglAttached === option.code}
|
|
| 937 |
+ onChange={() => setKoglAttached(option.code)}
|
|
| 964 | 938 |
className="h-3.5 w-3.5 accent-blue-600" |
| 965 | 939 |
/> |
| 966 |
- {option}
|
|
| 940 |
+ {option.label}
|
|
| 967 | 941 |
</label> |
| 968 | 942 |
))} |
| 969 | 943 |
<label className="flex items-center gap-1.5"> |
... | ... | @@ -975,8 +949,10 @@ |
| 975 | 949 |
className="rounded border border-gray-300 px-2 py-1 text-sm" |
| 976 | 950 |
> |
| 977 | 951 |
<option value="">선택</option> |
| 978 |
- {['0유형', '1유형', '2유형', '3유형', '4유형'].map((t) => (
|
|
| 979 |
- <option key={t} value={t}>{t}</option>
|
|
| 952 |
+ {/* 기관 조사표에서 온 값('유형 확인 필요' 등)도 여기 들어간다 - 없으면
|
|
| 953 |
+ 이미 그 값으로 저장된 1,900여 건이 "선택 안 됨"으로 보인다. */} |
|
| 954 |
+ {koglPrior.map((option) => (
|
|
| 955 |
+ <option key={option.code} value={option.code}>{option.label}</option>
|
|
| 980 | 956 |
))} |
| 981 | 957 |
</select> |
| 982 | 958 |
</label> |
... | ... | @@ -1012,10 +988,10 @@ |
| 1012 | 988 |
<td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
|
| 1013 | 989 |
<MatrixRadio |
| 1014 | 990 |
name="review-major" |
| 1015 |
- value={REVIEW_MAJOR_OPTIONS[0]}
|
|
| 991 |
+ value={majors[0].code}
|
|
| 1016 | 992 |
note="저작권성 없음, 배포용, 단순데이터" |
| 1017 |
- checked={reviewMajor === REVIEW_MAJOR_OPTIONS[0]}
|
|
| 1018 |
- onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[0])}
|
|
| 993 |
+ checked={reviewMajor === majors[0].code}
|
|
| 994 |
+ onChange={() => pickMajor(majors[0].code)}
|
|
| 1019 | 995 |
/> |
| 1020 | 996 |
</td> |
| 1021 | 997 |
<td className="border border-gray-300 px-3 py-2" /> |
... | ... | @@ -1025,10 +1001,10 @@ |
| 1025 | 1001 |
> |
| 1026 | 1002 |
<MatrixRadio |
| 1027 | 1003 |
name="review-result" |
| 1028 |
- value={REVIEW_RESULT_OPTIONS[0]}
|
|
| 1029 |
- checked={reviewResult === REVIEW_RESULT_OPTIONS[0]}
|
|
| 1030 |
- onChange={() => pickResult(REVIEW_RESULT_OPTIONS[0])}
|
|
| 1031 |
- disabled={!resultAllowed(REVIEW_RESULT_OPTIONS[0])}
|
|
| 1004 |
+ value={results[0].code}
|
|
| 1005 |
+ checked={reviewResult === results[0].code}
|
|
| 1006 |
+ onChange={() => pickResult(results[0].code)}
|
|
| 1007 |
+ disabled={!resultAllowed(results[0].code)}
|
|
| 1032 | 1008 |
bold |
| 1033 | 1009 |
/> |
| 1034 | 1010 |
</td> |
... | ... | @@ -1037,18 +1013,18 @@ |
| 1037 | 1013 |
<td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
|
| 1038 | 1014 |
<MatrixRadio |
| 1039 | 1015 |
name="review-major" |
| 1040 |
- value={REVIEW_MAJOR_OPTIONS[1]}
|
|
| 1041 |
- checked={reviewMajor === REVIEW_MAJOR_OPTIONS[1]}
|
|
| 1042 |
- onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[1])}
|
|
| 1016 |
+ value={majors[1].code}
|
|
| 1017 |
+ checked={reviewMajor === majors[1].code}
|
|
| 1018 |
+ onChange={() => pickMajor(majors[1].code)}
|
|
| 1043 | 1019 |
/> |
| 1044 | 1020 |
</td> |
| 1045 | 1021 |
<td className="border border-gray-300 px-3 py-2 align-top"> |
| 1046 | 1022 |
<MatrixRadio |
| 1047 | 1023 |
name="review-minor" |
| 1048 |
- value={REVIEW_MINOR_DISPLAY[0].value}
|
|
| 1049 |
- label={REVIEW_MINOR_DISPLAY[0].label}
|
|
| 1050 |
- checked={reviewMinor === REVIEW_MINOR_DISPLAY[0].value}
|
|
| 1051 |
- onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[0].value)}
|
|
| 1024 |
+ value={minors[0].code}
|
|
| 1025 |
+ label={minors[0].label}
|
|
| 1026 |
+ checked={reviewMinor === minors[0].code}
|
|
| 1027 |
+ onChange={() => pickMinor(minors[0].code)}
|
|
| 1052 | 1028 |
/> |
| 1053 | 1029 |
</td> |
| 1054 | 1030 |
</tr> |
... | ... | @@ -1056,19 +1032,19 @@ |
| 1056 | 1032 |
<td className="border border-gray-300 px-3 py-2 align-top" rowSpan={4}>
|
| 1057 | 1033 |
<MatrixRadio |
| 1058 | 1034 |
name="review-major" |
| 1059 |
- value={REVIEW_MAJOR_OPTIONS[2]}
|
|
| 1060 |
- checked={reviewMajor === REVIEW_MAJOR_OPTIONS[2]}
|
|
| 1061 |
- onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[2])}
|
|
| 1035 |
+ value={majors[2].code}
|
|
| 1036 |
+ checked={reviewMajor === majors[2].code}
|
|
| 1037 |
+ onChange={() => pickMajor(majors[2].code)}
|
|
| 1062 | 1038 |
/> |
| 1063 | 1039 |
</td> |
| 1064 | 1040 |
<td className="border border-gray-300 px-3 py-2 align-top"> |
| 1065 | 1041 |
<MatrixRadio |
| 1066 | 1042 |
name="review-minor" |
| 1067 |
- value={REVIEW_MINOR_DISPLAY[1].value}
|
|
| 1068 |
- label={REVIEW_MINOR_DISPLAY[1].label}
|
|
| 1069 |
- note={REVIEW_MINOR_DISPLAY[1].note}
|
|
| 1070 |
- checked={reviewMinor === REVIEW_MINOR_DISPLAY[1].value}
|
|
| 1071 |
- onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[1].value)}
|
|
| 1043 |
+ value={minors[1].code}
|
|
| 1044 |
+ label={minors[1].label}
|
|
| 1045 |
+ note={noteOf(minors[1])}
|
|
| 1046 |
+ checked={reviewMinor === minors[1].code}
|
|
| 1047 |
+ onChange={() => pickMinor(minors[1].code)}
|
|
| 1072 | 1048 |
/> |
| 1073 | 1049 |
</td> |
| 1074 | 1050 |
<td |
... | ... | @@ -1077,10 +1053,10 @@ |
| 1077 | 1053 |
> |
| 1078 | 1054 |
<MatrixRadio |
| 1079 | 1055 |
name="review-result" |
| 1080 |
- value={REVIEW_RESULT_OPTIONS[1]}
|
|
| 1081 |
- checked={reviewResult === REVIEW_RESULT_OPTIONS[1]}
|
|
| 1082 |
- onChange={() => pickResult(REVIEW_RESULT_OPTIONS[1])}
|
|
| 1083 |
- disabled={!resultAllowed(REVIEW_RESULT_OPTIONS[1])}
|
|
| 1056 |
+ value={results[1].code}
|
|
| 1057 |
+ checked={reviewResult === results[1].code}
|
|
| 1058 |
+ onChange={() => pickResult(results[1].code)}
|
|
| 1059 |
+ disabled={!resultAllowed(results[1].code)}
|
|
| 1084 | 1060 |
bold |
| 1085 | 1061 |
/> |
| 1086 | 1062 |
</td> |
... | ... | @@ -1089,11 +1065,11 @@ |
| 1089 | 1065 |
<td className="border border-gray-300 px-3 py-2 align-top"> |
| 1090 | 1066 |
<MatrixRadio |
| 1091 | 1067 |
name="review-minor" |
| 1092 |
- value={REVIEW_MINOR_DISPLAY[2].value}
|
|
| 1093 |
- label={REVIEW_MINOR_DISPLAY[2].label}
|
|
| 1094 |
- note={REVIEW_MINOR_DISPLAY[2].note}
|
|
| 1095 |
- checked={reviewMinor === REVIEW_MINOR_DISPLAY[2].value}
|
|
| 1096 |
- onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[2].value)}
|
|
| 1068 |
+ value={minors[2].code}
|
|
| 1069 |
+ label={minors[2].label}
|
|
| 1070 |
+ note={noteOf(minors[2])}
|
|
| 1071 |
+ checked={reviewMinor === minors[2].code}
|
|
| 1072 |
+ onChange={() => pickMinor(minors[2].code)}
|
|
| 1097 | 1073 |
/> |
| 1098 | 1074 |
</td> |
| 1099 | 1075 |
<td |
... | ... | @@ -1104,20 +1080,20 @@ |
| 1104 | 1080 |
<div className="flex flex-col items-start gap-1.5"> |
| 1105 | 1081 |
<MatrixRadio |
| 1106 | 1082 |
name="review-result" |
| 1107 |
- value={RESULT_PROCESSING}
|
|
| 1108 |
- checked={reviewResult === RESULT_PROCESSING}
|
|
| 1109 |
- onChange={() => pickResult(RESULT_PROCESSING)}
|
|
| 1110 |
- disabled={!resultAllowed(RESULT_PROCESSING)}
|
|
| 1083 |
+ value={results[2].code}
|
|
| 1084 |
+ checked={reviewResult === results[2].code}
|
|
| 1085 |
+ onChange={() => pickResult(results[2].code)}
|
|
| 1086 |
+ disabled={!resultAllowed(results[2].code)}
|
|
| 1111 | 1087 |
bold |
| 1112 | 1088 |
/> |
| 1113 | 1089 |
<MatrixRadio |
| 1114 | 1090 |
name="review-result" |
| 1115 |
- value={RESULT_NO_PROCESSING}
|
|
| 1116 |
- checked={reviewResult === RESULT_NO_PROCESSING}
|
|
| 1117 |
- onChange={() => pickResult(RESULT_NO_PROCESSING)}
|
|
| 1118 |
- disabled={!resultAllowed(RESULT_NO_PROCESSING)}
|
|
| 1091 |
+ value={results[4].code}
|
|
| 1092 |
+ checked={reviewResult === results[4].code}
|
|
| 1093 |
+ onChange={() => pickResult(results[4].code)}
|
|
| 1094 |
+ disabled={!resultAllowed(results[4].code)}
|
|
| 1119 | 1095 |
/> |
| 1120 |
- {reviewMinor === '초상권 포함' && (
|
|
| 1096 |
+ {reviewMinor === minors[4].code && (
|
|
| 1121 | 1097 |
<span className="text-[11px] text-gray-400">개방불가도 선택 가능</span> |
| 1122 | 1098 |
)} |
| 1123 | 1099 |
</div> |
... | ... | @@ -1127,11 +1103,11 @@ |
| 1127 | 1103 |
<td className="border border-gray-300 px-3 py-2 align-top"> |
| 1128 | 1104 |
<MatrixRadio |
| 1129 | 1105 |
name="review-minor" |
| 1130 |
- value={REVIEW_MINOR_DISPLAY[3].value}
|
|
| 1131 |
- label={REVIEW_MINOR_DISPLAY[3].label}
|
|
| 1132 |
- note={REVIEW_MINOR_DISPLAY[3].note}
|
|
| 1133 |
- checked={reviewMinor === REVIEW_MINOR_DISPLAY[3].value}
|
|
| 1134 |
- onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[3].value)}
|
|
| 1106 |
+ value={minors[3].code}
|
|
| 1107 |
+ label={minors[3].label}
|
|
| 1108 |
+ note={noteOf(minors[3])}
|
|
| 1109 |
+ checked={reviewMinor === minors[3].code}
|
|
| 1110 |
+ onChange={() => pickMinor(minors[3].code)}
|
|
| 1135 | 1111 |
/> |
| 1136 | 1112 |
</td> |
| 1137 | 1113 |
</tr> |
... | ... | @@ -1139,10 +1115,10 @@ |
| 1139 | 1115 |
<td className="border border-gray-300 px-3 py-2 align-top"> |
| 1140 | 1116 |
<MatrixRadio |
| 1141 | 1117 |
name="review-minor" |
| 1142 |
- value={REVIEW_MINOR_DISPLAY[4].value}
|
|
| 1143 |
- label={REVIEW_MINOR_DISPLAY[4].label}
|
|
| 1144 |
- checked={reviewMinor === REVIEW_MINOR_DISPLAY[4].value}
|
|
| 1145 |
- onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[4].value)}
|
|
| 1118 |
+ value={minors[4].code}
|
|
| 1119 |
+ label={minors[4].label}
|
|
| 1120 |
+ checked={reviewMinor === minors[4].code}
|
|
| 1121 |
+ onChange={() => pickMinor(minors[4].code)}
|
|
| 1146 | 1122 |
/> |
| 1147 | 1123 |
</td> |
| 1148 | 1124 |
</tr> |
... | ... | @@ -1150,10 +1126,10 @@ |
| 1150 | 1126 |
<td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
|
| 1151 | 1127 |
<MatrixRadio |
| 1152 | 1128 |
name="review-major" |
| 1153 |
- value={REVIEW_MAJOR_OPTIONS[3]}
|
|
| 1129 |
+ value={majors[3].code}
|
|
| 1154 | 1130 |
note="공공누리 적용 제외 대상, 검토불가" |
| 1155 |
- checked={reviewMajor === REVIEW_MAJOR_OPTIONS[3]}
|
|
| 1156 |
- onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[3])}
|
|
| 1131 |
+ checked={reviewMajor === majors[3].code}
|
|
| 1132 |
+ onChange={() => pickMajor(majors[3].code)}
|
|
| 1157 | 1133 |
/> |
| 1158 | 1134 |
</td> |
| 1159 | 1135 |
<td className="border border-gray-300 px-3 py-2" /> |
... | ... | @@ -1163,10 +1139,10 @@ |
| 1163 | 1139 |
> |
| 1164 | 1140 |
<MatrixRadio |
| 1165 | 1141 |
name="review-result" |
| 1166 |
- value={REVIEW_RESULT_OPTIONS[3]}
|
|
| 1167 |
- checked={reviewResult === REVIEW_RESULT_OPTIONS[3]}
|
|
| 1168 |
- onChange={() => pickResult(REVIEW_RESULT_OPTIONS[3])}
|
|
| 1169 |
- disabled={!resultAllowed(REVIEW_RESULT_OPTIONS[3])}
|
|
| 1142 |
+ value={results[3].code}
|
|
| 1143 |
+ checked={reviewResult === results[3].code}
|
|
| 1144 |
+ onChange={() => pickResult(results[3].code)}
|
|
| 1145 |
+ disabled={!resultAllowed(results[3].code)}
|
|
| 1170 | 1146 |
bold |
| 1171 | 1147 |
/> |
| 1172 | 1148 |
</td> |
... | ... | @@ -1179,32 +1155,32 @@ |
| 1179 | 1155 |
<InfoRow label="공공누리 유형"> |
| 1180 | 1156 |
<div className="flex flex-col gap-1.5"> |
| 1181 | 1157 |
<div className="flex flex-wrap gap-x-4 gap-y-1"> |
| 1182 |
- {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => KOGL_FIRST_ROW.includes(o)).map((option) => (
|
|
| 1183 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 1158 |
+ {kogl.first.map((option) => (
|
|
| 1159 |
+ <label key={option.code} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 1184 | 1160 |
<input |
| 1185 | 1161 |
type="radio" |
| 1186 | 1162 |
name="judged-kogl-type" |
| 1187 | 1163 |
// 처리결과에도 '개방불가'가 있어 라벨만으로는 구분되지 않는다. |
| 1188 |
- aria-label={`공공누리 유형 ${option}`}
|
|
| 1189 |
- checked={judgedKoglType === option}
|
|
| 1190 |
- onChange={() => setJudgedKoglType(option)}
|
|
| 1164 |
+ aria-label={`공공누리 유형 ${option.code}`}
|
|
| 1165 |
+ checked={judgedKoglType === option.code}
|
|
| 1166 |
+ onChange={() => setJudgedKoglType(option.code)}
|
|
| 1191 | 1167 |
/> |
| 1192 |
- {option}
|
|
| 1168 |
+ {option.label}
|
|
| 1193 | 1169 |
</label> |
| 1194 | 1170 |
))} |
| 1195 | 1171 |
</div> |
| 1196 | 1172 |
<div className="flex flex-wrap items-center gap-x-4 gap-y-1"> |
| 1197 |
- {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => !KOGL_FIRST_ROW.includes(o)).map((option) => (
|
|
| 1198 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 1173 |
+ {kogl.rest.map((option) => (
|
|
| 1174 |
+ <label key={option.code} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 1199 | 1175 |
<input |
| 1200 | 1176 |
type="radio" |
| 1201 | 1177 |
name="judged-kogl-type" |
| 1202 | 1178 |
// 처리결과에도 '개방불가'가 있어 라벨만으로는 구분되지 않는다. |
| 1203 |
- aria-label={`공공누리 유형 ${option}`}
|
|
| 1204 |
- checked={judgedKoglType === option}
|
|
| 1205 |
- onChange={() => setJudgedKoglType(option)}
|
|
| 1179 |
+ aria-label={`공공누리 유형 ${option.code}`}
|
|
| 1180 |
+ checked={judgedKoglType === option.code}
|
|
| 1181 |
+ onChange={() => setJudgedKoglType(option.code)}
|
|
| 1206 | 1182 |
/> |
| 1207 |
- {option}
|
|
| 1183 |
+ {option.label}
|
|
| 1208 | 1184 |
</label> |
| 1209 | 1185 |
))} |
| 1210 | 1186 |
<label className="ml-2 flex items-center gap-1.5 text-sm text-gray-800"> |
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?