diff --git a/src/pages/WorkListPage/WorkListPage.test.tsx b/src/pages/WorkListPage/WorkListPage.test.tsx index 17824a7..a6279a0 100644 --- a/src/pages/WorkListPage/WorkListPage.test.tsx +++ b/src/pages/WorkListPage/WorkListPage.test.tsx @@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter } from 'react-router-dom' import { describe, expect, it } from 'vitest' +import railStyles from '../../components/ui/WorkItemRow/WorkItemRow.module.css' import { WorkListPage } from './WorkListPage' import { WORK_ITEMS, WORK_TABS } from './workListData' @@ -103,4 +104,18 @@ describe('WorkListPage', () => { expect(screen.getByText('7월 외부기관 제출자료 취합')).toBeInTheDocument() expect(screen.queryByText('응웬반A 체류연장 준비')).not.toBeInTheDocument() }) + + it('colors the rail by urgency tier derived from dueDays', () => { + renderPage() + + const urgentItem = WORK_ITEMS.find((item) => item.dueDays <= 7)! + const mediumItem = WORK_ITEMS.find((item) => item.dueDays > 7)! + + const urgentRail = screen.getByText(urgentItem.title).closest('button')!.querySelector('[aria-hidden="true"]') + const mediumRail = screen.getByText(mediumItem.title).closest('button')!.querySelector('[aria-hidden="true"]') + + expect(urgentRail).toHaveClass(railStyles.railCritical) + expect(mediumRail).not.toHaveClass(railStyles.railCritical) + expect(mediumRail).not.toHaveClass(railStyles.railNeutral) + }) }) diff --git a/src/pages/WorkListPage/WorkListPage.tsx b/src/pages/WorkListPage/WorkListPage.tsx index f709849..98a512f 100644 --- a/src/pages/WorkListPage/WorkListPage.tsx +++ b/src/pages/WorkListPage/WorkListPage.tsx @@ -2,11 +2,18 @@ import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Dropdown } from '../../components/ui/Dropdown/Dropdown' import { EmptyState } from '../../components/ui/EmptyState/EmptyState' -import { WorkItemRow } from '../../components/ui/WorkItemRow/WorkItemRow' +import { WorkItemRow, type WorkItemUrgency } from '../../components/ui/WorkItemRow/WorkItemRow' import { useAsyncDemoData } from '../../hooks/useAsyncDemoData' +import { getUrgencyTier } from '../../utils/urgency' import styles from './WorkListPage.module.css' import { TOTAL_WORK_COUNT, WORK_ITEMS, WORK_TABS, type WorkTabId } from './workListData' +const URGENCY_TIER_ROW_CLASS: Record, WorkItemUrgency> = { + urgent: 'critical', + medium: 'warning', + comfortable: 'neutral', +} + const STATUS_OPTIONS = [ { value: 'all', label: '상태 · 전체' }, { value: 'pending', label: '상태 · 승인 대기' }, @@ -148,7 +155,7 @@ export function WorkListPage() { title={item.title} meta={item.meta} nextAction={item.nextAction} - urgency={item.urgency} + urgency={URGENCY_TIER_ROW_CLASS[getUrgencyTier(item.dueDays)]} onClick={() => navigate(`/tasks/${item.id}`)} /> ))} diff --git a/src/pages/WorkListPage/workListData.ts b/src/pages/WorkListPage/workListData.ts index f0bc553..d206206 100644 --- a/src/pages/WorkListPage/workListData.ts +++ b/src/pages/WorkListPage/workListData.ts @@ -1,5 +1,3 @@ -import type { WorkItemUrgency } from '../../components/ui/WorkItemRow/WorkItemRow' - // TODO(backend): GET /api/work-items?tab=&status=&due=&q= -> WORK_TABS, WORK_ITEMS 대체 // 백엔드 연동 전까지는 tabIds/status/dueDays 기준으로 클라이언트에서 직접 필터링한다. @@ -24,7 +22,6 @@ export interface WorkListItem { title: string meta: string nextAction: string - urgency: WorkItemUrgency tabIds: WorkTabId[] status: WorkItemStatus dueDays: number @@ -36,7 +33,6 @@ export const WORK_ITEMS: WorkListItem[] = [ title: '응웬반A 체류연장 준비', meta: 'D-12 · 승인 대기 · 체류 · 김경민', nextAction: '다음 · 요청문 승인', - urgency: 'warning', tabIds: ['mine', 'my-approval'], status: 'pending', dueDays: 12, @@ -46,7 +42,6 @@ export const WORK_ITEMS: WorkListItem[] = [ title: '외국인등록증 사본 제출 요청', meta: '오늘 · 근로자 응답 대기 · 서류', nextAction: '다음 · 응답 확인', - urgency: 'warning', tabIds: ['mine'], status: 'waiting-response', dueDays: 0, @@ -56,7 +51,6 @@ export const WORK_ITEMS: WorkListItem[] = [ title: '7월 외부기관 제출자료 취합', meta: 'D-2 · 증빙 필요 · 외부기관 · 박서준', nextAction: '다음 · 자료 검토', - urgency: 'warning', tabIds: ['follow-up'], status: 'other', dueDays: 2, @@ -66,7 +60,6 @@ export const WORK_ITEMS: WorkListItem[] = [ title: '신규 입사자 교육 일정 확정', meta: 'D-4 · 담당자 미배정 · 일반행정', nextAction: '다음 · 담당자 지정', - urgency: 'warning', tabIds: ['follow-up'], status: 'other', dueDays: 4, @@ -76,7 +69,6 @@ export const WORK_ITEMS: WorkListItem[] = [ title: '월간 기숙사 점검 결과 정리', meta: 'D-7 · 준비 중 · 연결 근로자 없음', nextAction: '다음 · 체크리스트', - urgency: 'warning', tabIds: ['follow-up'], status: 'other', dueDays: 7, diff --git a/src/pages/WorkerListPage/WorkerListPage.module.css b/src/pages/WorkerListPage/WorkerListPage.module.css index c5e6c6c..09446df 100644 --- a/src/pages/WorkerListPage/WorkerListPage.module.css +++ b/src/pages/WorkerListPage/WorkerListPage.module.css @@ -109,11 +109,18 @@ .workerDeadline { font-size: 13px; font-weight: 500; +} + +.workerDeadlineUrgent { + color: var(--status-critical); +} + +.workerDeadlineMedium { color: var(--status-warning); } -.workerDeadlineNormal { - color: var(--text-secondary); +.workerDeadlineComfortable { + color: var(--fowoco-green-600); } .workerMeta { diff --git a/src/pages/WorkerListPage/WorkerListPage.test.tsx b/src/pages/WorkerListPage/WorkerListPage.test.tsx index 73b66e7..48a702e 100644 --- a/src/pages/WorkerListPage/WorkerListPage.test.tsx +++ b/src/pages/WorkerListPage/WorkerListPage.test.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event' import { MemoryRouter, Route, Routes } from 'react-router-dom' import { describe, expect, it } from 'vitest' import { WorkerListPage } from './WorkerListPage' +import styles from './WorkerListPage.module.css' import { WORKERS } from './workerListData' function renderPage(demoState = 'success', initialPath = `/workers?demoState=${demoState}`) { @@ -101,4 +102,16 @@ describe('WorkerListPage', () => { expect(screen.getByText('솜차이E')).toBeInTheDocument() expect(screen.queryByText('아흐메드D')).not.toBeInTheDocument() }) + + it('colors the deadline text by urgency tier', () => { + renderPage() + + const mediumWorker = WORKERS[1] + const comfortableWorker = WORKERS.find((worker) => worker.deadlineDays === null)! + + expect(screen.getByText(mediumWorker.deadlineLabel)).toHaveClass(styles.workerDeadlineMedium) + expect(screen.getByText(comfortableWorker.deadlineLabel)).toHaveClass( + styles.workerDeadlineComfortable, + ) + }) }) diff --git a/src/pages/WorkerListPage/WorkerListPage.tsx b/src/pages/WorkerListPage/WorkerListPage.tsx index fe02487..f0632dd 100644 --- a/src/pages/WorkerListPage/WorkerListPage.tsx +++ b/src/pages/WorkerListPage/WorkerListPage.tsx @@ -4,6 +4,7 @@ import { Dropdown } from '../../components/ui/Dropdown/Dropdown' import { EmptyState } from '../../components/ui/EmptyState/EmptyState' import { StatusLabel } from '../../components/ui/StatusLabel/StatusLabel' import { useAsyncDemoData } from '../../hooks/useAsyncDemoData' +import { getUrgencyTier, URGENCY_TONE } from '../../utils/urgency' import styles from './WorkerListPage.module.css' import { TOTAL_WORKER_COUNT, WORKERS } from './workerListData' @@ -12,6 +13,12 @@ const TASK_LINK_CLASS = { primary: styles.taskLinkPrimary, } +const DEADLINE_TIER_CLASS = { + urgent: styles.workerDeadlineUrgent, + medium: styles.workerDeadlineMedium, + comfortable: styles.workerDeadlineComfortable, +} + const DEADLINE_OPTIONS = [ { value: '30', label: '기한 · 30일' }, { value: '60', label: '기한 · 60일' }, @@ -41,6 +48,7 @@ export function WorkerListPage() { }, [query, deadlineFilter]) const selectedWorker = WORKERS.find((worker) => worker.id === workerId) ?? WORKERS[0] + const selectedDeadlineTier = getUrgencyTier(selectedWorker.deadlineDays) function handleViewAllWorkers() { // TODO(backend): GET /api/workers?page= -> 전체 근로자 페이지네이션 @@ -130,7 +138,7 @@ export function WorkerListPage() {

