diff --git a/devfront/src/components/common/DeveloperAccessRequestCard.test.tsx b/devfront/src/components/common/DeveloperAccessRequestCard.test.tsx new file mode 100644 index 00000000..ab78c1f2 --- /dev/null +++ b/devfront/src/components/common/DeveloperAccessRequestCard.test.tsx @@ -0,0 +1,66 @@ +import { act } from "react-dom/test-utils"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { DeveloperAccessRequestCard } from "./DeveloperAccessRequestCard"; + +describe("DeveloperAccessRequestCard", () => { + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("renders the request CTA for pending and denied states", () => { + const onAction = vi.fn(); + const container = document.createElement("div"); + document.body.appendChild(container); + const root = createRoot(container); + + act(() => { + root.render( + , + ); + }); + + expect(container.querySelector("h2")?.textContent).toBe("운영 현황"); + expect(container.textContent).toContain("검토 중"); + expect(container.textContent).toContain("승인 대기"); + + const button = container.querySelector("button"); + expect(button?.textContent).toBe("개발자 권한 신청"); + + act(() => { + button?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + expect(onAction).toHaveBeenCalledTimes(1); + + act(() => { + root.render( + , + ); + }); + + expect(container.querySelector("h2")?.textContent).toBe("감사 로그"); + expect(container.textContent).toContain("거부됨"); + expect(container.textContent).toContain("신청 필요"); + expect(container.querySelector("button")).not.toBeNull(); + }); +}); diff --git a/devfront/src/components/common/DeveloperAccessRequestCard.tsx b/devfront/src/components/common/DeveloperAccessRequestCard.tsx new file mode 100644 index 00000000..80bf315a --- /dev/null +++ b/devfront/src/components/common/DeveloperAccessRequestCard.tsx @@ -0,0 +1,48 @@ +interface DeveloperAccessRequestCardProps { + title: string; + isPending: boolean; + canRequest: boolean; + pendingMessage: string; + deniedMessage: string; + pendingDetailMessage: string; + deniedDetailMessage: string; + actionLabel: string; + onAction: () => void; +} + +export function DeveloperAccessRequestCard({ + title, + isPending, + canRequest, + pendingMessage, + deniedMessage, + pendingDetailMessage, + deniedDetailMessage, + actionLabel, + onAction, +}: DeveloperAccessRequestCardProps) { + const showAction = isPending || canRequest; + + return ( +
+
+

{title}

+

+ {isPending ? pendingMessage : deniedMessage} +

+

+ {isPending ? pendingDetailMessage : deniedDetailMessage} +

+ {showAction && ( + + )} +
+
+ ); +} diff --git a/devfront/src/features/audit/AuditLogsPage.tsx b/devfront/src/features/audit/AuditLogsPage.tsx index 4cab4ebc..8b0469fe 100644 --- a/devfront/src/features/audit/AuditLogsPage.tsx +++ b/devfront/src/features/audit/AuditLogsPage.tsx @@ -9,6 +9,7 @@ import { AuditLogTable } from "../../../../common/core/components/audit"; import { PageHeader } from "../../../../common/core/components/page"; import { SearchFilterBar } from "../../../../common/ui/search-filter-bar"; import { ForbiddenMessage } from "../../components/common/ForbiddenMessage"; +import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { @@ -94,7 +95,7 @@ function AuditLogsPage() { const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({ queryKey: ["developer-request", tenantId], queryFn: () => fetchDeveloperRequestStatus(tenantId), - enabled: hasAccessToken && (profileRole === "user" || profileRole === "tenant_member"), + enabled: hasAccessToken && profileRole === "user", }); const hasDeveloperAccess = profileRole === "super_admin" || @@ -103,7 +104,7 @@ function AuditLogsPage() { requestStatus?.status === "approved"; const isDeveloperRequestPending = requestStatus?.status === "pending"; const canRequestDeveloperAccess = - (profileRole === "user" || profileRole === "tenant_member") && + profileRole === "user" && !isLoadingRequestStatus && !hasDeveloperAccess && !isDeveloperRequestPending; @@ -138,7 +139,7 @@ function AuditLogsPage() { }; if ( - (profileRole === "user" || profileRole === "tenant_member") && + profileRole === "user" && (isLoadingMe || isLoadingRequestStatus) ) { return ( @@ -150,44 +151,29 @@ function AuditLogsPage() { if (!hasDeveloperAccess) { return ( -
-
-

- {t("ui.common.audit.title", "Audit Logs")} -

-

- {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) && ( - - )} -
-
+ navigate("/developer-requests")} + /> ); } diff --git a/devfront/src/features/overview/GlobalOverviewPage.tsx b/devfront/src/features/overview/GlobalOverviewPage.tsx index 16e64ec5..3f2330ee 100644 --- a/devfront/src/features/overview/GlobalOverviewPage.tsx +++ b/devfront/src/features/overview/GlobalOverviewPage.tsx @@ -16,6 +16,7 @@ import { OverviewMetric, OverviewSelectionChips, } from "../../../../common/core/components/overview"; +import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard"; import { type ClientSummary, fetchClients, @@ -523,7 +524,7 @@ function GlobalOverviewPage() { requestStatus?.status === "approved"; const isDeveloperRequestPending = requestStatus?.status === "pending"; const canRequestDeveloperAccess = - (profileRole === "user" || profileRole === "tenant_member") && + profileRole === "user" && !isLoadingRequestStatus && !hasDeveloperAccess && !isDeveloperRequestPending; @@ -615,7 +616,7 @@ function GlobalOverviewPage() { }; if ( - (profileRole === "user" || profileRole === "tenant_member") && + profileRole === "user" && (isLoadingMe || isLoadingRequestStatus) ) { return ( @@ -627,44 +628,29 @@ function GlobalOverviewPage() { if (!hasDeveloperAccess) { return ( -
-
-

- {t("ui.common.overview.title", "운영 현황")} -

-

- {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) && ( - - )} -
-
+ navigate("/developer-requests")} + /> ); } diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 5b4387da..b94625b6 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -511,6 +511,10 @@ 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.audit] +access_denied = "Audit logs are available only to users with developer access." +access_denied_detail = "Submit a request on the developer access page and wait for approval." + [msg.dev.dashboard.hero] body = "Body" title_emphasis = "Title Emphasis" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index f5028042..42fe9402 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -511,6 +511,10 @@ access_pending = "개발자 권한 신청을 검토 중입니다." access_pending_detail = "super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다." description = "연동 앱 구성과 인증 운영 지표를 한 곳에서 확인합니다." +[msg.dev.audit] +access_denied = "감사 로그는 개발자 권한이 있어야 볼 수 있습니다." +access_denied_detail = "개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요." + [msg.dev.dashboard.hero] body = "Hydra Admin API와 동기화된 RP 목록, 상태 토글, Consent 회수까지 devfront에서 처리하도록 준비합니다." title_emphasis = " 하나의 화면" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 40b3d035..85b2c1dc 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -549,6 +549,10 @@ access_pending = "" access_pending_detail = "" description = "" +[msg.dev.audit] +access_denied = "" +access_denied_detail = "" + [msg.dev.dashboard.hero] body = "" title_emphasis = ""