diff --git a/devfront/src/features/overview/GlobalOverviewPage.tsx b/devfront/src/features/overview/GlobalOverviewPage.tsx index f60c244d..4ab57bc1 100644 --- a/devfront/src/features/overview/GlobalOverviewPage.tsx +++ b/devfront/src/features/overview/GlobalOverviewPage.tsx @@ -18,7 +18,6 @@ import { OverviewMetric, OverviewSelectionChips, } from "../../../../common/core/components/overview"; -import { formatAuditDateParts } from "../../../../common/core/audit"; import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; @@ -81,6 +80,7 @@ type UsageChartPalette = { }; const deletedRecentChangeFilterId = "__deleted_recent_clients__"; +const localeStorageKey = "locale"; type RecentChangePoint = { date: string; @@ -102,6 +102,73 @@ type RecentChangeSeries = { points: RecentChangePoint[]; }; +type AppLocale = "ko" | "en"; + +function resolveAppLocale(): AppLocale { + if (typeof window === "undefined") { + return "ko"; + } + + const stored = window.localStorage.getItem(localeStorageKey); + if (stored === "ko" || stored === "en") { + return stored; + } + + const pathLocale = window.location.pathname.split("/")[1]; + if (pathLocale === "ko" || pathLocale === "en") { + return pathLocale; + } + + return window.navigator.language.toLowerCase().startsWith("ko") + ? "ko" + : "en"; +} + +function formatRecentChangeTimestamp(value: string) { + if (!value) { + return { date: "-", time: "-" }; + } + + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return { date: value, time: "-" }; + } + + const locale = resolveAppLocale(); + if (locale === "ko") { + const date = parsed.toISOString().slice(0, 10); + const timeParts = new Intl.DateTimeFormat("ko-KR", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }).formatToParts(parsed); + const hour = timeParts.find((part) => part.type === "hour")?.value ?? "00"; + const minute = + timeParts.find((part) => part.type === "minute")?.value ?? "00"; + const second = + timeParts.find((part) => part.type === "second")?.value ?? "00"; + + return { + date, + time: `${hour}시 ${minute}분 ${second}초`, + }; + } + + return { + date: new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }).format(parsed), + time: new Intl.DateTimeFormat("en-US", { + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }).format(parsed), + }; +} + const usageChartPalettes: UsageChartPalette[] = [ { bar: "#7dd3fc", line: "#10b981", point: "#059669" }, { bar: "#f9a8d4", line: "#f97316", point: "#ea580c" }, @@ -1068,7 +1135,7 @@ function GlobalOverviewPage() { id: deletedRecentChangeFilterId, label: t( "ui.dev.dashboard.recent_changes.deleted_group", - "삭제된 RP", + "삭제된 앱", ), }, ]; @@ -1399,9 +1466,9 @@ function GlobalOverviewPage() { } label={t( - "ui.dev.dashboard.recent_changes.summary.deleted_clients", - "삭제된 RP 수", - )} + "ui.dev.dashboard.recent_changes.summary.deleted_clients", + "삭제된 앱 수", + )} value={deletedRecentChangedClientCount.toLocaleString()} /> ) : ( visibleRecentClientChanges.map((item) => { - const { date, time } = formatAuditDateParts(item.timestamp); + const { date, time } = + formatRecentChangeTimestamp(item.timestamp); return (
= { - name: "이름", - type: "유형", - status: "상태", - scopes: "스코프", - tenant_access_restricted: "테넌트 접근 제한", - allowed_tenants: "허용 테넌트", - id_token_claims: "커스텀 클레임", - token_endpoint_auth_method: "인증 방식", - jwks_uri: "JWKS URI", - backchannel_logout_uri: "Backchannel Logout URI", - backchannel_logout_session_required: "세션 필수", - headless_login_enabled: "헤드리스 로그인", - headless_token_endpoint_auth_method: "헤드리스 인증 방식", - headless_jwks_uri: "헤드리스 JWKS URI", - redirect_uri_count: "Redirect URI 수", - scope_count: "Scope 수", - relation: "관계", - subject: "대상", -}; - function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } @@ -56,24 +36,43 @@ function isRecord(value: unknown): value is Record { export function getRecentClientActionLabel(action: string) { switch (action) { case "CREATE_CLIENT": - return "클라이언트 생성"; + return t("ui.dev.clients.recent_changes.guide.create", "앱 생성"); case "UPDATE_CLIENT": - return "설정 변경"; + return t("ui.dev.clients.recent_changes.guide.settings", "설정 변경"); case "UPDATE_CLIENT_STATUS": - return "상태 변경"; + return t("ui.dev.clients.recent_changes.guide.status", "상태 변경"); case "ROTATE_SECRET": - return "클라이언트 시크릿 재발급"; + return t( + "ui.dev.clients.recent_changes.guide.secret", + "클라이언트 시크릿 재발급", + ); case "ADD_RELATION": - return "관계 추가"; + return t("ui.dev.clients.relationships.add_title", "관계 추가"); case "REMOVE_RELATION": - return "관계 삭제"; + return t("ui.common.remove", "Remove"); case "DELETE_CLIENT": - return "클라이언트 삭제"; + return t("ui.dev.clients.recent_changes.guide.delete", "앱 삭제"); default: return action; } } +function getRecentClientFieldLabel(key: string) { + switch (key) { + case "relation": + return t("ui.dev.clients.relationships.relation", "관계"); + case "subject": + return t("ui.dev.clients.relationships.subject", "대상"); + case "client_secret": + return t( + "ui.dev.clients.details.credentials.client_secret", + "클라이언트 시크릿", + ); + default: + return key; + } +} + export function buildRecentClientChangeDetails( action: string, details: AuditDetails, @@ -82,17 +81,32 @@ export function buildRecentClientChangeDetails( const after = isRecord(details.after) ? details.after : {}; if (action === "ROTATE_SECRET") { - return [{ label: "클라이언트 시크릿", value: "재발급" }]; + return [ + { + label: getRecentClientFieldLabel("client_secret"), + value: t("msg.dev.clients.secret_rotated", "재발급"), + }, + ]; } if (action === "ADD_RELATION" || action === "REMOVE_RELATION") { const source = action === "ADD_RELATION" ? after : before; return [ ...(source.relation - ? [{ label: "관계", value: formatAuditValue(source.relation) }] + ? [ + { + label: getRecentClientFieldLabel("relation"), + value: formatAuditValue(source.relation), + }, + ] : []), ...(source.subject - ? [{ label: "대상", value: formatAuditValue(source.subject) }] + ? [ + { + label: getRecentClientFieldLabel("subject"), + value: formatAuditValue(source.subject), + }, + ] : []), ]; } @@ -112,7 +126,7 @@ export function buildRecentClientChangeDetails( } } - const label = recentClientFieldLabels[key] ?? key; + const label = getRecentClientFieldLabel(key); if (action === "CREATE_CLIENT") { if (afterValue === undefined) { return null; diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index b41f378e..460fb853 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -1746,7 +1746,7 @@ title = "Quick links" title = "My Applications" [ui.dev.dashboard.recent_changes] -deleted_group = "Deleted RPs" +deleted_group = "Deleted applications" aria = "Recent application changes" period = "Recent change aggregation period" series = "Changes {{changes}} / Actors {{actors}}" @@ -1755,7 +1755,7 @@ y_axis = "Y axis: change count" [ui.dev.dashboard.recent_changes.summary] changed_clients = "Changed apps" -deleted_clients = "Deleted RPs" +deleted_clients = "Deleted applications" latest_change = "Latest change" total_changes = "Recent changes" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index d169bd38..444c776e 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -1745,7 +1745,7 @@ title = "빠른 이동" title = "내 애플리케이션" [ui.dev.dashboard.recent_changes] -deleted_group = "삭제된 RP" +deleted_group = "삭제된 앱" aria = "최근 변경된 앱 현황" period = "최근 변경 집계 단위" series = "변경 {{changes}} / 작업자 {{actors}}" @@ -1754,7 +1754,7 @@ y_axis = "Y축: 변경 수" [ui.dev.dashboard.recent_changes.summary] changed_clients = "변경된 앱 수" -deleted_clients = "삭제된 RP 수" +deleted_clients = "삭제된 앱 수" latest_change = "마지막 변경일" total_changes = "최근 변경 건수" diff --git a/locales/en.toml b/locales/en.toml index 7bd5e3cf..48d6f898 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -2314,7 +2314,7 @@ title = "My Applications" [ui.dev.dashboard.recent_changes] aria = "Recent changed application status" -deleted_group = "Deleted RPs" +deleted_group = "Deleted applications" period = "Recent change aggregation period" series = "Changes {{changes}} / Actors {{actors}}" title = "Recent Changed Apps" @@ -2322,7 +2322,7 @@ y_axis = "Y axis: change count" [ui.dev.dashboard.recent_changes.summary] changed_clients = "Changed applications" -deleted_clients = "Deleted RPs" +deleted_clients = "Deleted applications" latest_change = "Latest change" total_changes = "Recent change count" diff --git a/locales/ko.toml b/locales/ko.toml index 1f99c3d7..cf67c4ea 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -2778,7 +2778,7 @@ title = "내 애플리케이션" [ui.dev.dashboard.recent_changes] aria = "최근 변경된 앱 현황" -deleted_group = "삭제된 RP" +deleted_group = "삭제된 앱" period = "최근 변경 집계 단위" series = "변경 {{changes}} / 작업자 {{actors}}" title = "최근 변경된 앱" @@ -2786,7 +2786,7 @@ y_axis = "Y축: 변경 수" [ui.dev.dashboard.recent_changes.summary] changed_clients = "변경된 앱 수" -deleted_clients = "삭제된 RP 수" +deleted_clients = "삭제된 앱 수" latest_change = "마지막 변경일" total_changes = "최근 변경 건수"