diff --git a/src/pages/SettingsPage/SettingsPage.module.css b/src/pages/SettingsPage/SettingsPage.module.css index eeffdd4..88ab4fc 100644 --- a/src/pages/SettingsPage/SettingsPage.module.css +++ b/src/pages/SettingsPage/SettingsPage.module.css @@ -40,6 +40,13 @@ border-bottom-color: var(--brand-primary); } +.tabPanel { + display: flex; + flex-direction: column; + gap: 16px; + margin-top: 20px; +} + .cardRow { display: grid; grid-template-columns: 480px 1fr; diff --git a/src/pages/SettingsPage/SettingsPage.test.tsx b/src/pages/SettingsPage/SettingsPage.test.tsx index 0200e6f..32227b4 100644 --- a/src/pages/SettingsPage/SettingsPage.test.tsx +++ b/src/pages/SettingsPage/SettingsPage.test.tsx @@ -2,7 +2,14 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, expect, it } from 'vitest' import { SettingsPage } from './SettingsPage' -import { MEMBERS, SETTINGS_TABS } from './settingsData' +import { + COMPLETION_EVIDENCE_RULES, + DATA_LOG_SETTINGS, + MEMBERS, + PROCESS_STEP_RULES, + SECURITY_LINK_HISTORY, + SETTINGS_TABS, +} from './settingsData' describe('SettingsPage', () => { it('renders every member row', () => { @@ -24,7 +31,7 @@ describe('SettingsPage', () => { expect(toggle).toHaveAttribute('aria-checked', 'true') }) - it('switches the active settings tab', async () => { + it('switches to the security link tab and shows link history', async () => { const user = userEvent.setup() render() @@ -32,5 +39,34 @@ describe('SettingsPage', () => { await user.click(securityTab) expect(securityTab).toHaveAttribute('aria-selected', 'true') + expect(screen.getByText(SECURITY_LINK_HISTORY[0].workerName)).toBeInTheDocument() + expect(screen.queryByText('구성원과 승인 권한')).not.toBeInTheDocument() + }) + + it('switches to the completion evidence tab', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('tab', { name: SETTINGS_TABS[2] })) + + expect(screen.getByText(COMPLETION_EVIDENCE_RULES[0].caseType)).toBeInTheDocument() + }) + + it('switches to the process steps tab', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('tab', { name: SETTINGS_TABS[3] })) + + expect(screen.getByText(PROCESS_STEP_RULES[0].approvalSteps)).toBeInTheDocument() + }) + + it('switches to the data log tab', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('tab', { name: SETTINGS_TABS[4] })) + + expect(screen.getByText(DATA_LOG_SETTINGS[0].value)).toBeInTheDocument() }) }) diff --git a/src/pages/SettingsPage/SettingsPage.tsx b/src/pages/SettingsPage/SettingsPage.tsx index c611f44..e5b96a1 100644 --- a/src/pages/SettingsPage/SettingsPage.tsx +++ b/src/pages/SettingsPage/SettingsPage.tsx @@ -3,9 +3,14 @@ import { DetailRow } from '../../components/ui/DetailRow/DetailRow' import styles from './SettingsPage.module.css' import { APPROVAL_POLICY, + COMPLETION_EVIDENCE_RULES, + DATA_LOG_SETTINGS, FOOTNOTE, MEMBERS, POLICY_SUMMARY, + PROCESS_STEP_RULES, + SECURITY_LINK_HISTORY, + SECURITY_LINK_POLICY, SETTINGS_TABS, type Member, } from './settingsData' @@ -54,85 +59,162 @@ export function SettingsPage() { ))} -
-
-
-

승인 정책

- {APPROVAL_POLICY.badge} -
-

{APPROVAL_POLICY.title}

-

{APPROVAL_POLICY.description}

