style: 권리확인/권리처리 상세화면을 정부시스템 폼 스타일로 재구성
- ReviewBoard/ProcessBoard 상세 뷰를 회색 라벨 셀 + 테두리 표 레이아웃으로 교체 - 권리확인 대분류/세부/처리결과 라디오를 참조 양식과 동일한 rowSpan 표로 재구성 (접근성 이름은 aria-label로 원래 값 그대로 고정해 라벨/버튼 텍스트 불변 유지) - ProcessBoard 상세 액션바에 삭제 버튼 추가(목록의 삭제와 동일한 확인+호출 동작)
@de951975a6dd0628390d3f367335897f6d8f18e2
--- frontend/src/components/ProcessBoard.test.tsx
+++ frontend/src/components/ProcessBoard.test.tsx
... | ... | @@ -350,4 +350,25 @@ |
| 350 | 350 |
expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy()
|
| 351 | 351 |
expect(mocks.updateProcessing).not.toHaveBeenCalled() |
| 352 | 352 |
}) |
| 353 |
+ |
|
| 354 |
+ it('상세 화면에서 삭제를 누르면 확인 후 deleteProcessItem을 호출하고 목록으로 돌아간다', async () => {
|
|
| 355 |
+ mocks.getProcessPage.mockResolvedValue(page()) |
|
| 356 |
+ mocks.getProcessItem.mockResolvedValue(item()) |
|
| 357 |
+ mocks.deleteProcessItem.mockResolvedValue(undefined) |
|
| 358 |
+ const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) |
|
| 359 |
+ |
|
| 360 |
+ render(<ProcessBoard org={org()} />)
|
|
| 361 |
+ |
|
| 362 |
+ await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
|
|
| 363 |
+ fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
|
|
| 364 |
+ await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalled()) |
|
| 365 |
+ |
|
| 366 |
+ fireEvent.click(screen.getByRole('button', { name: '삭제' }))
|
|
| 367 |
+ |
|
| 368 |
+ expect(confirmSpy).toHaveBeenCalled() |
|
| 369 |
+ await waitFor(() => expect(mocks.deleteProcessItem).toHaveBeenCalledWith(1, 1)) |
|
| 370 |
+ await waitFor(() => expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy())
|
|
| 371 |
+ |
|
| 372 |
+ confirmSpy.mockRestore() |
|
| 373 |
+ }) |
|
| 353 | 374 |
}) |
--- frontend/src/components/ProcessBoard.tsx
+++ frontend/src/components/ProcessBoard.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { useEffect, useRef, useState } from 'react'
|
|
| 1 |
+import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 2 | 2 |
import {
|
| 3 | 3 |
deleteProcessItem, |
| 4 | 4 |
getProcessItem, |
... | ... | @@ -77,11 +77,76 @@ |
| 77 | 77 |
return text.length > max ? `${text.slice(0, max)}…` : text
|
| 78 | 78 |
} |
| 79 | 79 |
|
| 80 |
-function Field({ label, value }: { label: string; value: string | null }) {
|
|
| 80 |
+/** 소제목: 작은 파란 사각 불릿 + 굵은 제목. */ |
|
| 81 |
+function SectionHeader({ title }: { title: string }) {
|
|
| 81 | 82 |
return ( |
| 82 |
- <div className="flex gap-3 text-sm"> |
|
| 83 |
- <span className="w-24 shrink-0 text-gray-400">{label}</span>
|
|
| 84 |
- <span className={value ? 'text-gray-900' : 'text-gray-300'}>{value ?? '-'}</span>
|
|
| 83 |
+ <h3 className="flex items-center gap-2 text-sm font-bold"> |
|
| 84 |
+ <span className="h-2.5 w-2.5 shrink-0 bg-blue-700" /> |
|
| 85 |
+ {title}
|
|
| 86 |
+ </h3> |
|
| 87 |
+ ) |
|
| 88 |
+} |
|
| 89 |
+ |
|
| 90 |
+/** 정보 테이블의 회색 라벨 셀. required면 라벨 앞에 빨간 별표를 붙인다. */ |
|
| 91 |
+function LabelCell({ children, required }: { children: ReactNode; required?: boolean }) {
|
|
| 92 |
+ return ( |
|
| 93 |
+ <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600"> |
|
| 94 |
+ {required && <span className="text-red-500">*</span>}
|
|
| 95 |
+ {children}
|
|
| 96 |
+ </div> |
|
| 97 |
+ ) |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+/** 정보 테이블의 값 셀. */ |
|
| 101 |
+function ValueCell({ children }: { children: ReactNode }) {
|
|
| 102 |
+ return <div className="flex items-center px-3 py-1.5">{children}</div>
|
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+/** 읽기전용 값 표시용 회색 박스. */ |
|
| 106 |
+function ReadonlyBox({ value }: { value: ReactNode }) {
|
|
| 107 |
+ return ( |
|
| 108 |
+ <div className="w-full rounded border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-800"> |
|
| 109 |
+ {value || <span className="text-gray-400">-</span>}
|
|
| 110 |
+ </div> |
|
| 111 |
+ ) |
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+/** 단일 라벨/값 행(150px + 1fr). */ |
|
| 115 |
+function InfoRow({
|
|
| 116 |
+ label, |
|
| 117 |
+ required, |
|
| 118 |
+ children, |
|
| 119 |
+}: {
|
|
| 120 |
+ label: string |
|
| 121 |
+ required?: boolean |
|
| 122 |
+ children: ReactNode |
|
| 123 |
+}) {
|
|
| 124 |
+ return ( |
|
| 125 |
+ <div className="grid grid-cols-[150px_1fr]"> |
|
| 126 |
+ <LabelCell required={required}>{label}</LabelCell>
|
|
| 127 |
+ <ValueCell>{children}</ValueCell>
|
|
| 128 |
+ </div> |
|
| 129 |
+ ) |
|
| 130 |
+} |
|
| 131 |
+ |
|
| 132 |
+/** 라벨/값 두 쌍을 한 행에 나란히 배치(150px + 1fr + 150px + 1fr). */ |
|
| 133 |
+function InfoRowPair({
|
|
| 134 |
+ leftLabel, |
|
| 135 |
+ left, |
|
| 136 |
+ rightLabel, |
|
| 137 |
+ right, |
|
| 138 |
+}: {
|
|
| 139 |
+ leftLabel: string |
|
| 140 |
+ left: ReactNode |
|
| 141 |
+ rightLabel: string |
|
| 142 |
+ right: ReactNode |
|
| 143 |
+}) {
|
|
| 144 |
+ return ( |
|
| 145 |
+ <div className="grid grid-cols-[150px_1fr_150px_1fr]"> |
|
| 146 |
+ <LabelCell>{leftLabel}</LabelCell>
|
|
| 147 |
+ <ValueCell>{left}</ValueCell>
|
|
| 148 |
+ <LabelCell>{rightLabel}</LabelCell>
|
|
| 149 |
+ <ValueCell>{right}</ValueCell>
|
|
| 85 | 150 |
</div> |
| 86 | 151 |
) |
| 87 | 152 |
} |
... | ... | @@ -561,6 +626,25 @@ |
| 561 | 626 |
} |
| 562 | 627 |
} |
| 563 | 628 |
|
| 629 |
+ async function handleDelete() {
|
|
| 630 |
+ if (!item) {
|
|
| 631 |
+ return |
|
| 632 |
+ } |
|
| 633 |
+ if (!window.confirm(`"${item.postTitle ?? item.seq}" 게시물을 삭제할까요?`)) {
|
|
| 634 |
+ return |
|
| 635 |
+ } |
|
| 636 |
+ setBusy(true) |
|
| 637 |
+ setSaveError(null) |
|
| 638 |
+ try {
|
|
| 639 |
+ await deleteProcessItem(org.id, itemId) |
|
| 640 |
+ onSaved() |
|
| 641 |
+ } catch (e) {
|
|
| 642 |
+ setSaveError(e instanceof Error ? e.message : '삭제에 실패했습니다.') |
|
| 643 |
+ } finally {
|
|
| 644 |
+ setBusy(false) |
|
| 645 |
+ } |
|
| 646 |
+ } |
|
| 647 |
+ |
|
| 564 | 648 |
if (loading) {
|
| 565 | 649 |
return <p className="p-4 py-10 text-center text-sm text-gray-400">불러오는 중…</p> |
| 566 | 650 |
} |
... | ... | @@ -571,140 +655,192 @@ |
| 571 | 655 |
return ( |
| 572 | 656 |
<div className="p-4"> |
| 573 | 657 |
<section className="rounded-lg border border-gray-200 p-4"> |
| 574 |
- <h2 className="text-sm font-semibold">기본정보</h2> |
|
| 575 |
- <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2"> |
|
| 576 |
- <Field label="기관명" value={org.orgName} />
|
|
| 577 |
- <Field label="사이트명" value={item.siteName} />
|
|
| 578 |
- <Field label="게시판명" value={item.boardName} />
|
|
| 579 |
- <Field label="게시물제목" value={item.postTitle} />
|
|
| 580 |
- <div className="flex gap-3 text-sm"> |
|
| 581 |
- <span className="w-24 shrink-0 text-gray-400">URL</span> |
|
| 582 |
- {item.url ? (
|
|
| 583 |
- <a href={item.url} target="_blank" rel="noreferrer" className="break-all text-blue-600 hover:underline">
|
|
| 584 |
- {item.url}
|
|
| 585 |
- </a> |
|
| 586 |
- ) : ( |
|
| 587 |
- <span className="text-gray-300">-</span> |
|
| 588 |
- )} |
|
| 589 |
- </div> |
|
| 590 |
- <Field label="기존 공공누리" value={item.priorKoglType} />
|
|
| 658 |
+ <SectionHeader title="기본정보" /> |
|
| 659 |
+ |
|
| 660 |
+ <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200"> |
|
| 661 |
+ <InfoRow label="기관명"> |
|
| 662 |
+ <ReadonlyBox value={org.orgName} />
|
|
| 663 |
+ </InfoRow> |
|
| 664 |
+ <InfoRowPair |
|
| 665 |
+ leftLabel="사이트명" |
|
| 666 |
+ left={<ReadonlyBox value={item.siteName} />}
|
|
| 667 |
+ rightLabel="게시판명" |
|
| 668 |
+ right={<ReadonlyBox value={item.boardName} />}
|
|
| 669 |
+ /> |
|
| 670 |
+ <InfoRow label="게시물제목"> |
|
| 671 |
+ <div className="flex w-full items-center gap-1.5 rounded border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-800"> |
|
| 672 |
+ <span>{item.postTitle ?? '-'}</span>
|
|
| 673 |
+ {item.url && (
|
|
| 674 |
+ <a |
|
| 675 |
+ href={item.url}
|
|
| 676 |
+ target="_blank" |
|
| 677 |
+ rel="noreferrer" |
|
| 678 |
+ title="원문 보기" |
|
| 679 |
+ aria-label="원문 보기" |
|
| 680 |
+ className="text-blue-500 hover:underline" |
|
| 681 |
+ > |
|
| 682 |
+ 🔗 |
|
| 683 |
+ </a> |
|
| 684 |
+ )} |
|
| 685 |
+ </div> |
|
| 686 |
+ </InfoRow> |
|
| 687 |
+ <InfoRow label="기존 공공누리"> |
|
| 688 |
+ <ReadonlyBox value={item.priorKoglType} />
|
|
| 689 |
+ </InfoRow> |
|
| 591 | 690 |
</div> |
| 592 | 691 |
</section> |
| 593 | 692 |
|
| 594 | 693 |
<section className="mt-4 rounded-lg border border-gray-200 p-4"> |
| 595 |
- <h2 className="text-sm font-semibold">권리확인</h2> |
|
| 596 |
- <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2"> |
|
| 597 |
- <Field |
|
| 598 |
- label="권리확인" |
|
| 599 |
- value={item.reviewMajor ? `${item.reviewMajor}${item.reviewMinor ? ` > ${item.reviewMinor}` : ''}` : null}
|
|
| 600 |
- /> |
|
| 601 |
- <Field |
|
| 602 |
- label="공공누리유형" |
|
| 603 |
- value={item.reviewKoglType ? `${item.reviewKoglType}${item.reviewAiType === 'Y' ? ' + AI' : ''}` : null}
|
|
| 604 |
- /> |
|
| 605 |
- <Field label="처리 구분" value={item.reviewResult} />
|
|
| 606 |
- <Field label="의견" value={item.reviewOpinion} />
|
|
| 607 |
- <Field label="비고" value={item.reviewNote} />
|
|
| 694 |
+ <SectionHeader title="권리확인" /> |
|
| 695 |
+ |
|
| 696 |
+ <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200"> |
|
| 697 |
+ <InfoRow label="권리확인"> |
|
| 698 |
+ <ReadonlyBox |
|
| 699 |
+ value={item.reviewMajor ? `${item.reviewMajor}${item.reviewMinor ? ` > ${item.reviewMinor}` : ''}` : null}
|
|
| 700 |
+ /> |
|
| 701 |
+ </InfoRow> |
|
| 702 |
+ <InfoRow label="권리확인 공공누리유형"> |
|
| 703 |
+ <ReadonlyBox |
|
| 704 |
+ value={item.reviewKoglType ? `${item.reviewKoglType}${item.reviewAiType === 'Y' ? ' + AI' : ''}` : null}
|
|
| 705 |
+ /> |
|
| 706 |
+ </InfoRow> |
|
| 707 |
+ <InfoRow label="처리 구분"> |
|
| 708 |
+ <ReadonlyBox value={item.reviewResult} />
|
|
| 709 |
+ </InfoRow> |
|
| 710 |
+ <InfoRow label="의견"> |
|
| 711 |
+ <ReadonlyBox value={item.reviewOpinion} />
|
|
| 712 |
+ </InfoRow> |
|
| 713 |
+ <InfoRow label="비고"> |
|
| 714 |
+ <ReadonlyBox value={item.reviewNote} />
|
|
| 715 |
+ </InfoRow> |
|
| 608 | 716 |
</div> |
| 609 | 717 |
</section> |
| 610 | 718 |
|
| 611 | 719 |
<section className="mt-4 rounded-lg border border-gray-200 p-4"> |
| 612 |
- <h2 className="text-sm font-semibold">권리처리</h2> |
|
| 720 |
+ <SectionHeader title="권리처리" /> |
|
| 613 | 721 |
|
| 614 |
- <div className="mt-3 flex flex-col gap-4"> |
|
| 615 |
- <fieldset className="flex flex-col gap-1.5"> |
|
| 616 |
- <legend className="text-xs text-gray-500">계약서 유무</legend> |
|
| 617 |
- <div className="flex flex-wrap gap-x-4 gap-y-1"> |
|
| 618 |
- {CONTRACT_DOC_OPTIONS.map((option) => (
|
|
| 619 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 722 |
+ <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200"> |
|
| 723 |
+ <InfoRow label="계약서 유무"> |
|
| 724 |
+ <div className="flex flex-col gap-1.5"> |
|
| 725 |
+ <div className="flex flex-wrap gap-x-4 gap-y-1"> |
|
| 726 |
+ {CONTRACT_DOC_OPTIONS.map((option) => (
|
|
| 727 |
+ <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 728 |
+ <input |
|
| 729 |
+ type="checkbox" |
|
| 730 |
+ checked={contractSelected.has(option)}
|
|
| 731 |
+ onChange={() => toggleContractOption(option)}
|
|
| 732 |
+ /> |
|
| 733 |
+ {option}
|
|
| 734 |
+ </label> |
|
| 735 |
+ ))} |
|
| 736 |
+ <label className="flex items-center gap-1.5 text-sm text-gray-800"> |
|
| 620 | 737 |
<input |
| 621 | 738 |
type="checkbox" |
| 622 |
- checked={contractSelected.has(option)}
|
|
| 623 |
- onChange={() => toggleContractOption(option)}
|
|
| 739 |
+ checked={contractSelected.has(CONTRACT_ETC)}
|
|
| 740 |
+ onChange={() => toggleContractOption(CONTRACT_ETC)}
|
|
| 624 | 741 |
/> |
| 625 |
- {option}
|
|
| 742 |
+ {CONTRACT_ETC}
|
|
| 626 | 743 |
</label> |
| 627 |
- ))} |
|
| 628 |
- <label className="flex items-center gap-1.5 text-sm text-gray-800"> |
|
| 744 |
+ </div> |
|
| 745 |
+ {contractSelected.has(CONTRACT_ETC) && (
|
|
| 629 | 746 |
<input |
| 630 |
- type="checkbox" |
|
| 631 |
- checked={contractSelected.has(CONTRACT_ETC)}
|
|
| 632 |
- onChange={() => toggleContractOption(CONTRACT_ETC)}
|
|
| 747 |
+ type="text" |
|
| 748 |
+ aria-label="기타 계약서 내용" |
|
| 749 |
+ value={contractEtc}
|
|
| 750 |
+ onChange={(e) => setContractEtc(e.target.value)}
|
|
| 751 |
+ placeholder="기타 내용을 입력하세요" |
|
| 752 |
+ className="w-72 rounded-md border border-gray-300 px-2 py-1.5 text-sm" |
|
| 633 | 753 |
/> |
| 634 |
- {CONTRACT_ETC}
|
|
| 635 |
- </label> |
|
| 754 |
+ )} |
|
| 636 | 755 |
</div> |
| 637 |
- {contractSelected.has(CONTRACT_ETC) && (
|
|
| 638 |
- <input |
|
| 639 |
- type="text" |
|
| 640 |
- aria-label="기타 계약서 내용" |
|
| 641 |
- value={contractEtc}
|
|
| 642 |
- onChange={(e) => setContractEtc(e.target.value)}
|
|
| 643 |
- placeholder="기타 내용을 입력하세요" |
|
| 644 |
- className="mt-1 w-72 rounded-md border border-gray-300 px-2 py-1.5 text-sm" |
|
| 645 |
- /> |
|
| 646 |
- )} |
|
| 647 |
- </fieldset> |
|
| 756 |
+ </InfoRow> |
|
| 648 | 757 |
|
| 649 |
- <fieldset className="flex flex-col gap-1.5"> |
|
| 650 |
- <legend className="text-xs text-gray-500">공공누리 유형</legend> |
|
| 651 |
- <div className="flex flex-wrap gap-x-4 gap-y-1"> |
|
| 652 |
- {JUDGED_KOGL_TYPE_OPTIONS.map((option) => (
|
|
| 653 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 758 |
+ <InfoRow label="공공누리 유형"> |
|
| 759 |
+ <div className="flex flex-col gap-1.5"> |
|
| 760 |
+ <div className="flex flex-wrap gap-x-4 gap-y-1"> |
|
| 761 |
+ {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o === '0유형' || o === '개방불가').map((option) => (
|
|
| 762 |
+ <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 763 |
+ <input |
|
| 764 |
+ type="radio" |
|
| 765 |
+ name="judged-kogl-type" |
|
| 766 |
+ checked={judgedKoglType === option}
|
|
| 767 |
+ onChange={() => setJudgedKoglType(option)}
|
|
| 768 |
+ /> |
|
| 769 |
+ {option}
|
|
| 770 |
+ </label> |
|
| 771 |
+ ))} |
|
| 772 |
+ </div> |
|
| 773 |
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1"> |
|
| 774 |
+ {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o !== '0유형' && o !== '개방불가').map((option) => (
|
|
| 775 |
+ <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 776 |
+ <input |
|
| 777 |
+ type="radio" |
|
| 778 |
+ name="judged-kogl-type" |
|
| 779 |
+ checked={judgedKoglType === option}
|
|
| 780 |
+ onChange={() => setJudgedKoglType(option)}
|
|
| 781 |
+ /> |
|
| 782 |
+ {option}
|
|
| 783 |
+ </label> |
|
| 784 |
+ ))} |
|
| 785 |
+ <label className="ml-2 flex items-center gap-1.5 text-sm text-gray-800"> |
|
| 654 | 786 |
<input |
| 655 |
- type="radio" |
|
| 656 |
- name="judged-kogl-type" |
|
| 657 |
- checked={judgedKoglType === option}
|
|
| 658 |
- onChange={() => setJudgedKoglType(option)}
|
|
| 787 |
+ type="checkbox" |
|
| 788 |
+ checked={judgedAiType}
|
|
| 789 |
+ onChange={(e) => setJudgedAiType(e.target.checked)}
|
|
| 659 | 790 |
/> |
| 660 |
- {option}
|
|
| 791 |
+ AI유형 |
|
| 661 | 792 |
</label> |
| 662 |
- ))} |
|
| 793 |
+ </div> |
|
| 663 | 794 |
</div> |
| 664 |
- </fieldset> |
|
| 795 |
+ </InfoRow> |
|
| 665 | 796 |
|
| 666 |
- <label className="flex items-center gap-2 text-sm text-gray-800"> |
|
| 667 |
- <input type="checkbox" checked={judgedAiType} onChange={(e) => setJudgedAiType(e.target.checked)} />
|
|
| 668 |
- AI유형 |
|
| 669 |
- </label> |
|
| 670 |
- |
|
| 671 |
- <div className="flex flex-col gap-1 text-sm"> |
|
| 672 |
- <label htmlFor="process-final-opinion" className="text-xs text-gray-500"> |
|
| 673 |
- 최종의견 |
|
| 674 |
- </label> |
|
| 675 |
- <textarea |
|
| 676 |
- id="process-final-opinion" |
|
| 677 |
- rows={3}
|
|
| 678 |
- value={finalOpinion}
|
|
| 679 |
- onChange={(e) => setFinalOpinion(e.target.value)}
|
|
| 680 |
- placeholder="전부 양도 체결, 일부 양도 체결, 초상 이용 동의, 공공누리 동의 등 처리한 사유 기입" |
|
| 681 |
- className="rounded-md border border-gray-300 px-2 py-1.5" |
|
| 682 |
- /> |
|
| 797 |
+ <div className="grid grid-cols-[150px_1fr]"> |
|
| 798 |
+ <LabelCell>권리처리 결과</LabelCell> |
|
| 799 |
+ <div className="flex flex-col divide-y divide-gray-200"> |
|
| 800 |
+ <div className="grid grid-cols-[110px_1fr]"> |
|
| 801 |
+ <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600"> |
|
| 802 |
+ 최종의견 |
|
| 803 |
+ </div> |
|
| 804 |
+ <div className="flex flex-col gap-1 px-3 py-1.5"> |
|
| 805 |
+ <span className="text-[11px] text-gray-400"> |
|
| 806 |
+ 작성안내) 전부 양도 체결, 일부 양도 체결, 초상 이용 동의, 공공누리 동의 등 처리한 사유 기입 |
|
| 807 |
+ </span> |
|
| 808 |
+ <textarea |
|
| 809 |
+ id="process-final-opinion" |
|
| 810 |
+ aria-label="최종의견" |
|
| 811 |
+ rows={3}
|
|
| 812 |
+ value={finalOpinion}
|
|
| 813 |
+ onChange={(e) => setFinalOpinion(e.target.value)}
|
|
| 814 |
+ className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm" |
|
| 815 |
+ /> |
|
| 816 |
+ </div> |
|
| 817 |
+ </div> |
|
| 818 |
+ <div className="grid grid-cols-[110px_1fr]"> |
|
| 819 |
+ <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600"> |
|
| 820 |
+ 판단근거 |
|
| 821 |
+ </div> |
|
| 822 |
+ <div className="flex flex-col gap-1 px-3 py-1.5"> |
|
| 823 |
+ <span className="text-[11px] text-gray-400">작성안내) 계약서 명칭, 해당 문구 기입</span> |
|
| 824 |
+ <textarea |
|
| 825 |
+ id="process-judgment-basis" |
|
| 826 |
+ aria-label="판단근거" |
|
| 827 |
+ rows={3}
|
|
| 828 |
+ value={judgmentBasis}
|
|
| 829 |
+ onChange={(e) => setJudgmentBasis(e.target.value)}
|
|
| 830 |
+ className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm" |
|
| 831 |
+ /> |
|
| 832 |
+ </div> |
|
| 833 |
+ </div> |
|
| 834 |
+ </div> |
|
| 683 | 835 |
</div> |
| 684 | 836 |
|
| 685 |
- <div className="flex flex-col gap-1 text-sm"> |
|
| 686 |
- <label htmlFor="process-judgment-basis" className="text-xs text-gray-500"> |
|
| 687 |
- 판단근거 |
|
| 688 |
- </label> |
|
| 689 |
- <textarea |
|
| 690 |
- id="process-judgment-basis" |
|
| 691 |
- rows={3}
|
|
| 692 |
- value={judgmentBasis}
|
|
| 693 |
- onChange={(e) => setJudgmentBasis(e.target.value)}
|
|
| 694 |
- placeholder="계약서 명칭, 해당 문구 기입" |
|
| 695 |
- className="rounded-md border border-gray-300 px-2 py-1.5" |
|
| 696 |
- /> |
|
| 697 |
- </div> |
|
| 698 |
- |
|
| 699 |
- <div className="flex flex-col gap-1 text-sm"> |
|
| 700 |
- <label htmlFor="process-status" className="text-xs text-gray-500"> |
|
| 701 |
- *권리처리상태 |
|
| 702 |
- </label> |
|
| 837 |
+ <InfoRow label="권리처리상태" required> |
|
| 703 | 838 |
<select |
| 704 | 839 |
id="process-status" |
| 840 |
+ aria-label="*권리처리상태" |
|
| 705 | 841 |
value={processStatus}
|
| 706 | 842 |
onChange={(e) => setProcessStatus(e.target.value)}
|
| 707 |
- className="w-40 rounded-md border border-gray-300 px-2 py-1.5" |
|
| 843 |
+ className="w-40 rounded-md border border-gray-300 px-2 py-1.5 text-sm" |
|
| 708 | 844 |
> |
| 709 | 845 |
<option value="">선택</option> |
| 710 | 846 |
{PROCESS_STATUS_OPTIONS.map((option) => (
|
... | ... | @@ -713,7 +849,7 @@ |
| 713 | 849 |
</option> |
| 714 | 850 |
))} |
| 715 | 851 |
</select> |
| 716 |
- </div> |
|
| 852 |
+ </InfoRow> |
|
| 717 | 853 |
</div> |
| 718 | 854 |
|
| 719 | 855 |
{saveError && <p className="mt-3 text-sm text-red-600">{saveError}</p>}
|
... | ... | @@ -723,15 +859,23 @@ |
| 723 | 859 |
type="button" |
| 724 | 860 |
disabled={busy}
|
| 725 | 861 |
onClick={onBack}
|
| 726 |
- className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50" |
|
| 862 |
+ className="rounded-md bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-gray-700 disabled:opacity-50" |
|
| 727 | 863 |
> |
| 728 | 864 |
목록 |
| 729 | 865 |
</button> |
| 730 | 866 |
<button |
| 731 | 867 |
type="button" |
| 868 |
+ disabled={busy}
|
|
| 869 |
+ onClick={() => void handleDelete()}
|
|
| 870 |
+ className="rounded-md border border-red-300 px-3 py-1.5 text-sm font-semibold text-red-600 hover:bg-red-50 disabled:opacity-50" |
|
| 871 |
+ > |
|
| 872 |
+ 삭제 |
|
| 873 |
+ </button> |
|
| 874 |
+ <button |
|
| 875 |
+ type="button" |
|
| 732 | 876 |
disabled={busy || !processStatus}
|
| 733 | 877 |
onClick={() => void handleSave()}
|
| 734 |
- className="rounded-md bg-blue-600 px-4 py-1.5 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-300" |
|
| 878 |
+ className="rounded-md bg-blue-700 px-4 py-1.5 text-sm font-semibold text-white hover:bg-blue-800 disabled:cursor-not-allowed disabled:bg-gray-300" |
|
| 735 | 879 |
> |
| 736 | 880 |
수정 |
| 737 | 881 |
</button> |
--- frontend/src/components/ReviewBoard.tsx
+++ frontend/src/components/ReviewBoard.tsx
... | ... | @@ -1,4 +1,4 @@ |
| 1 |
-import { useEffect, useRef, useState } from 'react'
|
|
| 1 |
+import { useEffect, useRef, useState, type ReactNode } from 'react'
|
|
| 2 | 2 |
import {
|
| 3 | 3 |
deleteReviewItem, |
| 4 | 4 |
getReviewItem, |
... | ... | @@ -57,48 +57,111 @@ |
| 57 | 57 |
return null |
| 58 | 58 |
} |
| 59 | 59 |
|
| 60 |
-function Field({ label, value }: { label: string; value: string | null }) {
|
|
| 60 |
+/** 세부(review-minor) 라디오의 실제 저장값은 REVIEW_MINOR_OPTIONS 그대로 유지하고, |
|
| 61 |
+ * 참조 양식과 같은 화면 표기(줄바꿈된 부연설명)만 별도로 매핑한다. */ |
|
| 62 |
+const REVIEW_MINOR_DISPLAY: { value: string; label: string; note?: string }[] = [
|
|
| 63 |
+ { value: REVIEW_MINOR_OPTIONS[0], label: '1. 원시적 권리 전부 보유' },
|
|
| 64 |
+ { value: REVIEW_MINOR_OPTIONS[1], label: '2. 후천적 권리 전부 보유', note: '(계약에 의한 전부 양수)' },
|
|
| 65 |
+ {
|
|
| 66 |
+ value: REVIEW_MINOR_OPTIONS[2], |
|
| 67 |
+ label: '3. 권리 일부(공동) 보유', |
|
| 68 |
+ note: '(보도자료·제3자저작물, 계약에 의한 전부/공동 보유 등)', |
|
| 69 |
+ }, |
|
| 70 |
+ { value: REVIEW_MINOR_OPTIONS[3], label: '4. 권리 미보유', note: '(공모전 수상작 등)' },
|
|
| 71 |
+ { value: REVIEW_MINOR_OPTIONS[4], label: '초상권 포함' },
|
|
| 72 |
+] |
|
| 73 |
+ |
|
| 74 |
+/** 소제목: 작은 파란 사각 불릿 + 굵은 제목 + 회색 부연설명. */ |
|
| 75 |
+function SectionHeader({ title, note }: { title: string; note?: string }) {
|
|
| 61 | 76 |
return ( |
| 62 |
- <div className="flex gap-3 text-sm"> |
|
| 63 |
- <span className="w-24 shrink-0 text-gray-400">{label}</span>
|
|
| 64 |
- <span className={value ? 'text-gray-900' : 'text-gray-300'}>{value ?? '-'}</span>
|
|
| 77 |
+ <h3 className="flex items-center gap-2 text-sm font-bold"> |
|
| 78 |
+ <span className="h-2.5 w-2.5 shrink-0 bg-blue-700" /> |
|
| 79 |
+ {title}
|
|
| 80 |
+ {note && <span className="text-xs font-normal text-gray-400">{note}</span>}
|
|
| 81 |
+ </h3> |
|
| 82 |
+ ) |
|
| 83 |
+} |
|
| 84 |
+ |
|
| 85 |
+/** 정보 테이블의 회색 라벨 셀. */ |
|
| 86 |
+function LabelCell({ children }: { children: ReactNode }) {
|
|
| 87 |
+ return ( |
|
| 88 |
+ <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600">{children}</div>
|
|
| 89 |
+ ) |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+/** 정보 테이블의 값 셀. */ |
|
| 93 |
+function ValueCell({ children }: { children: ReactNode }) {
|
|
| 94 |
+ return <div className="flex items-center px-3 py-1.5">{children}</div>
|
|
| 95 |
+} |
|
| 96 |
+ |
|
| 97 |
+/** 읽기전용 값 표시용 회색 박스. */ |
|
| 98 |
+function ReadonlyBox({ value }: { value: string | null }) {
|
|
| 99 |
+ return ( |
|
| 100 |
+ <div className="w-full rounded border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-800"> |
|
| 101 |
+ {value ? value : <span className="text-gray-400">-</span>}
|
|
| 65 | 102 |
</div> |
| 66 | 103 |
) |
| 67 | 104 |
} |
| 68 | 105 |
|
| 69 |
-function RadioGroup({
|
|
| 70 |
- legend, |
|
| 71 |
- name, |
|
| 72 |
- options, |
|
| 73 |
- value, |
|
| 74 |
- onChange, |
|
| 75 |
- disabled, |
|
| 106 |
+/** 단일 라벨/값 행(150px + 1fr). */ |
|
| 107 |
+function InfoRow({ label, children }: { label: string; children: ReactNode }) {
|
|
| 108 |
+ return ( |
|
| 109 |
+ <div className="grid grid-cols-[150px_1fr]"> |
|
| 110 |
+ <LabelCell>{label}</LabelCell>
|
|
| 111 |
+ <ValueCell>{children}</ValueCell>
|
|
| 112 |
+ </div> |
|
| 113 |
+ ) |
|
| 114 |
+} |
|
| 115 |
+ |
|
| 116 |
+/** 라벨/값 두 쌍을 한 행에 나란히 배치(150px + 1fr + 150px + 1fr). */ |
|
| 117 |
+function InfoRowPair({
|
|
| 118 |
+ leftLabel, |
|
| 119 |
+ left, |
|
| 120 |
+ rightLabel, |
|
| 121 |
+ right, |
|
| 76 | 122 |
}: {
|
| 77 |
- legend: string |
|
| 78 |
- name: string |
|
| 79 |
- options: string[] |
|
| 80 |
- value: string |
|
| 81 |
- onChange: (value: string) => void |
|
| 82 |
- disabled?: boolean |
|
| 123 |
+ leftLabel: string |
|
| 124 |
+ left: ReactNode |
|
| 125 |
+ rightLabel: string |
|
| 126 |
+ right: ReactNode |
|
| 83 | 127 |
}) {
|
| 84 | 128 |
return ( |
| 85 |
- <fieldset className="flex flex-col gap-1.5"> |
|
| 86 |
- <legend className="text-xs text-gray-500">{legend}</legend>
|
|
| 87 |
- <div className="flex flex-wrap gap-x-4 gap-y-1"> |
|
| 88 |
- {options.map((option) => (
|
|
| 89 |
- <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 90 |
- <input |
|
| 91 |
- type="radio" |
|
| 92 |
- name={name}
|
|
| 93 |
- disabled={disabled}
|
|
| 94 |
- checked={value === option}
|
|
| 95 |
- onChange={() => onChange(option)}
|
|
| 96 |
- /> |
|
| 97 |
- {option}
|
|
| 98 |
- </label> |
|
| 99 |
- ))} |
|
| 100 |
- </div> |
|
| 101 |
- </fieldset> |
|
| 129 |
+ <div className="grid grid-cols-[150px_1fr_150px_1fr]"> |
|
| 130 |
+ <LabelCell>{leftLabel}</LabelCell>
|
|
| 131 |
+ <ValueCell>{left}</ValueCell>
|
|
| 132 |
+ <LabelCell>{rightLabel}</LabelCell>
|
|
| 133 |
+ <ValueCell>{right}</ValueCell>
|
|
| 134 |
+ </div> |
|
| 135 |
+ ) |
|
| 136 |
+} |
|
| 137 |
+ |
|
| 138 |
+/** 권리확인 매트릭스 표의 라디오 한 칸. aria-label로 접근성 이름을 실제 저장값 텍스트로 고정해 |
|
| 139 |
+ * 화면 표기(줄바꿈된 부연설명)가 늘어나도 접근성 이름은 항상 원래 값과 동일하게 유지한다. */ |
|
| 140 |
+function MatrixRadio({
|
|
| 141 |
+ name, |
|
| 142 |
+ value, |
|
| 143 |
+ label, |
|
| 144 |
+ note, |
|
| 145 |
+ checked, |
|
| 146 |
+ onChange, |
|
| 147 |
+ bold, |
|
| 148 |
+}: {
|
|
| 149 |
+ name: string |
|
| 150 |
+ value: string |
|
| 151 |
+ label?: string |
|
| 152 |
+ note?: string |
|
| 153 |
+ checked: boolean |
|
| 154 |
+ onChange: () => void |
|
| 155 |
+ bold?: boolean |
|
| 156 |
+}) {
|
|
| 157 |
+ return ( |
|
| 158 |
+ <label className="flex cursor-pointer flex-col gap-0.5"> |
|
| 159 |
+ <span className="flex items-center gap-1.5"> |
|
| 160 |
+ <input type="radio" name={name} aria-label={value} checked={checked} onChange={onChange} />
|
|
| 161 |
+ <span className={bold ? 'font-semibold text-gray-900' : 'text-gray-800'}>{label ?? value}</span>
|
|
| 162 |
+ </span> |
|
| 163 |
+ {note && <span className="pl-5 text-[11px] text-gray-400">{note}</span>}
|
|
| 164 |
+ </label> |
|
| 102 | 165 |
) |
| 103 | 166 |
} |
| 104 | 167 |
|
... | ... | @@ -552,116 +615,293 @@ |
| 552 | 615 |
return <p className="p-4 py-10 text-center text-sm text-red-600">{loadError ?? '게시물을 찾을 수 없습니다.'}</p>
|
| 553 | 616 |
} |
| 554 | 617 |
|
| 618 |
+ const koglSummary = `${item.koglAttached ?? '-'} / ${item.koglType ?? '-'}${item.aiType ? ' · AI' : ''}`
|
|
| 619 |
+ |
|
| 555 | 620 |
return ( |
| 556 | 621 |
<div className="p-4"> |
| 557 | 622 |
<section className="rounded-lg border border-gray-200 p-4"> |
| 558 |
- <h2 className="text-sm font-semibold">게시물 정보 (조사원)</h2> |
|
| 559 |
- <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2"> |
|
| 560 |
- <Field label="기관명" value={org.orgName} />
|
|
| 561 |
- <Field label="사이트명" value={item.siteName} />
|
|
| 562 |
- <Field label="범주" value={item.category} />
|
|
| 563 |
- <Field label="게시판명" value={item.boardName} />
|
|
| 564 |
- <Field label="게시물제목" value={item.postTitle} />
|
|
| 565 |
- <div className="flex gap-3 text-sm"> |
|
| 566 |
- <span className="w-24 shrink-0 text-gray-400">URL</span> |
|
| 623 |
+ <SectionHeader title="게시물 정보" note="(조사원)" /> |
|
| 624 |
+ |
|
| 625 |
+ <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200"> |
|
| 626 |
+ <InfoRow label="기관명"> |
|
| 627 |
+ <ReadonlyBox value={org.orgName} />
|
|
| 628 |
+ </InfoRow> |
|
| 629 |
+ <InfoRowPair |
|
| 630 |
+ leftLabel="사이트명" |
|
| 631 |
+ left={<ReadonlyBox value={item.siteName} />}
|
|
| 632 |
+ rightLabel="범주(카테고리)" |
|
| 633 |
+ right={<ReadonlyBox value={item.category} />}
|
|
| 634 |
+ /> |
|
| 635 |
+ <InfoRow label="게시판명"> |
|
| 636 |
+ <ReadonlyBox value={item.boardName} />
|
|
| 637 |
+ </InfoRow> |
|
| 638 |
+ <InfoRow label="게시물제목"> |
|
| 639 |
+ <ReadonlyBox value={item.postTitle} />
|
|
| 640 |
+ </InfoRow> |
|
| 641 |
+ <InfoRow label="URL주소"> |
|
| 567 | 642 |
{item.url ? (
|
| 568 |
- <a href={item.url} target="_blank" rel="noreferrer" className="break-all text-blue-600 hover:underline">
|
|
| 643 |
+ <a |
|
| 644 |
+ href={item.url}
|
|
| 645 |
+ target="_blank" |
|
| 646 |
+ rel="noreferrer" |
|
| 647 |
+ className="w-full break-all rounded border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-blue-600 hover:underline" |
|
| 648 |
+ > |
|
| 569 | 649 |
{item.url}
|
| 570 | 650 |
</a> |
| 571 | 651 |
) : ( |
| 572 |
- <span className="text-gray-300">-</span> |
|
| 652 |
+ <ReadonlyBox value={null} />
|
|
| 573 | 653 |
)} |
| 574 |
- </div> |
|
| 575 |
- <Field label="제작일" value={item.producedDate} />
|
|
| 576 |
- <Field label="공표일" value={item.publishedDate} />
|
|
| 577 |
- <Field label="첨부파일 여부" value={item.hasAttachment} />
|
|
| 578 |
- <Field label="기존 공공누리 부착" value={item.koglAttached} />
|
|
| 579 |
- <Field label="기존 공공누리유형" value={item.koglType} />
|
|
| 580 |
- <Field label="기존 AI유형" value={item.aiType} />
|
|
| 654 |
+ </InfoRow> |
|
| 655 |
+ <InfoRowPair |
|
| 656 |
+ leftLabel="제작일" |
|
| 657 |
+ left={<ReadonlyBox value={item.producedDate} />}
|
|
| 658 |
+ rightLabel="공표일" |
|
| 659 |
+ right={<ReadonlyBox value={item.publishedDate} />}
|
|
| 660 |
+ /> |
|
| 661 |
+ <InfoRow label="첨부파일 여부"> |
|
| 662 |
+ <ReadonlyBox value={item.hasAttachment} />
|
|
| 663 |
+ </InfoRow> |
|
| 664 |
+ <InfoRow label="기존 공공누리"> |
|
| 665 |
+ <ReadonlyBox value={koglSummary} />
|
|
| 666 |
+ </InfoRow> |
|
| 581 | 667 |
</div> |
| 582 | 668 |
</section> |
| 583 | 669 |
|
| 584 | 670 |
<section className="mt-4 rounded-lg border border-gray-200 p-4"> |
| 585 |
- <h2 className="text-sm font-semibold">권리확인 (변호사)</h2> |
|
| 671 |
+ <SectionHeader title="권리확인" note="(변호사)" /> |
|
| 586 | 672 |
|
| 587 |
- <div className="mt-3 flex flex-col gap-4"> |
|
| 588 |
- <RadioGroup |
|
| 589 |
- legend="권리확인(대분류)" |
|
| 590 |
- name="review-major" |
|
| 591 |
- options={REVIEW_MAJOR_OPTIONS}
|
|
| 592 |
- value={reviewMajor}
|
|
| 593 |
- onChange={pickMajor}
|
|
| 594 |
- /> |
|
| 673 |
+ <div className="mt-3 overflow-x-auto"> |
|
| 674 |
+ <table className="w-full border-collapse text-sm"> |
|
| 675 |
+ <thead> |
|
| 676 |
+ <tr className="bg-gray-50 text-xs font-semibold text-gray-600"> |
|
| 677 |
+ <th className="border border-gray-300 px-3 py-2">좌(대분류)</th> |
|
| 678 |
+ <th className="border border-gray-300 px-3 py-2">중(세부)</th> |
|
| 679 |
+ <th className="border border-gray-300 px-3 py-2">우(처리결과)</th> |
|
| 680 |
+ </tr> |
|
| 681 |
+ </thead> |
|
| 682 |
+ <tbody> |
|
| 683 |
+ <tr> |
|
| 684 |
+ <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
|
|
| 685 |
+ <MatrixRadio |
|
| 686 |
+ name="review-major" |
|
| 687 |
+ value={REVIEW_MAJOR_OPTIONS[0]}
|
|
| 688 |
+ note="저작권성 없음, 배포용, 단순데이터" |
|
| 689 |
+ checked={reviewMajor === REVIEW_MAJOR_OPTIONS[0]}
|
|
| 690 |
+ onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[0])}
|
|
| 691 |
+ /> |
|
| 692 |
+ </td> |
|
| 693 |
+ <td className="border border-gray-300 px-3 py-2" /> |
|
| 694 |
+ <td |
|
| 695 |
+ className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle" |
|
| 696 |
+ rowSpan={2}
|
|
| 697 |
+ > |
|
| 698 |
+ <MatrixRadio |
|
| 699 |
+ name="review-result" |
|
| 700 |
+ value={REVIEW_RESULT_OPTIONS[0]}
|
|
| 701 |
+ checked={reviewResult === REVIEW_RESULT_OPTIONS[0]}
|
|
| 702 |
+ onChange={() => pickResult(REVIEW_RESULT_OPTIONS[0])}
|
|
| 703 |
+ bold |
|
| 704 |
+ /> |
|
| 705 |
+ </td> |
|
| 706 |
+ </tr> |
|
| 707 |
+ <tr> |
|
| 708 |
+ <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
|
|
| 709 |
+ <MatrixRadio |
|
| 710 |
+ name="review-major" |
|
| 711 |
+ value={REVIEW_MAJOR_OPTIONS[1]}
|
|
| 712 |
+ checked={reviewMajor === REVIEW_MAJOR_OPTIONS[1]}
|
|
| 713 |
+ onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[1])}
|
|
| 714 |
+ /> |
|
| 715 |
+ </td> |
|
| 716 |
+ <td className="border border-gray-300 px-3 py-2 align-top"> |
|
| 717 |
+ <MatrixRadio |
|
| 718 |
+ name="review-minor" |
|
| 719 |
+ value={REVIEW_MINOR_DISPLAY[0].value}
|
|
| 720 |
+ label={REVIEW_MINOR_DISPLAY[0].label}
|
|
| 721 |
+ checked={reviewMinor === REVIEW_MINOR_DISPLAY[0].value}
|
|
| 722 |
+ onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[0].value)}
|
|
| 723 |
+ /> |
|
| 724 |
+ </td> |
|
| 725 |
+ </tr> |
|
| 726 |
+ <tr> |
|
| 727 |
+ <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={4}>
|
|
| 728 |
+ <MatrixRadio |
|
| 729 |
+ name="review-major" |
|
| 730 |
+ value={REVIEW_MAJOR_OPTIONS[2]}
|
|
| 731 |
+ checked={reviewMajor === REVIEW_MAJOR_OPTIONS[2]}
|
|
| 732 |
+ onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[2])}
|
|
| 733 |
+ /> |
|
| 734 |
+ </td> |
|
| 735 |
+ <td className="border border-gray-300 px-3 py-2 align-top"> |
|
| 736 |
+ <MatrixRadio |
|
| 737 |
+ name="review-minor" |
|
| 738 |
+ value={REVIEW_MINOR_DISPLAY[1].value}
|
|
| 739 |
+ label={REVIEW_MINOR_DISPLAY[1].label}
|
|
| 740 |
+ note={REVIEW_MINOR_DISPLAY[1].note}
|
|
| 741 |
+ checked={reviewMinor === REVIEW_MINOR_DISPLAY[1].value}
|
|
| 742 |
+ onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[1].value)}
|
|
| 743 |
+ /> |
|
| 744 |
+ </td> |
|
| 745 |
+ <td |
|
| 746 |
+ className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle" |
|
| 747 |
+ rowSpan={1}
|
|
| 748 |
+ > |
|
| 749 |
+ <MatrixRadio |
|
| 750 |
+ name="review-result" |
|
| 751 |
+ value={REVIEW_RESULT_OPTIONS[1]}
|
|
| 752 |
+ checked={reviewResult === REVIEW_RESULT_OPTIONS[1]}
|
|
| 753 |
+ onChange={() => pickResult(REVIEW_RESULT_OPTIONS[1])}
|
|
| 754 |
+ bold |
|
| 755 |
+ /> |
|
| 756 |
+ </td> |
|
| 757 |
+ </tr> |
|
| 758 |
+ <tr> |
|
| 759 |
+ <td className="border border-gray-300 px-3 py-2 align-top"> |
|
| 760 |
+ <MatrixRadio |
|
| 761 |
+ name="review-minor" |
|
| 762 |
+ value={REVIEW_MINOR_DISPLAY[2].value}
|
|
| 763 |
+ label={REVIEW_MINOR_DISPLAY[2].label}
|
|
| 764 |
+ note={REVIEW_MINOR_DISPLAY[2].note}
|
|
| 765 |
+ checked={reviewMinor === REVIEW_MINOR_DISPLAY[2].value}
|
|
| 766 |
+ onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[2].value)}
|
|
| 767 |
+ /> |
|
| 768 |
+ </td> |
|
| 769 |
+ <td |
|
| 770 |
+ className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle" |
|
| 771 |
+ rowSpan={3}
|
|
| 772 |
+ > |
|
| 773 |
+ <MatrixRadio |
|
| 774 |
+ name="review-result" |
|
| 775 |
+ value={REVIEW_RESULT_OPTIONS[2]}
|
|
| 776 |
+ checked={reviewResult === REVIEW_RESULT_OPTIONS[2]}
|
|
| 777 |
+ onChange={() => pickResult(REVIEW_RESULT_OPTIONS[2])}
|
|
| 778 |
+ bold |
|
| 779 |
+ /> |
|
| 780 |
+ </td> |
|
| 781 |
+ </tr> |
|
| 782 |
+ <tr> |
|
| 783 |
+ <td className="border border-gray-300 px-3 py-2 align-top"> |
|
| 784 |
+ <MatrixRadio |
|
| 785 |
+ name="review-minor" |
|
| 786 |
+ value={REVIEW_MINOR_DISPLAY[3].value}
|
|
| 787 |
+ label={REVIEW_MINOR_DISPLAY[3].label}
|
|
| 788 |
+ note={REVIEW_MINOR_DISPLAY[3].note}
|
|
| 789 |
+ checked={reviewMinor === REVIEW_MINOR_DISPLAY[3].value}
|
|
| 790 |
+ onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[3].value)}
|
|
| 791 |
+ /> |
|
| 792 |
+ </td> |
|
| 793 |
+ </tr> |
|
| 794 |
+ <tr> |
|
| 795 |
+ <td className="border border-gray-300 px-3 py-2 align-top"> |
|
| 796 |
+ <MatrixRadio |
|
| 797 |
+ name="review-minor" |
|
| 798 |
+ value={REVIEW_MINOR_DISPLAY[4].value}
|
|
| 799 |
+ label={REVIEW_MINOR_DISPLAY[4].label}
|
|
| 800 |
+ checked={reviewMinor === REVIEW_MINOR_DISPLAY[4].value}
|
|
| 801 |
+ onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[4].value)}
|
|
| 802 |
+ /> |
|
| 803 |
+ </td> |
|
| 804 |
+ </tr> |
|
| 805 |
+ <tr> |
|
| 806 |
+ <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
|
|
| 807 |
+ <MatrixRadio |
|
| 808 |
+ name="review-major" |
|
| 809 |
+ value={REVIEW_MAJOR_OPTIONS[3]}
|
|
| 810 |
+ note="공공누리 적용 제외 대상, 검토불가" |
|
| 811 |
+ checked={reviewMajor === REVIEW_MAJOR_OPTIONS[3]}
|
|
| 812 |
+ onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[3])}
|
|
| 813 |
+ /> |
|
| 814 |
+ </td> |
|
| 815 |
+ <td className="border border-gray-300 px-3 py-2" /> |
|
| 816 |
+ <td |
|
| 817 |
+ className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle" |
|
| 818 |
+ rowSpan={1}
|
|
| 819 |
+ > |
|
| 820 |
+ <MatrixRadio |
|
| 821 |
+ name="review-result" |
|
| 822 |
+ value={REVIEW_RESULT_OPTIONS[3]}
|
|
| 823 |
+ checked={reviewResult === REVIEW_RESULT_OPTIONS[3]}
|
|
| 824 |
+ onChange={() => pickResult(REVIEW_RESULT_OPTIONS[3])}
|
|
| 825 |
+ bold |
|
| 826 |
+ /> |
|
| 827 |
+ </td> |
|
| 828 |
+ </tr> |
|
| 829 |
+ </tbody> |
|
| 830 |
+ </table> |
|
| 831 |
+ </div> |
|
| 595 | 832 |
|
| 596 |
- {reviewMajor === '제3자 권리' && (
|
|
| 597 |
- <RadioGroup |
|
| 598 |
- legend="권리확인(세부)" |
|
| 599 |
- name="review-minor" |
|
| 600 |
- options={REVIEW_MINOR_OPTIONS}
|
|
| 601 |
- value={reviewMinor}
|
|
| 602 |
- onChange={pickMinor}
|
|
| 603 |
- /> |
|
| 604 |
- )} |
|
| 833 |
+ <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200"> |
|
| 834 |
+ <InfoRow label="공공누리 유형"> |
|
| 835 |
+ <div className="flex flex-col gap-1.5"> |
|
| 836 |
+ <div className="flex flex-wrap gap-x-4 gap-y-1"> |
|
| 837 |
+ {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o === '0유형' || o === '개방불가').map((option) => (
|
|
| 838 |
+ <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 839 |
+ <input |
|
| 840 |
+ type="radio" |
|
| 841 |
+ name="judged-kogl-type" |
|
| 842 |
+ checked={judgedKoglType === option}
|
|
| 843 |
+ onChange={() => setJudgedKoglType(option)}
|
|
| 844 |
+ /> |
|
| 845 |
+ {option}
|
|
| 846 |
+ </label> |
|
| 847 |
+ ))} |
|
| 848 |
+ </div> |
|
| 849 |
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1"> |
|
| 850 |
+ {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o !== '0유형' && o !== '개방불가').map((option) => (
|
|
| 851 |
+ <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
|
|
| 852 |
+ <input |
|
| 853 |
+ type="radio" |
|
| 854 |
+ name="judged-kogl-type" |
|
| 855 |
+ checked={judgedKoglType === option}
|
|
| 856 |
+ onChange={() => setJudgedKoglType(option)}
|
|
| 857 |
+ /> |
|
| 858 |
+ {option}
|
|
| 859 |
+ </label> |
|
| 860 |
+ ))} |
|
| 861 |
+ <label className="ml-2 flex items-center gap-1.5 text-sm text-gray-800"> |
|
| 862 |
+ <input |
|
| 863 |
+ type="checkbox" |
|
| 864 |
+ checked={judgedAiType}
|
|
| 865 |
+ onChange={(e) => setJudgedAiType(e.target.checked)}
|
|
| 866 |
+ /> |
|
| 867 |
+ AI유형 |
|
| 868 |
+ </label> |
|
| 869 |
+ </div> |
|
| 870 |
+ </div> |
|
| 871 |
+ </InfoRow> |
|
| 605 | 872 |
|
| 606 |
- <RadioGroup |
|
| 607 |
- legend="처리결과" |
|
| 608 |
- name="review-result" |
|
| 609 |
- options={REVIEW_RESULT_OPTIONS}
|
|
| 610 |
- value={reviewResult}
|
|
| 611 |
- onChange={pickResult}
|
|
| 612 |
- /> |
|
| 613 |
- |
|
| 614 |
- <RadioGroup |
|
| 615 |
- legend="공공누리유형(판정)" |
|
| 616 |
- name="judged-kogl-type" |
|
| 617 |
- options={JUDGED_KOGL_TYPE_OPTIONS}
|
|
| 618 |
- value={judgedKoglType}
|
|
| 619 |
- onChange={setJudgedKoglType}
|
|
| 620 |
- /> |
|
| 621 |
- |
|
| 622 |
- <label className="flex items-center gap-2 text-sm text-gray-800"> |
|
| 623 |
- <input |
|
| 624 |
- type="checkbox" |
|
| 625 |
- checked={judgedAiType}
|
|
| 626 |
- onChange={(e) => setJudgedAiType(e.target.checked)}
|
|
| 627 |
- /> |
|
| 628 |
- AI유형 |
|
| 629 |
- </label> |
|
| 630 |
- |
|
| 631 |
- <label className="flex items-center gap-2 text-sm text-gray-800"> |
|
| 632 |
- <input |
|
| 633 |
- type="checkbox" |
|
| 634 |
- checked={needsProcessing}
|
|
| 635 |
- onChange={(e) => setNeedsProcessing(e.target.checked)}
|
|
| 636 |
- /> |
|
| 637 |
- 권리처리필요 |
|
| 638 |
- </label> |
|
| 639 |
- |
|
| 640 |
- <div className="flex flex-col gap-1 text-sm"> |
|
| 641 |
- <label htmlFor="review-opinion" className="text-xs text-gray-500"> |
|
| 642 |
- 의견 |
|
| 873 |
+ <InfoRow label="권리처리필요"> |
|
| 874 |
+ <label className="flex items-center gap-1.5 text-sm text-gray-800"> |
|
| 875 |
+ <input |
|
| 876 |
+ type="checkbox" |
|
| 877 |
+ checked={needsProcessing}
|
|
| 878 |
+ onChange={(e) => setNeedsProcessing(e.target.checked)}
|
|
| 879 |
+ /> |
|
| 880 |
+ 권리처리필요 |
|
| 643 | 881 |
</label> |
| 882 |
+ </InfoRow> |
|
| 883 |
+ |
|
| 884 |
+ <InfoRow label="의견"> |
|
| 644 | 885 |
<textarea |
| 645 | 886 |
id="review-opinion" |
| 887 |
+ aria-label="의견" |
|
| 646 | 888 |
rows={3}
|
| 647 | 889 |
value={opinion}
|
| 648 | 890 |
onChange={(e) => setOpinion(e.target.value)}
|
| 649 |
- className="rounded-md border border-gray-300 px-2 py-1.5" |
|
| 891 |
+ className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm" |
|
| 650 | 892 |
/> |
| 651 |
- </div> |
|
| 893 |
+ </InfoRow> |
|
| 652 | 894 |
|
| 653 |
- <div className="flex flex-col gap-1 text-sm"> |
|
| 654 |
- <label htmlFor="review-lawyer-note" className="text-xs text-gray-500"> |
|
| 655 |
- 비고 |
|
| 656 |
- </label> |
|
| 895 |
+ <InfoRow label="비고"> |
|
| 657 | 896 |
<textarea |
| 658 | 897 |
id="review-lawyer-note" |
| 898 |
+ aria-label="비고" |
|
| 659 | 899 |
rows={3}
|
| 660 | 900 |
value={lawyerNote}
|
| 661 | 901 |
onChange={(e) => setLawyerNote(e.target.value)}
|
| 662 |
- className="rounded-md border border-gray-300 px-2 py-1.5" |
|
| 902 |
+ className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm" |
|
| 663 | 903 |
/> |
| 664 |
- </div> |
|
| 904 |
+ </InfoRow> |
|
| 665 | 905 |
</div> |
| 666 | 906 |
|
| 667 | 907 |
{saveError && <p className="mt-3 text-sm text-red-600">{saveError}</p>}
|
... | ... | @@ -671,7 +911,7 @@ |
| 671 | 911 |
type="button" |
| 672 | 912 |
disabled={busy}
|
| 673 | 913 |
onClick={onBack}
|
| 674 |
- className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50" |
|
| 914 |
+ className="rounded-md bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-gray-700 disabled:opacity-50" |
|
| 675 | 915 |
> |
| 676 | 916 |
목록 |
| 677 | 917 |
</button> |
... | ... | @@ -679,7 +919,7 @@ |
| 679 | 919 |
type="button" |
| 680 | 920 |
disabled={busy}
|
| 681 | 921 |
onClick={() => void handleSave()}
|
| 682 |
- className="rounded-md bg-blue-600 px-4 py-1.5 text-sm font-semibold text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-300" |
|
| 922 |
+ className="rounded-md bg-blue-700 px-4 py-1.5 text-sm font-semibold text-white hover:bg-blue-800 disabled:cursor-not-allowed disabled:bg-gray-300" |
|
| 683 | 923 |
> |
| 684 | 924 |
수정 |
| 685 | 925 |
</button> |
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?