ITN Dev 07-24
style: 권리확인/권리처리 상세화면을 정부시스템 폼 스타일로 재구성
- ReviewBoard/ProcessBoard 상세 뷰를 회색 라벨 셀 + 테두리 표 레이아웃으로 교체
- 권리확인 대분류/세부/처리결과 라디오를 참조 양식과 동일한 rowSpan 표로 재구성
  (접근성 이름은 aria-label로 원래 값 그대로 고정해 라벨/버튼 텍스트 불변 유지)
- ProcessBoard 상세 액션바에 삭제 버튼 추가(목록의 삭제와 동일한 확인+호출 동작)
@de951975a6dd0628390d3f367335897f6d8f18e2
frontend/src/components/ProcessBoard.test.tsx
--- frontend/src/components/ProcessBoard.test.tsx
+++ frontend/src/components/ProcessBoard.test.tsx
@@ -350,4 +350,25 @@
     expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy()
     expect(mocks.updateProcessing).not.toHaveBeenCalled()
   })
+
+  it('상세 화면에서 삭제를 누르면 확인 후 deleteProcessItem을 호출하고 목록으로 돌아간다', async () => {
+    mocks.getProcessPage.mockResolvedValue(page())
+    mocks.getProcessItem.mockResolvedValue(item())
+    mocks.deleteProcessItem.mockResolvedValue(undefined)
+    const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
+
+    render(<ProcessBoard org={org()} />)
+
+    await waitFor(() => expect(screen.getByText('2026년 사업 공고')).toBeTruthy())
+    fireEvent.click(screen.getByRole('button', { name: '처리등록' }))
+    await waitFor(() => expect(mocks.getProcessItem).toHaveBeenCalled())
+
+    fireEvent.click(screen.getByRole('button', { name: '삭제' }))
+
+    expect(confirmSpy).toHaveBeenCalled()
+    await waitFor(() => expect(mocks.deleteProcessItem).toHaveBeenCalledWith(1, 1))
+    await waitFor(() => expect(screen.getByRole('button', { name: '엑셀 업로드' })).toBeTruthy())
+
+    confirmSpy.mockRestore()
+  })
 })
frontend/src/components/ProcessBoard.tsx
--- frontend/src/components/ProcessBoard.tsx
+++ frontend/src/components/ProcessBoard.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react'
+import { useEffect, useRef, useState, type ReactNode } from 'react'
 import {
   deleteProcessItem,
   getProcessItem,
@@ -77,11 +77,76 @@
   return text.length > max ? `${text.slice(0, max)}…` : text
 }
 
-function Field({ label, value }: { label: string; value: string | null }) {
+/** 소제목: 작은 파란 사각 불릿 + 굵은 제목. */
+function SectionHeader({ title }: { title: string }) {
   return (
-    <div className="flex gap-3 text-sm">
-      <span className="w-24 shrink-0 text-gray-400">{label}</span>
-      <span className={value ? 'text-gray-900' : 'text-gray-300'}>{value ?? '-'}</span>
+    <h3 className="flex items-center gap-2 text-sm font-bold">
+      <span className="h-2.5 w-2.5 shrink-0 bg-blue-700" />
+      {title}
+    </h3>
+  )
+}
+
+/** 정보 테이블의 회색 라벨 셀. required면 라벨 앞에 빨간 별표를 붙인다. */
+function LabelCell({ children, required }: { children: ReactNode; required?: boolean }) {
+  return (
+    <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600">
+      {required && <span className="text-red-500">*</span>}
+      {children}
+    </div>
+  )
+}
+
+/** 정보 테이블의 값 셀. */
+function ValueCell({ children }: { children: ReactNode }) {
+  return <div className="flex items-center px-3 py-1.5">{children}</div>
+}
+
+/** 읽기전용 값 표시용 회색 박스. */
+function ReadonlyBox({ value }: { value: ReactNode }) {
+  return (
+    <div className="w-full rounded border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-800">
+      {value || <span className="text-gray-400">-</span>}
+    </div>
+  )
+}
+
+/** 단일 라벨/값 행(150px + 1fr). */
+function InfoRow({
+  label,
+  required,
+  children,
+}: {
+  label: string
+  required?: boolean
+  children: ReactNode
+}) {
+  return (
+    <div className="grid grid-cols-[150px_1fr]">
+      <LabelCell required={required}>{label}</LabelCell>
+      <ValueCell>{children}</ValueCell>
+    </div>
+  )
+}
+
+/** 라벨/값 두 쌍을 한 행에 나란히 배치(150px + 1fr + 150px + 1fr). */
+function InfoRowPair({
+  leftLabel,
+  left,
+  rightLabel,
+  right,
+}: {
+  leftLabel: string
+  left: ReactNode
+  rightLabel: string
+  right: ReactNode
+}) {
+  return (
+    <div className="grid grid-cols-[150px_1fr_150px_1fr]">
+      <LabelCell>{leftLabel}</LabelCell>
+      <ValueCell>{left}</ValueCell>
+      <LabelCell>{rightLabel}</LabelCell>
+      <ValueCell>{right}</ValueCell>
     </div>
   )
 }
@@ -561,6 +626,25 @@
     }
   }
 
