From f8d0cf411ad20b567dbc20c803927be57a9644c1 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 27 May 2026 15:51:58 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=9E=90=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=8B=A0=EC=B2=AD=20=EC=97=AD=ED=95=A0=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/components/layout/AppLayout.tsx | 12 +-- devfront/src/features/audit/AuditLogsPage.tsx | 92 ++++++++++++++++++- .../DeveloperRequestPage.tsx | 7 +- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/devfront/src/components/layout/AppLayout.tsx b/devfront/src/components/layout/AppLayout.tsx index f12a527d..9df95752 100644 --- a/devfront/src/components/layout/AppLayout.tsx +++ b/devfront/src/components/layout/AppLayout.tsx @@ -45,12 +45,6 @@ const navItems: ShellSidebarNavItem[] = [ icon: LayoutDashboard, end: true, }, - { - labelKey: "ui.dev.nav.developer_request", - labelFallback: "Developer Access Request", - to: "/developer-requests", - icon: ClipboardCheck, - }, { labelKey: "ui.dev.nav.clients", labelFallback: "Clients", @@ -63,6 +57,12 @@ const navItems: ShellSidebarNavItem[] = [ to: "/audit-logs", icon: NotebookTabs, }, + { + labelKey: "ui.dev.nav.developer_request", + labelFallback: "Developer Access Request", + to: "/developer-requests", + icon: ClipboardCheck, + }, ]; type SessionStatusProps = { diff --git a/devfront/src/features/audit/AuditLogsPage.tsx b/devfront/src/features/audit/AuditLogsPage.tsx index 14fe276f..4cab4ebc 100644 --- a/devfront/src/features/audit/AuditLogsPage.tsx +++ b/devfront/src/features/audit/AuditLogsPage.tsx @@ -1,7 +1,9 @@ -import { useInfiniteQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { Download, NotebookTabs, RefreshCw, Search } from "lucide-react"; import * as React from "react"; +import { useAuth } from "react-oidc-context"; +import { useNavigate } from "react-router-dom"; import { parseAuditDetails } from "../../../../common/core/audit"; import { AuditLogTable } from "../../../../common/core/components/audit"; import { PageHeader } from "../../../../common/core/components/page"; @@ -17,9 +19,12 @@ import { CardTitle, } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; +import { fetchDeveloperRequestStatus } from "../../lib/devApi"; import type { DevAuditLog } from "../../lib/devApi"; import { fetchDevAuditLogs } from "../../lib/devApi"; import { t } from "../../lib/i18n"; +import { resolveProfileRole } from "../../lib/role"; +import { fetchMe } from "../auth/authApi"; function toCsv(logs: DevAuditLog[]) { const header = [ @@ -65,6 +70,13 @@ function downloadCsv(content: string, filename: string) { } function AuditLogsPage() { + 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 [searchClientId, setSearchClientId] = React.useState(""); const [searchAction, setSearchAction] = React.useState(""); const [statusFilter, setStatusFilter] = React.useState("all"); @@ -73,6 +85,29 @@ function AuditLogsPage() { const deferredSearchClientId = React.useDeferredValue(searchClientId.trim()); const deferredSearchAction = React.useDeferredValue(searchAction.trim()); + const { data: me, isLoading: isLoadingMe } = useQuery({ + queryKey: ["userMe"], + queryFn: fetchMe, + enabled: hasAccessToken, + }); + const profileRole = me?.role?.trim() || role; + const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({ + queryKey: ["developer-request", tenantId], + queryFn: () => fetchDeveloperRequestStatus(tenantId), + enabled: hasAccessToken && (profileRole === "user" || profileRole === "tenant_member"), + }); + const hasDeveloperAccess = + profileRole === "super_admin" || + profileRole === "tenant_admin" || + profileRole === "rp_admin" || + requestStatus?.status === "approved"; + const isDeveloperRequestPending = requestStatus?.status === "pending"; + const canRequestDeveloperAccess = + (profileRole === "user" || profileRole === "tenant_member") && + !isLoadingRequestStatus && + !hasDeveloperAccess && + !isDeveloperRequestPending; + const query = useInfiniteQuery({ queryKey: [ "dev-audit-logs", @@ -88,6 +123,7 @@ function AuditLogsPage() { }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.next_cursor || undefined, + enabled: hasDeveloperAccess, }); const logs = @@ -101,6 +137,60 @@ function AuditLogsPage() { downloadCsv(csv, `dev-audit-logs-${stamp}.csv`); }; + if ( + (profileRole === "user" || profileRole === "tenant_member") && + (isLoadingMe || isLoadingRequestStatus) + ) { + return ( +
+ {t("ui.common.loading", "Loading...")} +
+ ); + } + + 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) && ( + + )} +
+
+ ); + } + if (query.error) { const axiosError = query.error as AxiosError<{ error?: string }>; if (axiosError.response?.status === 403) { diff --git a/devfront/src/features/developer-request/DeveloperRequestPage.tsx b/devfront/src/features/developer-request/DeveloperRequestPage.tsx index 39737c02..72b2b6de 100644 --- a/devfront/src/features/developer-request/DeveloperRequestPage.tsx +++ b/devfront/src/features/developer-request/DeveloperRequestPage.tsx @@ -51,9 +51,9 @@ import { fetchMe } from "../auth/authApi"; export default function DeveloperRequestPage() { const auth = useAuth(); const queryClient = useQueryClient(); + const hasAccessToken = Boolean(auth.user?.access_token); const userProfile = auth.user?.profile as Record | undefined; const role = resolveProfileRole(userProfile); - const isSuperAdmin = role === "super_admin"; const tenantId = userProfile?.tenant_id as string | undefined; const companyCode = userProfile?.companyCode as string | undefined; @@ -73,7 +73,7 @@ export default function DeveloperRequestPage() { const { data: me } = useQuery({ queryKey: ["userMe"], queryFn: fetchMe, - enabled: !!auth.user?.access_token, + enabled: hasAccessToken, }); const currentTenant = tenants?.find( @@ -87,7 +87,8 @@ export default function DeveloperRequestPage() { (userProfile?.phone as string | undefined) || (userProfile?.phone_number as string | undefined) || ""; - const profileRole = me?.role || role; + const profileRole = me?.role?.trim() || role; + const isSuperAdmin = profileRole === "super_admin"; const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole); const approveMutation = useMutation({