From 41e755b1c7a160763fe44c953247a140538fea50 Mon Sep 17 00:00:00 2001 From: kyy Date: Mon, 8 Jun 2026 13:52:40 +0900 Subject: [PATCH] =?UTF-8?q?devfront=20=ED=85=8C=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=AF=B8=EC=86=8C=EC=86=8D=20=EA=B0=9C=EB=B0=9C=EC=9E=90=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=95=88=EB=82=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/audit/AuditLogsPage.test.tsx | 16 ++++++ devfront/src/features/audit/AuditLogsPage.tsx | 29 +++++++--- .../src/features/clients/ClientsPage.test.tsx | 27 +++++++++ devfront/src/features/clients/ClientsPage.tsx | 55 ++++++++++++++++++- .../developer-access/developerAccessGate.ts | 13 ++++- .../DeveloperRequestPage.test.tsx | 29 ++++++++++ .../DeveloperRequestPage.tsx | 34 +++++++++++- .../features/overview/GlobalOverviewPage.tsx | 29 +++++++--- devfront/src/locales/en.toml | 6 ++ devfront/src/locales/ko.toml | 6 ++ devfront/src/locales/template.toml | 6 ++ 11 files changed, 228 insertions(+), 22 deletions(-) diff --git a/devfront/src/features/audit/AuditLogsPage.test.tsx b/devfront/src/features/audit/AuditLogsPage.test.tsx index a8b9f9db..284094d2 100644 --- a/devfront/src/features/audit/AuditLogsPage.test.tsx +++ b/devfront/src/features/audit/AuditLogsPage.test.tsx @@ -174,6 +174,22 @@ describe("AuditLogsPage", () => { expect(navigateMock).toHaveBeenCalledWith("/developer-requests"); }); + it("shows a tenant-required notice when tenant context is missing", async () => { + gateState = { + hasDeveloperAccess: false, + isDeveloperRequestPending: false, + canRequestDeveloperAccess: false, + isLoadingDeveloperAccessGate: false, + isTenantContextMissing: true, + }; + + const container = await renderPage(); + expect(container.textContent).toContain( + "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", + ); + expect(container.textContent).not.toContain("개발자 권한 신청"); + }); + it("exports the fetched logs as CSV", async () => { const createObjectURL = vi .spyOn(URL, "createObjectURL") diff --git a/devfront/src/features/audit/AuditLogsPage.tsx b/devfront/src/features/audit/AuditLogsPage.tsx index f7d939bb..2d665ff5 100644 --- a/devfront/src/features/audit/AuditLogsPage.tsx +++ b/devfront/src/features/audit/AuditLogsPage.tsx @@ -97,6 +97,7 @@ function AuditLogsPage() { isDeveloperRequestPending, canRequestDeveloperAccess, isLoadingDeveloperAccessGate, + isTenantContextMissing, } = useDeveloperAccessGate({ hasAccessToken, profileRole, @@ -142,6 +143,24 @@ function AuditLogsPage() { } if (!hasDeveloperAccess) { + const deniedMessage = isTenantContextMissing + ? t( + "msg.dev.request.tenant_required", + "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", + ) + : t( + "msg.dev.audit.access_denied", + "감사 로그는 개발자 권한이 있어야 볼 수 있습니다.", + ); + const deniedDetailMessage = isTenantContextMissing + ? t( + "msg.dev.request.tenant_required_detail", + "현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다.", + ) + : t( + "msg.dev.audit.access_denied_detail", + "개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요.", + ); return ( navigate("/developer-requests")} /> diff --git a/devfront/src/features/clients/ClientsPage.test.tsx b/devfront/src/features/clients/ClientsPage.test.tsx index 2f92ecba..6a958ff6 100644 --- a/devfront/src/features/clients/ClientsPage.test.tsx +++ b/devfront/src/features/clients/ClientsPage.test.tsx @@ -277,4 +277,31 @@ describe("ClientsPage", () => { expect(navigateMock).toHaveBeenCalledWith("/developer-requests"); }); + + it("shows a tenant-required notice when tenant context is missing", async () => { + authState = { + user: { + access_token: "access-token", + profile: { + role: "user", + companyCode: "HANMAC", + name: "Requester", + email: "requester@example.com", + phone: "010-1234-5678", + }, + }, + }; + fetchMeMock.mockResolvedValue({ + role: "user", + name: "Requester", + email: "requester@example.com", + phone: "010-1234-5678", + }); + + const container = await renderPage(); + expect(container.textContent).toContain( + "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", + ); + expect(fetchDeveloperRequestStatusMock).not.toHaveBeenCalled(); + }); }); diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index 6c22bc88..e82b2c5e 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -67,6 +67,7 @@ function ClientsPage() { const role = resolveProfileRole(userProfile); const tenantId = userProfile?.tenant_id as string | undefined; const companyCode = userProfile?.companyCode as string | undefined; + const isTenantContextMissing = !tenantId?.trim(); const { data, @@ -93,7 +94,8 @@ function ClientsPage() { } = useQuery({ queryKey: ["developer-request", tenantId], queryFn: () => fetchDeveloperRequestStatus(tenantId), - enabled: hasAccessToken && profileRole === "user", + enabled: + hasAccessToken && profileRole === "user" && !isTenantContextMissing, }); const { data: tenants } = useQuery({ queryKey: ["myTenants"], @@ -108,7 +110,9 @@ function ClientsPage() { const canCreateClient = createAccessState === "can_create"; const isDeveloperRequestPending = createAccessState === "pending"; const canRequestDeveloperAccess = - createAccessState === "request_required" && !isLoadingRequest; + createAccessState === "request_required" && + !isLoadingRequest && + !isTenantContextMissing; const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); @@ -229,7 +233,20 @@ function ClientsPage() { "OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.", )} actions={ - canCreateClient ? ( + isTenantContextMissing ? ( +
+

+ {t( + "msg.dev.clients.create_requires_tenant", + "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", + )} +

+ +
+ ) : canCreateClient ? ( + )} {!isFilteredOut && canRequestDeveloperAccess && (