feat: 채널관리/기관관리 화면에 아이티앤 담당자 배정 UI를 추가
문정원 담당자와 담당 변호사 사이에 아이티앤 담당자 배정 행/카드를 추가하고, 업무메모 담당자 선택 옵션에도 반영한다. 기관관리 담당자 카드 그리드를 4열로 조정하고, 관련 테스트 픽스처와 배정 페이로드 검증을 갱신한다.
@1a88e2d2e2986796145c7ca42bf28c245633583b
--- frontend/src/api/client.test.ts
+++ frontend/src/api/client.test.ts
... | ... | @@ -59,6 +59,7 @@ |
| 59 | 59 |
const promise = updateAssignments(1, {
|
| 60 | 60 |
applicantContactId: null, |
| 61 | 61 |
mjContactId: null, |
| 62 |
+ itnContactId: null, |
|
| 62 | 63 |
lawyerContactId: null, |
| 63 | 64 |
lawyerAssignedDate: null, |
| 64 | 65 |
}) |
... | ... | @@ -76,6 +77,7 @@ |
| 76 | 77 |
await updateAssignments(7, {
|
| 77 | 78 |
applicantContactId: 1, |
| 78 | 79 |
mjContactId: null, |
| 80 |
+ itnContactId: null, |
|
| 79 | 81 |
lawyerContactId: null, |
| 80 | 82 |
lawyerAssignedDate: null, |
| 81 | 83 |
}) |
--- frontend/src/api/client.ts
+++ frontend/src/api/client.ts
... | ... | @@ -32,16 +32,18 @@ |
| 32 | 32 |
status: OrgStatus |
| 33 | 33 |
applicant: Contact | null |
| 34 | 34 |
mj: Contact | null |
| 35 |
+ itn: Contact | null |
|
| 35 | 36 |
lawyer: Contact | null |
| 36 | 37 |
lawyerAssignedDate: string | null |
| 37 | 38 |
channelIdMj: string | null |
| 38 | 39 |
channelIdLaw: string | null |
| 39 | 40 |
} |
| 40 | 41 |
|
| 41 |
-/** 채널관리 화면이 신청기관/문정원/변호사 배정을 한 번에 바꿀 때 보내는 요청. null은 해제다. */ |
|
| 42 |
+/** 채널관리 화면이 신청기관/문정원/아이티앤/변호사 배정을 한 번에 바꿀 때 보내는 요청. null은 해제다. */ |
|
| 42 | 43 |
export interface AssignmentInput {
|
| 43 | 44 |
applicantContactId: number | null |
| 44 | 45 |
mjContactId: number | null |
| 46 |
+ itnContactId: number | null |
|
| 45 | 47 |
lawyerContactId: number | null |
| 46 | 48 |
lawyerAssignedDate: string | null |
| 47 | 49 |
} |
--- frontend/src/components/ChannelFiles.test.tsx
+++ frontend/src/components/ChannelFiles.test.tsx
... | ... | @@ -20,6 +20,7 @@ |
| 20 | 20 |
status: 'ACTIVE', |
| 21 | 21 |
applicant: null, |
| 22 | 22 |
mj: null, |
| 23 |
+ itn: null, |
|
| 23 | 24 |
lawyer: null, |
| 24 | 25 |
lawyerAssignedDate: null, |
| 25 | 26 |
channelIdMj: 'chan-mj', |
--- frontend/src/components/ChannelPosts.test.tsx
+++ frontend/src/components/ChannelPosts.test.tsx
... | ... | @@ -26,6 +26,7 @@ |
| 26 | 26 |
status: 'ACTIVE', |
| 27 | 27 |
applicant: null, |
| 28 | 28 |
mj: null, |
| 29 |
+ itn: null, |
|
| 29 | 30 |
lawyer: null, |
| 30 | 31 |
lawyerAssignedDate: null, |
| 31 | 32 |
channelIdMj: 'chan-mj', |
--- frontend/src/components/OrgDetail.test.tsx
+++ frontend/src/components/OrgDetail.test.tsx
... | ... | @@ -38,6 +38,7 @@ |
| 38 | 38 |
status: 'INFO_PENDING', |
| 39 | 39 |
applicant: null, |
| 40 | 40 |
mj: null, |
| 41 |
+ itn: null, |
|
| 41 | 42 |
lawyer: null, |
| 42 | 43 |
lawyerAssignedDate: null, |
| 43 | 44 |
channelIdMj: null, |
... | ... | @@ -68,7 +69,7 @@ |
| 68 | 69 |
it('배정되지 않은 역할은 미지정으로 표시된다', () => {
|
| 69 | 70 |
render(<OrgDetail org={org()} onChanged={() => {}} />)
|
| 70 | 71 |
|
| 71 |
- expect(screen.getAllByText('미지정')).toHaveLength(3)
|
|
| 72 |
+ expect(screen.getAllByText('미지정')).toHaveLength(4)
|
|
| 72 | 73 |
}) |
| 73 | 74 |
|
| 74 | 75 |
it('배정된 담당자는 요약(이름·부서·연락처)으로 표시된다', () => {
|
... | ... | @@ -115,6 +116,34 @@ |
| 115 | 116 |
expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
|
| 116 | 117 |
applicantContactId: 9, |
| 117 | 118 |
mjContactId: null, |
| 119 |
+ itnContactId: null, |
|
| 120 |
+ lawyerContactId: null, |
|
| 121 |
+ lawyerAssignedDate: null, |
|
| 122 |
+ }) |
|
| 123 |
+ expect(onChanged).toHaveBeenCalled() |
|
| 124 |
+ }) |
|
| 125 |
+ }) |
|
| 126 |
+ |
|
| 127 |
+ it('아이티앤 담당자 선택 모달에서 고르면 updateAssignments가 itnContactId로 호출된다', async () => {
|
|
| 128 |
+ mocks.getContacts.mockResolvedValue([contact({ id: 11, category: 'ITN', name: '박아이티' })])
|
|
| 129 |
+ mocks.updateAssignments.mockResolvedValue(org()) |
|
| 130 |
+ const onChanged = vi.fn() |
|
| 131 |
+ |
|
| 132 |
+ render(<OrgDetail org={org()} onChanged={onChanged} />)
|
|
| 133 |
+ |
|
| 134 |
+ fireEvent.click(screen.getByRole('button', { name: '아이티앤 담당자 선택' }))
|
|
| 135 |
+ |
|
| 136 |
+ expect(screen.getByText('아이티앤 담당자 선택')).toBeTruthy()
|
|
| 137 |
+ await waitFor(() => expect(screen.getByText('박아이티')).toBeTruthy())
|
|
| 138 |
+ expect(mocks.getContacts).toHaveBeenCalledWith('ITN')
|
|
| 139 |
+ |
|
| 140 |
+ fireEvent.click(screen.getByRole('button', { name: '선택' }))
|
|
| 141 |
+ |
|
| 142 |
+ await waitFor(() => {
|
|
| 143 |
+ expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
|
|
| 144 |
+ applicantContactId: null, |
|
| 145 |
+ mjContactId: null, |
|
| 146 |
+ itnContactId: 11, |
|
| 118 | 147 |
lawyerContactId: null, |
| 119 | 148 |
lawyerAssignedDate: null, |
| 120 | 149 |
}) |
... | ... | @@ -143,6 +172,7 @@ |
| 143 | 172 |
expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
|
| 144 | 173 |
applicantContactId: null, |
| 145 | 174 |
mjContactId: 6, |
| 175 |
+ itnContactId: null, |
|
| 146 | 176 |
lawyerContactId: null, |
| 147 | 177 |
lawyerAssignedDate: null, |
| 148 | 178 |
}) |
... | ... | @@ -173,6 +203,7 @@ |
| 173 | 203 |
expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
|
| 174 | 204 |
applicantContactId: null, |
| 175 | 205 |
mjContactId: null, |
| 206 |
+ itnContactId: null, |
|
| 176 | 207 |
lawyerContactId: 7, |
| 177 | 208 |
lawyerAssignedDate: '2026-07-21', |
| 178 | 209 |
}) |
--- frontend/src/components/OrgDetail.tsx
+++ frontend/src/components/OrgDetail.tsx
... | ... | @@ -13,10 +13,10 @@ |
| 13 | 13 |
import StatusBadge from './StatusBadge' |
| 14 | 14 |
|
| 15 | 15 |
// 채널관리 상세. 담당자 정보는 여기서 입력하지 않는다(담당자관리에서만 입력) - 이 화면은 |
| 16 |
-// 이미 등록된 담당자를 ContactPickerModal로 골라 신청기관/문정원/변호사 역할에 배정만 한다. |
|
| 16 |
+// 이미 등록된 담당자를 ContactPickerModal로 골라 신청기관/문정원/아이티앤/변호사 역할에 배정만 한다. |
|
| 17 | 17 |
|
| 18 | 18 |
interface RoleConfig {
|
| 19 |
- key: 'applicant' | 'mj' | 'lawyer' |
|
| 19 |
+ key: 'applicant' | 'mj' | 'itn' | 'lawyer' |
|
| 20 | 20 |
category: ContactCategory |
| 21 | 21 |
label: string |
| 22 | 22 |
pickerTitle: string |
... | ... | @@ -39,6 +39,13 @@ |
| 39 | 39 |
field: 'mjContactId', |
| 40 | 40 |
}, |
| 41 | 41 |
{
|
| 42 |
+ key: 'itn', |
|
| 43 |
+ category: 'ITN', |
|
| 44 |
+ label: '아이티앤 담당자', |
|
| 45 |
+ pickerTitle: '아이티앤 담당자 선택', |
|
| 46 |
+ field: 'itnContactId', |
|
| 47 |
+ }, |
|
| 48 |
+ {
|
|
| 42 | 49 |
key: 'lawyer', |
| 43 | 50 |
category: 'LAWYER', |
| 44 | 51 |
label: '담당 변호사', |
... | ... | @@ -53,6 +60,8 @@ |
| 53 | 60 |
return org.applicant |
| 54 | 61 |
case 'mj': |
| 55 | 62 |
return org.mj |
| 63 |
+ case 'itn': |
|
| 64 |
+ return org.itn |
|
| 56 | 65 |
case 'lawyer': |
| 57 | 66 |
return org.lawyer |
| 58 | 67 |
} |
... | ... | @@ -90,12 +99,13 @@ |
| 90 | 99 |
return {
|
| 91 | 100 |
applicantContactId: org.applicant?.id ?? null, |
| 92 | 101 |
mjContactId: org.mj?.id ?? null, |
| 102 |
+ itnContactId: org.itn?.id ?? null, |
|
| 93 | 103 |
lawyerContactId: org.lawyer?.id ?? null, |
| 94 | 104 |
lawyerAssignedDate: org.lawyerAssignedDate ?? null, |
| 95 | 105 |
} |
| 96 | 106 |
} |
| 97 | 107 |
|
| 98 |
- /** 매 호출이 세 배정값 + 배정일을 통째로 다시 보낸다(부분 갱신이 아니다). */ |
|
| 108 |
+ /** 매 호출이 네 배정값 + 배정일을 통째로 다시 보낸다(부분 갱신이 아니다). */ |
|
| 99 | 109 |
async function persistAssignments(overrides: Partial<AssignmentInput>) {
|
| 100 | 110 |
setError(null) |
| 101 | 111 |
setBusy(true) |
--- frontend/src/components/OrgList.test.tsx
+++ frontend/src/components/OrgList.test.tsx
... | ... | @@ -6,12 +6,12 @@ |
| 6 | 6 |
const orgs: Org[] = [ |
| 7 | 7 |
{
|
| 8 | 8 |
id: 1, orgNo: '001_00', orgName: '국제방송교류재단', status: 'ACTIVE', |
| 9 |
- applicant: null, mj: null, lawyer: null, lawyerAssignedDate: null, |
|
| 9 |
+ applicant: null, mj: null, itn: null, lawyer: null, lawyerAssignedDate: null, |
|
| 10 | 10 |
channelIdMj: 'a', channelIdLaw: 'b', |
| 11 | 11 |
}, |
| 12 | 12 |
{
|
| 13 | 13 |
id: 2, orgNo: '008_01', orgName: '경찰청_치안정책연구소', status: 'INFO_PENDING', |
| 14 |
- applicant: null, mj: null, lawyer: null, lawyerAssignedDate: null, |
|
| 14 |
+ applicant: null, mj: null, itn: null, lawyer: null, lawyerAssignedDate: null, |
|
| 15 | 15 |
channelIdMj: null, channelIdLaw: null, |
| 16 | 16 |
}, |
| 17 | 17 |
] |
--- frontend/src/components/OrgOverview.test.tsx
+++ frontend/src/components/OrgOverview.test.tsx
... | ... | @@ -51,6 +51,7 @@ |
| 51 | 51 |
email: 'ming@arirang.com', |
| 52 | 52 |
}), |
| 53 | 53 |
mj: null, |
| 54 |
+ itn: null, |
|
| 54 | 55 |
lawyer: null, |
| 55 | 56 |
lawyerAssignedDate: null, |
| 56 | 57 |
channelIdMj: 'chan-mj', |
... | ... | @@ -79,18 +80,20 @@ |
| 79 | 80 |
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled()) |
| 80 | 81 |
}) |
| 81 | 82 |
|
| 82 |
- it('문정원 담당자와 담당 변호사 정보가 비어 있으면 카드에 -로 표시된다', async () => {
|
|
| 83 |
+ it('문정원 담당자와 아이티앤 담당자와 담당 변호사 정보가 비어 있으면 카드에 -로 표시된다', async () => {
|
|
| 83 | 84 |
render(<OrgOverview org={org()} />)
|
| 84 | 85 |
|
| 85 | 86 |
const mjCard = screen.getByText('문정원 담당자').closest('section') as HTMLElement
|
| 87 |
+ const itnCard = screen.getByText('아이티앤 담당자').closest('section') as HTMLElement
|
|
| 86 | 88 |
const lawyerCard = screen.getByText('담당 변호사').closest('section') as HTMLElement
|
| 87 | 89 |
|
| 88 | 90 |
expect(within(mjCard).getAllByText('-')).toHaveLength(4)
|
| 91 |
+ expect(within(itnCard).getAllByText('-')).toHaveLength(4)
|
|
| 89 | 92 |
expect(within(lawyerCard).getAllByText('-')).toHaveLength(4)
|
| 90 | 93 |
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled()) |
| 91 | 94 |
}) |
| 92 | 95 |
|
| 93 |
- it('담당자 3종이 값을 가지고 있으면 각 카드에 값이 표시된다', async () => {
|
|
| 96 |
+ it('담당자 4종이 값을 가지고 있으면 각 카드에 값이 표시된다', async () => {
|
|
| 94 | 97 |
render( |
| 95 | 98 |
<OrgOverview |
| 96 | 99 |
org={org({
|
... | ... | @@ -101,6 +104,14 @@ |
| 101 | 104 |
deptName: '문화체육관광부 저작권정책과', |
| 102 | 105 |
phone: '02-1234-5678', |
| 103 | 106 |
email: 'mj@mcst.go.kr', |
| 107 |
+ }), |
|
| 108 |
+ itn: contact({
|
|
| 109 |
+ id: 4, |
|
| 110 |
+ category: 'ITN', |
|
| 111 |
+ name: '박아이티', |
|
| 112 |
+ deptName: '개발팀', |
|
| 113 |
+ phone: '02-1111-2222', |
|
| 114 |
+ email: 'itn@iten.co.kr', |
|
| 104 | 115 |
}), |
| 105 | 116 |
lawyer: contact({
|
| 106 | 117 |
id: 3, |
... | ... | @@ -116,6 +127,7 @@ |
| 116 | 127 |
|
| 117 | 128 |
const applicantCard = screen.getByText('신청기관 담당자').closest('section') as HTMLElement
|
| 118 | 129 |
const mjCard = screen.getByText('문정원 담당자').closest('section') as HTMLElement
|
| 130 |
+ const itnCard = screen.getByText('아이티앤 담당자').closest('section') as HTMLElement
|
|
| 119 | 131 |
const lawyerCard = screen.getByText('담당 변호사').closest('section') as HTMLElement
|
| 120 | 132 |
|
| 121 | 133 |
expect(within(applicantCard).getByText('데이터정보화팀')).toBeTruthy()
|
... | ... | @@ -125,6 +137,11 @@ |
| 125 | 137 |
expect(within(mjCard).getByText('김문정')).toBeTruthy()
|
| 126 | 138 |
expect(within(mjCard).getByText('02-1234-5678')).toBeTruthy()
|
| 127 | 139 |
expect(within(mjCard).getByText('mj@mcst.go.kr')).toBeTruthy()
|
| 140 |
+ |
|
| 141 |
+ expect(within(itnCard).getByText('개발팀')).toBeTruthy()
|
|
| 142 |
+ expect(within(itnCard).getByText('박아이티')).toBeTruthy()
|
|
| 143 |
+ expect(within(itnCard).getByText('02-1111-2222')).toBeTruthy()
|
|
| 144 |
+ expect(within(itnCard).getByText('itn@iten.co.kr')).toBeTruthy()
|
|
| 128 | 145 |
|
| 129 | 146 |
expect(within(lawyerCard).getByText('이변호')).toBeTruthy()
|
| 130 | 147 |
expect(within(lawyerCard).getByText('02-9876-5432')).toBeTruthy()
|
... | ... | @@ -139,6 +156,7 @@ |
| 139 | 156 |
|
| 140 | 157 |
expect(screen.getByRole('button', { name: '신청기관 담당자 변경' })).toBeTruthy()
|
| 141 | 158 |
expect(screen.getByRole('button', { name: '문정원 담당자 생성' })).toBeTruthy()
|
| 159 |
+ expect(screen.getByRole('button', { name: '아이티앤 담당자 생성' })).toBeTruthy()
|
|
| 142 | 160 |
expect(screen.getByRole('button', { name: '담당 변호사 생성' })).toBeTruthy()
|
| 143 | 161 |
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled()) |
| 144 | 162 |
}) |
... | ... | @@ -160,6 +178,32 @@ |
| 160 | 178 |
expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
|
| 161 | 179 |
applicantContactId: 1, |
| 162 | 180 |
mjContactId: 7, |
| 181 |
+ itnContactId: null, |
|
| 182 |
+ lawyerContactId: null, |
|
| 183 |
+ lawyerAssignedDate: null, |
|
| 184 |
+ }), |
|
| 185 |
+ ) |
|
| 186 |
+ expect(onChanged).toHaveBeenCalled() |
|
| 187 |
+ }) |
|
| 188 |
+ |
|
| 189 |
+ it('아이티앤 담당자 생성 버튼을 누르면 선택 모달이 열리고 담당자를 고르면 itnContactId로 배정 API를 부른다', async () => {
|
|
| 190 |
+ mocks.getContacts.mockResolvedValue([ |
|
| 191 |
+ contact({ id: 8, category: 'ITN', name: '박아이티', deptName: '개발팀' }),
|
|
| 192 |
+ ]) |
|
| 193 |
+ const onChanged = vi.fn() |
|
| 194 |
+ |
|
| 195 |
+ render(<OrgOverview org={org()} onChanged={onChanged} />)
|
|
| 196 |
+ |
|
| 197 |
+ fireEvent.click(screen.getByRole('button', { name: '아이티앤 담당자 생성' }))
|
|
| 198 |
+ |
|
| 199 |
+ await waitFor(() => expect(screen.getByText('아이티앤 담당자 선택')).toBeTruthy())
|
|
| 200 |
+ fireEvent.click(await screen.findByRole('button', { name: '선택' }))
|
|
| 201 |
+ |
|
| 202 |
+ await waitFor(() => |
|
| 203 |
+ expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
|
|
| 204 |
+ applicantContactId: 1, |
|
| 205 |
+ mjContactId: null, |
|
| 206 |
+ itnContactId: 8, |
|
| 163 | 207 |
lawyerContactId: null, |
| 164 | 208 |
lawyerAssignedDate: null, |
| 165 | 209 |
}), |
--- frontend/src/components/OrgOverview.tsx
+++ frontend/src/components/OrgOverview.tsx
... | ... | @@ -38,11 +38,12 @@ |
| 38 | 38 |
|
| 39 | 39 |
// 카드에서 담당자를 지정/변경할 때 쓰는 역할 정의 (채널관리의 배정과 같은 API를 쓴다) |
| 40 | 40 |
const CARD_ROLES: Record< |
| 41 |
- 'applicant' | 'mj' | 'lawyer', |
|
| 41 |
+ 'applicant' | 'mj' | 'itn' | 'lawyer', |
|
| 42 | 42 |
{ category: ContactCategory; pickerTitle: string }
|
| 43 | 43 |
> = {
|
| 44 | 44 |
applicant: { category: 'APPLICANT', pickerTitle: '신청기관 담당자 선택' },
|
| 45 | 45 |
mj: { category: 'MJ', pickerTitle: '문정원 담당자 선택' },
|
| 46 |
+ itn: { category: 'ITN', pickerTitle: '아이티앤 담당자 선택' },
|
|
| 46 | 47 |
lawyer: { category: 'LAWYER', pickerTitle: '담당 변호사 선택' },
|
| 47 | 48 |
} |
| 48 | 49 |
|
... | ... | @@ -111,11 +112,11 @@ |
| 111 | 112 |
onChanged?: () => void |
| 112 | 113 |
}) {
|
| 113 | 114 |
const [activeTab, setActiveTab] = useState(TABS[0]) |
| 114 |
- const [pickerRole, setPickerRole] = useState<'applicant' | 'mj' | 'lawyer' | null>(null) |
|
| 115 |
+ const [pickerRole, setPickerRole] = useState<'applicant' | 'mj' | 'itn' | 'lawyer' | null>(null) |
|
| 115 | 116 |
const [assignBusy, setAssignBusy] = useState(false) |
| 116 | 117 |
const [assignError, setAssignError] = useState<string | null>(null) |
| 117 | 118 |
|
| 118 |
- async function assign(role: 'applicant' | 'mj' | 'lawyer', contact: Contact) {
|
|
| 119 |
+ async function assign(role: 'applicant' | 'mj' | 'itn' | 'lawyer', contact: Contact) {
|
|
| 119 | 120 |
setPickerRole(null) |
| 120 | 121 |
setAssignError(null) |
| 121 | 122 |
setAssignBusy(true) |
... | ... | @@ -123,6 +124,7 @@ |
| 123 | 124 |
await updateAssignments(org.id, {
|
| 124 | 125 |
applicantContactId: role === 'applicant' ? contact.id : (org.applicant?.id ?? null), |
| 125 | 126 |
mjContactId: role === 'mj' ? contact.id : (org.mj?.id ?? null), |
| 127 |
+ itnContactId: role === 'itn' ? contact.id : (org.itn?.id ?? null), |
|
| 126 | 128 |
lawyerContactId: role === 'lawyer' ? contact.id : (org.lawyer?.id ?? null), |
| 127 | 129 |
lawyerAssignedDate: org.lawyerAssignedDate ?? null, |
| 128 | 130 |
}) |
... | ... | @@ -180,9 +182,9 @@ |
| 180 | 182 |
</div> |
| 181 | 183 |
</div> |
| 182 | 184 |
|
| 183 |
- {/* 담당자 카드 3개 (신청기관/문정원/변호사). 담당자 정보 입력은 담당자관리에서만 하고,
|
|
| 185 |
+ {/* 담당자 카드 4개 (신청기관/문정원/아이티앤/변호사). 담당자 정보 입력은 담당자관리에서만 하고,
|
|
| 184 | 186 |
여기서는 [생성/변경]으로 이미 등록된 담당자를 골라 배정만 한다(채널관리와 같은 API). */} |
| 185 |
- <div className="mt-5 grid grid-cols-1 gap-3 lg:grid-cols-3"> |
|
| 187 |
+ <div className="mt-5 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4"> |
|
| 186 | 188 |
<ContactCard |
| 187 | 189 |
title="신청기관 담당자" |
| 188 | 190 |
assigned={org.applicant !== null}
|
... | ... | @@ -215,6 +217,18 @@ |
| 215 | 217 |
]} |
| 216 | 218 |
/> |
| 217 | 219 |
<ContactCard |
| 220 |
+ title="아이티앤 담당자" |
|
| 221 |
+ assigned={org.itn !== null}
|
|
| 222 |
+ busy={assignBusy}
|
|
| 223 |
+ onEdit={() => setPickerRole('itn')}
|
|
| 224 |
+ fields={[
|
|
| 225 |
+ { label: '부서', value: org.itn?.deptName ?? null },
|
|
| 226 |
+ { label: '담당자', value: org.itn?.name ?? null },
|
|
| 227 |
+ { label: '연락처', value: org.itn?.phone ?? null },
|
|
| 228 |
+ { label: '이메일', value: org.itn?.email ?? null },
|
|
| 229 |
+ ]} |
|
| 230 |
+ /> |
|
| 231 |
+ <ContactCard |
|
| 218 | 232 |
title="담당 변호사" |
| 219 | 233 |
assigned={org.lawyer !== null}
|
| 220 | 234 |
busy={assignBusy}
|
--- frontend/src/components/WorkMemos.test.tsx
+++ frontend/src/components/WorkMemos.test.tsx
... | ... | @@ -47,6 +47,7 @@ |
| 47 | 47 |
email: 'ming@arirang.com', |
| 48 | 48 |
}), |
| 49 | 49 |
mj: null, |
| 50 |
+ itn: null, |
|
| 50 | 51 |
lawyer: null, |
| 51 | 52 |
lawyerAssignedDate: null, |
| 52 | 53 |
channelIdMj: 'chan-mj', |
... | ... | @@ -113,13 +114,14 @@ |
| 113 | 114 |
expect(screen.getByLabelText('담당자 이름')).toBeTruthy()
|
| 114 | 115 |
}) |
| 115 | 116 |
|
| 116 |
- it('신청기관 담당자, 문정원 담당자, 담당 변호사가 모두 있으면 셋 다 선택 옵션으로 나온다', async () => {
|
|
| 117 |
+ it('신청기관 담당자, 문정원 담당자, 아이티앤 담당자, 담당 변호사가 모두 있으면 넷 다 선택 옵션으로 나온다', async () => {
|
|
| 117 | 118 |
mocks.getMemos.mockResolvedValue([]) |
| 118 | 119 |
|
| 119 | 120 |
render( |
| 120 | 121 |
<WorkMemos |
| 121 | 122 |
org={org({
|
| 122 | 123 |
mj: contact({ id: 2, category: 'MJ', name: '김문정' }),
|
| 124 |
+ itn: contact({ id: 4, category: 'ITN', name: '박아이티' }),
|
|
| 123 | 125 |
lawyer: contact({ id: 3, category: 'LAWYER', name: '이변호' }),
|
| 124 | 126 |
})} |
| 125 | 127 |
/>, |
... | ... | @@ -129,6 +131,7 @@ |
| 129 | 131 |
|
| 130 | 132 |
expect(screen.getByRole('option', { name: '송민지 과장' })).toBeTruthy()
|
| 131 | 133 |
expect(screen.getByRole('option', { name: '김문정' })).toBeTruthy()
|
| 134 |
+ expect(screen.getByRole('option', { name: '박아이티' })).toBeTruthy()
|
|
| 132 | 135 |
expect(screen.getByRole('option', { name: '이변호' })).toBeTruthy()
|
| 133 | 136 |
expect(screen.getByRole('option', { name: '직접 입력' })).toBeTruthy()
|
| 134 | 137 |
}) |
--- frontend/src/components/WorkMemos.tsx
+++ frontend/src/components/WorkMemos.tsx
... | ... | @@ -22,10 +22,15 @@ |
| 22 | 22 |
return org.applicant.title ? `${org.applicant.name} ${org.applicant.title}` : org.applicant.name
|
| 23 | 23 |
} |
| 24 | 24 |
|
| 25 |
-/** 신청기관 담당자, 문정원 담당자, 담당 변호사 중 배정되어 있는 사람만 후보로 낸다. |
|
| 25 |
+/** 신청기관 담당자, 문정원 담당자, 아이티앤 담당자, 담당 변호사 중 배정되어 있는 사람만 후보로 낸다. |
|
| 26 | 26 |
* 이름이 같은 사람이 중복 등록돼 있어도 셀렉트 옵션은 한 번만 보이도록 중복을 없앤다. */ |
| 27 | 27 |
function contactOptions(org: Org): string[] {
|
| 28 |
- const candidates = [applicantOptionLabel(org), org.mj?.name ?? null, org.lawyer?.name ?? null] |
|
| 28 |
+ const candidates = [ |
|
| 29 |
+ applicantOptionLabel(org), |
|
| 30 |
+ org.mj?.name ?? null, |
|
| 31 |
+ org.itn?.name ?? null, |
|
| 32 |
+ org.lawyer?.name ?? null, |
|
| 33 |
+ ] |
|
| 29 | 34 |
const seen = new Set<string>() |
| 30 | 35 |
const options: string[] = [] |
| 31 | 36 |
for (const candidate of candidates) {
|
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?