From 250bc297faf8aef0efa800634289e5fb3037e978 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 12 May 2026 17:08:28 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EB=B3=BC=EB=A5=A8=20=EB=A7=88=EC=9A=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/staging_pull_compose.template.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/staging_pull_compose.template.yaml b/docker/staging_pull_compose.template.yaml index 44335644..5af14023 100644 --- a/docker/staging_pull_compose.template.yaml +++ b/docker/staging_pull_compose.template.yaml @@ -435,6 +435,8 @@ services: - "${ADMINFRONT_PORT:-5173}:5173" volumes: - ./adminfront:/app + - ./common:/common + - ./locales:/locales - /app/node_modules networks: - baron_net @@ -459,6 +461,8 @@ services: - "${DEVFRONT_PORT:-5174}:5173" volumes: - ./devfront:/app + - ./common:/common + - ./locales:/locales - /app/node_modules networks: - baron_net @@ -484,6 +488,8 @@ services: - "${ORGFRONT_PORT:-5175}:5175" volumes: - ./orgfront:/app + - ./common:/common + - ./locales:/locales - /app/node_modules networks: - baron_net From 878867f6ccfc0757dbacdfa8c5d9f1062343942f Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 12 May 2026 17:08:55 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/dashboard/DashboardPage.tsx | 233 ++++++++++++------ devfront/src/locales/en.toml | 14 +- devfront/src/locales/ko.toml | 14 +- devfront/src/locales/template.toml | 10 +- 4 files changed, 184 insertions(+), 87 deletions(-) diff --git a/devfront/src/features/dashboard/DashboardPage.tsx b/devfront/src/features/dashboard/DashboardPage.tsx index 45aebc80..468cd347 100644 --- a/devfront/src/features/dashboard/DashboardPage.tsx +++ b/devfront/src/features/dashboard/DashboardPage.tsx @@ -9,15 +9,19 @@ import { ShieldCheck, } from "lucide-react"; import { type ReactNode, useMemo, useState } from "react"; +import { useAuth } from "react-oidc-context"; +import { useNavigate } from "react-router-dom"; import { type ClientSummary, fetchClients, + fetchDeveloperRequestStatus, fetchDevRPUsageDaily, fetchDevStats, type RPUsageDailyMetric, type RPUsagePeriod, } from "../../lib/devApi"; import { t } from "../../lib/i18n"; +import { resolveProfileRole } from "../../lib/role"; type ClientDistribution = { activeClients: number; @@ -141,8 +145,7 @@ function summarizeSeries(rows: RPUsageDailyMetric[]): SeriesSummary[] { right.loginRequests + right.otherRequests - (left.loginRequests + left.otherRequests), - ) - .slice(0, 5); + ); } function buildMultiLineSeries(rows: RPUsageDailyMetric[]): MultiLineSeries[] { @@ -222,6 +225,17 @@ function getISOWeekThursday(year: number, month: number, day: number) { return date; } +function formatDate(value?: string) { + if (!value) { + return "-"; + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return value; + } + return parsed.toLocaleDateString(); +} + function formatPeriodLabel(date: string, period: RPUsagePeriod) { const parts = parseDateParts(date); if (!parts) { @@ -280,20 +294,17 @@ function RPUsageMixedChart({ const colors = palette ?? usageChartPalettes[0]; const daily = summarizeDaily(rows); const series = summarizeSeries(rows); + const topSeries = series.slice(0, 5); + const seriesByKey = new Map(series.map((item) => [item.key, item])); const chartWidth = 720; const chartHeight = 230; const padX = 48; - const padTop = 32; + const padTop = 40; const padBottom = 34; const innerWidth = chartWidth - padX * 2; const innerHeight = chartHeight - padTop - padBottom; - const maxValue = Math.max( - 1, - ...daily.map((point) => point.loginRequests + point.otherRequests), - ...daily.map((point) => point.loginRequests), - ); + const maxValue = Math.max(1, ...daily.map((point) => point.loginRequests)); const slot = daily.length > 0 ? innerWidth / daily.length : innerWidth; - const barWidth = Math.min(28, Math.max(10, slot * 0.42)); const y = (value: number) => padTop + innerHeight - (value / maxValue) * innerHeight; const x = (index: number) => padX + slot * index + slot / 2; @@ -325,40 +336,6 @@ function RPUsageMixedChart({ className="h-[235px] min-w-[720px] w-full" > {t("ui.dev.dashboard.chart.aria", "RP 요청 현황")} - - - - {t("ui.dev.dashboard.chart.other_requests", "기타 요청")} - - {!multiLinePoints || multiLinePoints.length === 0 ? ( - <> - - - {t("ui.dev.dashboard.chart.login_requests", "로그인 요청")} - - - ) : null} - {[0, 0.25, 0.5, 0.75, 1].map((ratio) => { const gridY = padTop + innerHeight * ratio; const label = Math.round(maxValue * (1 - ratio)); @@ -386,18 +363,8 @@ function RPUsageMixedChart({ })} {daily.map((point, index) => { const center = x(index); - const otherHeight = (point.otherRequests / maxValue) * innerHeight; return ( - +
+ + {t("ui.dev.dashboard.chart.x_axis", "X축: 기간")} + + + {t("ui.dev.dashboard.chart.y_axis", "Y축: 로그인 요청 수")} + +
+ {multiLinePoints && multiLinePoints.length > 0 ? (
{multiLinePoints.map((item) => ( -
+
- {item.clientLabel} + {item.clientLabel} + {seriesByKey.get(item.key) ? ( + + {t( + "ui.dev.dashboard.chart.series", + "로그인 {{login}} / 사용자 {{subjects}}", + { + login: seriesByKey.get(item.key)!.loginRequests.toLocaleString(), + subjects: seriesByKey.get(item.key)!.uniqueSubjects.toLocaleString(), + }, + )} + + ) : null}
))}
- ) : series.length > 0 ? ( + ) : topSeries.length > 0 ? (
- {series.map((item) => ( -
- {item.clientLabel} - + {topSeries.map((item) => ( +
+ {item.clientLabel} + {t( "ui.dev.dashboard.chart.series", - "로그인 {{login}} / 기타 {{other}} / 사용자 {{subjects}}", + "로그인 {{login}} / 사용자 {{subjects}}", { login: item.loginRequests.toLocaleString(), - other: item.otherRequests.toLocaleString(), subjects: item.uniqueSubjects.toLocaleString(), }, )} @@ -496,9 +489,23 @@ function RPUsageMixedChart({ } function DashboardPage() { + const navigate = useNavigate(); + const auth = useAuth(); + const hasAccessToken = Boolean(auth.user?.access_token); + const userProfile = auth.user?.profile as Record | undefined; + const role = resolveProfileRole(userProfile); + const tenantId = userProfile?.tenant_id as string | undefined; const [period, setPeriod] = useState("day"); const [selectedClientIds, setSelectedClientIds] = useState([]); const usageDays = period === "day" ? 14 : period === "week" ? 84 : 90; + const { + data: requestStatus, + isLoading: isLoadingRequestStatus, + } = useQuery({ + queryKey: ["developer-request", tenantId], + queryFn: () => fetchDeveloperRequestStatus(tenantId), + enabled: hasAccessToken && role === "user", + }); const statsQuery = useQuery({ queryKey: ["dev-dashboard-stats"], queryFn: fetchDevStats, @@ -520,6 +527,17 @@ function DashboardPage() { }); const clients = clientsQuery.data?.items ?? []; + const hasDeveloperAccess = + role === "super_admin" || + role === "tenant_admin" || + role === "rp_admin" || + requestStatus?.status === "approved"; + const isDeveloperRequestPending = requestStatus?.status === "pending"; + const canRequestDeveloperAccess = + (role === "user" || role === "tenant_member") && + !isLoadingRequestStatus && + !hasDeveloperAccess && + !isDeveloperRequestPending; const distribution = useMemo( () => buildClientDistribution(clients), [clients], @@ -607,6 +625,68 @@ function DashboardPage() { setSelectedClientIds([]); }; + if ( + (role === "user" || role === "tenant_member") && + isLoadingRequestStatus + ) { + return ( +
+ {t("ui.common.loading", "Loading...")} +
+ ); + } + + if (!hasDeveloperAccess) { + return ( +
+
+

+ {t("ui.dev.nav.overview", "개요")} +

+

+ {isDeveloperRequestPending + ? t( + "msg.dev.dashboard.access_pending", + "개발자 권한 신청을 검토 중입니다.", + ) + : t( + "msg.dev.dashboard.access_denied", + "개요는 개발자 권한이 있어야 볼 수 있습니다.", + )} +

+

+ {isDeveloperRequestPending + ? t( + "msg.dev.dashboard.access_pending_detail", + "super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다.", + ) + : t( + "msg.dev.dashboard.access_denied_detail", + "개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요.", + )} +

+ {(isDeveloperRequestPending || canRequestDeveloperAccess) && ( + + )} +
+
+ ); + } + return (
@@ -730,7 +810,7 @@ function DashboardPage() { )} -
+
@@ -744,10 +824,10 @@ function DashboardPage() {

{t( "msg.dev.dashboard.distribution.description", - "애플리케이션 유형과 headless login 사용 현황을 빠르게 확인합니다.", + "애플리케이션 유형 분포와 server side app의 headless login 사용 현황을 빠르게 확인합니다.", )}

-
+

{t("ui.dev.dashboard.distribution.private", "Server side App")} @@ -755,6 +835,15 @@ function DashboardPage() {

{distribution.privateClients.toLocaleString()}

+

+ {t( + "ui.dev.dashboard.distribution.headless_hint", + "이 중 Headless Login 사용 {{count}}", + { + count: distribution.headlessClients.toLocaleString(), + }, + )} +

@@ -764,14 +853,6 @@ function DashboardPage() { {distribution.pkceClients.toLocaleString()}

-
-

- {t("ui.dev.dashboard.distribution.headless", "Headless Login")} -

-

- {distribution.headlessClients.toLocaleString()} -

-
@@ -803,7 +884,7 @@ function DashboardPage() { className="flex items-center justify-between gap-3 rounded-lg border border-border/40 px-3 py-2" >
-

+

{client.name || t("ui.dev.clients.untitled", "Untitled")}

@@ -828,6 +909,10 @@ function DashboardPage() { ? t("ui.dev.clients.status.inactive", "비활성") : client.status || "-"}

+

+ {t("ui.dev.clients.table.created_at", "생성일")}{" "} + {formatDate(client.createdAt)} +

)) diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 33cbe458..4b3c8b91 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -500,6 +500,10 @@ openid = "Openid" profile = "Profile" [msg.dev.dashboard] +access_denied = "Overview is available only to users with developer access." +access_denied_detail = "Submit a request on the developer access page and wait for approval." +access_pending = "Your developer access request is under review." +access_pending_detail = "You can use the overview and developer features after a super admin approves it." description = "View connected application composition and authentication operations metrics in one place." [msg.dev.dashboard.hero] @@ -1664,7 +1668,7 @@ registry = "RP registry" rp_synced = "RP registry synced" [ui.dev.dashboard.distribution] -headless = "Headless Login" +headless_hint = "{{count}} with Headless Login enabled" pkce = "PKCE" private = "Server side App" title = "Application Distribution" @@ -1672,13 +1676,13 @@ title = "Application Distribution" [ui.dev.dashboard.chart] aria = "RP request overview" filter_all = "All" -login_requests = "Login requests" -other_requests = "Other requests" period_day = "Day" period_month = "Month" period_week = "Week" -series = "Login {{login}} / Other {{other}} / Users {{subjects}}" -title = "Login and other requests by application" +series = "Login {{login}} / Users {{subjects}}" +title = "Login requests by application" +x_axis = "X-axis: Period" +y_axis = "Y-axis: Login requests" [ui.dev.dashboard.next] subtitle = "Ship the RP controls" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index ba69c665..4fc30bf2 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -500,6 +500,10 @@ openid = "OIDC 인증 필수 스코프" profile = "기본 프로필 정보 접근" [msg.dev.dashboard] +access_denied = "개요는 개발자 권한이 있어야 볼 수 있습니다." +access_denied_detail = "개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요." +access_pending = "개발자 권한 신청을 검토 중입니다." +access_pending_detail = "super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다." description = "연동 앱 구성과 인증 운영 지표를 한 곳에서 확인합니다." [msg.dev.dashboard.hero] @@ -1663,7 +1667,7 @@ registry = "RP registry" rp_synced = "RP registry synced" [ui.dev.dashboard.distribution] -headless = "Headless Login" +headless_hint = "이 중 Headless Login 사용 {{count}}" pkce = "PKCE" private = "Server side App" title = "애플리케이션 구성 요약" @@ -1671,13 +1675,13 @@ title = "애플리케이션 구성 요약" [ui.dev.dashboard.chart] aria = "RP 요청 현황" filter_all = "전체" -login_requests = "로그인 요청" -other_requests = "기타 요청" period_day = "일" period_month = "월" period_week = "주" -series = "로그인 {{login}} / 기타 {{other}} / 사용자 {{subjects}}" -title = "애플리케이션별 로그인요청/기타 요청 현황" +series = "로그인 {{login}} / 사용자 {{subjects}}" +title = "애플리케이션별 로그인 요청 현황" +x_axis = "X축: 기간" +y_axis = "Y축: 로그인 요청 수" [ui.dev.dashboard.next] subtitle = "Ship the RP controls" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 01e5506e..e2eca1fb 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -538,6 +538,10 @@ openid = "" profile = "" [msg.dev.dashboard] +access_denied = "" +access_denied_detail = "" +access_pending = "" +access_pending_detail = "" description = "" [msg.dev.dashboard.hero] @@ -1720,7 +1724,7 @@ registry = "" rp_synced = "" [ui.dev.dashboard.distribution] -headless = "" +headless_hint = "" pkce = "" private = "" title = "" @@ -1728,13 +1732,13 @@ title = "" [ui.dev.dashboard.chart] aria = "" filter_all = "" -login_requests = "" -other_requests = "" period_day = "" period_month = "" period_week = "" series = "" title = "" +x_axis = "" +y_axis = "" [ui.dev.dashboard.next] subtitle = "" From 298b919d1a8691d2efb50e4343775c7b5c841c2f Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 12 May 2026 18:04:41 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EB=88=84=EB=9D=BD=20=ED=82=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=A6=B0=ED=8A=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/dashboard/DashboardPage.tsx | 92 ++++++++----------- locales/en.toml | 15 ++- locales/ko.toml | 15 ++- locales/template.toml | 11 ++- 4 files changed, 67 insertions(+), 66 deletions(-) diff --git a/devfront/src/features/dashboard/DashboardPage.tsx b/devfront/src/features/dashboard/DashboardPage.tsx index 468cd347..70062067 100644 --- a/devfront/src/features/dashboard/DashboardPage.tsx +++ b/devfront/src/features/dashboard/DashboardPage.tsx @@ -139,13 +139,12 @@ function summarizeSeries(rows: RPUsageDailyMetric[]): SeriesSummary[] { ); bySeries.set(key, current); } - return Array.from(bySeries.values()) - .sort( - (left, right) => - right.loginRequests + - right.otherRequests - - (left.loginRequests + left.otherRequests), - ); + return Array.from(bySeries.values()).sort( + (left, right) => + right.loginRequests + + right.otherRequests - + (left.loginRequests + left.otherRequests), + ); } function buildMultiLineSeries(rows: RPUsageDailyMetric[]): MultiLineSeries[] { @@ -427,40 +426,39 @@ function RPUsageMixedChart({
- - {t("ui.dev.dashboard.chart.x_axis", "X축: 기간")} - - - {t("ui.dev.dashboard.chart.y_axis", "Y축: 로그인 요청 수")} - + {t("ui.dev.dashboard.chart.x_axis", "X축: 기간")} + {t("ui.dev.dashboard.chart.y_axis", "Y축: 로그인 요청 수")}
{multiLinePoints && multiLinePoints.length > 0 ? (
- {multiLinePoints.map((item) => ( -
- - {item.clientLabel} - {seriesByKey.get(item.key) ? ( - - {t( - "ui.dev.dashboard.chart.series", - "로그인 {{login}} / 사용자 {{subjects}}", - { - login: seriesByKey.get(item.key)!.loginRequests.toLocaleString(), - subjects: seriesByKey.get(item.key)!.uniqueSubjects.toLocaleString(), - }, - )} - - ) : null} -
- ))} + {multiLinePoints.map((item) => { + const seriesItem = seriesByKey.get(item.key); + return ( +
+ + {item.clientLabel} + {seriesItem ? ( + + {t( + "ui.dev.dashboard.chart.series", + "로그인 {{login}} / 사용자 {{subjects}}", + { + login: seriesItem.loginRequests.toLocaleString(), + subjects: seriesItem.uniqueSubjects.toLocaleString(), + }, + )} + + ) : null} +
+ ); + })}
) : topSeries.length > 0 ? (
@@ -498,10 +496,7 @@ function DashboardPage() { const [period, setPeriod] = useState("day"); const [selectedClientIds, setSelectedClientIds] = useState([]); const usageDays = period === "day" ? 14 : period === "week" ? 84 : 90; - const { - data: requestStatus, - isLoading: isLoadingRequestStatus, - } = useQuery({ + const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({ queryKey: ["developer-request", tenantId], queryFn: () => fetchDeveloperRequestStatus(tenantId), enabled: hasAccessToken && role === "user", @@ -625,10 +620,7 @@ function DashboardPage() { setSelectedClientIds([]); }; - if ( - (role === "user" || role === "tenant_member") && - isLoadingRequestStatus - ) { + if ((role === "user" || role === "tenant_member") && isLoadingRequestStatus) { return (
{t("ui.common.loading", "Loading...")} @@ -672,14 +664,8 @@ function DashboardPage() { onClick={() => navigate("/developer-requests")} > {isDeveloperRequestPending - ? t( - "ui.dev.nav.developer_request", - "개발자 권한 신청", - ) - : t( - "ui.dev.welcome.btn_request", - "개발자 등록 신청하기", - )} + ? t("ui.dev.nav.developer_request", "개발자 권한 신청") + : t("ui.dev.welcome.btn_request", "개발자 등록 신청하기")} )}
diff --git a/locales/en.toml b/locales/en.toml index 648e1126..d5dd34d8 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -553,6 +553,10 @@ openid = "Openid" profile = "Profile" [msg.dev.dashboard] +access_denied = "Overview is available only to users with developer access." +access_denied_detail = "Submit a request on the developer access page and wait for approval." +access_pending = "Your developer access request is under review." +access_pending_detail = "You can use the overview and developer features after a super admin approves it." description = "Review RP composition and authentication operations in one place." [msg.dev.dashboard.hero] @@ -2210,21 +2214,21 @@ registry = "RP registry" rp_synced = "RP registry synced" [ui.dev.dashboard.distribution] -headless = "Headless Login" +headless_hint = "{{count}} with Headless Login enabled" pkce = "PKCE" private = "Server side App" title = "Application Distribution" [ui.dev.dashboard.chart] +x_axis = "X-axis: Period" +y_axis = "Y-axis: Login requests" aria = "RP request overview" filter_all = "All" -login_requests = "Login requests" -other_requests = "Other requests" period_day = "Day" period_month = "Month" period_week = "Week" -series = "Login {{login}} / Other {{other}} / Users {{subjects}}" -title = "Login and other requests by application" +series = "Login {{login}} / Users {{subjects}}" +title = "Login requests by application" [ui.dev.dashboard.next] subtitle = "Ship the RP controls" @@ -2267,6 +2271,7 @@ plane = "Dev Plane" subtitle = "Manage your applications" [ui.dev.nav] +overview = "Overview" clients = "Connected Application" logout = "Logout" developer_request = "Developer Access Request" diff --git a/locales/ko.toml b/locales/ko.toml index f4396f76..440b0d02 100644 --- a/locales/ko.toml +++ b/locales/ko.toml @@ -405,6 +405,7 @@ pending = "준비 중" success = "성공" [ui.dev.nav] +overview = "개요" clients = "연동 앱" logout = "로그아웃" developer_request = "개발자 권한 신청" @@ -1044,6 +1045,10 @@ openid = "OIDC 인증 필수 스코프" profile = "기본 프로필 정보 접근" [msg.dev.dashboard] +access_denied = "개요는 개발자 권한이 있어야 볼 수 있습니다." +access_denied_detail = "개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요." +access_pending = "개발자 권한 신청을 검토 중입니다." +access_pending_detail = "super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다." description = "연동 앱 구성과 인증 운영 지표를 한 곳에서 확인합니다." [msg.dev.dashboard.hero] @@ -2672,21 +2677,21 @@ registry = "RP registry" rp_synced = "RP registry synced" [ui.dev.dashboard.distribution] -headless = "Headless Login" +headless_hint = "이 중 Headless Login 사용 {{count}}" pkce = "PKCE" private = "Server side App" title = "애플리케이션 구성 요약" [ui.dev.dashboard.chart] +x_axis = "X축: 기간" +y_axis = "Y축: 로그인 요청 수" aria = "RP 요청 현황" filter_all = "전체" -login_requests = "로그인 요청" -other_requests = "기타 요청" period_day = "일" period_month = "월" period_week = "주" -series = "로그인 {{login}} / 기타 {{other}} / 사용자 {{subjects}}" -title = "애플리케이션별 로그인요청/기타 요청 현황" +series = "로그인 {{login}} / 사용자 {{subjects}}" +title = "애플리케이션별 로그인 요청 현황" [ui.dev.dashboard.next] subtitle = "Ship the RP controls" diff --git a/locales/template.toml b/locales/template.toml index f82577aa..0d25ebd8 100644 --- a/locales/template.toml +++ b/locales/template.toml @@ -908,6 +908,10 @@ openid = "" profile = "" [msg.dev.dashboard] +access_denied = "" +access_denied_detail = "" +access_pending = "" +access_pending_detail = "" description = "" [msg.dev.dashboard.hero] @@ -2551,16 +2555,16 @@ registry = "" rp_synced = "" [ui.dev.dashboard.distribution] -headless = "" +headless_hint = "" pkce = "" private = "" title = "" [ui.dev.dashboard.chart] +x_axis = "" +y_axis = "" aria = "" filter_all = "" -login_requests = "" -other_requests = "" period_day = "" period_month = "" period_week = "" @@ -2608,6 +2612,7 @@ plane = "" subtitle = "" [ui.dev.nav] +overview = "" clients = "" logout = ""