Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/pages/CreateWorkPage/CreateWorkPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,12 @@ describe('CreateWorkPage', () => {
})
})

it('jumps to a later review step when clicking it in the shared step indicator', async () => {
const user = userEvent.setup()
it('renders the shared step indicator as a non-interactive progress display', () => {
renderPage()

await user.click(screen.getByRole('button', { name: '3 초안 작성' }))

expect(await screen.findByText('검토 화면?step=2')).toBeInTheDocument()
expect(screen.getByText('1 요청 확인')).toBeInTheDocument()
expect(screen.getByText('2 분석 결과')).toBeInTheDocument()
expect(screen.queryByRole('button', { name: /분석 결과/ })).not.toBeInTheDocument()
})

it('opens the file import wizard from the work item panel', async () => {
Expand Down
7 changes: 1 addition & 6 deletions src/pages/CreateWorkPage/CreateWorkPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,6 @@ export function CreateWorkPage() {
}
}

function handleStepClick(index: number) {
if (index === 0) return
navigate(`/tasks/new/review?step=${index}`)
}

return (
<div>
<div className={styles.topBar}>
Expand All @@ -71,7 +66,7 @@ export function CreateWorkPage() {
</Link>
</div>

<WorkflowStepIndicator steps={REVIEW_STEPS} currentIndex={0} onStepClick={handleStepClick} />
<WorkflowStepIndicator steps={REVIEW_STEPS} currentIndex={0} />

<div className={styles.headerRow}>
<div>
Expand Down
91 changes: 3 additions & 88 deletions src/pages/ReviewWorkPage/ReviewWorkPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ToastViewport } from '../../components/ui/ToastViewport/ToastViewport'
import { useToastStore } from '../../store/toastStore'
import { ReviewWorkPage } from './ReviewWorkPage'
import { DRAFT_DOCUMENTS, HR_VERIFICATION_FIELDS, REVIEW_STEPS, STRUCTURED_FIELDS } from './reviewWorkData'
import type { AiRunResponse } from '../../api/aiRuns'

beforeEach(() => {
Expand All @@ -24,17 +21,10 @@ function renderPage(path = '/tasks/new/review', state?: unknown) {
<Route path="/tasks" element={<p>업무함</p>} />
<Route path="/tasks/new" element={<p>업무 생성 페이지</p>} />
</Routes>
<ToastViewport />
</MemoryRouter>,
)
}

async function fillVerificationFields(user: ReturnType<typeof userEvent.setup>) {
for (const field of HR_VERIFICATION_FIELDS) {
await user.type(screen.getByLabelText(field.label), '2027-01-01')
}
}

describe('ReviewWorkPage', () => {
it('uses the server AiRun result when an actual analysis is provided', () => {
const aiRun: AiRunResponse = {
Expand Down Expand Up @@ -67,84 +57,9 @@ describe('ReviewWorkPage', () => {
expect(screen.getByLabelText('신청 목표일을 입력해 주세요. *')).toBeInTheDocument()
})

it('renders every step of the shared progress indicator', () => {
renderPage()
const indicator = screen.getByRole('list', { name: '진행 단계' })
expect(within(indicator).getAllByRole('listitem')).toHaveLength(REVIEW_STEPS.length)
})

it('renders the information pending table with editable HR fields', () => {
renderPage()

expect(screen.getByText('누락정보를 해결 주체별로 확인해 주세요')).toBeInTheDocument()
for (const field of HR_VERIFICATION_FIELDS) {
expect(screen.getByLabelText(field.label)).toBeInTheDocument()
}
})

it('shows a toast when saving a temporary draft', async () => {
const user = userEvent.setup()
renderPage()

await user.click(screen.getByRole('button', { name: '임시 저장' }))

expect(screen.getByText('임시 저장했습니다.')).toBeInTheDocument()
})

it('disables the draft generation button until every HR verification field is filled', async () => {
const user = userEvent.setup()
renderPage()

const generate = screen.getByRole('button', { name: '초안 생성' })
expect(generate).toBeDisabled()

await fillVerificationFields(user)

expect(generate).toBeEnabled()
})

it('advances through draft preparation to final review and can navigate to the task list', async () => {
const user = userEvent.setup()
renderPage()
await fillVerificationFields(user)
await user.click(screen.getByRole('button', { name: '초안 생성' }))

expect(await screen.findByText('생성된 문서와 대기 중인 문서를 확인해 주세요')).toBeInTheDocument()
for (const doc of DRAFT_DOCUMENTS) {
expect(screen.getByText(doc.title)).toBeInTheDocument()
}

const draftReviewButtons = screen.getAllByRole('button', { name: '초안 검토' })
await user.click(draftReviewButtons[draftReviewButtons.length - 1])

expect(await screen.findByText('문서 검토본과 입력값을 최종 확인해 주세요')).toBeInTheDocument()
for (const field of STRUCTURED_FIELDS) {
expect(screen.getAllByText(field.label).length).toBeGreaterThan(0)
}

await user.click(screen.getByRole('button', { name: '승인 요청' }))

expect(await screen.findByRole('heading', { name: /승인이 완료됐습니다./ })).toBeInTheDocument()

await user.click(screen.getByRole('button', { name: '업무함으로 이동 →' }))

expect(await screen.findByText('업무함')).toBeInTheDocument()
})

it('opens directly on a given step via the ?step= query param', () => {
renderPage('/tasks/new/review?step=3')

expect(screen.getByText('문서 검토본과 입력값을 최종 확인해 주세요')).toBeInTheDocument()
})

it('jumps freely between steps by clicking the shared indicator', async () => {
const user = userEvent.setup()
renderPage('/tasks/new/review?step=3')

await user.click(screen.getByRole('button', { name: '✓ 정보 보완' }))
expect(screen.getByText('누락정보를 해결 주체별로 확인해 주세요')).toBeInTheDocument()
it('redirects to the request input screen when there is no aiRunId to review', async () => {
renderPage('/tasks/new/review')

await user.click(screen.getByRole('button', { name: '✓ 요청 확인' }))
expect(await screen.findByText('업무 생성 페이지')).toBeInTheDocument()
})
})
42 changes: 5 additions & 37 deletions src/pages/ReviewWorkPage/ReviewWorkPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,18 @@ import { useEffect, useState } from 'react'
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { ApiError, getErrorMessage } from '../../api/errors'
import { fetchAiRun, type AiRunResponse } from '../../api/aiRuns'
import { WorkflowStepIndicator } from '../../components/ui/WorkflowStepIndicator/WorkflowStepIndicator'
import {
readAiRunWorkRequestDraft,
type WorkRequestDraft,
} from '../CreateWorkPage/workRequestDraft'
import { AiRunReview } from './AiRunReview'
import styles from './ReviewWorkPage.module.css'
import { REVIEW_STEPS } from './reviewWorkData'
import { DraftPreparationStep } from './steps/DraftPreparationStep'
import { FinalReviewStep } from './steps/FinalReviewStep'
import { InformationPendingStep } from './steps/InformationPendingStep'

// REVIEW_STEPS 인덱스 기준 2~4단계(정보 보완/초안 준비/최종 검토) 내부 위저드.
// 1.요청 확인은 CreateWorkPage(/tasks/new)에서 진행되고, 같은 WorkflowStepIndicator를 공유한다.
type WizardStepIndex = 1 | 2 | 3

interface ReviewLocationState {
aiRun?: AiRunResponse
draft?: WorkRequestDraft
}

function parseStepIndex(value: string | null): WizardStepIndex {
const parsed = Number(value)
return parsed === 2 || parsed === 3 ? parsed : 1
}

export function ReviewWorkPage() {
const navigate = useNavigate()
const location = useLocation()
Expand All @@ -38,7 +24,6 @@ export function ReviewWorkPage() {
const [recoveredRun, setRecoveredRun] = useState<AiRunResponse | null>(null)
const [recovering, setRecovering] = useState(Boolean(aiRunId && !navigationRun))
const [recoveryError, setRecoveryError] = useState<string | null>(null)
const [stepIndex, setStepIndex] = useState<WizardStepIndex>(() => parseStepIndex(searchParams.get('step')))
const aiRun = navigationRun ?? recoveredRun
const draft = navigationState?.draft ?? (aiRunId ? readAiRunWorkRequestDraft(aiRunId) : null)

Expand Down Expand Up @@ -68,13 +53,10 @@ export function ReviewWorkPage() {
}
}, [aiRunId, navigationRun])

function handleStepClick(index: number) {
if (index === 0) {
navigate('/tasks/new')
return
}
setStepIndex(index as WizardStepIndex)
}
// 분석 실행 번호(aiRunId) 없이 이 화면에 직접 들어오면 검토할 내용이 없다 — 요청 입력으로 되돌린다.
useEffect(() => {
if (!aiRunId && !navigationRun) navigate('/tasks/new', { replace: true })
}, [aiRunId, navigationRun, navigate])

if (aiRun) {
return <AiRunReview initialRun={aiRun} initialDraft={draft} />
Expand Down Expand Up @@ -104,19 +86,5 @@ export function ReviewWorkPage() {
)
}

return (
<div>
<div className={styles.topBar}>
<Link to="/tasks/new" className={styles.back}>
← 업무 생성
</Link>
</div>

<WorkflowStepIndicator steps={REVIEW_STEPS} currentIndex={stepIndex} onStepClick={handleStepClick} />

{stepIndex === 1 && <InformationPendingStep onComplete={() => setStepIndex(2)} />}
{stepIndex === 2 && <DraftPreparationStep onDone={() => setStepIndex(3)} />}
{stepIndex === 3 && <FinalReviewStep />}
</div>
)
return null
}
Loading