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 && (
+
+ {actionLabel}
+
+ )}
+
+
+ );
+}
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")}
- >
- {t("ui.dev.nav.developer_request", "개발자 권한 신청")}
-
- )}
-
-
+ 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")}
- >
- {t("ui.dev.nav.developer_request", "개발자 권한 신청")}
-
- )}
-
-
+ 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 = ""