From 4dc274a5d791c01dabe1b267c6005307ec207185 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 22 Apr 2026 11:42:02 +0900 Subject: [PATCH] =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8?= =?UTF-8?q?=20=EB=B9=88=20=EB=AA=A9=EB=A1=9D=20=EB=8C=80=EC=9D=91=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=9E=90=20=EC=8B=A0=EC=B2=AD=20=EC=9D=B8?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=20=EB=A7=81=ED=81=AC=20=EB=B0=8F=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devfront/src/features/clients/ClientsPage.tsx | 217 +++++++++++++++++- devfront/src/lib/devApi.ts | 43 ++++ 2 files changed, 249 insertions(+), 11 deletions(-) diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index d47cd449..1f75d072 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -1,12 +1,14 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { BookOpenText, + Clock, Filter, Plus, Search, ServerCog, ShieldHalf, + X, } from "lucide-react"; import { useState } from "react"; import { useAuth } from "react-oidc-context"; @@ -27,6 +29,7 @@ import { CardTitle, } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; +import { Label } from "../../components/ui/label"; import { Separator } from "../../components/ui/separator"; import { Table, @@ -36,7 +39,14 @@ import { TableHeader, TableRow, } from "../../components/ui/table"; -import { fetchClients, fetchDevStats } from "../../lib/devApi"; +import { Textarea } from "../../components/ui/textarea"; +import { + type DeveloperRequest, + fetchClients, + fetchDevStats, + fetchDeveloperRequestStatus, + requestDeveloperAccess, +} from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { resolveProfileRole } from "../../lib/role"; import { cn } from "../../lib/utils"; @@ -45,9 +55,10 @@ function ClientsPage() { const navigate = useNavigate(); const auth = useAuth(); const hasAccessToken = Boolean(auth.user?.access_token); - const role = resolveProfileRole( - auth.user?.profile as Record | undefined, - ); + const userProfile = auth.user?.profile as Record | undefined; + const role = resolveProfileRole(userProfile); + const tenantId = userProfile?.tenant_id as string | undefined; + const canCreateClient = role !== "user" && role !== "tenant_member"; const { @@ -66,10 +77,21 @@ function ClientsPage() { enabled: hasAccessToken, }); + const { + data: requestStatus, + isLoading: isLoadingRequest, + refetch: refetchRequest, + } = useQuery({ + queryKey: ["developer-request", tenantId], + queryFn: () => fetchDeveloperRequestStatus(tenantId), + enabled: hasAccessToken && role === "user", + }); + const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); const [isAdvancedFilterOpen, setIsAdvancedFilterOpen] = useState(false); + const [isRequestModalOpen, setIsRequestModalOpen] = useState(false); const clients = data?.items || []; @@ -128,7 +150,7 @@ function ClientsPage() { }, ]; - const isLoading = isLoadingClients || isLoadingStats; + const isLoading = isLoadingClients || isLoadingStats || isLoadingRequest; if (auth.isLoading || !hasAccessToken || isLoading) { return ( @@ -154,6 +176,8 @@ function ClientsPage() { ); } + const devStatus = (requestStatus as DeveloperRequest)?.status || "none"; + return (
@@ -372,12 +396,44 @@ function ClientsPage() { "조회 가능한 RP가 없습니다.", )}

-

- {t( - "msg.dev.clients.empty_detail", - "RP 관계가 부여되면 이 목록에 해당 RP가 표시됩니다.", +

+

+ {t( + "msg.dev.clients.empty_detail", + "RP 관계가 부여되면 이 목록에 해당 RP가 표시됩니다.", + )} +

+ {role === "user" && devStatus === "none" && ( + )} -

+ {role === "user" && devStatus === "pending" && ( +

+ + {t("ui.dev.request.pending.title", "심사 진행 중")} +

+ )} + {role === "user" && devStatus === "rejected" && ( + + )} +
@@ -552,6 +608,145 @@ function ClientsPage() { + + setIsRequestModalOpen(false)} + onSuccess={() => { + refetchRequest(); + setIsRequestModalOpen(false); + }} + tenantId={tenantId || ""} + initialName={(userProfile?.name as string) || ""} + initialOrg={(userProfile?.companyCode as string) || ""} + /> + + ); +} + +interface RequestAccessModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; + tenantId: string; + initialName: string; + initialOrg: string; +} + +function RequestAccessModal({ + isOpen, + onClose, + onSuccess, + tenantId, + initialName, + initialOrg, +}: RequestAccessModalProps) { + const [name, setName] = useState(initialName); + const [organization, setOrganization] = useState(initialOrg); + const [reason, setReason] = useState(""); + + const mutation = useMutation({ + mutationFn: requestDeveloperAccess, + onSuccess: () => { + onSuccess(); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + mutation.mutate({ + name, + organization, + reason, + tenantId, + }); + }; + + if (!isOpen) return null; + + return ( +
+
+
+
+

+ {t("ui.dev.request.modal.title", "개발자 등록 신청")} +

+

+ {t( + "msg.dev.request.modal.desc", + "신청 사유를 입력해 주세요. 관리자 확인 후 승인됩니다.", + )} +

+
+ +
+ +
+
+
+ + setName(e.target.value)} + required + /> +
+
+ + setOrganization(e.target.value)} + required + /> +
+
+ +