ITN Dev 07-23
feat: 채널관리에 문정원·변호사 연락처 입력, 기관관리에 담당자 3종 표 추가
백엔드에 추가된 문정원 담당자·담당 변호사 필드를 화면에 반영한다.

- 사이드바 "채널생성"을 "채널관리"로 이름을 바꾸고 기관관리 위로 이동
- 채널관리(OrgDetail) 담당자 입력을 신청기관/문정원/변호사 3열 그리드로 확장,
  저장 버튼 하나로 세 그룹을 함께 저장 (필수값 규칙은 신청기관 담당자만 그대로 유지)
- 기관관리(OrgOverview) 카드 3개(신청기관 카드 + 추후연동 2개)를 담당자 3종
  가로 표 하나로 교체
- 업무메모 담당자 선택에 신청기관/문정원/변호사 중 이름이 채워진 사람을
  모두 옵션으로 제공 (중복 제거, 기존 직접입력 유지)
- 관련 컴포넌트 테스트와 org() 픽스처에 신규 필드 반영, 신규 동작 테스트 추가
@08e8209ac213d6d7f775fa37f56b68fbca92bf79
frontend/src/App.tsx
--- frontend/src/App.tsx
+++ frontend/src/App.tsx
@@ -8,12 +8,12 @@
 import Sidebar from './components/Sidebar'
 import { useCollapsiblePanel } from './hooks/useCollapsiblePanel'
 
-// 화면 2개: 기관관리(시안 레이아웃의 기관 상세)와 채널생성(명부 시드 + 담당자 입력 + 채널 생성).
+// 화면 2개: 기관관리(시안 레이아웃의 기관 상세)와 채널관리(명부 시드 + 담당자 입력 + 채널 생성).
 type Page = 'orgs' | 'channels'
 
 const PAGE_TITLE: Record<Page, string> = {
   orgs: '기관관리',
-  channels: '채널생성',
+  channels: '채널관리',
 }
 
 // 'checking'이 필요하다. true로 시작하면 첫 렌더에서 getOrgs()가 아직 안 끝났는데도
frontend/src/api/client.test.ts
--- frontend/src/api/client.test.ts
+++ frontend/src/api/client.test.ts
@@ -58,6 +58,8 @@
 
     const promise = updateContact(1, {
       deptName: '', managerName: '', managerTitle: '', managerPhone: '', managerEmail: '',
+      mjDeptName: '', mjManagerName: '', mjManagerPhone: '', mjManagerEmail: '',
+      lawyerName: '', lawyerPhone: '', lawyerEmail: '', lawyerAssignedDate: '',
     })
 
     await expect(promise).rejects.toThrow()
frontend/src/api/client.ts
--- frontend/src/api/client.ts
+++ frontend/src/api/client.ts
@@ -10,6 +10,14 @@
   managerTitle: string | null
   managerPhone: string | null
   managerEmail: string | null
+  mjDeptName: string | null
+  mjManagerName: string | null
+  mjManagerPhone: string | null
+  mjManagerEmail: string | null
+  lawyerName: string | null
+  lawyerPhone: string | null
+  lawyerEmail: string | null
+  lawyerAssignedDate: string | null
   channelIdMj: string | null
   channelIdLaw: string | null
 }
@@ -20,6 +28,14 @@
   managerTitle: string
   managerPhone: string
   managerEmail: string
+  mjDeptName: string
+  mjManagerName: string
+  mjManagerPhone: string
+  mjManagerEmail: string
+  lawyerName: string
+  lawyerPhone: string
+  lawyerEmail: string
+  lawyerAssignedDate: string
 }
 
 export type ProvisionOutcome = 'CACHED' | 'RECOVERED' | 'CREATED' | 'FAILED'
frontend/src/components/ChannelFiles.test.tsx
--- frontend/src/components/ChannelFiles.test.tsx
+++ frontend/src/components/ChannelFiles.test.tsx
@@ -23,6 +23,14 @@
     managerTitle: null,
     managerPhone: null,
     managerEmail: null,
+    mjDeptName: null,
+    mjManagerName: null,
+    mjManagerPhone: null,
+    mjManagerEmail: null,
+    lawyerName: null,
+    lawyerPhone: null,
+    lawyerEmail: null,
+    lawyerAssignedDate: null,
     channelIdMj: 'chan-mj',
     channelIdLaw: 'chan-law',
     ...overrides,
