From df09694ed62d4d79caf2f1cbcdecc33deaf346f6 Mon Sep 17 00:00:00 2001 From: kyy Date: Thu, 9 Apr 2026 09:46:40 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EB=A1=9C=EA=B3=A0=20URL=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/clients/ClientGeneralPage.tsx | 121 +++++++++++++++++- devfront/src/locales/en.toml | 7 + devfront/src/locales/ko.toml | 7 + devfront/src/locales/template.toml | 7 + 4 files changed, 137 insertions(+), 5 deletions(-) diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 2446daf9..84c4d470 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { ArrowLeft, + ExternalLink, Info, Plus, Save, @@ -133,6 +134,9 @@ function ClientGeneralPage() { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [logoUrl, setLogoUrl] = useState(""); + const [logoPreviewStatus, setLogoPreviewStatus] = useState< + "idle" | "loading" | "loaded" | "error" + >("idle"); const [clientType, setClientType] = useState("private"); const [status, setStatus] = useState("active"); const [initialStatus, setInitialStatus] = useState("active"); @@ -240,6 +244,21 @@ function ClientGeneralPage() { const securityProfile: SecurityProfile = clientType === "pkce" ? "pkce" : "private"; + const trimmedLogoUrl = logoUrl.trim(); + const hasLogoUrl = trimmedLogoUrl.length > 0; + const hasValidLogoUrl = !hasLogoUrl || isValidUrl(trimmedLogoUrl); + + useEffect(() => { + if (!hasLogoUrl) { + setLogoPreviewStatus("idle"); + return; + } + if (!hasValidLogoUrl) { + setLogoPreviewStatus("error"); + return; + } + setLogoPreviewStatus("loading"); + }, [hasLogoUrl, hasValidLogoUrl, trimmedLogoUrl]); const handleSecurityProfileChange = (profile: SecurityProfile) => { setClientType(profile); @@ -438,6 +457,15 @@ function ClientGeneralPage() { const mutation = useMutation({ mutationFn: async () => { + if (hasLogoUrl && !hasValidLogoUrl) { + throw new Error( + t( + "msg.dev.clients.general.identity.logo_invalid", + "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.", + ), + ); + } + const scopeNames = scopes.map((scope) => scope.name).filter(Boolean); const effectiveTokenEndpointAuthMethod = @@ -457,7 +485,7 @@ function ClientGeneralPage() { : undefined, metadata: { description, - logo_url: logoUrl, + logo_url: trimmedLogoUrl, structured_scopes: scopes, token_endpoint_auth_method: effectiveTokenEndpointAuthMethod, headless_login_enabled: headlessLoginEnabled, @@ -722,6 +750,8 @@ function ClientGeneralPage() { setLogoUrl(e.target.value)} + aria-invalid={!hasValidLogoUrl} + className={!hasValidLogoUrl ? "border-destructive" : ""} placeholder={t( "ui.dev.clients.general.identity.logo_placeholder", "https://example.com/logo.png", @@ -733,19 +763,100 @@ function ClientGeneralPage() { "인증 화면에 표시될 PNG/SVG URL입니다.", )}

+ {!hasValidLogoUrl ? ( +

+ {t( + "msg.dev.clients.general.identity.logo_invalid", + "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요.", + )} +

+ ) : null} + {hasLogoUrl && hasValidLogoUrl ? ( +
+ + {logoPreviewStatus === "loading" + ? t( + "msg.dev.clients.general.identity.logo_preview_loading", + "로고 미리보기를 불러오는 중입니다.", + ) + : logoPreviewStatus === "loaded" + ? t( + "msg.dev.clients.general.identity.logo_preview_ready", + "로고 미리보기를 확인했습니다.", + ) + : logoPreviewStatus === "error" + ? t( + "msg.dev.clients.general.identity.logo_preview_failed", + "로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요.", + ) + : null} + + + + {t( + "ui.dev.clients.general.identity.logo_open", + "새 탭에서 열기", + )} + +
+ ) : null} -
- {logoUrl ? ( +
+ {hasLogoUrl && hasValidLogoUrl ? ( {t( setLogoPreviewStatus("loaded")} + onError={() => setLogoPreviewStatus("error")} /> ) : ( - +
+ + {logoPreviewStatus === "error" ? ( + + {t( + "ui.dev.clients.general.identity.logo_preview_error_badge", + "미리보기 실패", + )} + + ) : ( + + {t( + "ui.dev.clients.general.identity.logo_preview_empty", + "미리보기", + )} + + )} +
)}
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index d3ae2a09..87256821 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -377,6 +377,10 @@ empty = "No IdP configurations found." [msg.dev.clients.general.identity] logo_help = "PNG or SVG URL shown on the consent and authentication screens." +logo_invalid = "The app logo URL format is invalid. Enter an http or https address." +logo_preview_loading = "Loading the logo preview." +logo_preview_ready = "Logo preview loaded." +logo_preview_failed = "Failed to load the logo preview. Check the URL or image access policy." subtitle = "Set the application name, description, and logo." [msg.dev.clients.general.redirect] @@ -1378,6 +1382,9 @@ description_placeholder = "Description Placeholder" logo = "App Logo URL" logo_placeholder = "https://example.com/logo.png" logo_preview = "Logo Preview" +logo_open = "Open in new tab" +logo_preview_error_badge = "Preview failed" +logo_preview_empty = "Preview" name = "Name" name_placeholder = "My Awesome Application" title = "Application Identity" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index b6abb943..2ac3c74a 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -377,6 +377,10 @@ subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다." [msg.dev.clients.general.identity] logo_help = "인증 화면에 표시될 PNG/SVG URL입니다." +logo_invalid = "앱 로고 URL 형식이 올바르지 않습니다. http 또는 https 주소를 입력하세요." +logo_preview_loading = "로고 미리보기를 불러오는 중입니다." +logo_preview_ready = "로고 미리보기를 확인했습니다." +logo_preview_failed = "로고 미리보기를 불러오지 못했습니다. URL 또는 이미지 접근 권한을 확인하세요." subtitle = "앱 이름과 설명, 로고를 설정합니다." [msg.dev.clients.general.redirect] @@ -1377,6 +1381,9 @@ description_placeholder = "앱에 대한 간단한 설명을 입력하세요." logo = "앱 로고 URL" logo_placeholder = "https://example.com/logo.png" logo_preview = "로고 미리보기" +logo_open = "새 탭에서 열기" +logo_preview_error_badge = "미리보기 실패" +logo_preview_empty = "미리보기" name = "앱 이름" name_placeholder = "예: 멋진 애플리케이션" title = "애플리케이션 정보" diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml index 87853adf..460baaec 100644 --- a/devfront/src/locales/template.toml +++ b/devfront/src/locales/template.toml @@ -377,6 +377,10 @@ empty = "" [msg.dev.clients.general.identity] logo_help = "" +logo_invalid = "" +logo_preview_loading = "" +logo_preview_ready = "" +logo_preview_failed = "" subtitle = "" [msg.dev.clients.general.redirect] @@ -1378,6 +1382,9 @@ description_placeholder = "" logo = "" logo_placeholder = "" logo_preview = "" +logo_open = "" +logo_preview_error_badge = "" +logo_preview_empty = "" name = "" name_placeholder = "" title = ""