From b2e83bac967db2d90c74895d885bcae92b4122ea Mon Sep 17 00:00:00 2001
From: mini
Date: Wed, 12 Aug 2026 10:49:56 +0900
Subject: [PATCH] =?UTF-8?q?fix:=20=EB=8D=B0=EB=AA=A8=20=EB=A1=9C=EA=B7=B8?=
=?UTF-8?q?=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95=20+=20=ED=94=84=EB=A1=9C=ED=95=84=20?=
=?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=8B=A4=EC=A0=9C=20=EA=B3=84=EC=A0=95=20?=
=?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=91=9C=EC=8B=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- DEMO_ACCOUNT.email을 mini@naver.com -> demo.admin@example.com으로 수정
(실제 서버 데모 시드 계정과 일치, '데모로 시작' 버튼이 여태 실패하고 있었음)
- authStore에 email 필드 추가, 로그인 시점에 저장
- ProfilePage 요약 카드·로그인 이메일 필드를 실제 로그인 계정 정보로 표시
(기존엔 로그인 계정과 무관한 '김민지' 목업 정보가 항상 떴음)
closes #329
---
.../HeaderActions/HeaderActions.test.tsx | 42 +++++++++-----
.../CaseDetailPage/CaseDetailPage.test.tsx | 4 +-
.../ProfilePage/CompanySettingsPanel.test.tsx | 56 +++++++++++--------
src/pages/ProfilePage/ProfilePage.test.tsx | 29 +++++++++-
src/pages/ProfilePage/ProfilePage.tsx | 52 ++++++++++++-----
src/store/authStore.test.ts | 21 +++++--
src/store/authStore.ts | 16 +++++-
7 files changed, 155 insertions(+), 65 deletions(-)
diff --git a/src/components/layout/HeaderActions/HeaderActions.test.tsx b/src/components/layout/HeaderActions/HeaderActions.test.tsx
index 53e4b7a..963a7ff 100644
--- a/src/components/layout/HeaderActions/HeaderActions.test.tsx
+++ b/src/components/layout/HeaderActions/HeaderActions.test.tsx
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { HeaderActions } from './HeaderActions'
import { getSafeNotificationRoute } from './notificationPresentation'
-const USER = { name: '김민지', workplace: '한빛정밀', role: 'HR' }
+const USER = { name: '김민지', email: 'kim@example.com', workplace: '한빛정밀', role: 'HR' }
const NOTIFICATIONS = {
items: [
{
@@ -94,23 +94,31 @@ describe('HeaderActions', () => {
await user.click(screen.getByRole('menuitem', { name: /체류연장 요청문 승인이 필요합니다/ }))
await waitFor(() => expect(router.state.location.pathname).toBe('/tasks/task-1'))
- expect(vi.mocked(fetch).mock.calls.some(([url, init]) =>
- String(url).includes('/notifications/n1/read') && init?.method === 'POST',
- )).toBe(true)
+ expect(
+ vi
+ .mocked(fetch)
+ .mock.calls.some(
+ ([url, init]) =>
+ String(url).includes('/notifications/n1/read') && init?.method === 'POST',
+ ),
+ ).toBe(true)
})
it('shows an error and keeps the panel open when read processing fails', async () => {
vi.mocked(fetch).mockImplementation(async (_input, init) => {
if (init?.method === 'POST') {
- return jsonResponse({
- timestamp: '2026-08-10T01:00:00Z',
- status: 500,
- code: 'INTERNAL_SERVER_ERROR',
- message: 'failed',
- path: '/api/v1/notifications/n1/read',
- request_id: 'request-1',
- field_errors: [],
- }, 500)
+ return jsonResponse(
+ {
+ timestamp: '2026-08-10T01:00:00Z',
+ status: 500,
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'failed',
+ path: '/api/v1/notifications/n1/read',
+ request_id: 'request-1',
+ field_errors: [],
+ },
+ 500,
+ )
}
return jsonResponse(NOTIFICATIONS)
})
@@ -120,7 +128,9 @@ describe('HeaderActions', () => {
await user.click(await screen.findByLabelText('알림 1건 안 읽음'))
await user.click(screen.getByRole('menuitem', { name: /체류연장 요청문 승인이 필요합니다/ }))
- expect(await screen.findByText('일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.')).toBeInTheDocument()
+ expect(
+ await screen.findByText('일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'),
+ ).toBeInTheDocument()
expect(router.state.location.pathname).toBe('/dashboard')
expect(screen.getByRole('menu', { name: '알림 목록' })).toBeInTheDocument()
})
@@ -156,7 +166,9 @@ describe('HeaderActions', () => {
describe('getSafeNotificationRoute', () => {
it('keeps expected internal routes and rejects external or unexpected routes', () => {
- expect(getSafeNotificationRoute('/documents/document-1?tab=ocr')).toBe('/documents/document-1?tab=ocr')
+ expect(getSafeNotificationRoute('/documents/document-1?tab=ocr')).toBe(
+ '/documents/document-1?tab=ocr',
+ )
expect(getSafeNotificationRoute('https://example.com')).toBe('/dashboard')
expect(getSafeNotificationRoute('//example.com/tasks/1')).toBe('/dashboard')
expect(getSafeNotificationRoute('/profile')).toBe('/dashboard')
diff --git a/src/pages/CaseDetailPage/CaseDetailPage.test.tsx b/src/pages/CaseDetailPage/CaseDetailPage.test.tsx
index bf8dd8d..0fee01b 100644
--- a/src/pages/CaseDetailPage/CaseDetailPage.test.tsx
+++ b/src/pages/CaseDetailPage/CaseDetailPage.test.tsx
@@ -706,7 +706,7 @@ describe('CaseDetailPage', () => {
it('does not offer the response review action to a viewer', async () => {
const user = userEvent.setup()
useAuthStore.setState({
- user: { name: 'viewer', workplace: 'FOWOCO', role: 'VIEWER' },
+ user: { name: 'viewer', email: 'viewer@example.com', workplace: 'FOWOCO', role: 'VIEWER' },
status: 'ready',
})
mockTaskAndActivities(
@@ -739,7 +739,7 @@ describe('CaseDetailPage', () => {
it('adopts a submitted file as an official worker document', async () => {
const user = userEvent.setup()
useAuthStore.setState({
- user: { name: 'hr', workplace: 'FOWOCO', role: 'HR' },
+ user: { name: 'hr', email: 'hr@example.com', workplace: 'FOWOCO', role: 'HR' },
status: 'ready',
})
mockTaskAndActivities(
diff --git a/src/pages/ProfilePage/CompanySettingsPanel.test.tsx b/src/pages/ProfilePage/CompanySettingsPanel.test.tsx
index e7cbb80..4edf2c2 100644
--- a/src/pages/ProfilePage/CompanySettingsPanel.test.tsx
+++ b/src/pages/ProfilePage/CompanySettingsPanel.test.tsx
@@ -53,7 +53,7 @@ function renderPanel() {
beforeEach(() => {
useAuthStore.setState({
- user: { name: 'admin', workplace: 'FOWOCO', role: 'ADMIN' },
+ user: { name: 'admin', email: 'admin@example.com', workplace: 'FOWOCO', role: 'ADMIN' },
status: 'ready',
})
useToastStore.setState({ toasts: [] })
@@ -108,7 +108,9 @@ describe('CompanySettingsPanel', () => {
})
it('renders HR and VIEWER settings as read-only', async () => {
- useAuthStore.setState({ user: { name: 'hr', workplace: 'FOWOCO', role: 'HR' } })
+ useAuthStore.setState({
+ user: { name: 'hr', email: 'hr@example.com', workplace: 'FOWOCO', role: 'HR' },
+ })
renderPanel()
expect(await screen.findByText('HR 조회 전용')).toBeInTheDocument()
@@ -122,21 +124,22 @@ describe('CompanySettingsPanel', () => {
const url = String(input)
if (url.includes('/company-members')) return jsonResponse(MEMBERS)
if (init?.method === 'PATCH') {
- return jsonResponse({
- timestamp: '2026-08-10T01:00:00Z',
- status: 409,
- code: 'CONCURRENT_MODIFICATION',
- message: 'conflict',
- path: '/api/v1/settings',
- request_id: 'request-1',
- field_errors: [],
- }, 409)
+ return jsonResponse(
+ {
+ timestamp: '2026-08-10T01:00:00Z',
+ status: 409,
+ code: 'CONCURRENT_MODIFICATION',
+ message: 'conflict',
+ path: '/api/v1/settings',
+ request_id: 'request-1',
+ field_errors: [],
+ },
+ 409,
+ )
}
settingsGetCount += 1
return jsonResponse(
- settingsGetCount === 1
- ? SETTINGS
- : { ...SETTINGS, link_expiry_hours: 24, version: 4 },
+ settingsGetCount === 1 ? SETTINGS : { ...SETTINGS, link_expiry_hours: 24, version: 4 },
)
})
const user = userEvent.setup()
@@ -153,20 +156,25 @@ describe('CompanySettingsPanel', () => {
it('shows a recoverable error when settings cannot be loaded', async () => {
vi.mocked(fetch).mockImplementation(async (input) => {
if (String(input).includes('/company-members')) return jsonResponse(MEMBERS)
- return jsonResponse({
- timestamp: '2026-08-10T01:00:00Z',
- status: 500,
- code: 'INTERNAL_SERVER_ERROR',
- message: 'failed',
- path: '/api/v1/settings',
- request_id: 'request-1',
- field_errors: [],
- }, 500)
+ return jsonResponse(
+ {
+ timestamp: '2026-08-10T01:00:00Z',
+ status: 500,
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'failed',
+ path: '/api/v1/settings',
+ request_id: 'request-1',
+ field_errors: [],
+ },
+ 500,
+ )
})
renderPanel()
- expect(await screen.findByText('일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.')).toBeInTheDocument()
+ expect(
+ await screen.findByText('일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.'),
+ ).toBeInTheDocument()
expect(screen.getByRole('button', { name: '다시 시도' })).toBeInTheDocument()
})
})
diff --git a/src/pages/ProfilePage/ProfilePage.test.tsx b/src/pages/ProfilePage/ProfilePage.test.tsx
index 8156761..dbf3b28 100644
--- a/src/pages/ProfilePage/ProfilePage.test.tsx
+++ b/src/pages/ProfilePage/ProfilePage.test.tsx
@@ -1,7 +1,8 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createMemoryRouter, RouterProvider } from 'react-router-dom'
-import { beforeEach, describe, expect, it } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
+import { useAuthStore } from '../../store/authStore'
import { ToastViewport } from '../../components/ui/ToastViewport/ToastViewport'
import { useToastStore } from '../../store/toastStore'
import { ProfilePage } from './ProfilePage'
@@ -31,6 +32,10 @@ beforeEach(() => {
useToastStore.setState({ toasts: [] })
})
+afterEach(() => {
+ useAuthStore.setState({ user: null })
+})
+
describe('ProfilePage', () => {
it('renders the profile summary and read-only fields', () => {
renderPage()
@@ -42,6 +47,24 @@ describe('ProfilePage', () => {
expect(screen.getByText('체류·문서 운영')).toBeInTheDocument()
})
+ it("shows the real logged-in user's identity instead of the fixture persona", () => {
+ useAuthStore.setState({
+ user: {
+ name: 'demo.admin',
+ email: 'demo.admin@example.com',
+ workplace: 'FOWOCO Demo Company',
+ role: 'ADMIN',
+ },
+ status: 'ready',
+ })
+ renderPage()
+
+ expect(screen.getAllByText('demo.admin').length).toBeGreaterThan(0)
+ expect(screen.getAllByText(/demo\.admin@example\.com/).length).toBeGreaterThan(0)
+ expect(screen.getByText('FOWOCO Demo Company')).toBeInTheDocument()
+ expect(screen.queryByText('hr.demo@fowoco.example')).not.toBeInTheDocument()
+ })
+
it('edits and saves the editable fields', async () => {
const user = userEvent.setup()
renderPage()
@@ -156,7 +179,9 @@ describe('ProfilePage', () => {
await user.type(screen.getByLabelText('연락처'), '9')
await user.click(screen.getByRole('button', { name: '비밀번호 변경' }))
- expect(screen.getByRole('dialog', { name: '저장하지 않은 변경사항이 있습니다.' })).toBeInTheDocument()
+ expect(
+ screen.getByRole('dialog', { name: '저장하지 않은 변경사항이 있습니다.' }),
+ ).toBeInTheDocument()
expect(screen.getByText(/변경사항 1개/)).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: '계속 수정' }))
diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx
index aade3f8..c15a2fb 100644
--- a/src/pages/ProfilePage/ProfilePage.tsx
+++ b/src/pages/ProfilePage/ProfilePage.tsx
@@ -4,6 +4,7 @@ import { Button } from '../../components/ui/Button/Button'
import { DetailRow } from '../../components/ui/DetailRow/DetailRow'
import { Modal } from '../../components/ui/Modal/Modal'
import { StatusLabel } from '../../components/ui/StatusLabel/StatusLabel'
+import { useAuthStore } from '../../store/authStore'
import { useToastStore } from '../../store/toastStore'
import { CompanySettingsPanel } from './CompanySettingsPanel'
import {
@@ -49,20 +50,28 @@ function validateFields(input: EditableProfileFields): FieldErrors {
export function ProfilePage() {
const navigate = useNavigate()
const showToast = useToastStore((state) => state.showToast)
+ const user = useAuthStore((state) => state.user)
- const [fields, setFields] = useState(INITIAL_PROFILE_FIELDS)
- const [draft, setDraft] = useState(INITIAL_PROFILE_FIELDS)
+ const initialFields: EditableProfileFields = {
+ ...INITIAL_PROFILE_FIELDS,
+ name: user?.name ?? INITIAL_PROFILE_FIELDS.name,
+ }
+ const [fields, setFields] = useState(initialFields)
+ const [draft, setDraft] = useState(initialFields)
const [editing, setEditing] = useState(false)
const [fieldErrors, setFieldErrors] = useState({})
const [notificationPrefs, setNotificationPrefs] = useState(INITIAL_NOTIFICATION_PREFS)
- const changedFieldCount = EDITABLE_FIELD_META.filter(({ key }) => draft[key] !== fields[key]).length
+ const changedFieldCount = EDITABLE_FIELD_META.filter(
+ ({ key }) => draft[key] !== fields[key],
+ ).length
const isDirty = editing && changedFieldCount > 0
// Figma "저장하지 않은 변경사항이 있습니다" 오버레이(node 1623:2530) — 편집 중 다른 화면으로
// 이동하려 하면 확인을 받는다.
const blocker = useBlocker(
- ({ currentLocation, nextLocation }) => isDirty && currentLocation.pathname !== nextLocation.pathname,
+ ({ currentLocation, nextLocation }) =>
+ isDirty && currentLocation.pathname !== nextLocation.pathname,
)
function handleStartEdit() {
@@ -94,7 +103,9 @@ export function ProfilePage() {
function handleToggleNotification(id: string) {
setNotificationPrefs((prev) =>
- prev.map((pref) => (pref.id === id && !pref.required ? { ...pref, enabled: !pref.enabled } : pref)),
+ prev.map((pref) =>
+ pref.id === id && !pref.required ? { ...pref, enabled: !pref.enabled } : pref,
+ ),
)
}
@@ -133,16 +144,18 @@ export function ProfilePage() {
- {PROFILE_SUMMARY.initial}
+ {fields.name.charAt(0) || PROFILE_SUMMARY.initial}
{fields.name}
- {PROFILE_SUMMARY.role} · {PROFILE_SUMMARY.email}
+ {user?.role ?? PROFILE_SUMMARY.role} · {user?.email ?? PROFILE_SUMMARY.email}
사용 중
- {PROFILE_SUMMARY.companyName}
+
+ {user?.workplace ?? PROFILE_SUMMARY.companyName}
+
@@ -175,7 +188,9 @@ export function ProfilePage() {
className={styles.fieldInput}
value={draft[key]}
aria-label={label}
- onChange={(event) => setDraft((prev) => ({ ...prev, [key]: event.target.value }))}
+ onChange={(event) =>
+ setDraft((prev) => ({ ...prev, [key]: event.target.value }))
+ }
/>
{fieldErrors[key] &&
{fieldErrors[key]}
}
>
@@ -191,8 +206,12 @@ export function ProfilePage() {
로그인 이메일
본인 확인 필요
-
{PROFILE_SUMMARY.email}
-
@@ -265,7 +284,10 @@ export function ProfilePage() {
계정 보호 상태와 최근 로그인 정보를 확인합니다.
- {SECURITY_INFO.accountStatus}} />
+ {SECURITY_INFO.accountStatus}}
+ />
@@ -296,7 +318,11 @@ export function ProfilePage() {
변경사항 {changedFieldCount}개 · 입력값은 현재 편집 화면에 유지됩니다.
-
+
계속 수정
diff --git a/src/store/authStore.test.ts b/src/store/authStore.test.ts
index 293f3c1..22c7a4e 100644
--- a/src/store/authStore.test.ts
+++ b/src/store/authStore.test.ts
@@ -75,14 +75,17 @@ describe('useAuthStore.login', () => {
expect(result).toEqual({ success: true })
expect(getAccessToken()).toBe('access-1')
- expect(useAuthStore.getState().user).toEqual({ name: 'mini', workplace: '한빛정밀', role: 'HR' })
+ expect(useAuthStore.getState().user).toEqual({
+ name: 'mini',
+ email: 'mini@naver.com',
+ workplace: '한빛정밀',
+ role: 'HR',
+ })
expect(useAuthStore.getState().status).toBe('ready')
})
it('returns a translated error message on invalid credentials', async () => {
- vi.mocked(fetch).mockResolvedValueOnce(
- errorResponse(401, 'INVALID_CREDENTIALS', 'raw'),
- )
+ vi.mocked(fetch).mockResolvedValueOnce(errorResponse(401, 'INVALID_CREDENTIALS', 'raw'))
const result = await useAuthStore.getState().login('wrong@example.com', 'wrongpass')
@@ -138,14 +141,19 @@ describe('useAuthStore.restoreSession', () => {
// 저장이 조용히 실패할 수 있다 (구현도 이 상황을 try/catch로 감내하도록 설계했다).
// 그래서 여기서는 실제로 저장에 성공했는지를 먼저 확인하고, 그 결과에 맞는 기대값으로
// 검증한다 — 저장에 성공하면 저장된 이름을, 실패하면 authStore의 fallback("사용자")을 기대한다.
- setTestLocalStorage('fowoco.auth.profile', JSON.stringify({ name: 'mini', workplace: '한빛정밀' }))
+ setTestLocalStorage(
+ 'fowoco.auth.profile',
+ JSON.stringify({ name: 'mini', email: 'mini@naver.com', workplace: '한빛정밀' }),
+ )
let expectedName = '사용자'
+ let expectedEmail = ''
let expectedWorkplace = ''
try {
const raw = localStorage.getItem('fowoco.auth.profile')
if (raw) {
- const parsed = JSON.parse(raw) as { name: string; workplace: string }
+ const parsed = JSON.parse(raw) as { name: string; email: string; workplace: string }
expectedName = parsed.name
+ expectedEmail = parsed.email
expectedWorkplace = parsed.workplace
}
} catch {
@@ -168,6 +176,7 @@ describe('useAuthStore.restoreSession', () => {
expect(useAuthStore.getState().status).toBe('ready')
expect(useAuthStore.getState().user).toEqual({
name: expectedName,
+ email: expectedEmail,
workplace: expectedWorkplace,
role: 'HR',
})
diff --git a/src/store/authStore.ts b/src/store/authStore.ts
index 294e932..33fcab1 100644
--- a/src/store/authStore.ts
+++ b/src/store/authStore.ts
@@ -7,12 +7,13 @@ import { ApiError, getErrorMessage } from '../api/errors'
// 계정 만들기" 참고). 비밀번호는 서버가 DEMO_SEED_ADMIN_PASSWORD 최소 길이(12자)를 강제하므로
// "1234"처럼 짧은 값은 쓸 수 없다 — 로컬 seed 값과 반드시 일치시켜야 한다.
export const DEMO_ACCOUNT = {
- email: 'mini@naver.com',
+ email: 'demo.admin@example.com',
password: 'fowoco-demo-1234',
}
export interface AuthUser {
name: string
+ email: string
workplace: string
role: string
}
@@ -60,6 +61,7 @@ const PROFILE_STORAGE_KEY = 'fowoco.auth.profile'
interface PersistedProfile {
name: string
+ email: string
workplace: string
}
@@ -126,12 +128,19 @@ export const useAuthStore = create
((set) => {
})
setAccessToken(body.access_token)
- const profile: PersistedProfile = { name: email.split('@')[0], workplace: body.company_name }
+ const profile: PersistedProfile = {
+ name: email.split('@')[0],
+ email,
+ workplace: body.company_name,
+ }
persistProfile(profile)
set({ user: { ...profile, role: body.role }, status: 'ready' })
return { success: true }
} catch (error) {
- return { success: false, message: toApiErrorMessage(error, '알 수 없는 오류가 발생했습니다.') }
+ return {
+ success: false,
+ message: toApiErrorMessage(error, '알 수 없는 오류가 발생했습니다.'),
+ }
}
},
@@ -166,6 +175,7 @@ export const useAuthStore = create((set) => {
set({
user: {
name: persisted?.name ?? '사용자',
+ email: persisted?.email ?? '',
workplace: persisted?.workplace ?? '',
role: me.roles[0] ?? '',
},