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
15 changes: 15 additions & 0 deletions src/pages/WorkListPage/WorkListPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
})
})
11 changes: 9 additions & 2 deletions src/pages/WorkListPage/WorkListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof getUrgencyTier>, WorkItemUrgency> = {
urgent: 'critical',
medium: 'warning',
comfortable: 'neutral',
}

const STATUS_OPTIONS = [
{ value: 'all', label: '상태 · 전체' },
{ value: 'pending', label: '상태 · 승인 대기' },
Expand Down Expand Up @@ -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}`)}
/>
))}
Expand Down
8 changes: 0 additions & 8 deletions src/pages/WorkListPage/workListData.ts
Original file line number Diff line number Diff line change
@@ -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 기준으로 클라이언트에서 직접 필터링한다.

Expand All @@ -24,7 +22,6 @@ export interface WorkListItem {
title: string
meta: string
nextAction: string
urgency: WorkItemUrgency
tabIds: WorkTabId[]
status: WorkItemStatus
dueDays: number
Expand All @@ -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,
Expand All @@ -46,7 +42,6 @@ export const WORK_ITEMS: WorkListItem[] = [
title: '외국인등록증 사본 제출 요청',
meta: '오늘 · 근로자 응답 대기 · 서류',
nextAction: '다음 · 응답 확인',
urgency: 'warning',
tabIds: ['mine'],
status: 'waiting-response',
dueDays: 0,
Expand All @@ -56,7 +51,6 @@ export const WORK_ITEMS: WorkListItem[] = [
title: '7월 외부기관 제출자료 취합',
meta: 'D-2 · 증빙 필요 · 외부기관 · 박서준',
nextAction: '다음 · 자료 검토',
urgency: 'warning',
tabIds: ['follow-up'],
status: 'other',
dueDays: 2,
Expand All @@ -66,7 +60,6 @@ export const WORK_ITEMS: WorkListItem[] = [
title: '신규 입사자 교육 일정 확정',
meta: 'D-4 · 담당자 미배정 · 일반행정',
nextAction: '다음 · 담당자 지정',
urgency: 'warning',
tabIds: ['follow-up'],
status: 'other',
dueDays: 4,
Expand All @@ -76,7 +69,6 @@ export const WORK_ITEMS: WorkListItem[] = [
title: '월간 기숙사 점검 결과 정리',
meta: 'D-7 · 준비 중 · 연결 근로자 없음',
nextAction: '다음 · 체크리스트',
urgency: 'warning',
tabIds: ['follow-up'],
status: 'other',
dueDays: 7,
Expand Down
11 changes: 9 additions & 2 deletions src/pages/WorkerListPage/WorkerListPage.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions src/pages/WorkerListPage/WorkerListPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`) {
Expand Down Expand Up @@ -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,
)
})
})
16 changes: 13 additions & 3 deletions src/pages/WorkerListPage/WorkerListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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일' },
Expand Down Expand Up @@ -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= -> 전체 근로자 페이지네이션
Expand Down Expand Up @@ -130,7 +138,7 @@ export function WorkerListPage() {
<p className={styles.workerName}>{worker.name}</p>
<span
className={`${styles.workerDeadline} ${
worker.deadlineHighlighted ? '' : styles.workerDeadlineNormal
DEADLINE_TIER_CLASS[getUrgencyTier(worker.deadlineDays)]
}`}
>
{worker.deadlineLabel}
Expand All @@ -152,8 +160,10 @@ export function WorkerListPage() {
<div className={styles.detailPanel}>
<div className={styles.detailHeader}>
<h2 className={styles.detailName}>{selectedWorker.name}</h2>
{selectedWorker.deadlineHighlighted && (
<StatusLabel tone="warning">{selectedWorker.deadlineLabel}</StatusLabel>
{selectedDeadlineTier !== 'comfortable' && (
<StatusLabel tone={URGENCY_TONE[selectedDeadlineTier]}>
{selectedWorker.deadlineLabel}
</StatusLabel>
)}
</div>
<p className={styles.detailMeta}>
Expand Down
6 changes: 0 additions & 6 deletions src/pages/WorkerListPage/workerListData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export interface Worker {
employeeId: string
phone: string
deadlineLabel: string
deadlineHighlighted: boolean
/** null이면 임박한 기한이 없어 기한 필터 값과 무관하게 항상 표시한다. */
deadlineDays: number | null
note: string
Expand All @@ -39,7 +38,6 @@ export const WORKERS: Worker[] = [
employeeId: 'F-021',
phone: '010-****-3182',
deadlineLabel: 'D-12 체류',
deadlineHighlighted: true,
deadlineDays: 12,
note: '진행 업무 2건',
currentTasks: [
Expand All @@ -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' }],
Expand All @@ -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' }],
Expand All @@ -89,7 +85,6 @@ export const WORKERS: Worker[] = [
employeeId: 'F-014',
phone: '010-****-5510',
deadlineLabel: 'D-62 체류',
deadlineHighlighted: true,
deadlineDays: 62,
note: '진행 업무 없음',
currentTasks: [],
Expand All @@ -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' }],
Expand Down
32 changes: 32 additions & 0 deletions src/utils/urgency.test.ts
Original file line number Diff line number Diff line change
@@ -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()
}
})
})
26 changes: 26 additions & 0 deletions src/utils/urgency.ts
Original file line number Diff line number Diff line change
@@ -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<UrgencyTier, StatusTone> = {
urgent: 'critical',
medium: 'warning',
comfortable: 'success',
}

export const URGENCY_LABEL: Record<UrgencyTier, string> = {
urgent: '긴급',
medium: '중간',
comfortable: '여유',
}