+  async function handleDelete() {
+    if (!item) {
+      return
+    }
+    if (!window.confirm(`"${item.postTitle ?? item.seq}" 게시물을 삭제할까요?`)) {
+      return
+    }
+    setBusy(true)
+    setSaveError(null)
+    try {
+      await deleteProcessItem(org.id, itemId)
+      onSaved()
+    } catch (e) {
+      setSaveError(e instanceof Error ? e.message : '삭제에 실패했습니다.')
+    } finally {
+      setBusy(false)
+    }
+  }
+
   if (loading) {
     return <p className="p-4 py-10 text-center text-sm text-gray-400">불러오는 중…</p>
   }
@@ -571,140 +655,192 @@
   return (
     <div className="p-4">
       <section className="rounded-lg border border-gray-200 p-4">
-        <h2 className="text-sm font-semibold">기본정보</h2>
-        <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
-          <Field label="기관명" value={org.orgName} />
-          <Field label="사이트명" value={item.siteName} />
-          <Field label="게시판명" value={item.boardName} />
-          <Field label="게시물제목" value={item.postTitle} />
-          <div className="flex gap-3 text-sm">
-            <span className="w-24 shrink-0 text-gray-400">URL</span>
-            {item.url ? (
-              <a href={item.url} target="_blank" rel="noreferrer" className="break-all text-blue-600 hover:underline">
-                {item.url}
-              </a>
-            ) : (
-              <span className="text-gray-300">-</span>
-            )}
-          </div>
-          <Field label="기존 공공누리" value={item.priorKoglType} />
+        <SectionHeader title="기본정보" />
+
+        <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200">
+          <InfoRow label="기관명">
+            <ReadonlyBox value={org.orgName} />
+          </InfoRow>
+          <InfoRowPair
+            leftLabel="사이트명"
+            left={<ReadonlyBox value={item.siteName} />}
+            rightLabel="게시판명"
+            right={<ReadonlyBox value={item.boardName} />}
+          />
+          <InfoRow label="게시물제목">
+            <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">
+              <span>{item.postTitle ?? '-'}</span>
+              {item.url && (
+                <a
+                  href={item.url}
+                  target="_blank"
+                  rel="noreferrer"
+                  title="원문 보기"
+                  aria-label="원문 보기"
+                  className="text-blue-500 hover:underline"
+                >
+                  🔗
+                </a>
+              )}
+            </div>
+          </InfoRow>
+          <InfoRow label="기존 공공누리">
+            <ReadonlyBox value={item.priorKoglType} />
+          </InfoRow>
         </div>
       </section>
 
       <section className="mt-4 rounded-lg border border-gray-200 p-4">
-        <h2 className="text-sm font-semibold">권리확인</h2>
-        <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
-          <Field
-            label="권리확인"
-            value={item.reviewMajor ? `${item.reviewMajor}${item.reviewMinor ? ` > ${item.reviewMinor}` : ''}` : null}
-          />
-          <Field
-            label="공공누리유형"
-            value={item.reviewKoglType ? `${item.reviewKoglType}${item.reviewAiType === 'Y' ? ' + AI' : ''}` : null}
-          />
-          <Field label="처리 구분" value={item.reviewResult} />
-          <Field label="의견" value={item.reviewOpinion} />
-          <Field label="비고" value={item.reviewNote} />
+        <SectionHeader title="권리확인" />
+
+        <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200">
+          <InfoRow label="권리확인">
+            <ReadonlyBox
+              value={item.reviewMajor ? `${item.reviewMajor}${item.reviewMinor ? ` > ${item.reviewMinor}` : ''}` : null}
+            />
+          </InfoRow>
+          <InfoRow label="권리확인 공공누리유형">
+            <ReadonlyBox
+              value={item.reviewKoglType ? `${item.reviewKoglType}${item.reviewAiType === 'Y' ? ' + AI' : ''}` : null}
+            />
+          </InfoRow>
+          <InfoRow label="처리 구분">
+            <ReadonlyBox value={item.reviewResult} />
+          </InfoRow>
+          <InfoRow label="의견">
+            <ReadonlyBox value={item.reviewOpinion} />
+          </InfoRow>
+          <InfoRow label="비고">
+            <ReadonlyBox value={item.reviewNote} />
+          </InfoRow>
         </div>
       </section>
 
       <section className="mt-4 rounded-lg border border-gray-200 p-4">
-        <h2 className="text-sm font-semibold">권리처리</h2>
+        <SectionHeader title="권리처리" />
 
-        <div className="mt-3 flex flex-col gap-4">
-          <fieldset className="flex flex-col gap-1.5">
-            <legend className="text-xs text-gray-500">계약서 유무</legend>
-            <div className="flex flex-wrap gap-x-4 gap-y-1">
-              {CONTRACT_DOC_OPTIONS.map((option) => (
-                <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
+        <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200">
+          <InfoRow label="계약서 유무">
+            <div className="flex flex-col gap-1.5">
+              <div className="flex flex-wrap gap-x-4 gap-y-1">
+                {CONTRACT_DOC_OPTIONS.map((option) => (
+                  <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
+                    <input
+                      type="checkbox"
+                      checked={contractSelected.has(option)}
+                      onChange={() => toggleContractOption(option)}
+                    />
+                    {option}
+                  </label>
+                ))}
+                <label className="flex items-center gap-1.5 text-sm text-gray-800">
                   <input
                     type="checkbox"
-                    checked={contractSelected.has(option)}
-                    onChange={() => toggleContractOption(option)}
+                    checked={contractSelected.has(CONTRACT_ETC)}
+                    onChange={() => toggleContractOption(CONTRACT_ETC)}
                   />
-                  {option}
+                  {CONTRACT_ETC}
                 </label>
-              ))}
-              <label className="flex items-center gap-1.5 text-sm text-gray-800">
+              </div>
+              {contractSelected.has(CONTRACT_ETC) && (
                 <input
-                  type="checkbox"
-                  checked={contractSelected.has(CONTRACT_ETC)}
-                  onChange={() => toggleContractOption(CONTRACT_ETC)}
+                  type="text"
+                  aria-label="기타 계약서 내용"
+                  value={contractEtc}
+                  onChange={(e) => setContractEtc(e.target.value)}
+                  placeholder="기타 내용을 입력하세요"
+                  className="w-72 rounded-md border border-gray-300 px-2 py-1.5 text-sm"
                 />
-                {CONTRACT_ETC}
-              </label>
+              )}
             </div>
-            {contractSelected.has(CONTRACT_ETC) && (
-              <input
-                type="text"
-                aria-label="기타 계약서 내용"
-                value={contractEtc}
-                onChange={(e) => setContractEtc(e.target.value)}
-                placeholder="기타 내용을 입력하세요"
-                className="mt-1 w-72 rounded-md border border-gray-300 px-2 py-1.5 text-sm"
-              />
-            )}
-          </fieldset>
+          </InfoRow>
 
-          <fieldset className="flex flex-col gap-1.5">
-            <legend className="text-xs text-gray-500">공공누리 유형</legend>
-            <div className="flex flex-wrap gap-x-4 gap-y-1">
-              {JUDGED_KOGL_TYPE_OPTIONS.map((option) => (
-                <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
+          <InfoRow label="공공누리 유형">
+            <div className="flex flex-col gap-1.5">
+              <div className="flex flex-wrap gap-x-4 gap-y-1">
+                {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o === '0유형' || o === '개방불가').map((option) => (
+                  <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
+                    <input
+                      type="radio"
+                      name="judged-kogl-type"
+                      checked={judgedKoglType === option}
+                      onChange={() => setJudgedKoglType(option)}
+                    />
+                    {option}
+                  </label>
+                ))}
+              </div>
+              <div className="flex flex-wrap items-center gap-x-4 gap-y-1">
+                {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o !== '0유형' && o !== '개방불가').map((option) => (
+                  <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
+                    <input
+                      type="radio"
+                      name="judged-kogl-type"
+                      checked={judgedKoglType === option}
+                      onChange={() => setJudgedKoglType(option)}
+                    />
+                    {option}
+                  </label>
+                ))}
+                <label className="ml-2 flex items-center gap-1.5 text-sm text-gray-800">
                   <input
-                    type="radio"
-                    name="judged-kogl-type"
-                    checked={judgedKoglType === option}
-                    onChange={() => setJudgedKoglType(option)}
+                    type="checkbox"
+                    checked={judgedAiType}
+                    onChange={(e) => setJudgedAiType(e.target.checked)}
                   />
-                  {option}
+                  AI유형
                 </label>
-              ))}
+              </div>
             </div>