frontend/src/components/ChannelPosts.test.tsx
--- frontend/src/components/ChannelPosts.test.tsx
+++ frontend/src/components/ChannelPosts.test.tsx
@@ -29,6 +29,14 @@
     managerTitle: null,
     managerPhone: null,
     managerEmail: null,
+    mjDeptName: null,
+    mjManagerName: null,
+    mjManagerPhone: null,
+    mjManagerEmail: null,
+    lawyerName: null,
+    lawyerPhone: null,
+    lawyerEmail: null,
+    lawyerAssignedDate: null,
     channelIdMj: 'chan-mj',
     channelIdLaw: 'chan-law',
     ...overrides,
frontend/src/components/OrgDetail.test.tsx
--- frontend/src/components/OrgDetail.test.tsx
+++ frontend/src/components/OrgDetail.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import OrgDetail from './OrgDetail'
 import type { Org } from '../api/client'
@@ -25,10 +25,26 @@
     managerTitle: null,
     managerPhone: null,
     managerEmail: null,
+    mjDeptName: null,
+    mjManagerName: null,
+    mjManagerPhone: null,
+    mjManagerEmail: null,
+    lawyerName: null,
+    lawyerPhone: null,
+    lawyerEmail: null,
+    lawyerAssignedDate: null,
     channelIdMj: null,
     channelIdLaw: null,
     ...overrides,
   }
