refactor: 권리처리 화면을 코드표에 연결 (Phase 2 화면 전환 완료)
처리상태 미처리/처리완료를 코드표로. 어느 값이 완료인지는 done 속성이 정한다 필터 칩 서버에 넘기는 키(PENDING/DONE)도 done 속성에서 나온다 계약서류 6종 '기타'는 freeText 속성으로 구분해 자유 입력을 붙인다 공공누리 유형 권리확인과 같은 코드표를 쓰되 process scope라 '보류'가 안 나온다 프론트에 흩어져 있던 '처리완료' 비교 6곳이 사라졌다. 남은 것은 SQL 5곳과 자바 1곳이고 Phase 3에서 정리한다. 이로써 화면 8개가 모두 코드표를 본다. 각 화면 상단의 옵션 배열은 전부 없어졌다. 백엔드 263건, 프론트 208건 전부 통과. Co-Authored-By: Claude Opus 5 (1M context)
@52b8e64dd10e73397200dfe9140f7a06e0efe5c1
--- frontend/src/components/ProcessBoard.test.tsx
+++ frontend/src/components/ProcessBoard.test.tsx
... | ... | @@ -1,6 +1,7 @@ |
| 1 |
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 1 |
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
| 2 | 2 |
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
| 3 | 3 |
import ProcessBoard from './ProcessBoard' |
| 4 |
+import { withCodes } from '../codes/fixtures'
|
|
| 4 | 5 |
import type { Org, ProcessItem, ProcessPage } from '../api/client'
|
| 5 | 6 |
|
| 6 | 7 |
const mocks = vi.hoisted(() => ({
|
... | ... | @@ -104,7 +105,7 @@ |
| 104 | 105 |
it('목록을 표로 보여주고 처리상태 배지가 나온다', async () => {
|
| 105 | 106 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 106 | 107 |
|
| 107 |
- render(<ProcessBoard org={org()} />)
|
|
| 108 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 108 | 109 |
|
| 109 | 110 |
await waitFor(() => {
|
| 110 | 111 |
expect(screen.getByText('2026년 사업 공고')).toBeTruthy()
|
... | ... | @@ -117,7 +118,7 @@ |
| 117 | 118 |
it('자료가 없으면 안내 문구를 보여준다', async () => {
|
| 118 | 119 |
mocks.getProcessPage.mockResolvedValue(page({ items: [], total: 0 }))
|
| 119 | 120 |
|
| 120 |
- render(<ProcessBoard org={org()} />)
|
|
| 121 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 121 | 122 |
|
| 122 | 123 |
await waitFor(() => {
|
| 123 | 124 |
expect( |
... | ... | @@ -129,7 +130,7 @@ |
| 129 | 130 |
it('상태 칩을 누르면 status 파라미터로 다시 조회한다', async () => {
|
| 130 | 131 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 131 | 132 |
|
| 132 |
- render(<ProcessBoard org={org()} />)
|
|
| 133 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 133 | 134 |
|
| 134 | 135 |
await waitFor(() => |
| 135 | 136 |
expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
|
... | ... | @@ -166,7 +167,7 @@ |
| 166 | 167 |
it('검색어를 입력하고 검색을 누르면 keyword로 다시 조회한다', async () => {
|
| 167 | 168 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 168 | 169 |
|
| 169 |
- render(<ProcessBoard org={org()} />)
|
|
| 170 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 170 | 171 |
|
| 171 | 172 |
await waitFor(() => expect(mocks.getProcessPage).toHaveBeenCalledTimes(1)) |
| 172 | 173 |
|
... | ... | @@ -187,7 +188,7 @@ |
| 187 | 188 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 188 | 189 |
mocks.importProcess.mockResolvedValue({ created: 3, updated: 2, total: 5 })
|
| 189 | 190 |
|
| 190 |
- const { container } = render(<ProcessBoard org={org()} />)
|
|
| 191 |
+ const { container } = render(withCodes(<ProcessBoard org={org()} />))
|
|
| 191 | 192 |
|
| 192 | 193 |
await waitFor(() => expect(mocks.getProcessPage).toHaveBeenCalledTimes(1)) |
| 193 | 194 |
|
... | ... | @@ -206,7 +207,7 @@ |
| 206 | 207 |
it('처리완료 건은 수정 버튼이, 미처리 건은 처리등록 버튼만 보인다', async () => {
|
| 207 | 208 |
mocks.getProcessPage.mockResolvedValue(page({ items: [item({ processStatus: '처리완료' })] }))
|
| 208 | 209 |
|
| 209 |
- render(<ProcessBoard org={org()} />)
|
|
| 210 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 210 | 211 |
|
| 211 | 212 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 212 | 213 |
expect(screen.getByRole('button', { name: '수정' })).toBeTruthy()
|
... | ... | @@ -220,7 +221,7 @@ |
| 220 | 221 |
mocks.bulkDeleteProcess.mockResolvedValue({ deleted: 1 })
|
| 221 | 222 |
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) |
| 222 | 223 |
|
| 223 |
- render(<ProcessBoard org={org()} />)
|
|
| 224 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 224 | 225 |
|
| 225 | 226 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 226 | 227 |
|
... | ... | @@ -237,7 +238,7 @@ |
| 237 | 238 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 238 | 239 |
mocks.bulkUpdateProcess.mockResolvedValue({ updated: 1 })
|
| 239 | 240 |
|
| 240 |
- render(<ProcessBoard org={org()} />)
|
|
| 241 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 241 | 242 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 242 | 243 |
|
| 243 | 244 |
fireEvent.click(screen.getByLabelText('1번 선택'))
|
... | ... | @@ -259,7 +260,7 @@ |
| 259 | 260 |
it('상세검색 3칸은 열별 조건으로 함께 넘어간다', async () => {
|
| 260 | 261 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 261 | 262 |
|
| 262 |
- render(<ProcessBoard org={org()} />)
|
|
| 263 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 263 | 264 |
await waitFor(() => expect(mocks.getProcessPage).toHaveBeenCalled()) |
| 264 | 265 |
|
| 265 | 266 |
fireEvent.click(screen.getByRole('button', { name: '상세검색' }))
|
... | ... | @@ -284,7 +285,7 @@ |
| 284 | 285 |
it('다음 페이지 버튼을 누르면 page+1로 다시 조회한다', async () => {
|
| 285 | 286 |
mocks.getProcessPage.mockResolvedValue(page({ total: 31 }))
|
| 286 | 287 |
|
| 287 |
- render(<ProcessBoard org={org()} />)
|
|
| 288 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 288 | 289 |
|
| 289 | 290 |
await waitFor(() => |
| 290 | 291 |
expect(mocks.getProcessPage).toHaveBeenCalledWith(1, {
|
... | ... | @@ -313,7 +314,7 @@ |
| 313 | 314 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 314 | 315 |
mocks.getProcessItem.mockResolvedValue(item({ finalOpinion: '검토 완료' }))
|
| 315 | 316 |
|
| 316 |
- render(<ProcessBoard org={org()} />)
|
|
| 317 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 317 | 318 |
|
| 318 | 319 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 319 | 320 |
fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
|
... | ... | @@ -328,7 +329,7 @@ |
| 328 | 329 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 329 | 330 |
mocks.getProcessItem.mockResolvedValue(item()) |
| 330 | 331 |
|
| 331 |
- render(<ProcessBoard org={org()} />)
|
|
| 332 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 332 | 333 |
|
| 333 | 334 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 334 | 335 |
fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
|
... | ... | @@ -346,7 +347,7 @@ |
| 346 | 347 |
mocks.getProcessItem.mockResolvedValue(item()) |
| 347 | 348 |
mocks.updateProcessing.mockResolvedValue(item({ processStatus: '처리완료' }))
|
| 348 | 349 |
|
| 349 |
- render(<ProcessBoard org={org()} />)
|
|
| 350 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 350 | 351 |
|
| 351 | 352 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 352 | 353 |
fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
|
... | ... | @@ -380,7 +381,7 @@ |
| 380 | 381 |
item({ contractDocs: '양도계약서, 기타:구두 동의' }),
|
| 381 | 382 |
) |
| 382 | 383 |
|
| 383 |
- render(<ProcessBoard org={org()} />)
|
|
| 384 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 384 | 385 |
|
| 385 | 386 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 386 | 387 |
fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
|
... | ... | @@ -395,7 +396,7 @@ |
| 395 | 396 |
mocks.getProcessPage.mockResolvedValue(page()) |
| 396 | 397 |
mocks.getProcessItem.mockResolvedValue(item()) |
| 397 | 398 |
|
| 398 |
- render(<ProcessBoard org={org()} />)
|
|
| 399 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 399 | 400 |
|
| 400 | 401 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 401 | 402 |
fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
|
... | ... | @@ -413,7 +414,7 @@ |
| 413 | 414 |
mocks.deleteProcessItem.mockResolvedValue(undefined) |
| 414 | 415 |
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) |
| 415 | 416 |
|
| 416 |
- render(<ProcessBoard org={org()} />)
|
|
| 417 |
+ render(withCodes(<ProcessBoard org={org()} />))
|
|
| 417 | 418 |
|
| 418 | 419 |
await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
| 419 | 420 |
fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
|
--- frontend/src/components/ProcessBoard.tsx
+++ frontend/src/components/ProcessBoard.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 |
bulkDeleteProcess, |
| 4 | 4 |
bulkUpdateProcess, |
... | ... | @@ -11,41 +11,47 @@ |
| 11 | 11 |
type Org, |
| 12 | 12 |
type ProcessImportReport, |
| 13 | 13 |
type ProcessItem, |
| 14 |
+ type Code, |
|
| 14 | 15 |
type ProcessStatusFilter, |
| 15 | 16 |
type ProcessingRequest, |
| 16 | 17 |
} from '../api/client' |
| 18 |
+import { useCodes, useSplitByFirstRow } from '../codes/useCodes'
|
|
| 17 | 19 |
|
| 18 | 20 |
const PAGE_SIZE = 30 |
| 19 | 21 |
|
| 20 |
-const STATUS_CHIPS: { key: ProcessStatusFilter | undefined; label: string }[] = [
|
|
| 21 |
- { key: undefined, label: '전체' },
|
|
| 22 |
- { key: 'PENDING', label: '미처리' },
|
|
| 23 |
- { key: 'DONE', label: '처리완료' },
|
|
| 24 |
-] |
|
| 22 |
+/** 공공누리 유형이 쓰이는 자리. 권리처리 판정에는 '보류'가 없다. */ |
|
| 23 |
+const SCOPE_PROCESS = 'process' |
|
| 25 | 24 |
|
| 26 |
-const CONTRACT_DOC_OPTIONS = ['양도계약서', '제안요청서', '초상이용동의서', '공공누리동의서', '공문'] |
|
| 27 |
-const CONTRACT_ETC = '기타' |
|
| 25 |
+/** 자유 입력이 붙는 계약서류('기타'). 저장값이 "기타:본문" 형태가 된다. */
|
|
| 26 |
+function etcDocOf(docs: Code[]): Code | undefined {
|
|
| 27 |
+ return docs.find((doc) => doc.attrs.freeText === true) |
|
| 28 |
+} |
|
| 28 | 29 |
|
| 29 |
-const JUDGED_KOGL_TYPE_OPTIONS = ['0유형', '개방불가', '1유형', '2유형', '3유형', '4유형'] |
|
| 30 |
- |
|
| 31 |
-const PROCESS_STATUS_OPTIONS = ['미처리', '처리완료'] |
|
| 30 |
+/** '처리완료'에 해당하는 코드. 처리일시를 찍고 배지를 초록으로 바꾸는 기준이다. */ |
|
| 31 |
+function doneStatusOf(statuses: Code[]): Code | undefined {
|
|
| 32 |
+ return statuses.find((status) => status.attrs.done === true) |
|
| 33 |
+} |
|
| 32 | 34 |
|
| 33 | 35 |
/** "양도계약서, 공문, 기타:직접 촬영본 사용동의" 형태의 저장값 → 체크박스 상태로 역파싱한다. */ |
| 34 |
-function parseContractDocs(value: string | null): { selected: Set<string>; etcText: string } {
|
|
| 36 |
+function parseContractDocs( |
|
| 37 |
+ value: string | null, |
|
| 38 |
+ docs: Code[], |
|
| 39 |
+): { selected: Set<string>; etcText: string } {
|
|
| 35 | 40 |
const selected = new Set<string>() |
| 36 | 41 |
let etcText = '' |
| 37 | 42 |
if (!value) {
|
| 38 | 43 |
return { selected, etcText }
|
| 39 | 44 |
} |
| 45 |
+ const etc = etcDocOf(docs) |
|
| 40 | 46 |
for (const raw of value.split(',')) {
|
| 41 | 47 |
const part = raw.trim() |
| 42 | 48 |
if (!part) {
|
| 43 | 49 |
continue |
| 44 | 50 |
} |
| 45 |
- if (part.startsWith(`${CONTRACT_ETC}:`)) {
|
|
| 46 |
- selected.add(CONTRACT_ETC) |
|
| 47 |
- etcText = part.slice(CONTRACT_ETC.length + 1) |
|
| 48 |
- } else if (CONTRACT_DOC_OPTIONS.includes(part)) {
|
|
| 51 |
+ if (etc && part.startsWith(`${etc.code}:`)) {
|
|
| 52 |
+ selected.add(etc.code) |
|
| 53 |
+ etcText = part.slice(etc.code.length + 1) |
|
| 54 |
+ } else if (docs.some((doc) => doc.code === part)) {
|
|
| 49 | 55 |
selected.add(part) |
| 50 | 56 |
} |
| 51 | 57 |
} |
... | ... | @@ -53,10 +59,17 @@ |
| 53 | 59 |
} |
| 54 | 60 |
|
| 55 | 61 |
/** 체크박스 상태 → 저장값. 아무것도 선택하지 않았으면 null. */ |
| 56 |
-function serializeContractDocs(selected: Set<string>, etcText: string): string | null {
|
|
| 57 |
- const parts: string[] = CONTRACT_DOC_OPTIONS.filter((opt) => selected.has(opt)) |
|
| 58 |
- if (selected.has(CONTRACT_ETC)) {
|
|
| 59 |
- parts.push(`${CONTRACT_ETC}:${etcText}`)
|
|
| 62 |
+function serializeContractDocs( |
|
| 63 |
+ selected: Set<string>, |
|
| 64 |
+ etcText: string, |
|
| 65 |
+ docs: Code[], |
|
| 66 |
+): string | null {
|
|
| 67 |
+ const etc = etcDocOf(docs) |
|
| 68 |
+ const parts: string[] = docs |
|
| 69 |
+ .filter((doc) => doc.attrs.freeText !== true && selected.has(doc.code)) |
|
| 70 |
+ .map((doc) => doc.code) |
|
| 71 |
+ if (etc && selected.has(etc.code)) {
|
|
| 72 |
+ parts.push(`${etc.code}:${etcText}`)
|
|
| 60 | 73 |
} |
| 61 | 74 |
return parts.length > 0 ? parts.join(', ') : null
|
| 62 | 75 |
} |
... | ... | @@ -154,20 +167,37 @@ |
| 154 | 167 |
} |
| 155 | 168 |
|
| 156 | 169 |
function ProcessStatusBadge({ value }: { value: string | null }) {
|
| 157 |
- const done = value === '처리완료' |
|
| 170 |
+ // 어느 값이 '완료'인지는 코드표의 done 속성이 정한다. |
|
| 171 |
+ const statuses = useCodes('PROCESS_STATUS')
|
|
| 172 |
+ const doneCode = doneStatusOf(statuses) |
|
| 173 |
+ const done = !!doneCode && value === doneCode.code |
|
| 174 |
+ const pendingLabel = statuses.find((status) => status.attrs.done !== true)?.label ?? '' |
|
| 158 | 175 |
return ( |
| 159 | 176 |
<span |
| 160 | 177 |
className={`rounded-full px-2 py-0.5 text-xs font-medium ${
|
| 161 | 178 |
done ? 'bg-emerald-50 text-emerald-700' : 'bg-gray-100 text-gray-500' |
| 162 | 179 |
}`} |
| 163 | 180 |
> |
| 164 |
- {done ? '처리완료' : '미처리'}
|
|
| 181 |
+ {done ? doneCode.label : pendingLabel}
|
|
| 165 | 182 |
</span> |
| 166 | 183 |
) |
| 167 | 184 |
} |
| 168 | 185 |
|
| 169 | 186 |
/** 기관관리 상세의 권리처리 탭. 목록↔상세 두 화면을 이 컴포넌트 하나에서 전환한다(ReviewBoard와 동일 구조). */ |
| 170 | 187 |
export default function ProcessBoard({ org }: { org: Org }) {
|
| 188 |
+ const statuses = useCodes('PROCESS_STATUS')
|
|
| 189 |
+ const doneStatusCode = doneStatusOf(statuses)?.code |
|
| 190 |
+ const koglProcess = useSplitByFirstRow('KOGL_TYPE', SCOPE_PROCESS)
|
|
| 191 |
+ // 필터 칩의 '전체'는 코드가 아니라 화면에서만 쓰는 항목이라 앞에 붙인다. |
|
| 192 |
+ // 나머지는 코드표에서 오되, 서버에 넘기는 키(PENDING/DONE)는 done 속성으로 정한다. |
|
| 193 |
+ const statusChips: { key: ProcessStatusFilter | undefined; label: string }[] = [
|
|
| 194 |
+ { key: undefined, label: '전체' },
|
|
| 195 |
+ ...statuses.map((status) => ({
|
|
| 196 |
+ key: (status.attrs.done === true ? 'DONE' : 'PENDING') as ProcessStatusFilter, |
|
| 197 |
+ label: status.label, |
|
| 198 |
+ })), |
|
| 199 |
+ ] |
|
| 200 |
+ |
|
| 171 | 201 |
const [mode, setMode] = useState<'list' | number>('list')
|
| 172 | 202 |
|
| 173 | 203 |
const [items, setItems] = useState<ProcessItem[]>([]) |
... | ... | @@ -363,7 +393,7 @@ |
| 363 | 393 |
<div className="flex flex-wrap items-center justify-between gap-3"> |
| 364 | 394 |
<div className="flex flex-wrap items-center gap-2"> |
| 365 | 395 |
<div className="flex items-center gap-1" role="group" aria-label="처리상태 필터"> |
| 366 |
- {STATUS_CHIPS.map((chip) => (
|
|
| 396 |
+ {statusChips.map((chip) => (
|
|
| 367 | 397 |
<button |
| 368 | 398 |
key={chip.label}
|
| 369 | 399 |
type="button" |
... | ... | @@ -510,8 +540,8 @@ |
| 510 | 540 |
className="rounded-md border border-gray-300 px-2 py-1.5 text-sm" |
| 511 | 541 |
> |
| 512 | 542 |
<option value="">공공누리 유형 그대로</option> |
| 513 |
- {JUDGED_KOGL_TYPE_OPTIONS.map((option) => (
|
|
| 514 |
- <option key={option} value={option}>{option}</option>
|
|
| 543 |
+ {[...koglProcess.first, ...koglProcess.rest].map((option) => (
|
|
| 544 |
+ <option key={option.code} value={option.code}>{option.label}</option>
|
|
| 515 | 545 |
))} |
| 516 | 546 |
</select> |
| 517 | 547 |
<input |
... | ... | @@ -644,11 +674,11 @@ |
| 644 | 674 |
<ProcessStatusBadge value={item.processStatus} />
|
| 645 | 675 |
</td> |
| 646 | 676 |
<td className="px-3 py-2 text-gray-600"> |
| 647 |
- {item.processStatus === '처리완료' ? formatDate(item.processedAt) : '-'}
|
|
| 677 |
+ {item.processStatus === doneStatusCode ? formatDate(item.processedAt) : '-'}
|
|
| 648 | 678 |
</td> |
| 649 | 679 |
<td className="px-3 py-2"> |
| 650 | 680 |
<div className="flex gap-2"> |
| 651 |
- {item.processStatus === '처리완료' ? (
|
|
| 681 |
+ {item.processStatus === doneStatusCode ? (
|
|
| 652 | 682 |
<> |
| 653 | 683 |
<button |
| 654 | 684 |
type="button" |
... | ... | @@ -713,6 +743,12 @@ |
| 713 | 743 |
onBack: () => void |
| 714 | 744 |
onSaved: () => void |
| 715 | 745 |
}) {
|
| 746 |
+ const contractDocs = useCodes('CONTRACT_DOC')
|
|
| 747 |
+ const etcDoc = etcDocOf(contractDocs) |
|
| 748 |
+ const statuses = useCodes('PROCESS_STATUS')
|
|
| 749 |
+ // 권리처리 판정에는 '보류'가 없다 - 변호사 판정과 목록이 다르다. |
|
| 750 |
+ const kogl = useSplitByFirstRow('KOGL_TYPE', SCOPE_PROCESS)
|
|
| 751 |
+ |
|
| 716 | 752 |
const [item, setItem] = useState<ProcessItem | null>(null) |
| 717 | 753 |
const [loading, setLoading] = useState(true) |
| 718 | 754 |
const [loadError, setLoadError] = useState<string | null>(null) |
... | ... | @@ -739,7 +775,7 @@ |
| 739 | 775 |
return |
| 740 | 776 |
} |
| 741 | 777 |
setItem(data) |
| 742 |
- const { selected, etcText } = parseContractDocs(data.contractDocs)
|
|
| 778 |
+ const { selected, etcText } = parseContractDocs(data.contractDocs, contractDocs)
|
|
| 743 | 779 |
setContractSelected(selected) |
| 744 | 780 |
setContractEtc(etcText) |
| 745 | 781 |
setJudgedKoglType(data.judgedKoglType ?? '') |
... | ... | @@ -784,7 +820,7 @@ |
| 784 | 820 |
setSaveError(null) |
| 785 | 821 |
try {
|
| 786 | 822 |
const body: ProcessingRequest = {
|
| 787 |
- contractDocs: serializeContractDocs(contractSelected, contractEtc), |
|
| 823 |
+ contractDocs: serializeContractDocs(contractSelected, contractEtc, contractDocs), |
|
| 788 | 824 |
judgedKoglType: judgedKoglType || null, |
| 789 | 825 |
judgedAiType: judgedAiType ? 'Y' : null, |
| 790 | 826 |
finalOpinion: finalOpinion || null, |
... | ... | @@ -897,26 +933,31 @@ |
| 897 | 933 |
<InfoRow label="계약서 유무"> |
| 898 | 934 |
<div className="flex flex-col gap-1.5"> |
| 899 | 935 |
<div className="flex flex-wrap gap-x-4 gap-y-1"> |
| 900 |
- {CONTRACT_DOC_OPTIONS.map((option) => (
|
|
| 901 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 936 |
+ {/* '기타'는 자유 입력이 딸려 있어 뒤에 따로 그린다. */}
|
|
| 937 |
+ {contractDocs
|
|
| 938 |
+ .filter((option) => option.attrs.freeText !== true) |
|
| 939 |
+ .map((option) => ( |
|
| 940 |
+ <label key={option.code} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 941 |
+ <input |
|
| 942 |
+ type="checkbox" |
|
| 943 |
+ checked={contractSelected.has(option.code)}
|
|
| 944 |
+ onChange={() => toggleContractOption(option.code)}
|
|
| 945 |
+ /> |
|
| 946 |
+ {option.label}
|
|
| 947 |
+ </label> |
|
| 948 |
+ ))} |
|
| 949 |
+ {etcDoc && (
|
|
| 950 |
+ <label className="flex items-center gap-1.5 text-sm text-gray-800"> |
|
| 902 | 951 |
<input |
| 903 | 952 |
type="checkbox" |
| 904 |
- checked={contractSelected.has(option)}
|
|
| 905 |
- onChange={() => toggleContractOption(option)}
|
|
| 953 |
+ checked={contractSelected.has(etcDoc.code)}
|
|
| 954 |
+ onChange={() => toggleContractOption(etcDoc.code)}
|
|
| 906 | 955 |
/> |
| 907 |
- {option}
|
|
| 956 |
+ {etcDoc.label}
|
|
| 908 | 957 |
</label> |
| 909 |
- ))} |
|
| 910 |
- <label className="flex items-center gap-1.5 text-sm text-gray-800"> |
|
| 911 |
- <input |
|
| 912 |
- type="checkbox" |
|
| 913 |
- checked={contractSelected.has(CONTRACT_ETC)}
|
|
| 914 |
- onChange={() => toggleContractOption(CONTRACT_ETC)}
|
|
| 915 |
- /> |
|
| 916 |
- {CONTRACT_ETC}
|
|
| 917 |
- </label> |
|
| 958 |
+ )} |
|
| 918 | 959 |
</div> |
| 919 |
- {contractSelected.has(CONTRACT_ETC) && (
|
|
| 960 |
+ {etcDoc && contractSelected.has(etcDoc.code) && (
|
|
| 920 | 961 |
<input |
| 921 | 962 |
type="text" |
| 922 | 963 |
aria-label="기타 계약서 내용" |
... | ... | @@ -932,28 +973,28 @@ |
| 932 | 973 |
<InfoRow label="공공누리 유형"> |
| 933 | 974 |
<div className="flex flex-col gap-1.5"> |
| 934 | 975 |
<div className="flex flex-wrap gap-x-4 gap-y-1"> |
| 935 |
- {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o === '0유형' || o === '개방불가').map((option) => (
|
|
| 936 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 976 |
+ {kogl.first.map((option) => (
|
|
| 977 |
+ <label key={option.code} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 937 | 978 |
<input |
| 938 | 979 |
type="radio" |
| 939 | 980 |
name="judged-kogl-type" |
| 940 |
- checked={judgedKoglType === option}
|
|
| 941 |
- onChange={() => setJudgedKoglType(option)}
|
|
| 981 |
+ checked={judgedKoglType === option.code}
|
|
| 982 |
+ onChange={() => setJudgedKoglType(option.code)}
|
|
| 942 | 983 |
/> |
| 943 |
- {option}
|
|
| 984 |
+ {option.label}
|
|
| 944 | 985 |
</label> |
| 945 | 986 |
))} |
| 946 | 987 |
</div> |
| 947 | 988 |
<div className="flex flex-wrap items-center gap-x-4 gap-y-1"> |
| 948 |
- {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o !== '0유형' && o !== '개방불가').map((option) => (
|
|
| 949 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 989 |
+ {kogl.rest.map((option) => (
|
|
| 990 |
+ <label key={option.code} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 950 | 991 |
<input |
| 951 | 992 |
type="radio" |
| 952 | 993 |
name="judged-kogl-type" |
| 953 |
- checked={judgedKoglType === option}
|
|
| 954 |
- onChange={() => setJudgedKoglType(option)}
|
|
| 994 |
+ checked={judgedKoglType === option.code}
|
|
| 995 |
+ onChange={() => setJudgedKoglType(option.code)}
|
|
| 955 | 996 |
/> |
| 956 |
- {option}
|
|
| 997 |
+ {option.label}
|
|
| 957 | 998 |
</label> |
| 958 | 999 |
))} |
| 959 | 1000 |
<label className="ml-2 flex items-center gap-1.5 text-sm text-gray-800"> |
... | ... | @@ -1017,9 +1058,9 @@ |
| 1017 | 1058 |
className="w-40 rounded-md border border-gray-300 px-2 py-1.5 text-sm" |
| 1018 | 1059 |
> |
| 1019 | 1060 |
<option value="">선택</option> |
| 1020 |
- {PROCESS_STATUS_OPTIONS.map((option) => (
|
|
| 1021 |
- <option key={option} value={option}>
|
|
| 1022 |
- {option}
|
|
| 1061 |
+ {statuses.map((option) => (
|
|
| 1062 |
+ <option key={option.code} value={option.code}>
|
|
| 1063 |
+ {option.label}
|
|
| 1023 | 1064 |
</option> |
| 1024 | 1065 |
))} |
| 1025 | 1066 |
</select> |
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?