-          </fieldset>
+          </InfoRow>
 
-          <label className="flex items-center gap-2 text-sm text-gray-800">
-            <input type="checkbox" checked={judgedAiType} onChange={(e) => setJudgedAiType(e.target.checked)} />
-            AI유형
-          </label>
-
-          <div className="flex flex-col gap-1 text-sm">
-            <label htmlFor="process-final-opinion" className="text-xs text-gray-500">
-              최종의견
-            </label>
-            <textarea
-              id="process-final-opinion"
-              rows={3}
-              value={finalOpinion}
-              onChange={(e) => setFinalOpinion(e.target.value)}
-              placeholder="전부 양도 체결, 일부 양도 체결, 초상 이용 동의, 공공누리 동의 등 처리한 사유 기입"
-              className="rounded-md border border-gray-300 px-2 py-1.5"
-            />
+          <div className="grid grid-cols-[150px_1fr]">
+            <LabelCell>권리처리 결과</LabelCell>
+            <div className="flex flex-col divide-y divide-gray-200">
+              <div className="grid grid-cols-[110px_1fr]">
+                <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600">
+                  최종의견
+                </div>
+                <div className="flex flex-col gap-1 px-3 py-1.5">
+                  <span className="text-[11px] text-gray-400">
+                    작성안내) 전부 양도 체결, 일부 양도 체결, 초상 이용 동의, 공공누리 동의 등 처리한 사유 기입
+                  </span>
+                  <textarea
+                    id="process-final-opinion"
+                    aria-label="최종의견"
+                    rows={3}
+                    value={finalOpinion}
+                    onChange={(e) => setFinalOpinion(e.target.value)}
+                    className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm"
+                  />
+                </div>
+              </div>
+              <div className="grid grid-cols-[110px_1fr]">
+                <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600">
+                  판단근거
+                </div>
+                <div className="flex flex-col gap-1 px-3 py-1.5">
+                  <span className="text-[11px] text-gray-400">작성안내) 계약서 명칭, 해당 문구 기입</span>
+                  <textarea
+                    id="process-judgment-basis"
+                    aria-label="판단근거"
+                    rows={3}
+                    value={judgmentBasis}
+                    onChange={(e) => setJudgmentBasis(e.target.value)}
+                    className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm"
+                  />
+                </div>
+              </div>
+            </div>
           </div>
 