+ {activeTab === SETTINGS_TABS[0] && ( + <> +
+
+
+

승인 정책

+ {APPROVAL_POLICY.badge} +
+

{APPROVAL_POLICY.title}

+

{APPROVAL_POLICY.description}

+ +
+ {APPROVAL_POLICY.mode} + {APPROVAL_POLICY.modeNote} +
+ +

{APPROVAL_POLICY.warning}

+
-
- {APPROVAL_POLICY.mode} - {APPROVAL_POLICY.modeNote} +
+

{POLICY_SUMMARY.title}

+ {POLICY_SUMMARY.rows.map((row) => ( + + ))} +
-

{APPROVAL_POLICY.warning}

-
- -
-

{POLICY_SUMMARY.title}

- {POLICY_SUMMARY.rows.map((row) => ( - - ))} -
-
- -
-

구성원과 승인 권한

- -
- 이름 / 역할 - 승인 가능 - 상태 -
+
+

구성원과 승인 권한

- {members.map((member, index) => ( -
-
-

{member.name}

-

{member.role}

+
+ 이름 / 역할 + 승인 가능 + 상태
- - {member.approval === 'canApprove' ? '승인 가능' : '승인 요청만'} - - - + + + {member.status} + +
+ ))} + + - - - {member.status} -
- ))} + + )} + + {activeTab === SETTINGS_TABS[1] && ( +
+ {/* TODO(backend): GET /api/settings/security-links -> SECURITY_LINK_POLICY, SECURITY_LINK_HISTORY 대체 */} +
+

{SECURITY_LINK_POLICY.title}

+

{SECURITY_LINK_POLICY.description}

+ +
- -
+
+

최근 발급 이력

+
+ 근로자 + 발급 시각 + 상태 +
+ {SECURITY_LINK_HISTORY.map((entry, index) => ( +
+
+

{entry.workerName}

+
+ {entry.issuedAt} + {entry.status} +
+ ))} +
+
+ )} + + {activeTab === SETTINGS_TABS[2] && ( +
+ {/* TODO(backend): GET /api/settings/completion-evidence -> COMPLETION_EVIDENCE_RULES 대체 */} +
+

업무 유형별 완료 증빙

+ {COMPLETION_EVIDENCE_RULES.map((rule) => ( + + ))} +
+
+ )} + + {activeTab === SETTINGS_TABS[3] && ( +
+ {/* TODO(backend): GET /api/settings/process-steps -> PROCESS_STEP_RULES 대체 */} +
+

업무 유형별 승인 단계

+ {PROCESS_STEP_RULES.map((rule) => ( + + ))} +
+
+ )} + + {activeTab === SETTINGS_TABS[4] && ( +
+ {/* TODO(backend): GET /api/settings/data-log -> DATA_LOG_SETTINGS 대체 */} +
+

데이터·AI 로그 설정

+ {DATA_LOG_SETTINGS.map((setting) => ( + + ))} +
+
+ )}

{FOOTNOTE}

diff --git a/src/pages/SettingsPage/settingsData.ts b/src/pages/SettingsPage/settingsData.ts index f2878a6..ff05ee8 100644 --- a/src/pages/SettingsPage/settingsData.ts +++ b/src/pages/SettingsPage/settingsData.ts @@ -1,3 +1,5 @@ +import type { DetailRowTone } from '../../components/ui/DetailRow/DetailRow' + // TODO(backend): GET /api/settings -> SETTINGS_TABS 이하 상수 대체 // TODO(backend): PATCH /api/settings/members/:id { canApprove } -> 멤버 토글 반영 @@ -43,3 +45,59 @@ export const MEMBERS: Member[] = [ export const FOOTNOTE = 'MVP 운영정책은 출시 전 개인정보·노무·법률 검토가 필요합니다. 일반 HR 사용자는 보관기간을 무기한으로 변경할 수 없습니다.' + +export const SECURITY_LINK_POLICY = { + title: '근로자 업로드 링크 만료 정책', + validity: '72시간', + description: 'LINK-001/002에서 발급되는 보안 링크는 발급 후 72시간이 지나면 자동 만료됩니다.', +} + +export type SecurityLinkStatus = '사용 완료' | '만료' | '대기 중' + +export interface SecurityLinkHistoryEntry { + id: string + workerName: string + issuedAt: string + status: SecurityLinkStatus +} + +export const SECURITY_LINK_HISTORY: SecurityLinkHistoryEntry[] = [ + { id: 'LNK-1', workerName: '응웬반A', issuedAt: '07.19 14:02', status: '사용 완료' }, + { id: 'LNK-2', workerName: '쩐티B', issuedAt: '07.18 09:40', status: '만료' }, + { id: 'LNK-3', workerName: '수라즈C', issuedAt: '07.20 08:15', status: '대기 중' }, +] + +export interface CompletionEvidenceRule { + caseType: string + requirement: string + tone: DetailRowTone +} + +export const COMPLETION_EVIDENCE_RULES: CompletionEvidenceRule[] = [ + { caseType: '일반 내부업무', requirement: '파일 첨부 불필요', tone: 'default' }, + { caseType: '외부기관 업무', requirement: '증빙 1개 이상', tone: 'warning' }, + { caseType: '체류·비자 업무', requirement: '접수번호 필수', tone: 'warning' }, +] + +export interface ProcessStepRule { + caseType: string + approvalSteps: string +} + +export const PROCESS_STEP_RULES: ProcessStepRule[] = [ + { caseType: '일반 내부업무', approvalSteps: '담당자 1인 승인' }, + { caseType: '외부기관 업무', approvalSteps: '담당자 승인 + 증빙 확인' }, + { caseType: '체류·비자 업무', approvalSteps: 'HR 승인 + 접수번호 확인' }, +] + +export interface DataLogSetting { + label: string + value: string + tone: DetailRowTone +} + +export const DATA_LOG_SETTINGS: DataLogSetting[] = [ + { label: 'Agent 접근 범위', value: '근로자 기본정보·업무 컨텍스트', tone: 'default' }, + { label: 'AI 원문 로그', value: '30일 후 삭제·비식별화', tone: 'warning' }, + { label: '비식별 품질 로그', value: '12개월 보관', tone: 'default' }, +]