diff --git a/adminfront/src/components/layout/RoleSwitcher.tsx b/adminfront/src/components/layout/RoleSwitcher.tsx index 145328d5..a173b7d5 100644 --- a/adminfront/src/components/layout/RoleSwitcher.tsx +++ b/adminfront/src/components/layout/RoleSwitcher.tsx @@ -100,7 +100,12 @@ const RoleSwitcher: FC = () => { }} > {( - ["super_admin", "tenant_admin", "rp_admin", "tenant_member"] as const + [ + "super_admin", + "tenant_admin", + "rp_admin", + "tenant_member", + ] as const ).map((role) => ( ))} diff --git a/adminfront/src/components/ui/badge.tsx b/adminfront/src/components/ui/badge.tsx index 8ef586fd..8f9d0789 100644 --- a/adminfront/src/components/ui/badge.tsx +++ b/adminfront/src/components/ui/badge.tsx @@ -26,7 +26,8 @@ const badgeVariants = cva( ); export interface BadgeProps - extends React.HTMLAttributes, + extends + React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { diff --git a/adminfront/src/components/ui/button.test.tsx b/adminfront/src/components/ui/button.test.tsx index 25fa7808..5925035b 100644 --- a/adminfront/src/components/ui/button.test.tsx +++ b/adminfront/src/components/ui/button.test.tsx @@ -6,7 +6,9 @@ import { Button } from "./button"; describe("Button Component", () => { it("renders correctly with children", () => { render(); - expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: /click me/i }), + ).toBeInTheDocument(); }); it("applies variant classes correctly", () => { @@ -23,7 +25,7 @@ describe("Button Component", () => { const onClick = vi.fn(); const user = userEvent.setup(); render(); - + await user.click(screen.getByRole("button", { name: /click me/i })); expect(onClick).toHaveBeenCalledTimes(1); }); diff --git a/adminfront/src/components/ui/button.tsx b/adminfront/src/components/ui/button.tsx index ee1a84b4..91d21e58 100644 --- a/adminfront/src/components/ui/button.tsx +++ b/adminfront/src/components/ui/button.tsx @@ -34,7 +34,8 @@ const buttonVariants = cva( ); export interface ButtonProps - extends React.ButtonHTMLAttributes, + extends + React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } diff --git a/adminfront/src/components/ui/input.tsx b/adminfront/src/components/ui/input.tsx index 41955477..eb2a9c6e 100644 --- a/adminfront/src/components/ui/input.tsx +++ b/adminfront/src/components/ui/input.tsx @@ -1,8 +1,7 @@ import * as React from "react"; import { cn } from "../../lib/utils"; -export interface InputProps - extends React.InputHTMLAttributes {} +export interface InputProps extends React.InputHTMLAttributes {} const Input = React.forwardRef( ({ className, type, ...props }, ref) => { diff --git a/adminfront/src/components/ui/textarea.tsx b/adminfront/src/components/ui/textarea.tsx index 80f0abc2..78dbae91 100644 --- a/adminfront/src/components/ui/textarea.tsx +++ b/adminfront/src/components/ui/textarea.tsx @@ -1,8 +1,7 @@ import * as React from "react"; import { cn } from "../../lib/utils"; -export interface TextareaProps - extends React.TextareaHTMLAttributes {} +export interface TextareaProps extends React.TextareaHTMLAttributes {} const Textarea = React.forwardRef( ({ className, ...props }, ref) => { diff --git a/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx b/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx index 82ccc6dd..308166e7 100644 --- a/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx +++ b/adminfront/src/features/tenants/routes/TenantAdminsTab.tsx @@ -1,6 +1,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { Plus, Search, ShieldCheck, Trash2, UserPlus, Users } from "lucide-react"; +import { + Plus, + Search, + ShieldCheck, + Trash2, + UserPlus, + Users, +} from "lucide-react"; import { useState } from "react"; import { useParams } from "react-router-dom"; import { toast } from "sonner"; @@ -64,11 +71,16 @@ export function TenantAdminsTab() { mutationFn: (userId: string) => addTenantAdmin(tenantId, userId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] }); - toast.success(t("msg.admin.tenants.admins.add_success", "관리자가 추가되었습니다.")); + toast.success( + t("msg.admin.tenants.admins.add_success", "관리자가 추가되었습니다."), + ); setSearchTerm(""); }, onError: (err: AxiosError<{ error?: string }>) => { - toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다.")); + toast.error( + err.response?.data?.error || + t("msg.common.error", "오류가 발생했습니다."), + ); }, }); @@ -76,10 +88,15 @@ export function TenantAdminsTab() { mutationFn: (userId: string) => removeTenantAdmin(tenantId, userId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tenant-admins", tenantId] }); - toast.success(t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다.")); + toast.success( + t("msg.admin.tenants.admins.remove_success", "권한이 회수되었습니다."), + ); }, onError: (err: AxiosError<{ error?: string }>) => { - toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다.")); + toast.error( + err.response?.data?.error || + t("msg.common.error", "오류가 발생했습니다."), + ); }, }); @@ -88,7 +105,15 @@ export function TenantAdminsTab() { }; const handleRemoveAdmin = (userId: string, userName: string) => { - if (window.confirm(t("msg.admin.tenants.admins.remove_confirm", "관리자를 삭제하시겠습니까?", { name: userName }))) { + if ( + window.confirm( + t( + "msg.admin.tenants.admins.remove_confirm", + "관리자를 삭제하시겠습니까?", + { name: userName }, + ), + ) + ) { removeMutation.mutate(userId); } }; @@ -106,14 +131,20 @@ export function TenantAdminsTab() { {t("ui.admin.tenants.admins.title", "테넌트 관리자")} - {t("msg.admin.tenants.admins.subtitle", "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.")} + {t( + "msg.admin.tenants.admins.subtitle", + "이 테넌트의 자원을 관리할 수 있는 사용자 목록입니다.", + )} - { - setIsAddDialogOpen(open); - if (!open) setSearchTerm(""); - }}> + { + setIsAddDialogOpen(open); + if (!open) setSearchTerm(""); + }} + > @@ -219,16 +284,27 @@ export function TenantAdminsTab() { ) : currentAdmins.length === 0 ? ( - +
-

{t("msg.admin.tenants.admins.empty", "등록된 관리자가 없습니다.")}

+

+ {t( + "msg.admin.tenants.admins.empty", + "등록된 관리자가 없습니다.", + )} +

) : ( currentAdmins.map((admin) => ( - +
@@ -245,9 +321,14 @@ export function TenantAdminsTab() { variant="ghost" size="icon" className="opacity-0 group-hover:opacity-100 text-destructive hover:text-destructive hover:bg-destructive/10 transition-all" - onClick={() => handleRemoveAdmin(admin.id, admin.name)} + onClick={() => + handleRemoveAdmin(admin.id, admin.name) + } disabled={removeMutation.isPending} - title={t("ui.admin.tenants.admins.remove_title", "관리자 권한 회수")} + title={t( + "ui.admin.tenants.admins.remove_title", + "관리자 권한 회수", + )} > diff --git a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx index e7c9b173..3e7d7331 100644 --- a/adminfront/src/features/tenants/routes/TenantCreatePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantCreatePage.tsx @@ -117,10 +117,27 @@ function TenantCreatePage() { value={type} onChange={(e) => setType(e.target.value)} > - - - - + + + +
diff --git a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx index 6ccee5b7..d30b96a1 100644 --- a/adminfront/src/features/tenants/routes/TenantDetailPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantDetailPage.tsx @@ -25,21 +25,32 @@ function TenantDetailPage() {
- + {t("ui.admin.tenants.detail.breadcrumb_list", "테넌트 목록")} / - {t("ui.admin.tenants.detail.title", "상세")} + + {t("ui.admin.tenants.detail.title", "상세")} +

- {tenantQuery.data?.name ?? t("ui.admin.tenants.detail.loading", "불러오는 중...")} + {tenantQuery.data?.name ?? + t("ui.admin.tenants.detail.loading", "불러오는 중...")}

- {t("ui.admin.tenants.detail.header_subtitle", "테넌트 정보를 수정하거나 연동 설정을 관리합니다.")} + {t( + "ui.admin.tenants.detail.header_subtitle", + "테넌트 정보를 수정하거나 연동 설정을 관리합니다.", + )}

- {t("ui.common.admin_only", "관리자 전용")} + + {t("ui.common.admin_only", "관리자 전용")} +
{/* Tabs */} diff --git a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx index c92e411b..4c483215 100644 --- a/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantGroupsPage.tsx @@ -43,9 +43,15 @@ import { import { t } from "../../../lib/i18n"; import { toast } from "sonner"; -type UserGroupNode = GroupSummary & { children: UserGroupNode[]; isExpanded?: boolean }; +type UserGroupNode = GroupSummary & { + children: UserGroupNode[]; + isExpanded?: boolean; +}; -function buildGroupTree(groups: GroupSummary[], parentId: string | null = null): UserGroupNode[] { +function buildGroupTree( + groups: GroupSummary[], + parentId: string | null = null, +): UserGroupNode[] { const nodes: UserGroupNode[] = []; const childrenOf = new Map(); @@ -56,7 +62,10 @@ function buildGroupTree(groups: GroupSummary[], parentId: string | null = null): // Second pass: Populate children groups.forEach((group) => { - const node: UserGroupNode = { ...group, children: childrenOf.get(group.id)! }; + const node: UserGroupNode = { + ...group, + children: childrenOf.get(group.id)!, + }; if (group.parentId === parentId) { nodes.push(node); } else { @@ -73,7 +82,7 @@ function buildGroupTree(groups: GroupSummary[], parentId: string | null = null): // Sort children for consistent rendering (optional, but good for UI) nodes.sort((a, b) => a.name.localeCompare(b.name)); - nodes.forEach(node => { + nodes.forEach((node) => { node.children.sort((a, b) => a.name.localeCompare(b.name)); }); @@ -130,11 +139,16 @@ const UserGroupTreeNode: React.FC = ({ )} - ) : (level > 0 && ( + ) : ( + level > 0 && ( - + - ))} + ) + )} {node.name} @@ -221,7 +235,12 @@ function TenantGroupsPage() { parentId: newGroupParentId || undefined, }), onSuccess: () => { - toast.success(t("msg.admin.groups.list.create_success", "그룹이 성공적으로 생성되었습니다.")); + toast.success( + t( + "msg.admin.groups.list.create_success", + "그룹이 성공적으로 생성되었습니다.", + ), + ); groupsQuery.refetch(); setNewGroupName(""); setNewGroupNameDesc(""); @@ -229,21 +248,27 @@ function TenantGroupsPage() { setNewGroupParentId(null); }, onError: (error: AxiosError<{ error?: string }>) => { - toast.error(t("msg.admin.groups.list.create_error", "그룹 생성 실패"), { description: error.response?.data?.error || error.message }); - } + toast.error(t("msg.admin.groups.list.create_error", "그룹 생성 실패"), { + description: error.response?.data?.error || error.message, + }); + }, }); // 그룹 삭제 const deleteMutation = useMutation({ mutationFn: (id: string) => deleteGroup(tenantId, id), onSuccess: () => { - toast.success(t("msg.admin.groups.list.delete_success", "그룹이 삭제되었습니다.")); + toast.success( + t("msg.admin.groups.list.delete_success", "그룹이 삭제되었습니다."), + ); groupsQuery.refetch(); setSelectedGroupId(null); }, onError: (error: AxiosError<{ error?: string }>) => { - toast.error(t("msg.admin.groups.list.delete_error", "그룹 삭제 실패"), { description: error.response?.data?.error || error.message }); - } + toast.error(t("msg.admin.groups.list.delete_error", "그룹 삭제 실패"), { + description: error.response?.data?.error || error.message, + }); + }, }); // 멤버 추가 @@ -251,12 +276,16 @@ function TenantGroupsPage() { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => addGroupMember(tenantId, groupId, userId), onSuccess: () => { - toast.success(t("msg.admin.groups.members.add_success", "멤버가 추가되었습니다.")); + toast.success( + t("msg.admin.groups.members.add_success", "멤버가 추가되었습니다."), + ); groupsQuery.refetch(); }, onError: (error: AxiosError<{ error?: string }>) => { - toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message }); - } + toast.error(t("msg.common.error", "오류 발생"), { + description: error.response?.data?.error || error.message, + }); + }, }); // 멤버 제거 @@ -264,15 +293,21 @@ function TenantGroupsPage() { mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) => removeGroupMember(tenantId, groupId, userId), onSuccess: () => { - toast.success(t("msg.admin.groups.members.remove_success", "멤버가 제거되었습니다.")); + toast.success( + t("msg.admin.groups.members.remove_success", "멤버가 제거되었습니다."), + ); groupsQuery.refetch(); }, onError: (error: AxiosError<{ error?: string }>) => { - toast.error(t("msg.common.error", "오류 발생"), { description: error.response?.data?.error || error.message }); - } + toast.error(t("msg.common.error", "오류 발생"), { + description: error.response?.data?.error || error.message, + }); + }, }); - const groupTree = groupsQuery.data ? buildGroupTree(groupsQuery.data, tenantId) : []; + const groupTree = groupsQuery.data + ? buildGroupTree(groupsQuery.data, tenantId) + : []; const handleAddSubGroup = (parentId: string) => { setNewGroupParentId(parentId); @@ -304,7 +339,10 @@ function TenantGroupsPage() { {t("ui.admin.groups.create.title", "새 그룹 생성")} - {t("ui.admin.groups.create.description", "새로운 사용자 그룹을 생성하고 계층 구조를 설정합니다.")} + {t( + "ui.admin.groups.create.description", + "새로운 사용자 그룹을 생성하고 계층 구조를 설정합니다.", + )} @@ -425,8 +463,14 @@ function TenantGroupsPage() { )} {!groupsQuery.isLoading && groupTree.length === 0 && ( - - {t("msg.admin.groups.list.empty", "아직 등록된 그룹이 없습니다.")} + + {t( + "msg.admin.groups.list.empty", + "아직 등록된 그룹이 없습니다.", + )} )} @@ -438,9 +482,16 @@ function TenantGroupsPage() { onSelect={setSelectedGroupId} selectedGroupId={selectedGroupId} onDelete={(id) => { - if (window.confirm(t("msg.admin.groups.list.delete_confirm", "그룹을 삭제하시겠습니까?"))) { - deleteMutation.mutate(id); - } + if ( + window.confirm( + t( + "msg.admin.groups.list.delete_confirm", + "그룹을 삭제하시겠습니까?", + ), + ) + ) { + deleteMutation.mutate(id); + } }} onAddSubGroup={handleAddSubGroup} addMemberMutation={addMemberMutation} @@ -464,15 +515,22 @@ function TenantGroupsPage() { })} - {t("ui.admin.groups.detail.members_subtitle", "그룹에 속한 멤버들을 확인하고 관리합니다.")} + {t( + "ui.admin.groups.detail.members_subtitle", + "그룹에 속한 멤버들을 확인하고 관리합니다.", + )}
- +
diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index a9f5b994..8e326978 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -1,12 +1,6 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { - CornerDownRight, - Pencil, - Plus, - RefreshCw, - Trash2, -} from "lucide-react"; +import { CornerDownRight, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; import React from "react"; import { Link, useNavigate } from "react-router-dom"; import { Badge } from "../../../components/ui/badge"; @@ -72,7 +66,9 @@ const TenantRow: React.FC<{
- {level > 0 && } + {level > 0 && ( + + )} {tenant.name}
@@ -88,8 +84,8 @@ const TenantRow: React.FC<{ tenant.status === "active" ? "default" : tenant.status === "pending" - ? "secondary" - : "muted" + ? "secondary" + : "muted" } > {t(`ui.common.status.${tenant.status}`, tenant.status)} @@ -267,7 +263,10 @@ function TenantListPage() { )} {!query.isLoading && tenantTree.length === 0 && ( - + {t( "msg.admin.tenants.empty", "아직 등록된 테넌트가 없습니다.", @@ -293,4 +292,3 @@ function TenantListPage() { } export default TenantListPage; - diff --git a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx index 96c2541d..aba6f9d5 100644 --- a/adminfront/src/features/tenants/routes/TenantProfilePage.tsx +++ b/adminfront/src/features/tenants/routes/TenantProfilePage.tsx @@ -29,7 +29,9 @@ export function TenantProfilePage() { const queryClient = useQueryClient(); if (!tenantId) { - return
{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}
; + return ( +
{t("msg.admin.tenants.missing_id", "테넌트 ID가 없습니다.")}
+ ); } const tenantQuery = useQuery({ @@ -74,8 +76,11 @@ export function TenantProfilePage() { toast.success(t("msg.info.saved_success", "저장되었습니다.")); }, onError: (err: AxiosError<{ error?: string }>) => { - toast.error(err.response?.data?.error || t("err.common.unknown", "오류가 발생했습니다.")); - } + toast.error( + err.response?.data?.error || + t("err.common.unknown", "오류가 발생했습니다."), + ); + }, }); const approveMutation = useMutation({ @@ -83,18 +88,25 @@ export function TenantProfilePage() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tenants"] }); queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] }); - toast.success(t("msg.admin.tenants.approve_success", "테넌트가 승인되었습니다.")); + toast.success( + t("msg.admin.tenants.approve_success", "테넌트가 승인되었습니다."), + ); }, onError: (err: AxiosError<{ error?: string }>) => { - toast.error(err.response?.data?.error || t("err.common.unknown", "오류가 발생했습니다.")); - } + toast.error( + err.response?.data?.error || + t("err.common.unknown", "오류가 발생했습니다."), + ); + }, }); const deleteMutation = useMutation({ mutationFn: () => deleteTenant(tenantId), onSuccess: () => { navigate("/tenants"); - toast.success(t("msg.admin.tenants.delete_success", "테넌트가 삭제되었습니다.")); + toast.success( + t("msg.admin.tenants.delete_success", "테넌트가 삭제되었습니다."), + ); }, }); @@ -104,13 +116,23 @@ export function TenantProfilePage() { ?.response?.data?.error; const handleDelete = () => { - if (window.confirm(t("msg.admin.tenants.delete_confirm", "삭제하시겠습니까?", { name: tenantQuery.data?.name ?? "" }))) { + if ( + window.confirm( + t("msg.admin.tenants.delete_confirm", "삭제하시겠습니까?", { + name: tenantQuery.data?.name ?? "", + }), + ) + ) { deleteMutation.mutate(); } }; const handleApprove = () => { - if (window.confirm(t("msg.admin.tenants.approve_confirm", "이 테넌트를 승인하시겠습니까?"))) { + if ( + window.confirm( + t("msg.admin.tenants.approve_confirm", "이 테넌트를 승인하시겠습니까?"), + ) + ) { approveMutation.mutate(); } }; @@ -119,9 +141,14 @@ export function TenantProfilePage() { <> - {t("ui.admin.tenants.profile.title", "테넌트 프로필")} + + {t("ui.admin.tenants.profile.title", "테넌트 프로필")} + - {t("ui.admin.tenants.profile.subtitle", "슬러그 및 상태 변경은 즉시 적용됩니다.")} + {t( + "ui.admin.tenants.profile.subtitle", + "슬러그 및 상태 변경은 즉시 적용됩니다.", + )} @@ -132,30 +159,54 @@ export function TenantProfilePage() { )}
setName(e.target.value)} />
- +
- + setSlug(e.target.value)} />
- +