diff --git a/frontend/src/pages/settings/AccountAppPasswordsPage.spec.ts b/frontend/src/pages/settings/AccountAppPasswordsPage.spec.ts index 6d9ff9d..31ef97e 100644 --- a/frontend/src/pages/settings/AccountAppPasswordsPage.spec.ts +++ b/frontend/src/pages/settings/AccountAppPasswordsPage.spec.ts @@ -17,30 +17,36 @@ import AccountAppPasswordsPage from './AccountAppPasswordsPage.vue' // 설정 › 계정 › 앱 비밀번호 (12단계 — 디자인 원본 SettingsAppPasswords · 계약 5절 "앱 비밀번호") const USER = { email: 'ara.kim@arasystem.it.kr', name: '김아라' } -/** 한국 시간 2026-10-31 (토) 낮 12시 */ -const NOW = Date.parse('2026-10-31T03:00:00Z') +/** + * 2026년 그 시각을 이 PC 의 시간대(지역 시각)로 — 화면은 시각을 브라우저 시간대로 보인다. + * "오늘 오전 9:12" 같은 글을 그대로 보려면 시각을 지역 시각으로 적어야 어느 시간대에서도 같다 + * (UTC 로 적었더니 개발 PC(KST)에서만 통과하고 CI(UTC)에서 실패했다 — 2026-11-02). + */ +const local = (month: number, day: number, hour: number, minute = 0) => new Date(2026, month - 1, day, hour, minute).toISOString() +/** 2026-10-31 (토) 낮 12시 */ +const NOW = Date.parse(local(10, 31, 12)) const VALUE = 'app_7qm2xk9vtn3wpl1srb3hcy7djf2gaz1euk0n' const ITEMS: AppPassword[] = [ - { id: 'c', name: '노트북 썬더버드', createdAt: '2026-10-31T00:20:00Z', lastUsed: null }, + { id: 'c', name: '노트북 썬더버드', createdAt: local(10, 31, 9, 20), lastUsed: null }, { id: 'b', name: '회사 PC 아웃룩', - createdAt: '2026-10-27T00:00:00Z', - lastUsed: { at: '2026-10-31T00:12:00Z', method: 'imap', ip: '211.186.95.252' }, + createdAt: local(10, 27, 9), + lastUsed: { at: local(10, 31, 9, 12), method: 'imap', ip: '211.186.95.252' }, }, { id: 'a', name: '휴대폰 메일 앱', - createdAt: '2026-10-20T00:00:00Z', - lastUsed: { at: '2026-10-30T09:40:00Z', method: 'smtp', ip: '223.38.120.77' }, + createdAt: local(10, 20, 9), + lastUsed: { at: local(10, 30, 18, 40), method: 'smtp', ip: '223.38.120.77' }, }, ] const page = (items: AppPassword[]) => ({ items, pageInfo: { hasNext: false, nextCursor: null, total: items.length } }) function created(name: string): CreatedAppPassword { - return { id: 'd', name, createdAt: '2026-10-31T02:59:00Z', lastUsed: null, password: VALUE } + return { id: 'd', name, createdAt: local(10, 31, 11, 59), lastUsed: null, password: VALUE } } interface Server { @@ -123,12 +129,12 @@ describe('AccountAppPasswordsPage', () => { [ '회사 PC 아웃룩', formatBlockedDay(ITEMS[1]?.createdAt ?? '', nowDate), - `${formatRelativeDayTime('2026-10-31T00:12:00Z', nowDate)} · IMAP · 211.186.95.252`, + `${formatRelativeDayTime(ITEMS[1]?.lastUsed?.at ?? '', nowDate)} · IMAP · 211.186.95.252`, ], [ '휴대폰 메일 앱', formatBlockedDay(ITEMS[2]?.createdAt ?? '', nowDate), - `${formatRelativeDayTime('2026-10-30T09:40:00Z', nowDate)} · SMTP · 223.38.120.77`, + `${formatRelativeDayTime(ITEMS[2]?.lastUsed?.at ?? '', nowDate)} · SMTP · 223.38.120.77`, ], ]) expect(rows(wrapper)[1]?.[2]).toBe('오늘 오전 9:12 · IMAP · 211.186.95.252') diff --git a/frontend/src/pages/settings/AccountLoginHistoryPage.spec.ts b/frontend/src/pages/settings/AccountLoginHistoryPage.spec.ts index 8606bef..faf6048 100644 --- a/frontend/src/pages/settings/AccountLoginHistoryPage.spec.ts +++ b/frontend/src/pages/settings/AccountLoginHistoryPage.spec.ts @@ -16,30 +16,36 @@ import AccountLoginHistoryPage from './AccountLoginHistoryPage.vue' // 설정 › 계정 › 로그인 기록 (12단계 — 디자인 원본 SettingsLoginHistory · 계약 5절 "로그인된 브라우저" · "로그인 기록") const USER = { email: 'ara.kim@arasystem.it.kr', name: '김아라' } -/** 한국 시간 2026-10-31 (토) 낮 12시 */ -const NOW = Date.parse('2026-10-31T03:00:00Z') +/** + * 2026년 그 시각을 이 PC 의 시간대(지역 시각)로 — 화면은 시각을 브라우저 시간대로 보인다. + * "오늘 오전 9:05" 같은 글을 그대로 보려면 시각을 지역 시각으로 적어야 어느 시간대에서도 같다 + * (UTC 로 적었더니 개발 PC(KST)에서만 통과하고 CI(UTC)에서 실패했다 — 2026-11-02). + */ +const local = (month: number, day: number, hour: number, minute = 0) => new Date(2026, month - 1, day, hour, minute).toISOString() +/** 2026-10-31 (토) 낮 12시 */ +const NOW = Date.parse(local(10, 31, 12)) const SESSIONS: AuthSession[] = [ { id: 'now', - createdAt: '2026-10-31T00:00:00Z', - lastActiveAt: '2026-10-31T02:50:00Z', + createdAt: local(10, 31, 9), + lastActiveAt: local(10, 31, 11, 50), ip: '211.186.95.252', device: { browser: 'Chrome', os: 'Windows' }, isCurrent: true, }, { id: 'edge', - createdAt: '2026-10-30T00:00:00Z', - lastActiveAt: '2026-10-31T00:05:00Z', + createdAt: local(10, 30, 9), + lastActiveAt: local(10, 31, 9, 5), ip: '211.186.95.252', device: { browser: 'Edge', os: 'Windows' }, isCurrent: false, }, { id: 'phone', - createdAt: '2026-10-29T00:00:00Z', - lastActiveAt: '2026-10-30T13:31:00Z', + createdAt: local(10, 29, 9), + lastActiveAt: local(10, 30, 22, 31), ip: null, device: { browser: 'Safari', os: 'iOS' }, isCurrent: false, @@ -49,7 +55,7 @@ const SESSIONS: AuthSession[] = [ function loginEvent(id: string, changes: Partial): LoginEvent { return { id, - occurredAt: '2026-10-31T00:30:00Z', + occurredAt: local(10, 31, 9, 30), method: 'webmail', isSuccess: true, isTwoFactor: false, @@ -62,23 +68,23 @@ function loginEvent(id: string, changes: Partial): LoginEvent { } const EVENTS: LoginEvent[] = [ - loginEvent('e1', { occurredAt: '2026-10-31T00:30:00Z', isTwoFactor: true }), - loginEvent('e2', { occurredAt: '2026-10-31T00:29:00Z', isSuccess: false, failureReason: 'wrongCode' }), + loginEvent('e1', { occurredAt: local(10, 31, 9, 30), isTwoFactor: true }), + loginEvent('e2', { occurredAt: local(10, 31, 9, 29), isSuccess: false, failureReason: 'wrongCode' }), loginEvent('e3', { - occurredAt: '2026-10-31T00:12:00Z', + occurredAt: local(10, 31, 9, 12), method: 'imap', device: null, appPassword: { id: 'b', name: '회사 PC 아웃룩' }, }), loginEvent('e4', { - occurredAt: '2026-10-30T14:58:00Z', + occurredAt: local(10, 30, 23, 58), method: 'smtp', device: null, isSuccess: false, failureReason: 'appPasswordRequired', ip: '45.133.10.21', }), - loginEvent('e5', { occurredAt: '2026-10-29T06:21:00Z', device: null, isSuccess: false, failureReason: 'wrongPassword' }), + loginEvent('e5', { occurredAt: local(10, 29, 15, 21), device: null, isSuccess: false, failureReason: 'wrongPassword' }), ] const pageOf = (items: T[], next: string | null = null, total: number | null = items.length) => ({ @@ -175,8 +181,8 @@ describe('AccountLoginHistoryPage', () => { ]) expect(sessionRows(wrapper)).toEqual([ ['Chrome · Windows', '211.186.95.252', '지금 이 브라우저'], - ['Edge · Windows', '211.186.95.252', formatRelativeDayTime('2026-10-31T00:05:00Z', nowDate)], - ['Safari · iOS', '—알 수 없음', formatRelativeDayTime('2026-10-30T13:31:00Z', nowDate)], + ['Edge · Windows', '211.186.95.252', formatRelativeDayTime(SESSIONS[1]?.lastActiveAt ?? '', nowDate)], + ['Safari · iOS', '—알 수 없음', formatRelativeDayTime(SESSIONS[2]?.lastActiveAt ?? '', nowDate)], ]) expect(sessionRows(wrapper)[1]?.[2]).toBe('오늘 오전 9:05') expect(wrapper.findAll('[data-session] svg').map((icon) => icon.attributes('aria-label'))).toEqual(['PC', 'PC', '휴대폰']) @@ -319,15 +325,15 @@ describe('AccountLoginHistoryPage', () => { it('시각 · 방식(앱 비밀번호 이름) · IP · 기기(메일 프로그램은 —) · 결과 — 실패는 빨간 굵은 글자만', async () => { const wrapper = await mountPage() const nowDate = new Date(NOW) - const time = (value: string) => formatRelativeDayTime(value, nowDate) + const time = (index: number) => formatRelativeDayTime(EVENTS[index]?.occurredAt ?? '', nowDate) expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 웹메일과 메일 프로그램') expect(eventRows(wrapper)).toEqual([ - [time('2026-10-31T00:30:00Z'), '웹메일', '211.186.95.252', 'Chrome · Windows', '성공 · 2단계 인증'], - [time('2026-10-31T00:29:00Z'), '웹메일', '211.186.95.252', 'Chrome · Windows', '실패 — 코드 틀림'], - [time('2026-10-31T00:12:00Z'), '메일 프로그램 · IMAP앱 비밀번호 ‘회사 PC 아웃룩’', '211.186.95.252', '—알 수 없음', '성공'], - [time('2026-10-30T14:58:00Z'), '메일 프로그램 · SMTP', '45.133.10.21', '—알 수 없음', '실패 — 앱 비밀번호 필요'], - [time('2026-10-29T06:21:00Z'), '웹메일', '211.186.95.252', '알 수 없는 브라우저', '실패 — 비밀번호 틀림'], + [time(0), '웹메일', '211.186.95.252', 'Chrome · Windows', '성공 · 2단계 인증'], + [time(1), '웹메일', '211.186.95.252', 'Chrome · Windows', '실패 — 코드 틀림'], + [time(2), '메일 프로그램 · IMAP앱 비밀번호 ‘회사 PC 아웃룩’', '211.186.95.252', '—알 수 없음', '성공'], + [time(3), '메일 프로그램 · SMTP', '45.133.10.21', '—알 수 없음', '실패 — 앱 비밀번호 필요'], + [time(4), '웹메일', '211.186.95.252', '알 수 없는 브라우저', '실패 — 비밀번호 틀림'], ]) expect(eventRows(wrapper)[0]?.[0]).toBe('오늘 오전 9:30') expect(wrapper.findAll('[data-event-result]').map((cell) => cell.classes().includes('events__result--failure'))).toEqual([ @@ -383,7 +389,7 @@ describe('AccountLoginHistoryPage', () => { it('[더 보기] — 커서로 20개씩 이어 받는다(실패만이면 실패만) · 다 받으면 단추가 사라진다', async () => { const failures = EVENTS.filter((event) => !event.isSuccess) - const more = [loginEvent('e9', { occurredAt: '2026-10-12T19:02:00Z', isSuccess: false, failureReason: 'wrongPassword' })] + const more = [loginEvent('e9', { occurredAt: local(10, 13, 4, 2), isSuccess: false, failureReason: 'wrongPassword' })] const wrapper = await mountPage({ events: (request) => { if (request.params.cursor === 'next-failures') return ok(pageOf(more, null, 4))