+}
+
+/** 세 담당자 그룹은 부서명/연락처/이메일 같은 라벨을 공유하므로, 그룹 제목으로 찾은
+ * 컨테이너 안에서만 찾도록 스코프를 좁힌다 - 그렇지 않으면 getByLabelText가
+ * "여러 개 일치"로 실패한다. */
+function group(title: string) {
+  const heading = screen.getByText(title)
+  return within(heading.closest('div') as HTMLElement)
 }
 
 beforeEach(() => {
@@ -55,11 +71,12 @@
 
     render(<OrgDetail org={org()} onChanged={onChanged} />)
 
-    fireEvent.change(screen.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
-    fireEvent.change(screen.getByLabelText('담당자명'), { target: { value: '송민지' } })
-    fireEvent.change(screen.getByLabelText('직급/직함'), { target: { value: '과장' } })
-    fireEvent.change(screen.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
-    fireEvent.change(screen.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
+    const applicant = group('신청기관 담당자')
+    fireEvent.change(applicant.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
+    fireEvent.change(applicant.getByLabelText('담당자명'), { target: { value: '송민지' } })
+    fireEvent.change(applicant.getByLabelText('직급/직함'), { target: { value: '과장' } })
+    fireEvent.change(applicant.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
+    fireEvent.change(applicant.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
     fireEvent.click(screen.getByRole('button', { name: '정보 저장' }))
 
     await waitFor(() => {
@@ -69,6 +86,60 @@
         managerTitle: '과장',
         managerPhone: '02-3475-5434',
         managerEmail: 'ming@arirang.com',
+        mjDeptName: '',
+        mjManagerName: '',
+        mjManagerPhone: '',
+        mjManagerEmail: '',
+        lawyerName: '',
+        lawyerPhone: '',
+        lawyerEmail: '',
+        lawyerAssignedDate: '',
+      })
+      expect(onChanged).toHaveBeenCalled()
+    })
+  })
+
+  it('문정원 담당자와 담당 변호사 정보를 입력하면 한 번의 저장으로 함께 전송된다', async () => {
+    mocks.updateContact.mockResolvedValue(org({ status: 'READY' }))
+    const onChanged = vi.fn()
+
+    render(<OrgDetail org={org()} onChanged={onChanged} />)
+
+    const applicant = group('신청기관 담당자')
+    fireEvent.change(applicant.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
+    fireEvent.change(applicant.getByLabelText('담당자명'), { target: { value: '송민지' } })
+    fireEvent.change(applicant.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
+    fireEvent.change(applicant.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
+
+    const mj = group('문정원 담당자')
+    fireEvent.change(mj.getByLabelText('부서명'), { target: { value: '문화체육관광부 저작권정책과' } })
+    fireEvent.change(mj.getByLabelText('담당자명'), { target: { value: '김문정' } })
+    fireEvent.change(mj.getByLabelText('연락처'), { target: { value: '02-1234-5678' } })
+    fireEvent.change(mj.getByLabelText('이메일'), { target: { value: 'mj@mcst.go.kr' } })
+
+    const lawyer = group('담당 변호사')
+    fireEvent.change(lawyer.getByLabelText('성명'), { target: { value: '이변호' } })
+    fireEvent.change(lawyer.getByLabelText('연락처'), { target: { value: '02-9876-5432' } })
+    fireEvent.change(lawyer.getByLabelText('이메일'), { target: { value: 'lawyer@lawfirm.kr' } })
+    fireEvent.change(lawyer.getByLabelText('배정일'), { target: { value: '2026-07-21' } })
+
+    fireEvent.click(screen.getByRole('button', { name: '정보 저장' }))
+
+    await waitFor(() => {
+      expect(mocks.updateContact).toHaveBeenCalledWith(1, {
+        deptName: '데이터정보화팀',
+        managerName: '송민지',
+        managerTitle: '',
+        managerPhone: '02-3475-5434',
+        managerEmail: 'ming@arirang.com',
+        mjDeptName: '문화체육관광부 저작권정책과',
+        mjManagerName: '김문정',
+        mjManagerPhone: '02-1234-5678',
+        mjManagerEmail: 'mj@mcst.go.kr',
+        lawyerName: '이변호',
+        lawyerPhone: '02-9876-5432',
+        lawyerEmail: 'lawyer@lawfirm.kr',
+        lawyerAssignedDate: '2026-07-21',
       })
       expect(onChanged).toHaveBeenCalled()
     })
@@ -183,14 +254,27 @@
   it('직급/직함이 비어있어도 나머지 필수 항목만 채우면 정보 저장 버튼이 활성화된다', () => {
     render(<OrgDetail org={org()} onChanged={() => {}} />)
 
-    fireEvent.change(screen.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
-    fireEvent.change(screen.getByLabelText('담당자명'), { target: { value: '송민지' } })
-    fireEvent.change(screen.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
-    fireEvent.change(screen.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
+    const applicant = group('신청기관 담당자')
+    fireEvent.change(applicant.getByLabelText('부서명'), { target: { value: '데이터정보화팀' } })
+    fireEvent.change(applicant.getByLabelText('담당자명'), { target: { value: '송민지' } })
+    fireEvent.change(applicant.getByLabelText('연락처'), { target: { value: '02-3475-5434' } })
+    fireEvent.change(applicant.getByLabelText('이메일'), { target: { value: 'ming@arirang.com' } })
 
     expect(screen.getByRole('button', { name: '정보 저장' })).toBeEnabled()
   })
 
+  it('문정원 담당자와 담당 변호사 항목을 채워도 신청기관 필수 항목이 비어있으면 저장 버튼은 비활성이다', () => {
+    render(<OrgDetail org={org()} onChanged={() => {}} />)
+
+    const mj = group('문정원 담당자')
+    fireEvent.change(mj.getByLabelText('담당자명'), { target: { value: '김문정' } })
+
+    const lawyer = group('담당 변호사')
+    fireEvent.change(lawyer.getByLabelText('성명'), { target: { value: '이변호' } })
+
+    expect(screen.getByRole('button', { name: '정보 저장' })).toBeDisabled()
+  })
+
   it('reload로 org 객체가 새로 만들어져도 같은 기관이면 결과가 지워지지 않는다', async () => {
     mocks.provisionChannels.mockResolvedValue({
       mj: 'CREATED',
frontend/src/components/OrgDetail.tsx
--- frontend/src/components/OrgDetail.tsx
+++ frontend/src/components/OrgDetail.tsx
@@ -9,12 +9,26 @@
 import ConfirmDialog from './ConfirmDialog'
 import StatusBadge from './StatusBadge'
 
-const FIELDS: { key: keyof Contact; label: string }[] = [
+const APPLICANT_FIELDS: { key: keyof Contact; label: string }[] = [
   { key: 'deptName', label: '부서명' },
   { key: 'managerName', label: '담당자명' },
   { key: 'managerTitle', label: '직급/직함' },
   { key: 'managerPhone', label: '연락처' },
   { key: 'managerEmail', label: '이메일' },
+]
+
+const MJ_FIELDS: { key: keyof Contact; label: string; type?: string }[] = [
+  { key: 'mjDeptName', label: '부서명' },
+  { key: 'mjManagerName', label: '담당자명' },
+  { key: 'mjManagerPhone', label: '연락처' },
+  { key: 'mjManagerEmail', label: '이메일' },
+]
+
+const LAWYER_FIELDS: { key: keyof Contact; label: string; type?: string }[] = [
+  { key: 'lawyerName', label: '성명' },
+  { key: 'lawyerPhone', label: '연락처' },
+  { key: 'lawyerEmail', label: '이메일' },
+  { key: 'lawyerAssignedDate', label: '배정일', type: 'date' },
 ]
 
 function emptyContact(org: Org): Contact {
@@ -24,6 +38,14 @@
     managerTitle: org.managerTitle ?? '',
     managerPhone: org.managerPhone ?? '',
     managerEmail: org.managerEmail ?? '',
+    mjDeptName: org.mjDeptName ?? '',
+    mjManagerName: org.mjManagerName ?? '',
+    mjManagerPhone: org.mjManagerPhone ?? '',
+    mjManagerEmail: org.mjManagerEmail ?? '',
+    lawyerName: org.lawyerName ?? '',
+    lawyerPhone: org.lawyerPhone ?? '',
+    lawyerEmail: org.lawyerEmail ?? '',
+    lawyerAssignedDate: org.lawyerAssignedDate ?? '',
   }
 }
 
@@ -33,6 +55,40 @@
   'managerPhone',
   'managerEmail',
 ]
+
+function ContactFieldGroup({
+  title,
+  fields,
+  contact,
+  onChange,
+}: {
+  title: string
+  fields: { key: keyof Contact; label: string; type?: string }[]
+  contact: Contact
+  onChange: (key: keyof Contact, value: string) => void
+}) {
+  return (
+    <div className="rounded-lg border border-gray-200 p-4">
+      <h3 className="text-sm font-medium text-gray-700">{title}</h3>
+      <div className="mt-3 space-y-3">
+        {fields.map((field) => (
+          <div key={field.key} className="flex items-center gap-3">
+            <label htmlFor={field.key} className="w-20 shrink-0 text-sm text-gray-500">
+              {field.label}
+            </label>
+            <input
+              id={field.key}
+              type={field.type ?? 'text'}
+              className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm"
+              value={contact[field.key]}
+              onChange={(e) => onChange(field.key, e.target.value)}
+            />
+          </div>
+        ))}
+      </div>
+    </div>
+  )
+}
 
 export default function OrgDetail({ org, onChanged }: { org: Org; onChanged: () => void }) {
   const [contact, setContact] = useState<Contact>(emptyContact(org))
@@ -56,6 +112,10 @@
   const displayLaw = `${org.orgNo}_${org.orgName} (법률검토)`
   const canProvision = org.status !== 'INFO_PENDING'
   const requiredFilled = REQUIRED_FIELDS.every((key) => contact[key].trim() !== '')
+
+  function setField(key: keyof Contact, value: string) {
+    setContact((prev) => ({ ...prev, [key]: value }))
+  }
 
   async function save() {
     setError(null)
@@ -94,45 +154,46 @@
         <StatusBadge status={org.status} />
       </div>
 
-      <section className="mt-6 max-w-xl rounded-lg border border-gray-200 p-5">
-        <h2 className="text-sm font-medium">신청기관 담당자</h2>
-
-        <div className="mt-4 space-y-3">
-          {FIELDS.map((field) => (
-            <div key={field.key} className="flex items-center gap-3">
-              <label htmlFor={field.key} className="w-20 shrink-0 text-sm text-gray-500">
-                {field.label}
-              </label>
-              <input
-                id={field.key}
-                className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm"
-                value={contact[field.key]}
-                onChange={(e) => setContact({ ...contact, [field.key]: e.target.value })}
-              />
-            </div>
-          ))}
-        </div>
-
-        <div className="mt-5 flex gap-2">
-          <button
-            type="button"
-            disabled={busy || !requiredFilled}
-            onClick={() => void save()}
-            className="rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50"
-          >
-            정보 저장
-          </button>
-          <button
-            type="button"
-            disabled={busy || !canProvision}
-            onClick={() => setConfirming(true)}
-            className="rounded-md bg-blue-600 px-3 py-1.5 text-sm text-white disabled:bg-gray-300"
-          >
-            채널 생성
-          </button>
-        </div>
+      <section className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3">
+        <ContactFieldGroup
+          title="신청기관 담당자"
+          fields={APPLICANT_FIELDS}
+          contact={contact}
+          onChange={setField}
+        />
+        <ContactFieldGroup
+          title="문정원 담당자"
+          fields={MJ_FIELDS}
+          contact={contact}
+          onChange={setField}
+        />
+        <ContactFieldGroup
+          title="담당 변호사"
+          fields={LAWYER_FIELDS}
+          contact={contact}
+          onChange={setField}
+        />
       </section>
 
+      <div className="mt-5 flex gap-2">
+        <button
+          type="button"
+          disabled={busy || !requiredFilled}
+          onClick={() => void save()}
+          className="rounded-md border border-gray-300 px-3 py-1.5 text-sm disabled:opacity-50"
+        >
+          정보 저장
+        </button>
+        <button
+          type="button"
+          disabled={busy || !canProvision}
+          onClick={() => setConfirming(true)}
+          className="rounded-md bg-blue-600 px-3 py-1.5 text-sm text-white disabled:bg-gray-300"
+        >
+          채널 생성
+        </button>
+      </div>
+
       {error && <p className="mt-4 text-sm text-red-600">{error}</p>}
 
       {result && (
frontend/src/components/OrgList.test.tsx
--- frontend/src/components/OrgList.test.tsx
+++ frontend/src/components/OrgList.test.tsx
@@ -8,12 +8,16 @@
     id: 1, orgNo: '001', orgName: '국제방송교류재단', status: 'ACTIVE',
     deptName: null, managerName: null, managerTitle: null,
     managerPhone: null, managerEmail: null,
+    mjDeptName: null, mjManagerName: null, mjManagerPhone: null, mjManagerEmail: null,
+    lawyerName: null, lawyerPhone: null, lawyerEmail: null, lawyerAssignedDate: null,
     channelIdMj: 'a', channelIdLaw: 'b',
   },
   {
     id: 2, orgNo: '008', orgName: '경찰청_치안정책연구소', status: 'INFO_PENDING',
     deptName: null, managerName: null, managerTitle: null,
     managerPhone: null, managerEmail: null,
+    mjDeptName: null, mjManagerName: null, mjManagerPhone: null, mjManagerEmail: null,
+    lawyerName: null, lawyerPhone: null, lawyerEmail: null, lawyerAssignedDate: null,
     channelIdMj: null, channelIdLaw: null,
   },
 ]
frontend/src/components/OrgOverview.test.tsx
--- frontend/src/components/OrgOverview.test.tsx
+++ frontend/src/components/OrgOverview.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import OrgOverview from './OrgOverview'
 import type { Org } from '../api/client'
@@ -27,6 +27,14 @@
     managerTitle: '과장',
     managerPhone: '02-3475-5434',
     managerEmail: 'ming@arirang.com',
+    mjDeptName: null,
+    mjManagerName: null,
+    mjManagerPhone: null,
+    mjManagerEmail: null,
+    lawyerName: null,
+    lawyerPhone: null,
+    lawyerEmail: null,
+    lawyerAssignedDate: null,
     channelIdMj: 'chan-mj',
     channelIdLaw: 'chan-law',
     ...overrides,
@@ -51,12 +59,54 @@
     await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
   })
 
-  it('아직 연동 전인 문정원 담당자와 담당 변호사 카드는 자리만 보여준다', async () => {
+  it('문정원 담당자와 담당 변호사 정보가 비어 있으면 표에 -로 표시된다', async () => {
     render(<OrgOverview org={org()} />)
 
-    expect(screen.getByText('문정원 담당자')).toBeTruthy()
-    expect(screen.getByText('담당 변호사')).toBeTruthy()
-    expect(screen.getAllByText('추후 연동')).toHaveLength(2)
+    const rows = screen.getAllByRole('row')
+    const mjRow = rows.find((row) => row.textContent?.includes('문정원 담당자'))
+    const lawyerRow = rows.find((row) => row.textContent?.includes('담당 변호사'))
+
+    expect(mjRow).toBeTruthy()
+    expect(lawyerRow).toBeTruthy()
+    expect(within(mjRow as HTMLElement).getAllByText('-')).toHaveLength(5)
+    expect(within(lawyerRow as HTMLElement).getAllByText('-')).toHaveLength(5)
+    await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
+  })
+
+  it('담당자 3종이 값을 가지고 있으면 표의 각 행에 값이 표시된다', async () => {
+    render(
+      <OrgOverview
+        org={org({
+          mjDeptName: '문화체육관광부 저작권정책과',
+          mjManagerName: '김문정',
+          mjManagerPhone: '02-1234-5678',
+          mjManagerEmail: 'mj@mcst.go.kr',
+          lawyerName: '이변호',
+          lawyerPhone: '02-9876-5432',
+          lawyerEmail: 'lawyer@lawfirm.kr',
+          lawyerAssignedDate: '2026-07-21',
+        })}
+      />,
+    )
+
+    const rows = screen.getAllByRole('row')
+    const applicantRow = rows.find((row) => row.textContent?.includes('신청기관 담당자'))
+    const mjRow = rows.find((row) => row.textContent?.includes('문정원 담당자'))
+    const lawyerRow = rows.find((row) => row.textContent?.includes('담당 변호사'))
+
+    expect(within(applicantRow as HTMLElement).getByText('데이터정보화팀')).toBeTruthy()
+    expect(within(applicantRow as HTMLElement).getByText('송민지 과장')).toBeTruthy()
+
+    expect(within(mjRow as HTMLElement).getByText('문화체육관광부 저작권정책과')).toBeTruthy()
+    expect(within(mjRow as HTMLElement).getByText('김문정')).toBeTruthy()
+    expect(within(mjRow as HTMLElement).getByText('02-1234-5678')).toBeTruthy()
+    expect(within(mjRow as HTMLElement).getByText('mj@mcst.go.kr')).toBeTruthy()
+
+    expect(within(lawyerRow as HTMLElement).getByText('이변호')).toBeTruthy()
+    expect(within(lawyerRow as HTMLElement).getByText('02-9876-5432')).toBeTruthy()
+    expect(within(lawyerRow as HTMLElement).getByText('lawyer@lawfirm.kr')).toBeTruthy()
+    expect(within(lawyerRow as HTMLElement).getByText('2026-07-21')).toBeTruthy()
+
     await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
   })
 
frontend/src/components/OrgOverview.tsx
--- frontend/src/components/OrgOverview.tsx
+++ frontend/src/components/OrgOverview.tsx
@@ -6,9 +6,9 @@
 import WorkMemos from './WorkMemos'
 
 // 기관관리 상세 화면. 3단계 대시보드 시안의 "기관관리" 레이아웃을 따른다.
-// 웹 DB에 있는 것(연번·기관명·신청기관 담당자·채널)만 실데이터고,
-// 문정원 담당자/담당 변호사/업무단계/통계/탭 내용은 아직 백엔드가 없어
-// 시안과 같은 골격에 "추후 연동" 자리만 잡아 둔다.
+// 웹 DB에 있는 것(연번·기관명·담당자 3종·채널)만 실데이터고,
+// 업무단계/통계/일부 탭 내용은 아직 백엔드가 없어 시안과 같은 골격에
+// "추후 연동" 자리만 잡아 둔다.
 
 const STEPS = [
   '신청',
@@ -41,24 +41,50 @@
   return MATTERMOST_BASE + name
 }
 
-function Field({ label, value }: { label: string; value: string | null }) {
-  return (
-    <div className="flex gap-3 text-sm">
-      <span className="w-14 shrink-0 text-gray-400">{label}</span>
-      <span className={value ? 'font-medium text-gray-900' : 'text-gray-300'}>
-        {value ?? '-'}
-      </span>
-    </div>
-  )
+function Cell({ value }: { value: string | null }) {
+  return <span className={value ? 'text-gray-900' : 'text-gray-300'}>{value ?? '-'}</span>
 }
 
-function PendingCard({ title }: { title: string }) {
-  return (
-    <section className="rounded-lg border border-gray-200 p-4">
-      <h2 className="text-sm font-semibold">{title}</h2>
-      <p className="mt-3 text-sm text-gray-300">추후 연동</p>
-    </section>
-  )
+interface ContactRow {
+  role: string
+  dept: string | null
+  manager: string | null
+  phone: string | null
+  email: string | null
+  assignedDate: string | null
+}
+
+function contactRows(org: Org): ContactRow[] {
+  return [
+    {
+      role: '신청기관 담당자',
+      dept: org.deptName,
+      manager: org.managerName
+        ? org.managerTitle
+          ? `${org.managerName} ${org.managerTitle}`
+          : org.managerName
+        : null,
+      phone: org.managerPhone,
+      email: org.managerEmail,
+      assignedDate: null,
+    },
+    {
+      role: '문정원 담당자',
+      dept: org.mjDeptName,
+      manager: org.mjManagerName,
+      phone: org.mjManagerPhone,
+      email: org.mjManagerEmail,
+      assignedDate: null,
+    },
+    {
+      role: '담당 변호사',
+      dept: null,
+      manager: org.lawyerName,
+      phone: org.lawyerPhone,
+      email: org.lawyerEmail,
+      assignedDate: org.lawyerAssignedDate,
+    },
+  ]
 }
 
 export default function OrgOverview({ org }: { org: Org }) {
@@ -110,30 +136,43 @@
           </div>
         </div>
 
-        {/* 담당자 카드 3개 */}
-        <div className="mt-5 grid grid-cols-1 gap-3 lg:grid-cols-3">
-          <section className="rounded-lg border border-gray-200 p-4">
-            <h2 className="text-sm font-semibold">신청기관 담당자</h2>
-            <div className="mt-3 space-y-1.5">
-              <Field label="부서" value={org.deptName} />
-              <Field
-                label="담당자"
-                value={
-                  org.managerName
-                    ? org.managerTitle
-                      ? `${org.managerName} ${org.managerTitle}`
-                      : org.managerName
-                    : null
-                }
-              />
-              <Field label="연락처" value={org.managerPhone} />
-              <Field label="이메일" value={org.managerEmail} />
-            </div>
-          </section>
-
-          <PendingCard title="문정원 담당자" />
-          <PendingCard title="담당 변호사" />
-        </div>
+        {/* 담당자 3종 (신청기관/문정원/변호사) */}
+        <section className="mt-5 overflow-x-auto rounded-lg border border-gray-200">
+          <table className="w-full text-left text-sm">
+            <thead>
+              <tr className="border-b border-gray-200 text-xs text-gray-400">
+                <th className="px-4 py-2 font-medium">구분</th>
+                <th className="px-4 py-2 font-medium">부서</th>
+                <th className="px-4 py-2 font-medium">담당자</th>
+                <th className="px-4 py-2 font-medium">연락처</th>
+                <th className="px-4 py-2 font-medium">이메일</th>
+                <th className="px-4 py-2 font-medium">배정일</th>
+              </tr>
+            </thead>
+            <tbody>
+              {contactRows(org).map((row) => (
+                <tr key={row.role} className="border-b border-gray-100 last:border-b-0">
+                  <td className="px-4 py-2 font-medium text-gray-700">{row.role}</td>
+                  <td className="px-4 py-2">
+                    <Cell value={row.dept} />
+                  </td>
+                  <td className="px-4 py-2">
+                    <Cell value={row.manager} />
+                  </td>
+                  <td className="px-4 py-2">
+                    <Cell value={row.phone} />
+                  </td>
+                  <td className="px-4 py-2">
+                    <Cell value={row.email} />
+                  </td>
+                  <td className="px-4 py-2">
+                    <Cell value={row.assignedDate} />
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </section>
 
         {/* 업무단계 현황 */}
         <section className="mt-5">
frontend/src/components/Sidebar.test.tsx
--- frontend/src/components/Sidebar.test.tsx
+++ frontend/src/components/Sidebar.test.tsx
@@ -8,8 +8,8 @@
 
     const labels = [
       'Dashboard',
+      '채널관리',
       '기관관리',
-      '채널생성',
       '사업관리',
       '권리확인',
       '권리처리',
@@ -24,7 +24,7 @@
   it('activeKey에 해당하는 메뉴가 현재 페이지로 표시된다', () => {
     render(<Sidebar activeKey="channels" />)
 
-    expect(screen.getByRole('link', { name: '채널생성' }))
+    expect(screen.getByRole('link', { name: '채널관리' }))
       .toHaveAttribute('aria-current', 'page')
     expect(screen.getByRole('link', { name: '기관관리' }))
       .not.toHaveAttribute('aria-current')
@@ -34,7 +34,7 @@
     const onSelect = vi.fn()
     render(<Sidebar activeKey="orgs" onSelect={onSelect} />)
 
-    fireEvent.click(screen.getByRole('link', { name: '채널생성' }))
+    fireEvent.click(screen.getByRole('link', { name: '채널관리' }))
 
     expect(onSelect).toHaveBeenCalledWith('channels')
   })
frontend/src/components/Sidebar.tsx
--- frontend/src/components/Sidebar.tsx
+++ frontend/src/components/Sidebar.tsx
@@ -26,22 +26,22 @@
     ),
   },
   {
+    key: 'channels',
+    label: '채널관리',
+    enabled: true,
+    icon: (
+      <svg className={ICON_CLASS} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
+        <path d="M9 3L7 21M17 3l-2 18M4 8h17M3 16h17" />
+      </svg>
+    ),
+  },
+  {
     key: 'orgs',
     label: '기관관리',
     enabled: true,
     icon: (
       <svg className={ICON_CLASS} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
         <path d="M3 21h18M5 21V5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v16M9 8h2M9 12h2M9 16h2M15 10h4v11" />
-      </svg>
-    ),
-  },
-  {
-    key: 'channels',
-    label: '채널생성',
-    enabled: true,
-    icon: (
-      <svg className={ICON_CLASS} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
-        <path d="M9 3L7 21M17 3l-2 18M4 8h17M3 16h17" />
       </svg>
     ),
   },
frontend/src/components/WorkMemos.test.tsx
--- frontend/src/components/WorkMemos.test.tsx
+++ frontend/src/components/WorkMemos.test.tsx
@@ -27,6 +27,14 @@
     managerTitle: '과장',
     managerPhone: '02-3475-5434',
     managerEmail: 'ming@arirang.com',
+    mjDeptName: null,
+    mjManagerName: null,
+    mjManagerPhone: null,
+    mjManagerEmail: null,
+    lawyerName: null,
+    lawyerPhone: null,
+    lawyerEmail: null,
+    lawyerAssignedDate: null,
     channelIdMj: 'chan-mj',
     channelIdLaw: 'chan-law',
     ...overrides,
@@ -91,6 +99,26 @@
     expect(screen.getByLabelText('담당자 이름')).toBeTruthy()
   })
 
+  it('신청기관 담당자, 문정원 담당자, 담당 변호사가 모두 있으면 셋 다 선택 옵션으로 나온다', async () => {
+    mocks.getMemos.mockResolvedValue([])
+
+    render(
+      <WorkMemos
+        org={org({
+          mjManagerName: '김문정',
+          lawyerName: '이변호',
+        })}
+      />,
+    )
+
+    await waitFor(() => expect(mocks.getMemos).toHaveBeenCalled())
+
+    expect(screen.getByRole('option', { name: '송민지 과장' })).toBeTruthy()
+    expect(screen.getByRole('option', { name: '김문정' })).toBeTruthy()
+    expect(screen.getByRole('option', { name: '이변호' })).toBeTruthy()
+    expect(screen.getByRole('option', { name: '직접 입력' })).toBeTruthy()
+  })
+
   it('메모 내용이 비어 있으면 등록 버튼이 비활성이고, 입력하면 활성화된다', async () => {
     mocks.getMemos.mockResolvedValue([])
 
frontend/src/components/WorkMemos.tsx
--- frontend/src/components/WorkMemos.tsx
+++ frontend/src/components/WorkMemos.tsx
@@ -22,13 +22,29 @@
   return org.managerTitle ? `${org.managerName} ${org.managerTitle}` : org.managerName
 }
 
+/** 신청기관 담당자, 문정원 담당자, 담당 변호사 중 이름이 채워진 사람만 후보로 낸다.
+ * 이름이 같은 사람이 중복 등록돼 있어도 셀렉트 옵션은 한 번만 보이도록 중복을 없앤다. */
+function contactOptions(org: Org): string[] {
+  const candidates = [managerOptionLabel(org), org.mjManagerName, org.lawyerName]
+  const seen = new Set<string>()
+  const options: string[] = []
+  for (const candidate of candidates) {
+    if (!candidate || candidate.trim() === '' || seen.has(candidate)) {
+      continue
+    }
+    seen.add(candidate)
+    options.push(candidate)
+  }
+  return options
+}
+
 /** 기관관리 상세의 업무메모 탭. 통화 등 담당자와의 연락 내용을 기록/조회/삭제한다. */
 export default function WorkMemos({ org }: { org: Org }) {
-  const managerLabel = managerOptionLabel(org)
+  const options = contactOptions(org)
 
   const [memos, setMemos] = useState<WorkMemo[]>([])
   const [loading, setLoading] = useState(false)
-  const [contactChoice, setContactChoice] = useState(managerLabel ?? CUSTOM_CONTACT_VALUE)
+  const [contactChoice, setContactChoice] = useState(options[0] ?? CUSTOM_CONTACT_VALUE)
   const [customContactName, setCustomContactName] = useState('')
   const [body, setBody] = useState('')
   const [busy, setBusy] = useState(false)
@@ -102,7 +118,11 @@
               onChange={(e) => setContactChoice(e.target.value)}
               className="rounded-md border border-gray-300 px-2 py-1.5 text-sm"
             >
-              {managerLabel && <option value={managerLabel}>{managerLabel}</option>}
+              {options.map((option) => (
+                <option key={option} value={option}>
+                  {option}
+                </option>
+              ))}
               <option value={CUSTOM_CONTACT_VALUE}>직접 입력</option>
             </select>
           </div>
Add a comment
List