1
0
forked from baron/baron-sso

devfront UI 일관성 및 정렬 정책 적용

This commit is contained in:
2026-02-25 16:07:41 +09:00
parent 85538ae672
commit 3cfece2a33
8 changed files with 287 additions and 257 deletions

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Plus, Shield, Sparkles, Trash2, Upload } from "lucide-react";
import { Plus, Save, 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";
@@ -16,6 +16,7 @@ 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 { toast } from "../../components/ui/use-toast";
import {
createClient,
deleteClient,
@@ -128,6 +129,21 @@ function ClientGeneralPage() {
setScopes(scopes.filter((s) => s.id !== id));
};
const handleStatusChange = (nextStatus: ClientStatus) => {
setStatus(nextStatus);
const statusLabel =
nextStatus === "active"
? t("ui.common.status.active", "Active")
: t("ui.common.status.inactive", "Inactive");
toast(
t(
"msg.dev.clients.general.status_changed",
"상태가 {{status}}로 변경되었습니다.",
{ status: statusLabel },
),
);
};
const mutation = useMutation({
mutationFn: async () => {
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
@@ -204,7 +220,7 @@ function ClientGeneralPage() {
window.confirm(
t(
"msg.dev.clients.delete_confirm",
"정말로 이 앱을 삭제하시습니까? 이 작업은 되돌릴 수 없습니다.",
"정말로 이 앱을 삭제하시습니까? 이 작업은 되돌릴 수 없습니다.",
),
)
) {
@@ -309,33 +325,6 @@ function ClientGeneralPage() {
)}
</CardDescription>
</div>
{!isCreate && (
<div className="flex flex-col items-end gap-2">
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.table.status", "상태")}
</Label>
<div className="flex items-center gap-3">
<Switch
checked={status === "active"}
onCheckedChange={(checked) =>
setStatus(checked ? "active" : "inactive")
}
/>
<span
className={cn(
"text-sm font-medium",
status === "active"
? "text-emerald-400"
: "text-muted-foreground",
)}
>
{status === "active"
? t("ui.common.status.active", "활성")
: t("ui.common.status.inactive", "비활성")}
</span>
</div>
</div>
)}
</div>
<div className="grid gap-8 md:grid-cols-2">
<div className="space-y-5">
@@ -371,40 +360,66 @@ function ClientGeneralPage() {
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.dev.clients.general.identity.logo", "App Logo URL")}
</Label>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Input
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
placeholder={t(
"ui.dev.clients.general.identity.logo_placeholder",
"https://example.com/logo.png",
)}
/>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.identity.logo_help",
"인증 화면에 표시될 PNG/SVG URL입니다.",
)}
</p>
</div>
<div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/40 shrink-0">
{logoUrl ? (
<img
src={logoUrl}
alt={t(
"ui.dev.clients.general.identity.logo_preview",
"Logo Preview",
<div className="space-y-5">
<div className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.dev.clients.general.identity.logo", "App Logo URL")}
</Label>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Input
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
placeholder={t(
"ui.dev.clients.general.identity.logo_placeholder",
"https://example.com/logo.png",
)}
className="h-full w-full object-contain"
/>
) : (
<Upload className="h-5 w-5 text-muted-foreground" />
)}
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.identity.logo_help",
"인증 화면에 표시될 PNG/SVG URL입니다.",
)}
</p>
</div>
<div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/40 shrink-0">
{logoUrl ? (
<img
src={logoUrl}
alt={t(
"ui.dev.clients.general.identity.logo_preview",
"Logo Preview",
)}
className="h-full w-full object-contain"
/>
) : (
<Upload className="h-5 w-5 text-muted-foreground" />
)}
</div>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.dev.clients.table.status", "상태")}
</Label>
<div className="flex gap-2">
<Button
type="button"
size="sm"
variant={status === "active" ? "default" : "outline"}
onClick={() => handleStatusChange("active")}
>
{t("ui.common.status.active", "활성")}
</Button>
<Button
type="button"
size="sm"
variant={status === "inactive" ? "default" : "outline"}
onClick={() => handleStatusChange("inactive")}
>
{t("ui.common.status.inactive", "비활성")}
</Button>
</div>
</div>
</div>
@@ -658,20 +673,30 @@ function ClientGeneralPage() {
</Button>
)}
</div>
<div className="flex items-center gap-3">
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
<Button variant="outline" onClick={() => navigate("/clients")}>
{t("ui.common.cancel", "취소")}
</Button>
<Button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
className="px-8 shadow-lg shadow-primary/20"
disabled={
mutation.isPending ||
isLoading ||
name.trim() === "" ||
(isCreate && redirectUris.trim() === "")
}
className="shadow-lg shadow-primary/20"
>
{mutation.isPending ? (
<div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
) : (
<Save size={16} className="mr-2" />
)}
{mutation.isPending
? t("msg.common.saving", "저장 중...")
: isCreate
? t("ui.dev.clients.general.create", "클라이언트 생성")
: t("ui.dev.clients.general.save", "설정 저장")}
: t("ui.common.save", "저장")}
</Button>
</div>
</div>