generated from ttipo/project-base
feat(frontend): 로그인된 기기 — 앱 세션 · 앱 로그인 기록을 보인다
앱 로그인 서버가 생겨 계정 보안 화면에 앱 세션 · 앱 로그인이 함께 들어온다. - "로그인된 브라우저" → "로그인된 기기"(계약 5절), 개수는 세션의 client 로 "웹메일 2곳 · ARA SYSTEM 앱 1곳"(0곳인 쪽은 쓰지 않음), 앱 세션은 늘 휴대폰 아이콘 - 로그인 기록 방식 "ARA SYSTEM 앱", 앱 줄의 기기 칸은 운영체제만(방식 칸이 이미 앱 이름) - [다른 곳 모두 로그아웃] · 비밀번호 바꾸기 · 안내 글이 웹메일만이 아니라 앱까지 끊는다고 말한다 - 웹 · 앱은 client · method 로만 가른다(User-Agent 이름으로 짐작하지 않는다) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
// [다른 곳 모두 로그아웃] 확인 — alertdialog 400(medium) · 처음 초점 「취소」, Esc 도 취소 (디자인 원본 SettingsLoginHistory "다른 곳 모두 로그아웃 — 확인 창")
|
||||
// "다른 곳의 로그인을 모두 끊을까요?" / "이 브라우저를 뺀 2곳(Edge · Windows, Safari · iOS)의 웹메일 로그인을 끊습니다. …" + 작은 글 "메일 프로그램은 끊기지 않습니다 — …"
|
||||
// "다른 곳에서 모두 로그아웃할까요?" / "이 브라우저를 뺀 2곳(Edge · Windows, ARA SYSTEM 앱 · Android)에서 로그아웃합니다. …"(웹메일 · 앱 세션을 함께 끊는다)
|
||||
// + 작은 글 "메일 프로그램은 로그아웃되지 않습니다. …"
|
||||
// [모두 로그아웃](청록 주 단추 — 데이터를 지우는 일이 아니다) [취소]. 끊는 동안 [모두 로그아웃]은 aria-disabled · aria-busy(초점을 지킨다)
|
||||
// 지금 이 브라우저 줄이 없으면(서버가 이 세션을 알아보지 못함) 제목 · 글이 "이 브라우저도 로그아웃된다"로 바뀐다 — 부모가 넘긴다
|
||||
import ModalDialog from '@/components/feedback/ModalDialog.vue'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
// 설정 › 로그인 기록 › 최근 로그인 (디자인 원본 SettingsLoginHistory · 계약 5절 "로그인 기록")
|
||||
// 「최근 로그인」 + "최근 90일 · 웹메일과 메일 프로그램"(실패만이면 "최근 90일 · 실패 4번") · 오른쪽 전환 [전체 · 실패만](36 — .segmented 밀도 예외)
|
||||
// 「최근 로그인」 + "최근 90일 · 웹메일, ARA SYSTEM 앱, 메일 프로그램"(실패만이면 "최근 90일 · 실패 4번") · 오른쪽 전환 [전체 · 실패만](36 — .segmented 밀도 예외)
|
||||
// 표(구현은 실제 table): 시각 · 방식 · IP · 기기 · 결과 — 최근 순서 · 20개씩 [더 보기](44)
|
||||
// - 방식: "웹메일" / "메일 프로그램 · IMAP" · "메일 프로그램 · SMTP" + 앱 비밀번호로 들어왔으면 아래 줄 "앱 비밀번호 ‘회사 PC 아웃룩’"
|
||||
// - 기기: 웹메일은 "Chrome · Windows"(모르면 "알 수 없는 브라우저"), 메일 프로그램은 「—」. 나라 · 지역은 보이지 않는다
|
||||
// - 방식: "웹메일" / "ARA SYSTEM 앱"(계약 1절 "앱 로그인" — method app) / "메일 프로그램 · IMAP" · "메일 프로그램 · SMTP" + 앱 비밀번호로 들어왔으면 아래 줄 "앱 비밀번호 ‘회사 PC 아웃룩’"
|
||||
// - 기기: 웹메일은 "Chrome · Windows"(모르면 "알 수 없는 브라우저"), 앱은 운영체제 "Android"(방식 칸이 앱 이름이라), 메일 프로그램은 「—」. 나라 · 지역은 보이지 않는다
|
||||
// - 결과: 「성공」 · 「2단계 인증으로 성공」 · 「비밀번호가 틀려 실패」 · 「코드가 틀려 실패」 · 「앱 비밀번호가 필요해 실패」 — 실패는 빨간 굵은 글자만(뱃지 없음)
|
||||
// - 빈 상태(실패만): "최근 90일 동안 실패한 로그인이 없습니다" / "[전체]를 누르면 성공한 로그인도 볼 수 있습니다."
|
||||
// - 보기를 바꾸면 그 보기로 처음부터 받는다 — 받는 동안 지난 목록은 그대로 두고 aria-busy (login-history.store)
|
||||
@@ -120,7 +120,7 @@ function timeText(event: LoginEvent): string {
|
||||
{{ LOGIN_HISTORY_TEXT.columnIp }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ LOGIN_HISTORY_TEXT.columnKind }}
|
||||
{{ LOGIN_HISTORY_TEXT.columnDevice }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ LOGIN_HISTORY_TEXT.columnResult }}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
// 설정 › 로그인 기록 › 로그인된 브라우저 (디자인 원본 SettingsLoginHistory · 계약 5절 "로그인된 브라우저")
|
||||
// 「로그인된 브라우저」 + "웹메일 3곳" · 오른쪽 [다른 곳 모두 로그아웃](44 — 다른 곳이 없으면 그리지 않는다)
|
||||
// 표(구현은 실제 table): PC · 휴대폰 아이콘(16) · 브라우저 · 운영체제 / IP / 마지막 활동 / [로그아웃](36 — 밀도 예외).
|
||||
// 설정 › 로그인 기록 › 로그인된 기기 (디자인 원본 SettingsLoginHistory · 계약 5절 "로그인된 브라우저" · 1절 "앱 로그인")
|
||||
// 웹메일 세션과 ARA SYSTEM 앱 세션을 함께 보인다 — 웹 · 앱은 세션의 client 로만 가른다(device 의 이름으로 짐작하지 않는다)
|
||||
// 「로그인된 기기」 + "웹메일 3곳"(앱 세션이 있으면 "웹메일 2곳 · ARA SYSTEM 앱 1곳") · 오른쪽 [다른 곳 모두 로그아웃](44 — 다른 곳이 없으면 그리지 않는다)
|
||||
// 표(구현은 실제 table): PC · 휴대폰 아이콘(16 — 앱 세션은 늘 휴대폰) · 기기 "Edge · Windows" · "ARA SYSTEM 앱 · Android" / IP / 마지막 활동 / [로그아웃](36 — 밀도 예외).
|
||||
// 맨 위는 지금 이 브라우저 — 단추 대신 「지금 이 브라우저」 글자만. 모르는 브라우저는 "알 수 없는 브라우저", 모르는 IP 는 「—」.
|
||||
// 아래 "웹메일 로그인만 보입니다. 메일 프로그램은 앱 비밀번호를 지워 끊습니다."(링크 — 다른 곳이 없으면 앞에 "다른 곳에 로그인된 브라우저가 없습니다.")
|
||||
// - 줄의 [로그아웃]은 묻지 않고 바로 끊는다 → 「‘Edge · Windows’의 로그인을 끊었습니다.」(되돌리기 없음). 누른 줄이 사라지면 초점은 다음 줄(없으면 앞 줄 · 묶음 제목)
|
||||
// - [다른 곳 모두 로그아웃] → 확인 창(처음 초점 「취소」) → 「다른 곳의 로그인 2곳을 끊었습니다.」 — 지금 이 브라우저만 남고 초점은 묶음 제목으로
|
||||
// 아래 "메일 프로그램은 여기에 보이지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요."(링크 — 다른 곳이 없으면 앞에 "다른 곳에 로그인된 기기가 없습니다.")
|
||||
// - 줄의 [로그아웃]은 묻지 않고 바로 끊는다 → 「‘Edge · Windows’에서 로그아웃했습니다.」(되돌리기 없음). 누른 줄이 사라지면 초점은 다음 줄(없으면 앞 줄 · 묶음 제목)
|
||||
// - [다른 곳 모두 로그아웃] → 확인 창(처음 초점 「취소」) → 「다른 2곳에서 로그아웃했습니다.」 — 지금 이 브라우저만 남고 초점은 묶음 제목으로
|
||||
// 지금 이 브라우저 줄이 없으면(서버가 이 세션을 알아보지 못함 — 서버는 모두 끊는다) 확인 창이 이 브라우저도 로그아웃된다고 알리고,
|
||||
// 끊은 뒤에는 목록을 다시 받는다(스토어 — 이 브라우저도 끊겼으면 세션 만료 창으로 이어진다)
|
||||
// - 세션이 바뀌면 확인 창을 닫는다
|
||||
@@ -18,7 +19,13 @@ import { useToast } from '@/composables/useToast'
|
||||
import { END_OTHER_SESSIONS_TEXT, LOGIN_HISTORY_TEXT } from '@/shared/constants/account-security'
|
||||
import { SETTINGS_ROUTES } from '@/shared/constants/settings-menu'
|
||||
import type { AuthSession } from '@/shared/types/account-security'
|
||||
import { deviceLabel, endOtherSessionsMessage, isMobileDevice, otherSessions } from '@/shared/utils/account-security'
|
||||
import {
|
||||
endOtherSessionsMessage,
|
||||
isMobileSession,
|
||||
otherSessions,
|
||||
sessionCountText,
|
||||
sessionDeviceLabel,
|
||||
} from '@/shared/utils/account-security'
|
||||
import { formatCount, formatRelativeDayTime } from '@/shared/utils/format'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { useLoginHistoryStore } from '@/stores/login-history.store'
|
||||
@@ -46,8 +53,9 @@ const others = computed(() => otherSessions(props.sessions))
|
||||
/** 서버가 이 브라우저를 알아봤다(지금 이 브라우저 줄이 있다) — 없으면 [다른 곳 모두 로그아웃]이 이 브라우저까지 끊는다 */
|
||||
const hasCurrent = computed(() => props.sessions.some((session) => session.isCurrent))
|
||||
const confirmTitle = computed(() => (hasCurrent.value ? END_OTHER_SESSIONS_TEXT.title : END_OTHER_SESSIONS_TEXT.titleAll))
|
||||
const countText = computed(() => LOGIN_HISTORY_TEXT.sessionsCount(formatCount(props.sessions.length)))
|
||||
/** 아래 안내의 앞 글 — 다른 곳이 없으면 "다른 곳에 로그인된 브라우저가 없습니다." 를 앞에 */
|
||||
/** "웹메일 3곳" · "웹메일 2곳 · ARA SYSTEM 앱 1곳" — 세션의 client 로 센다 */
|
||||
const countText = computed(() => sessionCountText(props.sessions))
|
||||
/** 아래 안내의 앞 글 — 다른 곳이 없으면 "다른 곳에 로그인된 기기가 없습니다." 를 앞에 */
|
||||
const noteBefore = computed(() =>
|
||||
others.value.length === 0
|
||||
? `${LOGIN_HISTORY_TEXT.noOthers}${LOGIN_HISTORY_TEXT.sessionsNoteBefore}`
|
||||
@@ -77,7 +85,7 @@ async function endSession(session: AuthSession) {
|
||||
endingId.value = session.id
|
||||
try {
|
||||
await loginHistoryStore.endSession(session.id)
|
||||
toast.success(LOGIN_HISTORY_TEXT.sessionEnded(deviceLabel(session.device)))
|
||||
toast.success(LOGIN_HISTORY_TEXT.sessionEnded(sessionDeviceLabel(session)))
|
||||
await focusAfterRemoval(index)
|
||||
} catch {
|
||||
// 인터셉터가 알렸다 — 줄은 그대로
|
||||
@@ -188,15 +196,15 @@ watch(
|
||||
>
|
||||
<td class="sessions__icon">
|
||||
<AppIcon
|
||||
:name="isMobileDevice(session.device) ? 'smartphone' : 'monitor'"
|
||||
:label="isMobileDevice(session.device) ? LOGIN_HISTORY_TEXT.kindMobile : LOGIN_HISTORY_TEXT.kindPc"
|
||||
:name="isMobileSession(session) ? 'smartphone' : 'monitor'"
|
||||
:label="isMobileSession(session) ? LOGIN_HISTORY_TEXT.kindMobile : LOGIN_HISTORY_TEXT.kindPc"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
class="sessions__device"
|
||||
data-session-device
|
||||
>
|
||||
{{ deviceLabel(session.device) }}
|
||||
{{ sessionDeviceLabel(session) }}
|
||||
</td>
|
||||
<td class="sessions__ip">
|
||||
<template v-if="session.ip">
|
||||
@@ -218,7 +226,7 @@ watch(
|
||||
v-if="!session.isCurrent"
|
||||
type="button"
|
||||
class="sessions__end"
|
||||
:aria-label="LOGIN_HISTORY_TEXT.endSessionLabel(deviceLabel(session.device))"
|
||||
:aria-label="LOGIN_HISTORY_TEXT.endSessionLabel(sessionDeviceLabel(session))"
|
||||
:aria-disabled="endingId || endingOthers ? 'true' : undefined"
|
||||
:aria-busy="endingId === session.id ? 'true' : undefined"
|
||||
data-session-end
|
||||
@@ -321,7 +329,7 @@ watch(
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 칸: 아이콘 16 · 브라우저(나머지) · IP 112 · 마지막 활동 150 · 단추 84, 칸 사이 12 · 줄 앞 14 · 뒤 8 (디자인 원본 .ss-grid) */
|
||||
/* 칸: 아이콘 16 · 기기(나머지) · IP 112 · 마지막 활동 150 · 단추 84, 칸 사이 12 · 줄 앞 14 · 뒤 8 (디자인 원본 .ss-grid) */
|
||||
.sessions__table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// - 서버: 지금 비밀번호가 틀림(AUTH_005)은 지금 비밀번호 칸 아래, 새 비밀번호가 엔진 규칙에 어긋남(VAL_001)은 새 비밀번호 칸 아래에
|
||||
// 사전 문구로 (알림은 끈다). 그 밖(요청 과다 · 메일 서버 장애)은 인터셉터의 알림. 기다리는 사이 화면을 떠났으면 끈 거절도 알림으로 (notifyApiError).
|
||||
// 요청 한도는 30초다 (useAuth — 서버가 엔진을 두 번 부르고 다른 세션을 끊는다)
|
||||
// - 바꾸면 "비밀번호를 바꿨습니다. 다른 기기 · 브라우저의 로그인은 끊었습니다." — 세 칸을 비우고 보기를 끈다. 무엇이든 다시 치면 내린다
|
||||
// - 바꾸면 "비밀번호를 바꿨습니다. 다른 기기와 브라우저에서는 로그아웃했습니다."(서버가 웹메일 · ARA SYSTEM 앱 세션을 끊는다) — 세 칸을 비우고 보기를 끈다. 무엇이든 다시 치면 내린다
|
||||
// - 2단계 인증을 켠 직원(12단계 — 계약 5절 "비밀번호 바꾸기")은 맨 아래 「인증 앱 코드」 칸(코드가 30초마다 바뀌어 다른 칸을 다 쓴 뒤 넣는다).
|
||||
// 6자리가 아니면 보내지 않는다(사전 문구 AUTH_007). 코드 틀림(AUTH_006)은 그 칸 아래 · 칸을 비운다.
|
||||
// 다른 탭에서 켜서 코드 없이 보냈으면(AUTH_007) 2단계 상태를 다시 받고 칸을 보인다 — 못 받아도 칸은 보인다
|
||||
|
||||
Reference in New Issue
Block a user