-          <div className="flex flex-col gap-1 text-sm">
-            <label htmlFor="process-judgment-basis" className="text-xs text-gray-500">
-              판단근거
-            </label>
-            <textarea
-              id="process-judgment-basis"
-              rows={3}
-              value={judgmentBasis}
-              onChange={(e) => setJudgmentBasis(e.target.value)}
-              placeholder="계약서 명칭, 해당 문구 기입"
-              className="rounded-md border border-gray-300 px-2 py-1.5"
-            />
-          </div>
-
-          <div className="flex flex-col gap-1 text-sm">
-            <label htmlFor="process-status" className="text-xs text-gray-500">
-              *권리처리상태
-            </label>
+          <InfoRow label="권리처리상태" required>
             <select
               id="process-status"
+              aria-label="*권리처리상태"
               value={processStatus}
               onChange={(e) => setProcessStatus(e.target.value)}
-              className="w-40 rounded-md border border-gray-300 px-2 py-1.5"
+              className="w-40 rounded-md border border-gray-300 px-2 py-1.5 text-sm"
             >
               <option value="">선택</option>
               {PROCESS_STATUS_OPTIONS.map((option) => (
@@ -713,7 +849,7 @@
                 </option>
               ))}
             </select>
-          </div>
+          </InfoRow>
         </div>
 
         {saveError && <p className="mt-3 text-sm text-red-600">{saveError}</p>}
@@ -723,15 +859,23 @@
             type="button"
             disabled={busy}
             onClick={onBack}
-            className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
+            className="rounded-md bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-gray-700 disabled:opacity-50"
           >
             목록
           </button>
           <button
             type="button"
+            disabled={busy}
+            onClick={() => void handleDelete()}
+            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"
+          >
+            삭제
+          </button>
+          <button
+            type="button"
             disabled={busy || !processStatus}
             onClick={() => void handleSave()}
-            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"
+            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"
           >
             수정
           </button>
frontend/src/components/ReviewBoard.tsx
--- frontend/src/components/ReviewBoard.tsx
+++ frontend/src/components/ReviewBoard.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react'
+import { useEffect, useRef, useState, type ReactNode } from 'react'
 import {
   deleteReviewItem,
   getReviewItem,
@@ -57,48 +57,111 @@
   return null
 }
 
