diff --git a/docs/SCREEN_CATALOG.md b/docs/SCREEN_CATALOG.md index 5d4512d..935a07c 100644 --- a/docs/SCREEN_CATALOG.md +++ b/docs/SCREEN_CATALOG.md @@ -4,22 +4,21 @@ Figma 파일: [FOWOCO](https://www.figma.com/design/eaOD8OXZOGq6vK4H9pGXNi/FOWOC 상태 값: `design:needs-review`(디자인 검토 필요) / `design:figma-ready`(구현 가능) — 실제 상태는 담당자가 Figma 확인 후 갱신합니다. -| # | 화면 | Figma 노드(참고) | 대상 | area 라벨 | 상태 | -| --- | --- | --- | --- | --- | --- | -| 1 | 로그인 | 19:2 | HR | `area:auth` | `design:needs-review` | -| 2 | 대시보드 | 8:5 | HR | `area:dashboard` | `design:needs-review` | -| 3 | 근로자 목록 관리 | 20:2 | HR | `area:worker` | `design:needs-review` | -| 4 | 근로자 상세 / 계약 및 체류 관리 | 17:2 | HR | `area:worker` | `design:needs-review` | -| 5a | 서류관리 / OCR | 22:2 | HR | `area:document` | `design:needs-review` | -| 5b | 서류 초안 작성 화면 | 90:33 | HR | `area:document` | `design:needs-review` | -| 5c | 서류 최종 결정 화면 | 98:9 | HR | `area:document` | `design:needs-review` | -| 6a | 업무 카드 보드 | 17:42 | HR | `area:task-board` | `design:needs-review` | -| 6b | 업무 카드 상세 / AI 승인 | 22:88 | HR | `area:task-board` | `design:needs-review` | -| 7a | Agent 패널 | 34:2 | HR | `area:agent-panel` | `design:needs-review` | -| 7b | AI 처리 이력 / 리포트 | 20:68 | HR | `area:agent-panel` | `design:needs-review` | -| 8 | 티켓 관리 | 17:111 | HR | `area:ticket` | `design:needs-review` | -| 9a | 근로자 모바일 안내 확인 | 22:142 | 근로자 | `area:mobile-worker` | `design:needs-review` | -| 9b | 근로자 모바일 질문 작성 화면 | 217:68 | 근로자 | `area:mobile-worker` | `design:needs-review` | +| # | 화면 | Figma 노드(참고) | 대상 | area 라벨 | 상태 | +| --- | ------------------------------- | ---------------- | ------ | -------------------- | --------------------- | +| 1 | 로그인 | 19:2 | HR | `area:auth` | `design:needs-review` | +| 2 | 대시보드 | 8:5 | HR | `area:dashboard` | `design:needs-review` | +| 3 | 근로자 목록 관리 | 20:2 | HR | `area:worker` | `design:needs-review` | +| 4 | 근로자 상세 / 계약 및 체류 관리 | 17:2 | HR | `area:worker` | `design:needs-review` | +| 5a | 서류관리 / OCR | 22:2 | HR | `area:document` | `design:needs-review` | +| 5b | 서류 초안 작성 화면 | 90:33 | HR | `area:document` | `design:needs-review` | +| 5c | 서류 최종 결정 화면 | 98:9 | HR | `area:document` | `design:needs-review` | +| 6a | 업무 카드 보드 | 17:42 | HR | `area:task-board` | `design:needs-review` | +| 6b | 업무 카드 상세 / AI 승인 | 22:88 | HR | `area:task-board` | `design:needs-review` | +| 7a | Agent 패널 | 34:2 | HR | `area:agent-panel` | `design:needs-review` | +| 7b | AI 처리 이력 / 리포트 | 20:68 | HR | `area:agent-panel` | `design:needs-review` | +| 8a | 근로자 모바일 안내 확인 | 22:142 | 근로자 | `area:mobile-worker` | `design:needs-review` | +| 8b | 근로자 모바일 질문 작성 화면 | 217:68 | 근로자 | `area:mobile-worker` | `design:needs-review` | > 노드 ID는 와이어프레임 내 프레임을 빠르게 찾기 위한 참고용이며, 실제 구현 착수 전 담당자가 Figma에서 최신 상태를 다시 확인해야 합니다. diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index 3972ed3..26a0547 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -31,8 +31,6 @@ function getPageTitle(pathname: string) { if (pathname === '/profile') return '설정' if (pathname === '/workers') return '근로자 정보' if (pathname.startsWith('/workers/')) return '근로자 정보' - if (pathname.startsWith('/tickets/')) return '지원 요청 상세' - if (pathname === '/tickets') return '지원 요청' return 'FOWOCO' } diff --git a/src/components/layout/HelpModal/helpContent.ts b/src/components/layout/HelpModal/helpContent.ts index 64fa266..87907a1 100644 --- a/src/components/layout/HelpModal/helpContent.ts +++ b/src/components/layout/HelpModal/helpContent.ts @@ -11,15 +11,18 @@ export const HELP_FAQ: HelpFaqItem[] = [ }, { question: '승인은 누가 할 수 있나요?', - answer: '설정 > 구성원과 승인 권한에서 지정된 구성원만 승인할 수 있습니다. Agent는 승인자가 될 수 없습니다.', + answer: + '설정 > 구성원과 승인 권한에서 지정된 구성원만 승인할 수 있습니다. Agent는 승인자가 될 수 없습니다.', }, { question: '근로자 서류는 어디서 확인하나요?', - answer: '서류 메뉴에서 근로자별 제출 현황을 상태별(미제출/확인 대기/확인 완료)로 확인할 수 있습니다.', + answer: + '서류 메뉴에서 근로자별 제출 현황을 상태별(미제출/확인 대기/확인 완료)로 확인할 수 있습니다.', }, { question: '근로자가 남긴 질문은 어디서 답하나요?', - answer: '티켓 메뉴에서 근로자 모바일로 들어온 질문·이슈를 신규/응답 대기/완료 탭으로 확인하고 답변합니다.', + answer: + '업무 상세의 소통 영역에서 근로자 모바일 응답과 제출 파일을 확인하고 후속 처리를 진행합니다.', }, ] diff --git a/src/pages/IntroPage/introData.ts b/src/pages/IntroPage/introData.ts index 393dc34..7310e75 100644 --- a/src/pages/IntroPage/introData.ts +++ b/src/pages/IntroPage/introData.ts @@ -36,13 +36,7 @@ export const HERO_HIGHLIGHTS: HeroHighlight[] = [ { icon: 'lock', label: '전달·제출은 담당자가 직접 수행' }, ] -export type FeatureIconKey = - | 'agent' - | 'calendar' - | 'contract' - | 'folder' - | 'globe' - | 'radar' +export type FeatureIconKey = 'agent' | 'calendar' | 'contract' | 'folder' | 'globe' | 'radar' export interface Feature { icon: FeatureIconKey @@ -90,12 +84,16 @@ export interface Step { } // FOWOCO_Intent·Domain·Slot 학습가이드의 처리 파이프라인(Intent → Domain → Slot → -// Workflow Resolver → Case/티켓)을 그대로 반영한 6단계. +// Workflow Resolver → Case/Task)를 그대로 반영한 6단계. export const STEPS: Step[] = [ { no: '01', title: '자연어 요청', description: '"응웬반A 체류연장 준비해줘"처럼 편하게 요청' }, - { no: '02', title: 'Intent·Domain 분류', description: '무슨 일을, 어느 업무 영역에서 하려는지 파악' }, + { + no: '02', + title: 'Intent·Domain 분류', + description: '무슨 일을, 어느 업무 영역에서 하려는지 파악', + }, { no: '03', title: 'Slot 추출·검증', description: '근로자·날짜·서류 등 실행에 필요한 값 확인' }, - { no: '04', title: 'Case·티켓 생성', description: '새 Case 또는 기존 Case의 Subflow로 연결' }, + { no: '04', title: 'Case·Task 생성', description: '새 Case와 실행할 업무 단계로 연결' }, { no: '05', title: '담당자 검토·승인', description: '근거를 확인하고 최종 결정' }, { no: '06', title: '직접 실행·기록', description: '전달·제출은 사람이, 결과는 Case에 기록' }, ] @@ -131,7 +129,8 @@ export const TRUST_ITEMS: TrustItem[] = [ }, ] -export type WorkflowIconKey = 'contract' | 'workerAdd' | 'exit' | 'folder' | 'payroll' | 'instruction' +export type WorkflowIconKey = + 'contract' | 'workerAdd' | 'exit' | 'folder' | 'payroll' | 'instruction' export interface Workflow { icon: WorkflowIconKey @@ -146,19 +145,22 @@ export const WORKFLOWS: Workflow[] = [ icon: 'contract', kind: 'Master', title: '재계약·연장', - description: '계약·고용허가·취업활동·체류기간, 4개의 만료일을 각각 확인해 하나의 Case로 관리합니다.', + description: + '계약·고용허가·취업활동·체류기간, 4개의 만료일을 각각 확인해 하나의 Case로 관리합니다.', }, { icon: 'workerAdd', kind: 'Master', title: '신규등록', - description: '여권·외국인등록증·계약서를 분석해 근로자 프로필 초안을 만들고 초기 업무를 생성합니다.', + description: + '여권·외국인등록증·계약서를 분석해 근로자 프로필 초안을 만들고 초기 업무를 생성합니다.', }, { icon: 'exit', kind: 'Master', title: '퇴사·고용변동', - description: '사건 유형과 기준일을 확정하면 신고기한 후보를 계산하고 처리 결과를 Case에 남깁니다.', + description: + '사건 유형과 기준일을 확정하면 신고기한 후보를 계산하고 처리 결과를 Case에 남깁니다.', }, { icon: 'folder', @@ -176,7 +178,8 @@ export const WORKFLOWS: Workflow[] = [ icon: 'instruction', kind: 'Reusable', title: '업무·근무일정 안내', - description: '모호한 지시를 날짜·장소·수량이 분명한 문장으로 바꿔 쉬운 한국어와 모국어로 전달합니다.', + description: + '모호한 지시를 날짜·장소·수량이 분명한 문장으로 바꿔 쉬운 한국어와 모국어로 전달합니다.', }, ] diff --git a/src/pages/TicketDetailPage/TicketDetailPage.module.css b/src/pages/TicketDetailPage/TicketDetailPage.module.css deleted file mode 100644 index 8c45dc2..0000000 --- a/src/pages/TicketDetailPage/TicketDetailPage.module.css +++ /dev/null @@ -1,153 +0,0 @@ -.topBar { - display: flex; - align-items: center; - margin: -32px -32px 0; - padding: 19px 32px; - background: var(--surface-default); - border-bottom: 1px solid var(--border-default); - font-size: 13px; - color: var(--text-secondary); -} - -@media (max-width: 768px) { - .topBar { - margin: -16px -16px 0; - padding: 16px; - } -} - -.back { - background: none; - border: none; - color: var(--text-secondary); - font-size: 13px; - cursor: pointer; - padding: 0; - text-decoration: none; -} - -.headerRow { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 12px; - margin-top: 34px; -} - -.title { - margin: 0; - font-size: 26px; - font-weight: 700; - color: var(--text-primary); -} - -.meta { - margin: 12px 0 0; - font-size: 13px; - color: var(--text-secondary); -} - -.sectionCard { - margin-top: 16px; - padding: var(--fowoco-spacing-16) var(--fowoco-spacing-24); - background: var(--surface-default); - border: 1px solid var(--border-default); - border-radius: var(--fowoco-radius-10); - box-shadow: var(--shadow-sm); -} - -.cardTitle { - margin: 0 0 16px; - font-size: 18px; - font-weight: 700; - color: var(--text-primary); -} - -.question { - margin: 0; - font-size: 14px; - line-height: 22px; - color: var(--text-primary); -} - -.relatedCaseLink { - background: none; - border: none; - padding: 0; - font-size: 13px; - font-weight: 500; - color: var(--brand-primary); - cursor: pointer; -} - -.answerInput { - width: 100%; - box-sizing: border-box; - padding: 12px 14px; - background: var(--surface-subtle); - border: 1px solid var(--border-default); - border-radius: var(--fowoco-radius-6); - font-size: 14px; - font-family: inherit; - color: var(--text-primary); - resize: vertical; -} - -.answerInput:focus-visible { - outline: 2px solid var(--brand-primary); - outline-offset: 2px; -} - -.answerActions { - display: flex; - justify-content: flex-end; - margin-top: 12px; -} - -.sentList { - display: flex; - flex-direction: column; - gap: 12px; - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid var(--border-default); -} - -.sentRow { - display: flex; - align-items: flex-start; - gap: 16px; -} - -.sentAt { - flex-shrink: 0; - width: 50px; - font-size: 12px; - color: var(--text-secondary); -} - -.sentText { - flex: 1; - margin: 0; - font-size: 13px; - color: var(--text-primary); -} - -.statusDock { - display: flex; - align-items: center; - gap: 24px; - margin-top: 16px; - padding: var(--fowoco-spacing-12) var(--fowoco-spacing-20); - background: var(--surface-default); - border: 1px solid var(--border-default); - border-radius: var(--fowoco-radius-10); - box-shadow: var(--shadow-sm); -} - -.statusNote { - flex: 1; - font-size: 13px; - font-weight: 500; - color: var(--text-secondary); -} diff --git a/src/pages/TicketDetailPage/TicketDetailPage.test.tsx b/src/pages/TicketDetailPage/TicketDetailPage.test.tsx deleted file mode 100644 index 111ed53..0000000 --- a/src/pages/TicketDetailPage/TicketDetailPage.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { MemoryRouter, Route, Routes } from 'react-router-dom' -import { describe, expect, it } from 'vitest' -import { TicketDetailPage } from './TicketDetailPage' -import { TICKETS } from '../TicketListPage/ticketListData' - -function renderPage(ticketId: string) { - render( - - - } /> - 티켓 목록

} /> - 업무 상세

} /> -
-
, - ) -} - -describe('TicketDetailPage', () => { - it('renders the question and worker name for the selected ticket', () => { - const ticket = TICKETS[0] - renderPage(ticket.id) - - expect(screen.getByRole('heading', { name: ticket.workerName })).toBeInTheDocument() - expect(screen.getByText(ticket.question)).toBeInTheDocument() - }) - - it('navigates to the related task when one exists', async () => { - const user = userEvent.setup() - const ticket = TICKETS.find((item) => item.relatedCaseId !== null)! - renderPage(ticket.id) - - await user.click(screen.getByRole('button', { name: `${ticket.relatedCaseId} 업무 상세 열기 →` })) - - expect(await screen.findByText('업무 상세')).toBeInTheDocument() - }) - - it('shows an empty state when there is no related task', () => { - const ticket = TICKETS.find((item) => item.relatedCaseId === null)! - renderPage(ticket.id) - - expect(screen.getByText('연결된 업무가 없습니다')).toBeInTheDocument() - }) - - it('sends an answer and advances a 신규 ticket to 응답 대기', async () => { - const user = userEvent.setup() - const ticket = TICKETS.find((item) => item.status === 'new')! - renderPage(ticket.id) - - await user.type(screen.getByPlaceholderText('근로자에게 보낼 답변을 입력하세요'), '확인 후 안내드리겠습니다.') - await user.click(screen.getByRole('button', { name: '답변 발송' })) - - expect(screen.getByText('확인 후 안내드리겠습니다.')).toBeInTheDocument() - expect(screen.getByText('응답 대기')).toBeInTheDocument() - }) - - it('advances status via the status action button', async () => { - const user = userEvent.setup() - const ticket = TICKETS.find((item) => item.status === 'waiting')! - renderPage(ticket.id) - - await user.click(screen.getByRole('button', { name: '완료 처리' })) - - expect(screen.getByText('현재 상태: 완료')).toBeInTheDocument() - }) - - it('falls back to the first ticket when the ticketId is invalid', () => { - renderPage('does-not-exist') - - expect(screen.getByRole('heading', { name: TICKETS[0].workerName })).toBeInTheDocument() - }) -}) diff --git a/src/pages/TicketDetailPage/TicketDetailPage.tsx b/src/pages/TicketDetailPage/TicketDetailPage.tsx deleted file mode 100644 index 25f3d3b..0000000 --- a/src/pages/TicketDetailPage/TicketDetailPage.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useState } from 'react' -import { Link, useNavigate, useParams } from 'react-router-dom' -import { Button } from '../../components/ui/Button/Button' -import { EmptyState } from '../../components/ui/EmptyState/EmptyState' -import { StatusLabel } from '../../components/ui/StatusLabel/StatusLabel' -import { useToastStore } from '../../store/toastStore' -import styles from './TicketDetailPage.module.css' -import { - TICKET_STATUS_LABEL, - TICKET_STATUS_TONE, - TICKETS, - type TicketStatus, -} from '../TicketListPage/ticketListData' - -const NEXT_STATUS: Record = { - new: 'waiting', - waiting: 'done', - done: null, -} - -const NEXT_STATUS_ACTION_LABEL: Record = { - new: '응답 대기로 전환', - waiting: '완료 처리', - done: '완료됨', -} - -interface SentAnswer { - id: string - text: string - sentAt: string -} - -export function TicketDetailPage() { - const { ticketId } = useParams() - const navigate = useNavigate() - const showToast = useToastStore((state) => state.showToast) - - const ticket = TICKETS.find((item) => item.id === ticketId) ?? TICKETS[0] - - const [status, setStatus] = useState(ticket.status) - const [draft, setDraft] = useState('') - const [sentAnswers, setSentAnswers] = useState([]) - - function handleSendAnswer() { - const text = draft.trim() - if (!text) return - - // TODO(backend): POST /api/tickets/:id/answers { text } -> 근로자 모바일로 답변 발송 - setSentAnswers((prev) => [...prev, { id: crypto.randomUUID(), text, sentAt: '방금' }]) - setDraft('') - showToast('답변을 발송했습니다.') - setStatus((current) => (current === 'new' ? 'waiting' : current)) - } - - function handleAdvanceStatus() { - const next = NEXT_STATUS[status] - if (!next) return - - // TODO(backend): PATCH /api/tickets/:id { status } -> 상태 전환 - setStatus(next) - showToast(`상태를 "${TICKET_STATUS_LABEL[next]}"(으)로 변경했습니다.`) - } - - return ( -
-
- - ← 티켓 - -
- -
-

{ticket.workerName}

- {TICKET_STATUS_LABEL[status]} -
-

접수 {ticket.receivedAt}

- -
-

질문 원문

-

{ticket.question}

-
- -
-

관련 업무

- {ticket.relatedCaseId ? ( - - ) : ( - - )} -
- -
-

답변 작성

-