- 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.
417 lines
14 KiB
TypeScript
417 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { toast } from "sonner";
|
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
|
import { Plus, Pencil, Trash2, Link2, Database } from "lucide-react";
|
|
import {
|
|
getFieldJoins,
|
|
createFieldJoin,
|
|
updateFieldJoin,
|
|
deleteFieldJoin,
|
|
FieldJoin,
|
|
} from "@/lib/api/screenGroup";
|
|
|
|
interface FieldJoinPanelProps {
|
|
screenId: number;
|
|
componentId?: string;
|
|
layoutId?: number;
|
|
}
|
|
|
|
export default function FieldJoinPanel({ screenId, componentId, layoutId }: FieldJoinPanelProps) {
|
|
// 상태 관리
|
|
const [fieldJoins, setFieldJoins] = useState<FieldJoin[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [selectedJoin, setSelectedJoin] = useState<FieldJoin | null>(null);
|
|
const [formData, setFormData] = useState({
|
|
field_name: "",
|
|
save_table: "",
|
|
save_column: "",
|
|
join_table: "",
|
|
join_column: "",
|
|
display_column: "",
|
|
join_type: "LEFT",
|
|
filter_condition: "",
|
|
sort_column: "",
|
|
sort_direction: "ASC",
|
|
is_active: "Y",
|
|
});
|
|
|
|
// 데이터 로드
|
|
const loadFieldJoins = useCallback(async () => {
|
|
if (!screenId) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const response = await getFieldJoins(screenId);
|
|
if (response.success && response.data) {
|
|
// 현재 컴포넌트에 해당하는 조인만 필터링
|
|
const filtered = componentId
|
|
? response.data.filter(join => join.component_id === componentId)
|
|
: response.data;
|
|
setFieldJoins(filtered);
|
|
}
|
|
} catch (error) {
|
|
console.error("필드 조인 로드 실패:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [screenId, componentId]);
|
|
|
|
useEffect(() => {
|
|
loadFieldJoins();
|
|
}, [loadFieldJoins]);
|
|
|
|
// 모달 열기
|
|
const openModal = (join?: FieldJoin) => {
|
|
if (join) {
|
|
setSelectedJoin(join);
|
|
setFormData({
|
|
field_name: join.field_name || "",
|
|
save_table: join.save_table,
|
|
save_column: join.save_column,
|
|
join_table: join.join_table,
|
|
join_column: join.join_column,
|
|
display_column: join.display_column,
|
|
join_type: join.join_type,
|
|
filter_condition: join.filter_condition || "",
|
|
sort_column: join.sort_column || "",
|
|
sort_direction: join.sort_direction || "ASC",
|
|
is_active: join.is_active,
|
|
});
|
|
} else {
|
|
setSelectedJoin(null);
|
|
setFormData({
|
|
field_name: "",
|
|
save_table: "",
|
|
save_column: "",
|
|
join_table: "",
|
|
join_column: "",
|
|
display_column: "",
|
|
join_type: "LEFT",
|
|
filter_condition: "",
|
|
sort_column: "",
|
|
sort_direction: "ASC",
|
|
is_active: "Y",
|
|
});
|
|
}
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
// 저장
|
|
const handleSave = async () => {
|
|
if (!formData.save_table || !formData.save_column || !formData.join_table || !formData.join_column || !formData.display_column) {
|
|
toast.error("필수 필드를 모두 입력해주세요.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const payload = {
|
|
screen_id: screenId,
|
|
layout_id: layoutId,
|
|
component_id: componentId,
|
|
...formData,
|
|
};
|
|
|
|
let response;
|
|
if (selectedJoin) {
|
|
response = await updateFieldJoin(selectedJoin.id, payload);
|
|
} else {
|
|
response = await createFieldJoin(payload);
|
|
}
|
|
|
|
if (response.success) {
|
|
toast.success(selectedJoin ? "조인 설정이 수정되었습니다." : "조인 설정이 추가되었습니다.");
|
|
setIsModalOpen(false);
|
|
loadFieldJoins();
|
|
} else {
|
|
toast.error(response.message || "저장에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
showErrorToast("필드 조인 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요." });
|
|
}
|
|
};
|
|
|
|
// 삭제
|
|
const handleDelete = async (id: number) => {
|
|
if (!confirm("이 조인 설정을 삭제하시겠습니까?")) return;
|
|
|
|
try {
|
|
const response = await deleteFieldJoin(id);
|
|
if (response.success) {
|
|
toast.success("조인 설정이 삭제되었습니다.");
|
|
loadFieldJoins();
|
|
} else {
|
|
toast.error(response.message || "삭제에 실패했습니다.");
|
|
}
|
|
} catch (error) {
|
|
showErrorToast("필드 조인 설정 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Link2 className="h-4 w-4 text-primary" />
|
|
<h3 className="text-sm font-semibold">필드 조인 설정</h3>
|
|
</div>
|
|
<Button variant="outline" size="sm" onClick={() => openModal()} className="h-8 gap-1 text-xs">
|
|
<Plus className="h-3 w-3" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 설명 */}
|
|
<p className="text-xs text-muted-foreground">
|
|
이 필드가 다른 테이블의 값을 참조하여 표시할 때 조인 설정을 추가하세요.
|
|
</p>
|
|
|
|
{/* 조인 목록 */}
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
|
</div>
|
|
) : fieldJoins.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-8">
|
|
<Database className="h-8 w-8 text-muted-foreground/50" />
|
|
<p className="mt-2 text-xs text-muted-foreground">설정된 조인이 없습니다</p>
|
|
</div>
|
|
) : (
|
|
<div className="rounded-lg border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-muted/50">
|
|
<TableHead className="h-8 text-xs">저장 테이블.컬럼</TableHead>
|
|
<TableHead className="h-8 text-xs">조인 테이블.컬럼</TableHead>
|
|
<TableHead className="h-8 text-xs">표시 컬럼</TableHead>
|
|
<TableHead className="h-8 w-[60px] text-xs">관리</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{fieldJoins.map((join) => (
|
|
<TableRow key={join.id} className="text-xs">
|
|
<TableCell className="py-2">
|
|
<span className="font-mono">{join.save_table}.{join.save_column}</span>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<span className="font-mono">{join.join_table}.{join.join_column}</span>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<span className="font-mono">{join.display_column}</span>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => openModal(join)}>
|
|
<Pencil className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-6 w-6 text-destructive hover:text-destructive"
|
|
onClick={() => handleDelete(join.id)}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
|
|
{/* 추가/수정 모달 */}
|
|
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">
|
|
{selectedJoin ? "조인 설정 수정" : "조인 설정 추가"}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
필드가 참조할 테이블과 컬럼을 설정합니다
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
{/* 필드명 */}
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">필드명</Label>
|
|
<Input
|
|
value={formData.field_name}
|
|
onChange={(e) => setFormData({ ...formData, field_name: e.target.value })}
|
|
placeholder="화면에 표시될 필드명"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* 저장 테이블/컬럼 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">저장 테이블 *</Label>
|
|
<Input
|
|
value={formData.save_table}
|
|
onChange={(e) => setFormData({ ...formData, save_table: e.target.value })}
|
|
placeholder="예: work_orders"
|
|
className="h-8 font-mono text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">저장 컬럼 *</Label>
|
|
<Input
|
|
value={formData.save_column}
|
|
onChange={(e) => setFormData({ ...formData, save_column: e.target.value })}
|
|
placeholder="예: item_code"
|
|
className="h-8 font-mono text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 조인 테이블/컬럼 */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">조인 테이블 *</Label>
|
|
<Input
|
|
value={formData.join_table}
|
|
onChange={(e) => setFormData({ ...formData, join_table: e.target.value })}
|
|
placeholder="예: item_mng"
|
|
className="h-8 font-mono text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">조인 컬럼 *</Label>
|
|
<Input
|
|
value={formData.join_column}
|
|
onChange={(e) => setFormData({ ...formData, join_column: e.target.value })}
|
|
placeholder="예: id"
|
|
className="h-8 font-mono text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 표시 컬럼 */}
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">표시 컬럼 *</Label>
|
|
<Input
|
|
value={formData.display_column}
|
|
onChange={(e) => setFormData({ ...formData, display_column: e.target.value })}
|
|
placeholder="예: item_name (화면에 표시될 컬럼)"
|
|
className="h-8 font-mono text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* 조인 타입/정렬 */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">조인 타입</Label>
|
|
<Select
|
|
value={formData.join_type}
|
|
onValueChange={(value) => setFormData({ ...formData, join_type: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="LEFT">LEFT JOIN</SelectItem>
|
|
<SelectItem value="INNER">INNER JOIN</SelectItem>
|
|
<SelectItem value="RIGHT">RIGHT JOIN</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">정렬 컬럼</Label>
|
|
<Input
|
|
value={formData.sort_column}
|
|
onChange={(e) => setFormData({ ...formData, sort_column: e.target.value })}
|
|
placeholder="예: name"
|
|
className="h-8 font-mono text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">정렬 방향</Label>
|
|
<Select
|
|
value={formData.sort_direction}
|
|
onValueChange={(value) => setFormData({ ...formData, sort_direction: value })}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="ASC">오름차순</SelectItem>
|
|
<SelectItem value="DESC">내림차순</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 필터 조건 */}
|
|
<div>
|
|
<Label className="text-xs sm:text-sm">필터 조건 (선택)</Label>
|
|
<Textarea
|
|
value={formData.filter_condition}
|
|
onChange={(e) => setFormData({ ...formData, filter_condition: e.target.value })}
|
|
placeholder="예: is_active = 'Y'"
|
|
className="min-h-[60px] font-mono text-xs sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsModalOpen(false)}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleSave}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
{selectedJoin ? "수정" : "추가"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|