-function Field({ label, value }: { label: string; value: string | null }) {
+/** 세부(review-minor) 라디오의 실제 저장값은 REVIEW_MINOR_OPTIONS 그대로 유지하고,
+ * 참조 양식과 같은 화면 표기(줄바꿈된 부연설명)만 별도로 매핑한다. */
+const REVIEW_MINOR_DISPLAY: { value: string; label: string; note?: string }[] = [
+  { value: REVIEW_MINOR_OPTIONS[0], label: '1. 원시적 권리 전부 보유' },
+  { value: REVIEW_MINOR_OPTIONS[1], label: '2. 후천적 권리 전부 보유', note: '(계약에 의한 전부 양수)' },
+  {
+    value: REVIEW_MINOR_OPTIONS[2],
+    label: '3. 권리 일부(공동) 보유',
+    note: '(보도자료·제3자저작물, 계약에 의한 전부/공동 보유 등)',
+  },
+  { value: REVIEW_MINOR_OPTIONS[3], label: '4. 권리 미보유', note: '(공모전 수상작 등)' },
+  { value: REVIEW_MINOR_OPTIONS[4], label: '초상권 포함' },
+]
+
+/** 소제목: 작은 파란 사각 불릿 + 굵은 제목 + 회색 부연설명. */
+function SectionHeader({ title, note }: { title: string; note?: string }) {
   return (
-    <div className="flex gap-3 text-sm">
-      <span className="w-24 shrink-0 text-gray-400">{label}</span>
-      <span className={value ? 'text-gray-900' : 'text-gray-300'}>{value ?? '-'}</span>
+    <h3 className="flex items-center gap-2 text-sm font-bold">
+      <span className="h-2.5 w-2.5 shrink-0 bg-blue-700" />
+      {title}
+      {note && <span className="text-xs font-normal text-gray-400">{note}</span>}
+    </h3>
+  )
+}
+
+/** 정보 테이블의 회색 라벨 셀. */
+function LabelCell({ children }: { children: ReactNode }) {
+  return (
+    <div className="flex items-center bg-gray-50 px-3 py-2 text-xs font-medium text-gray-600">{children}</div>
+  )
+}
+
+/** 정보 테이블의 값 셀. */
+function ValueCell({ children }: { children: ReactNode }) {
+  return <div className="flex items-center px-3 py-1.5">{children}</div>
+}
+
+/** 읽기전용 값 표시용 회색 박스. */
+function ReadonlyBox({ value }: { value: string | null }) {
+  return (
+    <div className="w-full rounded border border-gray-200 bg-gray-100 px-3 py-1.5 text-sm text-gray-800">
+      {value ? value : <span className="text-gray-400">-</span>}
     </div>
   )
 }
 
-function RadioGroup({
-  legend,
-  name,
-  options,
-  value,
-  onChange,
-  disabled,
+/** 단일 라벨/값 행(150px + 1fr). */
+function InfoRow({ label, children }: { label: string; children: ReactNode }) {
+  return (
+    <div className="grid grid-cols-[150px_1fr]">
+      <LabelCell>{label}</LabelCell>
+      <ValueCell>{children}</ValueCell>
+    </div>
+  )
+}
+
+/** 라벨/값 두 쌍을 한 행에 나란히 배치(150px + 1fr + 150px + 1fr). */
+function InfoRowPair({
+  leftLabel,
+  left,
+  rightLabel,
+  right,
 }: {
-  legend: string
-  name: string
-  options: string[]
-  value: string
-  onChange: (value: string) => void
-  disabled?: boolean
+  leftLabel: string
+  left: ReactNode
+  rightLabel: string
+  right: ReactNode
 }) {
   return (
-    <fieldset className="flex flex-col gap-1.5">
-      <legend className="text-xs text-gray-500">{legend}</legend>
-      <div className="flex flex-wrap gap-x-4 gap-y-1">
-        {options.map((option) => (
-          <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
-            <input
-              type="radio"
-              name={name}
-              disabled={disabled}
-              checked={value === option}
-              onChange={() => onChange(option)}
-            />
-            {option}
-          </label>
-        ))}
-      </div>
-    </fieldset>
+    <div className="grid grid-cols-[150px_1fr_150px_1fr]">
+      <LabelCell>{leftLabel}</LabelCell>
+      <ValueCell>{left}</ValueCell>
+      <LabelCell>{rightLabel}</LabelCell>
+      <ValueCell>{right}</ValueCell>
+    </div>
+  )
+}
+
+/** 권리확인 매트릭스 표의 라디오 한 칸. aria-label로 접근성 이름을 실제 저장값 텍스트로 고정해
+ * 화면 표기(줄바꿈된 부연설명)가 늘어나도 접근성 이름은 항상 원래 값과 동일하게 유지한다. */
+function MatrixRadio({
+  name,
+  value,
+  label,
+  note,
+  checked,
+  onChange,
+  bold,
+}: {
+  name: string
+  value: string
+  label?: string
+  note?: string
+  checked: boolean
+  onChange: () => void
+  bold?: boolean
+}) {
+  return (
+    <label className="flex cursor-pointer flex-col gap-0.5">
+      <span className="flex items-center gap-1.5">
+        <input type="radio" name={name} aria-label={value} checked={checked} onChange={onChange} />
+        <span className={bold ? 'font-semibold text-gray-900' : 'text-gray-800'}>{label ?? value}</span>
+      </span>
+      {note && <span className="pl-5 text-[11px] text-gray-400">{note}</span>}
+    </label>
   )
 }
 
@@ -552,116 +615,293 @@
     return <p className="p-4 py-10 text-center text-sm text-red-600">{loadError ?? '게시물을 찾을 수 없습니다.'}</p>
   }
 
+  const koglSummary = `${item.koglAttached ?? '-'} / ${item.koglType ?? '-'}${item.aiType ? ' · AI' : ''}`
+
   return (
     <div className="p-4">
       <section className="rounded-lg border border-gray-200 p-4">
-        <h2 className="text-sm font-semibold">게시물 정보 (조사원)</h2>
-        <div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
-          <Field label="기관명" value={org.orgName} />
-          <Field label="사이트명" value={item.siteName} />
-          <Field label="범주" value={item.category} />
-          <Field label="게시판명" value={item.boardName} />
-          <Field label="게시물제목" value={item.postTitle} />
-          <div className="flex gap-3 text-sm">
-            <span className="w-24 shrink-0 text-gray-400">URL</span>
+        <SectionHeader title="게시물 정보" note="(조사원)" />
+
+        <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200">
+          <InfoRow label="기관명">
+            <ReadonlyBox value={org.orgName} />
+          </InfoRow>
+          <InfoRowPair
+            leftLabel="사이트명"
+            left={<ReadonlyBox value={item.siteName} />}
+            rightLabel="범주(카테고리)"
+            right={<ReadonlyBox value={item.category} />}
+          />
+          <InfoRow label="게시판명">
+            <ReadonlyBox value={item.boardName} />
+          </InfoRow>
+          <InfoRow label="게시물제목">
+            <ReadonlyBox value={item.postTitle} />
+          </InfoRow>
+          <InfoRow label="URL주소">
             {item.url ? (
-              <a href={item.url} target="_blank" rel="noreferrer" className="break-all text-blue-600 hover:underline">
+              <a
+                href={item.url}
+                target="_blank"
+                rel="noreferrer"
+                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"
+              >
                 {item.url}
               </a>
             ) : (
-              <span className="text-gray-300">-</span>
+              <ReadonlyBox value={null} />
             )}
-          </div>
-          <Field label="제작일" value={item.producedDate} />
-          <Field label="공표일" value={item.publishedDate} />
-          <Field label="첨부파일 여부" value={item.hasAttachment} />
-          <Field label="기존 공공누리 부착" value={item.koglAttached} />
-          <Field label="기존 공공누리유형" value={item.koglType} />
-          <Field label="기존 AI유형" value={item.aiType} />
+          </InfoRow>
+          <InfoRowPair
+            leftLabel="제작일"
+            left={<ReadonlyBox value={item.producedDate} />}
+            rightLabel="공표일"
+            right={<ReadonlyBox value={item.publishedDate} />}
+          />
+          <InfoRow label="첨부파일 여부">
+            <ReadonlyBox value={item.hasAttachment} />
+          </InfoRow>
+          <InfoRow label="기존 공공누리">
+            <ReadonlyBox value={koglSummary} />
+          </InfoRow>
         </div>
       </section>
 
       <section className="mt-4 rounded-lg border border-gray-200 p-4">
-        <h2 className="text-sm font-semibold">권리확인 (변호사)</h2>
+        <SectionHeader title="권리확인" note="(변호사)" />
 
-        <div className="mt-3 flex flex-col gap-4">
-          <RadioGroup
-            legend="권리확인(대분류)"
-            name="review-major"
-            options={REVIEW_MAJOR_OPTIONS}
-            value={reviewMajor}
-            onChange={pickMajor}
-          />
+        <div className="mt-3 overflow-x-auto">
+          <table className="w-full border-collapse text-sm">
+            <thead>
+              <tr className="bg-gray-50 text-xs font-semibold text-gray-600">
+                <th className="border border-gray-300 px-3 py-2">좌(대분류)</th>
+                <th className="border border-gray-300 px-3 py-2">중(세부)</th>
+                <th className="border border-gray-300 px-3 py-2">우(처리결과)</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
+                  <MatrixRadio
+                    name="review-major"
+                    value={REVIEW_MAJOR_OPTIONS[0]}
+                    note="저작권성 없음, 배포용, 단순데이터"
+                    checked={reviewMajor === REVIEW_MAJOR_OPTIONS[0]}
+                    onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[0])}
+                  />
+                </td>
+                <td className="border border-gray-300 px-3 py-2" />
+                <td
+                  className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle"
+                  rowSpan={2}
+                >
+                  <MatrixRadio
+                    name="review-result"
+                    value={REVIEW_RESULT_OPTIONS[0]}
+                    checked={reviewResult === REVIEW_RESULT_OPTIONS[0]}
+                    onChange={() => pickResult(REVIEW_RESULT_OPTIONS[0])}
+                    bold
+                  />
+                </td>
+              </tr>
+              <tr>
+                <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
+                  <MatrixRadio
+                    name="review-major"
+                    value={REVIEW_MAJOR_OPTIONS[1]}
+                    checked={reviewMajor === REVIEW_MAJOR_OPTIONS[1]}
+                    onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[1])}
+                  />
+                </td>
+                <td className="border border-gray-300 px-3 py-2 align-top">
+                  <MatrixRadio
+                    name="review-minor"
+                    value={REVIEW_MINOR_DISPLAY[0].value}
+                    label={REVIEW_MINOR_DISPLAY[0].label}
+                    checked={reviewMinor === REVIEW_MINOR_DISPLAY[0].value}
+                    onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[0].value)}
+                  />
+                </td>
+              </tr>
+              <tr>
+                <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={4}>
+                  <MatrixRadio
+                    name="review-major"
+                    value={REVIEW_MAJOR_OPTIONS[2]}
+                    checked={reviewMajor === REVIEW_MAJOR_OPTIONS[2]}
+                    onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[2])}
+                  />
+                </td>
+                <td className="border border-gray-300 px-3 py-2 align-top">
+                  <MatrixRadio
+                    name="review-minor"
+                    value={REVIEW_MINOR_DISPLAY[1].value}
+                    label={REVIEW_MINOR_DISPLAY[1].label}
+                    note={REVIEW_MINOR_DISPLAY[1].note}
+                    checked={reviewMinor === REVIEW_MINOR_DISPLAY[1].value}
+                    onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[1].value)}
+                  />
+                </td>
+                <td
+                  className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle"
+                  rowSpan={1}
+                >
+                  <MatrixRadio
+                    name="review-result"
+                    value={REVIEW_RESULT_OPTIONS[1]}
+                    checked={reviewResult === REVIEW_RESULT_OPTIONS[1]}
+                    onChange={() => pickResult(REVIEW_RESULT_OPTIONS[1])}
+                    bold
+                  />
+                </td>
+              </tr>
+              <tr>
+                <td className="border border-gray-300 px-3 py-2 align-top">
+                  <MatrixRadio
+                    name="review-minor"
+                    value={REVIEW_MINOR_DISPLAY[2].value}
+                    label={REVIEW_MINOR_DISPLAY[2].label}
+                    note={REVIEW_MINOR_DISPLAY[2].note}
+                    checked={reviewMinor === REVIEW_MINOR_DISPLAY[2].value}
+                    onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[2].value)}
+                  />
+                </td>
+                <td
+                  className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle"
+                  rowSpan={3}
+                >
+                  <MatrixRadio
+                    name="review-result"
+                    value={REVIEW_RESULT_OPTIONS[2]}
+                    checked={reviewResult === REVIEW_RESULT_OPTIONS[2]}
+                    onChange={() => pickResult(REVIEW_RESULT_OPTIONS[2])}
+                    bold
+                  />
+                </td>
+              </tr>
+              <tr>
+                <td className="border border-gray-300 px-3 py-2 align-top">
+                  <MatrixRadio
+                    name="review-minor"
+                    value={REVIEW_MINOR_DISPLAY[3].value}
+                    label={REVIEW_MINOR_DISPLAY[3].label}
+                    note={REVIEW_MINOR_DISPLAY[3].note}
+                    checked={reviewMinor === REVIEW_MINOR_DISPLAY[3].value}
+                    onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[3].value)}
+                  />
+                </td>
+              </tr>
+              <tr>
+                <td className="border border-gray-300 px-3 py-2 align-top">
+                  <MatrixRadio
+                    name="review-minor"
+                    value={REVIEW_MINOR_DISPLAY[4].value}
+                    label={REVIEW_MINOR_DISPLAY[4].label}
+                    checked={reviewMinor === REVIEW_MINOR_DISPLAY[4].value}
+                    onChange={() => pickMinor(REVIEW_MINOR_DISPLAY[4].value)}
+                  />
+                </td>
+              </tr>
+              <tr>
+                <td className="border border-gray-300 px-3 py-2 align-top" rowSpan={1}>
+                  <MatrixRadio
+                    name="review-major"
+                    value={REVIEW_MAJOR_OPTIONS[3]}
+                    note="공공누리 적용 제외 대상, 검토불가"
+                    checked={reviewMajor === REVIEW_MAJOR_OPTIONS[3]}
+                    onChange={() => pickMajor(REVIEW_MAJOR_OPTIONS[3])}
+                  />
+                </td>
+                <td className="border border-gray-300 px-3 py-2" />
+                <td
+                  className="border border-gray-300 bg-gray-50 px-3 py-2 text-center align-middle"
+                  rowSpan={1}
+                >
+                  <MatrixRadio
+                    name="review-result"
+                    value={REVIEW_RESULT_OPTIONS[3]}
+                    checked={reviewResult === REVIEW_RESULT_OPTIONS[3]}
+                    onChange={() => pickResult(REVIEW_RESULT_OPTIONS[3])}
+                    bold
+                  />
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
 
