feat: 권리확인 조사원 영역 첨부·기존공공누리 편집 지원, 메모 담당자 최상단 시스템 고정
@8adfcdbb2a1e3932a337ded2fb839175b1d0ad8f
--- frontend/src/api/client.ts
+++ frontend/src/api/client.ts
... | ... | @@ -354,6 +354,10 @@ |
| 354 | 354 |
|
| 355 | 355 |
/** 권리확인 상세의 [수정] 저장 요청. 전부 선택값이다. */ |
| 356 | 356 |
export interface JudgmentRequest {
|
| 357 |
+ hasAttachment: string | null |
|
| 358 |
+ koglAttached: string | null |
|
| 359 |
+ koglType: string | null |
|
| 360 |
+ aiType: string | null |
|
| 357 | 361 |
openable: string | null |
| 358 | 362 |
reviewMajor: string | null |
| 359 | 363 |
reviewMinor: string | null |
--- frontend/src/components/ReviewBoard.test.tsx
+++ frontend/src/components/ReviewBoard.test.tsx
... | ... | @@ -246,6 +246,10 @@ |
| 246 | 246 |
|
| 247 | 247 |
await waitFor(() => |
| 248 | 248 |
expect(mocks.updateReviewJudgment).toHaveBeenCalledWith(1, 1, {
|
| 249 |
+ hasAttachment: 'N', |
|
| 250 |
+ koglAttached: 'Y', |
|
| 251 |
+ koglType: '1유형', |
|
| 252 |
+ aiType: null, |
|
| 249 | 253 |
openable: 'Y', |
| 250 | 254 |
reviewMajor: '만료 저작물', |
| 251 | 255 |
reviewMinor: null, |
--- frontend/src/components/ReviewBoard.tsx
+++ frontend/src/components/ReviewBoard.tsx
... | ... | @@ -506,6 +506,10 @@ |
| 506 | 506 |
const [loading, setLoading] = useState(true) |
| 507 | 507 |
const [loadError, setLoadError] = useState<string | null>(null) |
| 508 | 508 |
|
| 509 |
+ const [hasAttachment, setHasAttachment] = useState('')
|
|
| 510 |
+ const [koglAttached, setKoglAttached] = useState('')
|
|
| 511 |
+ const [koglType, setKoglType] = useState('')
|
|
| 512 |
+ const [aiType, setAiType] = useState(false) |
|
| 509 | 513 |
const [reviewMajor, setReviewMajor] = useState('')
|
| 510 | 514 |
const [reviewMinor, setReviewMinor] = useState('')
|
| 511 | 515 |
const [reviewResult, setReviewResult] = useState('')
|
... | ... | @@ -530,6 +534,10 @@ |
| 530 | 534 |
return |
| 531 | 535 |
} |
| 532 | 536 |
setItem(data) |
| 537 |
+ setHasAttachment(data.hasAttachment ?? '') |
|
| 538 |
+ setKoglAttached(data.koglAttached ?? '') |
|
| 539 |
+ setKoglType(data.koglType ?? '') |
|
| 540 |
+ setAiType(data.aiType === 'Y') |
|
| 533 | 541 |
setReviewMajor(data.reviewMajor ?? '') |
| 534 | 542 |
setReviewMinor(data.reviewMinor ?? '') |
| 535 | 543 |
setReviewResult(data.reviewResult ?? '') |
... | ... | @@ -588,6 +596,10 @@ |
| 588 | 596 |
setSaveError(null) |
| 589 | 597 |
try {
|
| 590 | 598 |
const body: JudgmentRequest = {
|
| 599 |
+ hasAttachment: hasAttachment || null, |
|
| 600 |
+ koglAttached: koglAttached || null, |
|
| 601 |
+ koglType: koglType || null, |
|
| 602 |
+ aiType: aiType ? 'Y' : null, |
|
| 591 | 603 |
// 개방가능여부는 별도 입력칸 없이 처리결과에서 파생한다("신유형 개방"일 때만 개방 가능).
|
| 592 | 604 |
openable: reviewResult === '신유형 개방' ? 'Y' : reviewResult ? 'N' : null, |
| 593 | 605 |
reviewMajor: reviewMajor || null, |
... | ... | @@ -614,8 +626,6 @@ |
| 614 | 626 |
if (loadError || !item) {
|
| 615 | 627 |
return <p className="p-4 py-10 text-center text-sm text-red-600">{loadError ?? '게시물을 찾을 수 없습니다.'}</p>
|
| 616 | 628 |
} |
| 617 |
- |
|
| 618 |
- const koglSummary = `${item.koglAttached ?? '-'} / ${item.koglType ?? '-'}${item.aiType ? ' · AI' : ''}`
|
|
| 619 | 629 |
|
| 620 | 630 |
return ( |
| 621 | 631 |
<div className="p-4"> |
... | ... | @@ -659,10 +669,62 @@ |
| 659 | 669 |
right={<ReadonlyBox value={item.publishedDate} />}
|
| 660 | 670 |
/> |
| 661 | 671 |
<InfoRow label="첨부파일 여부"> |
| 662 |
- <ReadonlyBox value={item.hasAttachment} />
|
|
| 672 |
+ <div className="flex items-center gap-5 text-sm"> |
|
| 673 |
+ {['있음', '없음'].map((option) => (
|
|
| 674 |
+ <label key={option} className="flex cursor-pointer items-center gap-1.5">
|
|
| 675 |
+ <input |
|
| 676 |
+ type="radio" |
|
| 677 |
+ name="has-attachment" |
|
| 678 |
+ aria-label={`첨부파일 ${option}`}
|
|
| 679 |
+ checked={hasAttachment === option}
|
|
| 680 |
+ onChange={() => setHasAttachment(option)}
|
|
| 681 |
+ className="h-3.5 w-3.5 accent-blue-600" |
|
| 682 |
+ /> |
|
| 683 |
+ {option}
|
|
| 684 |
+ </label> |
|
| 685 |
+ ))} |
|
| 686 |
+ </div> |
|
| 663 | 687 |
</InfoRow> |
| 664 | 688 |
<InfoRow label="기존 공공누리"> |
| 665 |
- <ReadonlyBox value={koglSummary} />
|
|
| 689 |
+ <div className="flex flex-wrap items-center gap-5 text-sm"> |
|
| 690 |
+ {['부착', '미부착'].map((option) => (
|
|
| 691 |
+ <label key={option} className="flex cursor-pointer items-center gap-1.5">
|
|
| 692 |
+ <input |
|
| 693 |
+ type="radio" |
|
| 694 |
+ name="kogl-attached" |
|
| 695 |
+ aria-label={`기존 공공누리 ${option}`}
|
|
| 696 |
+ checked={koglAttached === option}
|
|
| 697 |
+ onChange={() => setKoglAttached(option)}
|
|
| 698 |
+ className="h-3.5 w-3.5 accent-blue-600" |
|
| 699 |
+ /> |
|
| 700 |
+ {option}
|
|
| 701 |
+ </label> |
|
| 702 |
+ ))} |
|
| 703 |
+ <label className="flex items-center gap-1.5"> |
|
| 704 |
+ 유형 |
|
| 705 |
+ <select |
|
| 706 |
+ aria-label="기존 공공누리 유형" |
|
| 707 |
+ value={koglType}
|
|
| 708 |
+ onChange={(e) => setKoglType(e.target.value)}
|
|
| 709 |
+ className="rounded border border-gray-300 px-2 py-1 text-sm" |
|
| 710 |
+ > |
|
| 711 |
+ <option value="">선택</option> |
|
| 712 |
+ {['0유형', '1유형', '2유형', '3유형', '4유형'].map((t) => (
|
|
| 713 |
+ <option key={t} value={t}>{t}</option>
|
|
| 714 |
+ ))} |
|
| 715 |
+ </select> |
|
| 716 |
+ </label> |
|
| 717 |
+ <label className="flex cursor-pointer items-center gap-1.5"> |
|
| 718 |
+ <input |
|
| 719 |
+ type="checkbox" |
|
| 720 |
+ aria-label="기존 공공누리 AI유형" |
|
| 721 |
+ checked={aiType}
|
|
| 722 |
+ onChange={(e) => setAiType(e.target.checked)}
|
|
| 723 |
+ className="h-3.5 w-3.5 accent-blue-600" |
|
| 724 |
+ /> |
|
| 725 |
+ AI유형 |
|
| 726 |
+ </label> |
|
| 727 |
+ </div> |
|
| 666 | 728 |
</InfoRow> |
| 667 | 729 |
</div> |
| 668 | 730 |
</section> |
--- frontend/src/components/WorkMemos.test.tsx
+++ frontend/src/components/WorkMemos.test.tsx
... | ... | @@ -158,6 +158,8 @@ |
| 158 | 158 |
|
| 159 | 159 |
render(<WorkMemos org={org()} />)
|
| 160 | 160 |
|
| 161 |
+ fireEvent.change(screen.getByLabelText('담당자'), { target: { value: '송민지 과장' } })
|
|
| 162 |
+ |
|
| 161 | 163 |
await waitFor(() => expect(mocks.getMemos).toHaveBeenCalled()) |
| 162 | 164 |
|
| 163 | 165 |
fireEvent.change(screen.getByLabelText('메모 내용'), { target: { value: '통화 내용' } })
|
--- frontend/src/components/WorkMemos.tsx
+++ frontend/src/components/WorkMemos.tsx
... | ... | @@ -32,7 +32,8 @@ |
| 32 | 32 |
org.lawyer?.name ?? null, |
| 33 | 33 |
] |
| 34 | 34 |
const seen = new Set<string>() |
| 35 |
- const options: string[] = [] |
|
| 35 |
+ // 요구사항: 선택박스 최상단은 항상 '시스템'이다 (자동/시스템성 메모 구분용) |
|
| 36 |
+ const options: string[] = ['시스템'] |
|
| 36 | 37 |
for (const candidate of candidates) {
|
| 37 | 38 |
if (!candidate || candidate.trim() === '' || seen.has(candidate)) {
|
| 38 | 39 |
continue |
--- src/main/java/kr/itn/itnhub/review/JudgmentRequest.java
+++ src/main/java/kr/itn/itnhub/review/JudgmentRequest.java
... | ... | @@ -2,6 +2,10 @@ |
| 2 | 2 |
|
| 3 | 3 |
/** 권리확인 상세의 [수정] 저장 요청. 전부 선택값이며, 저장 시 web_edited_at이 채워진다. */ |
| 4 | 4 |
public record JudgmentRequest( |
| 5 |
+ String hasAttachment, |
|
| 6 |
+ String koglAttached, |
|
| 7 |
+ String koglType, |
|
| 8 |
+ String aiType, |
|
| 5 | 9 |
String openable, |
| 6 | 10 |
String reviewMajor, |
| 7 | 11 |
String reviewMinor, |
--- src/main/java/kr/itn/itnhub/review/ReviewItemMapper.java
+++ src/main/java/kr/itn/itnhub/review/ReviewItemMapper.java
... | ... | @@ -32,6 +32,10 @@ |
| 32 | 32 |
int upsertFromFile(ReviewItem item); |
| 33 | 33 |
|
| 34 | 34 |
int updateJudgment(@Param("orgId") Long orgId, @Param("id") Long id,
|
| 35 |
+ @Param("hasAttachment") String hasAttachment,
|
|
| 36 |
+ @Param("koglAttached") String koglAttached,
|
|
| 37 |
+ @Param("koglType") String koglType,
|
|
| 38 |
+ @Param("aiType") String aiType,
|
|
| 35 | 39 |
@Param("openable") String openable,
|
| 36 | 40 |
@Param("reviewMajor") String reviewMajor,
|
| 37 | 41 |
@Param("reviewMinor") String reviewMinor,
|
--- src/main/java/kr/itn/itnhub/review/ReviewService.java
+++ src/main/java/kr/itn/itnhub/review/ReviewService.java
... | ... | @@ -56,6 +56,7 @@ |
| 56 | 56 |
public ReviewItem updateJudgment(Long orgId, Long itemId, JudgmentRequest request) {
|
| 57 | 57 |
requireOrg(orgId); |
| 58 | 58 |
int updated = mapper.updateJudgment(orgId, itemId, |
| 59 |
+ request.hasAttachment(), request.koglAttached(), request.koglType(), request.aiType(), |
|
| 59 | 60 |
request.openable(), request.reviewMajor(), request.reviewMinor(), request.reviewResult(), |
| 60 | 61 |
request.judgedKoglType(), request.judgedAiType(), request.opinion(), request.lawyerNote(), |
| 61 | 62 |
request.needsProcessing()); |
--- src/main/resources/mapper/ReviewItemMapper.xml
+++ src/main/resources/mapper/ReviewItemMapper.xml
... | ... | @@ -94,10 +94,10 @@ |
| 94 | 94 |
post_registered = excluded.post_registered, |
| 95 | 95 |
produced_date = excluded.produced_date, |
| 96 | 96 |
published_date = excluded.published_date, |
| 97 |
- has_attachment = excluded.has_attachment, |
|
| 98 |
- kogl_attached = excluded.kogl_attached, |
|
| 99 |
- kogl_type = excluded.kogl_type, |
|
| 100 |
- ai_type = excluded.ai_type, |
|
| 97 |
+ has_attachment = case when review_item.web_edited_at is null then excluded.has_attachment else review_item.has_attachment end, |
|
| 98 |
+ kogl_attached = case when review_item.web_edited_at is null then excluded.kogl_attached else review_item.kogl_attached end, |
|
| 99 |
+ kogl_type = case when review_item.web_edited_at is null then excluded.kogl_type else review_item.kogl_type end, |
|
| 100 |
+ ai_type = case when review_item.web_edited_at is null then excluded.ai_type else review_item.ai_type end, |
|
| 101 | 101 |
surveyor_note = excluded.surveyor_note, |
| 102 | 102 |
openable = case when review_item.web_edited_at is null then excluded.openable else review_item.openable end, |
| 103 | 103 |
review_major = case when review_item.web_edited_at is null then excluded.review_major else review_item.review_major end, |
... | ... | @@ -113,6 +113,10 @@ |
| 113 | 113 |
|
| 114 | 114 |
<update id="updateJudgment"> |
| 115 | 115 |
update review_item set |
| 116 |
+ has_attachment = #{hasAttachment},
|
|
| 117 |
+ kogl_attached = #{koglAttached},
|
|
| 118 |
+ kogl_type = #{koglType},
|
|
| 119 |
+ ai_type = #{aiType},
|
|
| 116 | 120 |
openable = #{openable},
|
| 117 | 121 |
review_major = #{reviewMajor},
|
| 118 | 122 |
review_minor = #{reviewMinor},
|
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?