410 lines
14 KiB
TypeScript
410 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>
|
|
);
|
|
}
|
|
|
|
|