-          {reviewMajor === '제3자 권리' && (
-            <RadioGroup
-              legend="권리확인(세부)"
-              name="review-minor"
-              options={REVIEW_MINOR_OPTIONS}
-              value={reviewMinor}
-              onChange={pickMinor}
-            />
-          )}
+        <div className="mt-3 overflow-hidden rounded border border-gray-300 divide-y divide-gray-200">
+          <InfoRow label="공공누리 유형">
+            <div className="flex flex-col gap-1.5">
+              <div className="flex flex-wrap gap-x-4 gap-y-1">
+                {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o === '0유형' || o === '개방불가').map((option) => (
+                  <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
+                    <input
+                      type="radio"
+                      name="judged-kogl-type"
+                      checked={judgedKoglType === option}
+                      onChange={() => setJudgedKoglType(option)}
+                    />
+                    {option}
+                  </label>
+                ))}
+              </div>
+              <div className="flex flex-wrap items-center gap-x-4 gap-y-1">
+                {JUDGED_KOGL_TYPE_OPTIONS.filter((o) => o !== '0유형' && o !== '개방불가').map((option) => (
+                  <label key={option} className="flex items-center gap-1.5 text-sm text-gray-800">
+                    <input
+                      type="radio"
+                      name="judged-kogl-type"
+                      checked={judgedKoglType === option}
+                      onChange={() => setJudgedKoglType(option)}
+                    />
+                    {option}
+                  </label>
+                ))}
+                <label className="ml-2 flex items-center gap-1.5 text-sm text-gray-800">
+                  <input
+                    type="checkbox"
+                    checked={judgedAiType}
+                    onChange={(e) => setJudgedAiType(e.target.checked)}
+                  />
+                  AI유형
+                </label>
+              </div>
+            </div>
+          </InfoRow>
 
