ERP-node/frontend/components/screen/panels/FieldJoinPanel.tsx

418 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 { 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) {
toast.error("저장 중 오류가 발생했습니다.");
}
};
// 삭제
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) {
toast.error("삭제 중 오류가 발생했습니다.");
}
};
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>
);
}