import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { Plus, Shield, Sparkles, Trash2, Upload } from "lucide-react"; import { useEffect, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; import { Label } from "../../components/ui/label"; import { Switch } from "../../components/ui/switch"; import { Textarea } from "../../components/ui/textarea"; import { createClient, fetchClient, updateClient } from "../../lib/devApi"; import type { ClientStatus, ClientType, ClientUpsertRequest, } from "../../lib/devApi"; import { t } from "../../lib/i18n"; import { cn } from "../../lib/utils"; interface ScopeItem { id: string; name: string; description: string; mandatory: boolean; } function ClientGeneralPage() { const params = useParams(); const navigate = useNavigate(); const queryClient = useQueryClient(); const clientId = params.id; const isCreate = !clientId; const { data, isLoading, error } = useQuery({ queryKey: ["client", clientId], queryFn: () => fetchClient(clientId as string), enabled: !isCreate, }); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [logoUrl, setLogoUrl] = useState(""); const [clientType, setClientType] = useState("confidential"); const [status, setStatus] = useState("active"); const [redirectUris, setRedirectUris] = useState(""); const [scopes, setScopes] = useState(() => [ { id: "1", name: "openid", description: t("msg.dev.clients.scopes.openid", "OIDC 인증 필수 스코프"), mandatory: true, }, { id: "2", name: "profile", description: t("msg.dev.clients.scopes.profile", "기본 프로필 정보 접근"), mandatory: false, }, { id: "3", name: "email", description: t("msg.dev.clients.scopes.email", "이메일 주소 접근"), mandatory: false, }, ]); useEffect(() => { if (!data) return; const { client } = data; setName(client.name || client.id); setClientType(client.type); setStatus(client.status); const metadata = client.metadata ?? {}; if (typeof metadata.description === "string") setDescription(metadata.description); if (typeof metadata.logo_url === "string") setLogoUrl(metadata.logo_url); // Metadata에 저장된 구조화된 scope 정보가 있으면 사용, 없으면 기본 scopes 문자열에서 생성 const savedScopes = metadata.structured_scopes as ScopeItem[] | undefined; if (savedScopes && Array.isArray(savedScopes)) { setScopes(savedScopes); } else { setScopes( client.scopes.map((s, idx) => ({ id: String(idx + 1), name: s, description: "", mandatory: s === "openid", })), ); } }, [data]); const addScope = () => { const newId = String(Date.now()); setScopes([ ...scopes, { id: newId, name: "", description: "", mandatory: false }, ]); }; const updateScope = ( id: string, field: K, value: ScopeItem[K], ) => { setScopes( scopes.map((scope) => scope.id === id ? { ...scope, [field]: value } : scope, ), ); }; const removeScope = (id: string) => { setScopes(scopes.filter((s) => s.id !== id)); }; const mutation = useMutation({ mutationFn: async () => { const scopeNames = scopes.map((scope) => scope.name).filter(Boolean); const payload: ClientUpsertRequest = { name, type: clientType, status, scopes: scopeNames, metadata: { description, logo_url: logoUrl, structured_scopes: scopes, // 향후 보존을 위해 metadata에 저장 }, }; // 생성 시에는 Redirect URIs를 포함해서 전송 if (isCreate) { payload.redirectUris = redirectUris .split(",") .map((uri) => uri.trim()) .filter(Boolean); return createClient(payload); } // 수정 시에는 Redirect URIs는 별도 탭에서 관리하므로 제외 (빈 배열이나 undefined로 보내지 않음) return updateClient(clientId as string, payload); }, onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ["clients"] }); if (result?.client?.id) { navigate(`/clients/${result.client.id}/settings`); } alert(t("msg.dev.clients.general.saved", "설정이 저장되었습니다.")); }, }); if (!isCreate && isLoading) { return (
{t("msg.dev.clients.general.loading", "Loading client...")}
); } if (!isCreate && (error || !data)) { const errMsg = (error as AxiosError<{ error?: string }>).response?.data?.error ?? (error as Error)?.message; return (
{t( "msg.dev.clients.general.load_error", "Error loading client: {{error}}", { error: errMsg || t("msg.common.unknown_error", "unknown error"), }, )}
); } const displayName = isCreate ? t("ui.dev.clients.general.display_new", "새 클라이언트") : data?.client?.name || data?.client?.id; return (
{t("ui.dev.clients.general.breadcrumb.section", "Applications")} / {displayName}

{isCreate ? t("ui.dev.clients.general.title_create", "Create Client") : t("ui.dev.clients.general.title_edit", "Client Settings")}

{status === "active" ? t("ui.common.status.active", "Active") : t("ui.common.status.inactive", "Inactive")}
{!isCreate && ( <> {t("ui.dev.clients.details.tab.connection", "Connection")} {t("ui.dev.clients.details.tab.consents", "Consent & Users")} {t("ui.dev.clients.details.tab.settings", "Settings")} )}
{/* 1. Application Identity */}
{t("ui.dev.clients.general.identity.title", "Application Identity")} {t( "msg.dev.clients.general.identity.subtitle", "앱 이름과 설명, 로고를 설정합니다.", )}
setName(e.target.value)} placeholder={t( "ui.dev.clients.general.identity.name_placeholder", "My Awesome Application", )} />