-          <RadioGroup
-            legend="처리결과"
-            name="review-result"
-            options={REVIEW_RESULT_OPTIONS}
-            value={reviewResult}
-            onChange={pickResult}
-          />
-
-          <RadioGroup
-            legend="공공누리유형(판정)"
-            name="judged-kogl-type"
-            options={JUDGED_KOGL_TYPE_OPTIONS}
-            value={judgedKoglType}
-            onChange={setJudgedKoglType}
-          />
-
-          <label className="flex items-center gap-2 text-sm text-gray-800">
-            <input
-              type="checkbox"
-              checked={judgedAiType}
-              onChange={(e) => setJudgedAiType(e.target.checked)}
-            />
-            AI유형
-          </label>
-
-          <label className="flex items-center gap-2 text-sm text-gray-800">
-            <input
-              type="checkbox"
-              checked={needsProcessing}
-              onChange={(e) => setNeedsProcessing(e.target.checked)}
-            />
-            권리처리필요
-          </label>
-
-          <div className="flex flex-col gap-1 text-sm">
-            <label htmlFor="review-opinion" className="text-xs text-gray-500">
-              의견
+          <InfoRow label="권리처리필요">
+            <label className="flex items-center gap-1.5 text-sm text-gray-800">
+              <input
+                type="checkbox"
+                checked={needsProcessing}
+                onChange={(e) => setNeedsProcessing(e.target.checked)}
+              />
+              권리처리필요
             </label>
