File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import OrgOverview from './OrgOverview'
import type { Contact, Org } from '../api/client'
const mocks = vi.hoisted(() => ({
getPosts: vi.fn(),
getFiles: vi.fn(),
getMemos: vi.fn(),
getContacts: vi.fn(),
updateAssignments: vi.fn(),
}))
vi.mock('../api/client', async (importOriginal) => ({
...(await importOriginal<typeof import('../api/client')>()),
getPosts: mocks.getPosts,
getFiles: mocks.getFiles,
getMemos: mocks.getMemos,
getContacts: mocks.getContacts,
updateAssignments: mocks.updateAssignments,
}))
function contact(overrides: Partial<Contact> = {}): Contact {
return {
id: 1,
category: 'APPLICANT',
name: '이름없음',
affiliation: null,
deptName: null,
title: null,
phone: null,
email: null,
...overrides,
}
}
function org(overrides: Partial<Org> = {}): Org {
return {
id: 1,
orgNo: '001_00',
orgName: '국제방송교류재단',
status: 'ACTIVE',
applicant: contact({
id: 1,
category: 'APPLICANT',
name: '송민지',
affiliation: '국제방송교류재단',
deptName: '데이터정보화팀',
title: '과장',
phone: '02-3475-5434',
email: 'ming@arirang.com',
}),
mj: null,
itn: null,
lawyer: null,
lawyerAssignedDate: null,
channelIdMj: 'chan-mj',
channelIdLaw: 'chan-law',
...overrides,
}
}
beforeEach(() => {
mocks.getPosts.mockReset().mockResolvedValue([])
mocks.getFiles.mockReset().mockResolvedValue([])
mocks.getMemos.mockReset().mockResolvedValue([])
mocks.getContacts.mockReset().mockResolvedValue([])
mocks.updateAssignments.mockReset().mockResolvedValue(undefined)
})
describe('OrgOverview', () => {
it('연번_기관명 헤더와 신청기관 담당자 정보를 보여준다', async () => {
render(<OrgOverview org={org()} />)
expect(screen.getByText('001_00_국제방송교류재단')).toBeTruthy()
expect(screen.getByText('데이터정보화팀')).toBeTruthy()
expect(screen.getByText('송민지 과장')).toBeTruthy()
expect(screen.getByText('02-3475-5434')).toBeTruthy()
expect(screen.getByText('ming@arirang.com')).toBeTruthy()
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('문정원 담당자와 아이티앤 담당자와 담당 변호사 정보가 비어 있으면 카드에 -로 표시된다', async () => {
render(<OrgOverview org={org()} />)
const mjCard = screen.getByText('문정원 담당자').closest('section') as HTMLElement
const itnCard = screen.getByText('아이티앤 담당자').closest('section') as HTMLElement
const lawyerCard = screen.getByText('담당 변호사').closest('section') as HTMLElement
expect(within(mjCard).getAllByText('-')).toHaveLength(4)
expect(within(itnCard).getAllByText('-')).toHaveLength(4)
expect(within(lawyerCard).getAllByText('-')).toHaveLength(4)
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('담당자 4종이 값을 가지고 있으면 각 카드에 값이 표시된다', async () => {
render(
<OrgOverview
org={org({
mj: contact({
id: 2,
category: 'MJ',
name: '김문정',
deptName: '문화체육관광부 저작권정책과',
phone: '02-1234-5678',
email: 'mj@mcst.go.kr',
}),
itn: contact({
id: 4,
category: 'ITN',
name: '박아이티',
deptName: '개발팀',
phone: '02-1111-2222',
email: 'itn@iten.co.kr',
}),
lawyer: contact({
id: 3,
category: 'LAWYER',
name: '이변호',
phone: '02-9876-5432',
email: 'lawyer@lawfirm.kr',
}),
lawyerAssignedDate: '2026-07-21',
})}
/>,
)
const applicantCard = screen.getByText('신청기관 담당자').closest('section') as HTMLElement
const mjCard = screen.getByText('문정원 담당자').closest('section') as HTMLElement
const itnCard = screen.getByText('아이티앤 담당자').closest('section') as HTMLElement
const lawyerCard = screen.getByText('담당 변호사').closest('section') as HTMLElement
expect(within(applicantCard).getByText('데이터정보화팀')).toBeTruthy()
expect(within(applicantCard).getByText('송민지 과장')).toBeTruthy()
expect(within(mjCard).getByText('문화체육관광부 저작권정책과')).toBeTruthy()
expect(within(mjCard).getByText('김문정')).toBeTruthy()
expect(within(mjCard).getByText('02-1234-5678')).toBeTruthy()
expect(within(mjCard).getByText('mj@mcst.go.kr')).toBeTruthy()
expect(within(itnCard).getByText('개발팀')).toBeTruthy()
expect(within(itnCard).getByText('박아이티')).toBeTruthy()
expect(within(itnCard).getByText('02-1111-2222')).toBeTruthy()
expect(within(itnCard).getByText('itn@iten.co.kr')).toBeTruthy()
expect(within(lawyerCard).getByText('이변호')).toBeTruthy()
expect(within(lawyerCard).getByText('02-9876-5432')).toBeTruthy()
expect(within(lawyerCard).getByText('lawyer@lawfirm.kr')).toBeTruthy()
expect(within(lawyerCard).getByText('2026-07-21')).toBeTruthy()
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('배정된 카드에는 변경, 빈 카드에는 생성 버튼이 보인다', async () => {
render(<OrgOverview org={org()} />)
expect(screen.getByRole('button', { name: '신청기관 담당자 변경' })).toBeTruthy()
expect(screen.getByRole('button', { name: '문정원 담당자 생성' })).toBeTruthy()
expect(screen.getByRole('button', { name: '아이티앤 담당자 생성' })).toBeTruthy()
expect(screen.getByRole('button', { name: '담당 변호사 생성' })).toBeTruthy()
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('생성 버튼을 누르면 선택 모달이 열리고 담당자를 고르면 배정 API를 부른다', async () => {
mocks.getContacts.mockResolvedValue([
contact({ id: 7, category: 'MJ', name: '이연경', deptName: '공공저작물팀' }),
])
const onChanged = vi.fn()
render(<OrgOverview org={org()} onChanged={onChanged} />)
fireEvent.click(screen.getByRole('button', { name: '문정원 담당자 생성' }))
await waitFor(() => expect(screen.getByText('문정원 담당자 선택')).toBeTruthy())
fireEvent.click(await screen.findByRole('button', { name: '선택' }))
await waitFor(() =>
expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
applicantContactId: 1,
mjContactId: 7,
itnContactId: null,
lawyerContactId: null,
lawyerAssignedDate: null,
}),
)
expect(onChanged).toHaveBeenCalled()
})
it('아이티앤 담당자 생성 버튼을 누르면 선택 모달이 열리고 담당자를 고르면 itnContactId로 배정 API를 부른다', async () => {
mocks.getContacts.mockResolvedValue([
contact({ id: 8, category: 'ITN', name: '박아이티', deptName: '개발팀' }),
])
const onChanged = vi.fn()
render(<OrgOverview org={org()} onChanged={onChanged} />)
fireEvent.click(screen.getByRole('button', { name: '아이티앤 담당자 생성' }))
await waitFor(() => expect(screen.getByText('아이티앤 담당자 선택')).toBeTruthy())
fireEvent.click(await screen.findByRole('button', { name: '선택' }))
await waitFor(() =>
expect(mocks.updateAssignments).toHaveBeenCalledWith(1, {
applicantContactId: 1,
mjContactId: null,
itnContactId: 8,
lawyerContactId: null,
lawyerAssignedDate: null,
}),
)
expect(onChanged).toHaveBeenCalled()
})
it('상단 동작 버튼에서 담당 변호사 변경은 사라졌다', async () => {
render(<OrgOverview org={org()} />)
expect(screen.queryByRole('button', { name: '담당 변호사 변경' })).toBeNull()
expect(screen.getByRole('button', { name: '진행단계 변경' })).toBeDisabled()
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('업무단계 12개가 순서대로 보인다', async () => {
render(<OrgOverview org={org()} />)
expect(screen.getByText('신청')).toBeTruthy()
expect(screen.getByText('변호사 배당')).toBeTruthy()
expect(screen.getByText('보고서 작성')).toBeTruthy()
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('기본 탭인 채팅은 자리표시 대신 채널 선택 화면을 보여준다', async () => {
render(<OrgOverview org={org()} />)
expect(screen.getByRole('button', { name: '문정원' })).toBeTruthy()
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalledWith(1, 'mj'))
})
it('탭을 누르면 해당 탭의 자리표시 내용으로 바뀐다', async () => {
render(<OrgOverview org={org()} />)
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
fireEvent.click(screen.getByRole('button', { name: '타임라인' }))
expect(screen.getByText('타임라인 상세 화면은 추후 확정')).toBeTruthy()
expect(screen.queryByRole('button', { name: '문정원' })).toBeNull()
})
it('자료 탭을 누르면 자료 목록 화면으로 바뀐다', async () => {
render(<OrgOverview org={org()} />)
fireEvent.click(screen.getByRole('button', { name: '자료' }))
await waitFor(() => expect(mocks.getFiles).toHaveBeenCalledWith(1, 'mj'))
expect(screen.getByRole('button', { name: '새로고침' })).toBeTruthy()
})
it('채널이 있으면 Mattermost 열기가 채널 링크로 활성화된다', async () => {
render(<OrgOverview org={org()} />)
const link = screen.getByRole('link', { name: 'Mattermost 열기' })
expect(link).toHaveAttribute(
'href',
'https://hub.iten.co.kr/itn-hub/channels/org-001-00-mj',
)
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('채널이 없으면 Mattermost 열기가 비활성이다', () => {
render(<OrgOverview org={org({ channelIdMj: null, channelIdLaw: null, status: 'READY' })} />)
expect(screen.queryByRole('link', { name: 'Mattermost 열기' })).toBeNull()
expect(screen.getByRole('button', { name: 'Mattermost 열기' })).toBeDisabled()
})
it('치안정책연구소도 합성코드 슬러그로 채널 링크를 만든다', async () => {
render(
<OrgOverview
org={org({ orgNo: '008_01', orgName: '경찰청_치안정책연구소', channelIdMj: 'x' })}
/>,
)
expect(screen.getByRole('link', { name: 'Mattermost 열기' })).toHaveAttribute(
'href',
'https://hub.iten.co.kr/itn-hub/channels/org-008-01-mj',
)
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
it('미구현 동작 버튼들은 비활성이다', async () => {
render(<OrgOverview org={org()} />)
expect(screen.getByRole('button', { name: '진행단계 변경' })).toBeDisabled()
expect(screen.getByRole('button', { name: '보고서 관리' })).toBeDisabled()
await waitFor(() => expect(mocks.getPosts).toHaveBeenCalled())
})
})