- Replaced existing toast error messages with the new `showErrorToast` utility across multiple components, improving consistency in error reporting. - Updated error messages to provide more specific guidance for users, enhancing the overall user experience during error scenarios. - Ensured that all relevant error handling in batch management, external call configurations, cascading management, and screen management components now utilizes the new utility for better maintainability.
371 lines
17 KiB
TypeScript
371 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useMemo } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { toast } from "sonner";
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
|
import { Plus, Search, Edit, Trash2, Eye, Filter, RotateCcw, Settings, SortAsc, SortDesc } from "lucide-react";
|
|
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
|
import Link from "next/link";
|
|
|
|
export default function WebTypesManagePage() {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [categoryFilter, setCategoryFilter] = useState<string>("all");
|
|
const [activeFilter, setActiveFilter] = useState<string>("Y");
|
|
const [sortField, setSortField] = useState<string>("sort_order");
|
|
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
|
|
|
// 웹타입 데이터 조회
|
|
const { webTypes, isLoading, error, deleteWebType, isDeleting, deleteError, refetch } = useWebTypes({
|
|
active: activeFilter === "all" ? undefined : activeFilter,
|
|
search: searchTerm || undefined,
|
|
category: categoryFilter === "all" ? undefined : categoryFilter,
|
|
});
|
|
|
|
// 카테고리 목록 생성
|
|
const categories = useMemo(() => {
|
|
const uniqueCategories = Array.from(new Set(webTypes.map((wt) => wt.category).filter(Boolean)));
|
|
return uniqueCategories.sort();
|
|
}, [webTypes]);
|
|
|
|
// 필터링 및 정렬된 데이터
|
|
const filteredAndSortedWebTypes = useMemo(() => {
|
|
let filtered = [...webTypes];
|
|
|
|
// 정렬
|
|
filtered.sort((a, b) => {
|
|
let aValue: any = a[sortField as keyof typeof a];
|
|
let bValue: any = b[sortField as keyof typeof b];
|
|
|
|
// 숫자 필드 처리
|
|
if (sortField === "sort_order") {
|
|
aValue = aValue || 0;
|
|
bValue = bValue || 0;
|
|
}
|
|
|
|
// 문자열 필드 처리
|
|
if (typeof aValue === "string") {
|
|
aValue = aValue.toLowerCase();
|
|
}
|
|
if (typeof bValue === "string") {
|
|
bValue = bValue.toLowerCase();
|
|
}
|
|
|
|
if (aValue < bValue) return sortDirection === "asc" ? -1 : 1;
|
|
if (aValue > bValue) return sortDirection === "asc" ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
return filtered;
|
|
}, [webTypes, sortField, sortDirection]);
|
|
|
|
// 정렬 변경 핸들러
|
|
const handleSort = (field: string) => {
|
|
if (sortField === field) {
|
|
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
|
|
} else {
|
|
setSortField(field);
|
|
setSortDirection("asc");
|
|
}
|
|
};
|
|
|
|
// 삭제 핸들러
|
|
const handleDelete = async (webType: string, typeName: string) => {
|
|
try {
|
|
await deleteWebType(webType);
|
|
toast.success(`웹타입 '${typeName}'이 삭제되었습니다.`);
|
|
} catch (error) {
|
|
showErrorToast("웹타입 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
|
}
|
|
};
|
|
|
|
// 필터 초기화
|
|
const resetFilters = () => {
|
|
setSearchTerm("");
|
|
setCategoryFilter("all");
|
|
setActiveFilter("Y");
|
|
setSortField("sort_order");
|
|
setSortDirection("asc");
|
|
};
|
|
|
|
// 로딩 상태
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-96 items-center justify-center">
|
|
<div className="text-lg">웹타입 목록을 불러오는 중...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 에러 상태
|
|
if (error) {
|
|
return (
|
|
<div className="flex h-96 items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="mb-2 text-lg text-destructive">웹타입 목록을 불러오는데 실패했습니다.</div>
|
|
<Button onClick={() => refetch()} variant="outline">
|
|
다시 시도
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<div className="w-full max-w-none px-4 py-8 space-y-8">
|
|
{/* 페이지 제목 */}
|
|
<div className="flex items-center justify-between bg-background rounded-lg shadow-sm border p-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground">웹타입 관리</h1>
|
|
<p className="mt-2 text-muted-foreground">화면관리에서 사용할 웹타입들을 관리합니다</p>
|
|
</div>
|
|
<Link href="/admin/standards/new">
|
|
<Button className="shadow-sm">
|
|
<Plus className="mr-2 h-4 w-4" />새 웹타입 추가
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* 필터 및 검색 */}
|
|
<Card className="shadow-sm">
|
|
<CardHeader className="bg-muted/50">
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Filter className="h-5 w-5 text-muted-foreground" />
|
|
필터 및 검색
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
{/* 검색 */}
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="웹타입명, 설명 검색..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
|
|
{/* 카테고리 필터 */}
|
|
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="카테고리 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체 카테고리</SelectItem>
|
|
{categories.map((category) => (
|
|
<SelectItem key={category} value={category}>
|
|
{category}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 활성화 상태 필터 */}
|
|
<Select value={activeFilter} onValueChange={setActiveFilter}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="상태 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">전체</SelectItem>
|
|
<SelectItem value="Y">활성화</SelectItem>
|
|
<SelectItem value="N">비활성화</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 초기화 버튼 */}
|
|
<Button variant="outline" onClick={resetFilters}>
|
|
<RotateCcw className="mr-2 h-4 w-4" />
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 결과 통계 */}
|
|
<div className="bg-background rounded-lg border px-4 py-3">
|
|
<p className="text-foreground text-sm font-medium">총 {filteredAndSortedWebTypes.length}개의 웹타입이 있습니다.</p>
|
|
</div>
|
|
|
|
{/* 웹타입 목록 테이블 */}
|
|
<div className="bg-card shadow-sm">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-background">
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("sort_order")}>
|
|
<div className="flex items-center gap-2">
|
|
순서
|
|
{sortField === "sort_order" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("web_type")}>
|
|
<div className="flex items-center gap-2">
|
|
웹타입 코드
|
|
{sortField === "web_type" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("type_name")}>
|
|
<div className="flex items-center gap-2">
|
|
웹타입명
|
|
{sortField === "type_name" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("category")}>
|
|
<div className="flex items-center gap-2">
|
|
카테고리
|
|
{sortField === "category" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold">설명</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("component_name")}>
|
|
<div className="flex items-center gap-2">
|
|
연결된 컴포넌트
|
|
{sortField === "component_name" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("config_panel")}>
|
|
<div className="flex items-center gap-2">
|
|
설정 패널
|
|
{sortField === "config_panel" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("is_active")}>
|
|
<div className="flex items-center gap-2">
|
|
상태
|
|
{sortField === "is_active" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold cursor-pointer hover:bg-muted/50" onClick={() => handleSort("updated_date")}>
|
|
<div className="flex items-center gap-2">
|
|
최종 수정일
|
|
{sortField === "updated_date" &&
|
|
(sortDirection === "asc" ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />)}
|
|
</div>
|
|
</TableHead>
|
|
<TableHead className="h-12 px-6 py-3 text-sm font-semibold text-center">작업</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredAndSortedWebTypes.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={10} className="py-8 text-center">
|
|
조건에 맞는 웹타입이 없습니다.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filteredAndSortedWebTypes.map((webType) => (
|
|
<TableRow key={webType.web_type} className="bg-background transition-colors hover:bg-muted/50">
|
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{webType.sort_order || 0}</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 font-mono text-sm">{webType.web_type}</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 font-medium text-sm">
|
|
{webType.type_name}
|
|
{webType.type_name_eng && (
|
|
<div className="text-muted-foreground text-xs">{webType.type_name_eng}</div>
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
<Badge variant="secondary">{webType.category}</Badge>
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 max-w-xs truncate text-sm">{webType.description || "-"}</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
<Badge variant="outline" className="font-mono text-xs">
|
|
{webType.component_name || "TextWidget"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
<Badge variant="secondary" className="font-mono text-xs">
|
|
{webType.config_panel === "none" || !webType.config_panel ? "기본 설정" : webType.config_panel}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
<Badge variant={webType.is_active === "Y" ? "default" : "secondary"}>
|
|
{webType.is_active === "Y" ? "활성화" : "비활성화"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-muted-foreground text-sm">
|
|
{webType.updated_date ? new Date(webType.updated_date).toLocaleDateString("ko-KR") : "-"}
|
|
</TableCell>
|
|
<TableCell className="h-16 px-6 py-3 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<Link href={`/admin/standards/${webType.web_type}`}>
|
|
<Button variant="ghost" size="sm">
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
</Link>
|
|
<Link href={`/admin/standards/${webType.web_type}/edit`}>
|
|
<Button variant="ghost" size="sm">
|
|
<Edit className="h-4 w-4" />
|
|
</Button>
|
|
</Link>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>웹타입 삭제</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
'{webType.type_name}' 웹타입을 삭제하시겠습니까?
|
|
<br />이 작업은 되돌릴 수 없습니다.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>취소</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => handleDelete(webType.web_type, webType.type_name)}
|
|
disabled={isDeleting}
|
|
className="bg-destructive hover:bg-destructive/90"
|
|
>
|
|
{isDeleting ? "삭제 중..." : "삭제"}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{deleteError && (
|
|
<div className="mt-4 rounded-md border border-destructive/30 bg-destructive/10 p-4">
|
|
<p className="text-destructive">
|
|
삭제 중 오류가 발생했습니다: {deleteError instanceof Error ? deleteError.message : "알 수 없는 오류"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|