+          </InfoRow>
+
+          <InfoRow label="의견">
             <textarea
               id="review-opinion"
+              aria-label="의견"
               rows={3}
               value={opinion}
               onChange={(e) => setOpinion(e.target.value)}
-              className="rounded-md border border-gray-300 px-2 py-1.5"
+              className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm"
             />
-          </div>
+          </InfoRow>
 
-          <div className="flex flex-col gap-1 text-sm">
-            <label htmlFor="review-lawyer-note" className="text-xs text-gray-500">
-              비고
-            </label>
+          <InfoRow label="비고">
             <textarea
               id="review-lawyer-note"
+              aria-label="비고"
               rows={3}
               value={lawyerNote}
               onChange={(e) => setLawyerNote(e.target.value)}
-              className="rounded-md border border-gray-300 px-2 py-1.5"
+              className="w-full rounded border border-gray-300 px-2 py-1.5 text-sm"
             />
-          </div>
+          </InfoRow>
         </div>
 
         {saveError && <p className="mt-3 text-sm text-red-600">{saveError}</p>}
@@ -671,7 +911,7 @@
             type="button"
             disabled={busy}
             onClick={onBack}
-            className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
+            className="rounded-md bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-gray-700 disabled:opacity-50"
           >
             목록
           </button>
@@ -679,7 +919,7 @@
             type="button"
             disabled={busy}
             onClick={() => void handleSave()}
-            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"
+            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"
           >
             수정
           </button>
Add a comment
List