{worker.name}

{worker.deadlineLabel} @@ -152,8 +160,10 @@ export function WorkerListPage() {

{selectedWorker.name}

- {selectedWorker.deadlineHighlighted && ( - {selectedWorker.deadlineLabel} + {selectedDeadlineTier !== 'comfortable' && ( + + {selectedWorker.deadlineLabel} + )}

diff --git a/src/pages/WorkerListPage/workerListData.ts b/src/pages/WorkerListPage/workerListData.ts index 5165f6b..8ad3fad 100644 --- a/src/pages/WorkerListPage/workerListData.ts +++ b/src/pages/WorkerListPage/workerListData.ts @@ -22,7 +22,6 @@ export interface Worker { employeeId: string phone: string deadlineLabel: string - deadlineHighlighted: boolean /** null이면 임박한 기한이 없어 기한 필터 값과 무관하게 항상 표시한다. */ deadlineDays: number | null note: string @@ -39,7 +38,6 @@ export const WORKERS: Worker[] = [ employeeId: 'F-021', phone: '010-****-3182', deadlineLabel: 'D-12 체류', - deadlineHighlighted: true, deadlineDays: 12, note: '진행 업무 2건', currentTasks: [ @@ -61,7 +59,6 @@ export const WORKERS: Worker[] = [ employeeId: 'F-018', phone: '010-****-2091', deadlineLabel: 'D-21 서류', - deadlineHighlighted: true, deadlineDays: 21, note: '서류 누락 1건', currentTasks: [{ title: '외국인등록증 사본 제출 요청', detail: '오늘 · 근로자 응답 대기', linkTone: 'warning' }], @@ -75,7 +72,6 @@ export const WORKERS: Worker[] = [ employeeId: 'F-032', phone: '010-****-7745', deadlineLabel: 'D-35 체류', - deadlineHighlighted: true, deadlineDays: 35, note: '응답 대기 1건', currentTasks: [{ title: '체류연장 안내 응답 대기', detail: '오늘 · 근로자 응답 대기', linkTone: 'warning' }], @@ -89,7 +85,6 @@ export const WORKERS: Worker[] = [ employeeId: 'F-014', phone: '010-****-5510', deadlineLabel: 'D-62 체류', - deadlineHighlighted: true, deadlineDays: 62, note: '진행 업무 없음', currentTasks: [], @@ -103,7 +98,6 @@ export const WORKERS: Worker[] = [ employeeId: 'F-027', phone: '010-****-6623', deadlineLabel: '정상', - deadlineHighlighted: false, deadlineDays: null, note: '교육 일정 1건', currentTasks: [{ title: '신규 교육 일정 확정', detail: 'D-4 · 담당자 미배정', linkTone: 'warning' }], diff --git a/src/utils/urgency.test.ts b/src/utils/urgency.test.ts new file mode 100644 index 0000000..f1dc620 --- /dev/null +++ b/src/utils/urgency.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { getUrgencyTier, URGENCY_LABEL, URGENCY_TONE } from './urgency' + +describe('getUrgencyTier', () => { + it('returns urgent within 7 days', () => { + expect(getUrgencyTier(0)).toBe('urgent') + expect(getUrgencyTier(7)).toBe('urgent') + }) + + it('returns medium between 8 and 30 days', () => { + expect(getUrgencyTier(8)).toBe('medium') + expect(getUrgencyTier(30)).toBe('medium') + }) + + it('returns comfortable beyond 30 days', () => { + expect(getUrgencyTier(31)).toBe('comfortable') + expect(getUrgencyTier(90)).toBe('comfortable') + }) + + it('treats null as comfortable (no imminent deadline)', () => { + expect(getUrgencyTier(null)).toBe('comfortable') + }) +}) + +describe('URGENCY_TONE / URGENCY_LABEL', () => { + it('maps every tier to a tone and a Korean label', () => { + for (const tier of ['urgent', 'medium', 'comfortable'] as const) { + expect(URGENCY_TONE[tier]).toBeDefined() + expect(URGENCY_LABEL[tier]).toBeDefined() + } + }) +}) diff --git a/src/utils/urgency.ts b/src/utils/urgency.ts new file mode 100644 index 0000000..0ea9f6c --- /dev/null +++ b/src/utils/urgency.ts @@ -0,0 +1,26 @@ +import type { StatusTone } from '../components/ui/StatusLabel/StatusLabel' + +export type UrgencyTier = 'urgent' | 'medium' | 'comfortable' + +const URGENT_WITHIN_DAYS = 7 +const MEDIUM_WITHIN_DAYS = 30 + +/** null은 임박한 기한이 없다는 뜻이라 항상 '여유'로 취급한다. */ +export function getUrgencyTier(daysUntilDue: number | null): UrgencyTier { + if (daysUntilDue === null) return 'comfortable' + if (daysUntilDue <= URGENT_WITHIN_DAYS) return 'urgent' + if (daysUntilDue <= MEDIUM_WITHIN_DAYS) return 'medium' + return 'comfortable' +} + +export const URGENCY_TONE: Record = { + urgent: 'critical', + medium: 'warning', + comfortable: 'success', +} + +export const URGENCY_LABEL: Record = { + urgent: '긴급', + medium: '중간', + comfortable: '여유', +}