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 ? (

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 = ""