Merge origin/main into ksh - resolve conflicts

This commit is contained in:
SeongHyun Kim
2026-01-09 14:55:16 +09:00
61 changed files with 10564 additions and 524 deletions

View File

@@ -1272,6 +1272,71 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
}
}, [config.rightPanel?.tableName]);
// 🆕 좌측/우측 테이블이 모두 선택되면 엔티티 관계 자동 감지
const [autoDetectedRelations, setAutoDetectedRelations] = useState<
Array<{
leftColumn: string;
rightColumn: string;
direction: "left_to_right" | "right_to_left";
inputType: string;
displayColumn?: string;
}>
>([]);
const [isDetectingRelations, setIsDetectingRelations] = useState(false);
useEffect(() => {
const detectRelations = async () => {
const leftTable = config.leftPanel?.tableName || screenTableName;
const rightTable = config.rightPanel?.tableName;
// 조인 모드이고 양쪽 테이블이 모두 있을 때만 감지
if (relationshipType !== "join" || !leftTable || !rightTable) {
setAutoDetectedRelations([]);
return;
}
setIsDetectingRelations(true);
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableEntityRelations(leftTable, rightTable);
if (response.success && response.data?.relations) {
console.log("🔍 엔티티 관계 자동 감지:", response.data.relations);
setAutoDetectedRelations(response.data.relations);
// 감지된 관계가 있고, 현재 설정된 키가 없으면 자동으로 첫 번째 관계를 설정
const currentKeys = config.rightPanel?.relation?.keys || [];
if (response.data.relations.length > 0 && currentKeys.length === 0) {
// 첫 번째 관계만 자동 설정 (사용자가 추가로 설정 가능)
const firstRel = response.data.relations[0];
console.log("✅ 첫 번째 엔티티 관계 자동 설정:", firstRel);
updateRightPanel({
relation: {
...config.rightPanel?.relation,
type: "join",
useMultipleKeys: true,
keys: [
{
leftColumn: firstRel.leftColumn,
rightColumn: firstRel.rightColumn,
},
],
},
});
}
}
} catch (error) {
console.error("❌ 엔티티 관계 감지 실패:", error);
setAutoDetectedRelations([]);
} finally {
setIsDetectingRelations(false);
}
};
detectRelations();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.leftPanel?.tableName, config.rightPanel?.tableName, screenTableName, relationshipType]);
console.log("🔧 SplitPanelLayoutConfigPanel 렌더링");
console.log(" - config:", config);
console.log(" - tables:", tables);
@@ -2476,234 +2541,50 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
</div>
)}
{/* 컬럼 매핑 - 조인 모드에서만 표시 */}
{/* 엔티티 관계 자동 감지 (읽기 전용) - 조인 모드에서만 표시 */}
{relationshipType !== "detail" && (
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
<div className="flex items-center justify-between">
<div>
<Label className="text-sm font-semibold"> ( )</Label>
<p className="text-xs text-gray-600"> </p>
</div>
<Button
size="sm"
variant="ghost"
className="h-6 text-xs"
onClick={() => {
const currentKeys = config.rightPanel?.relation?.keys || [];
// 단일키에서 복합키로 전환 시 기존 값 유지
if (
currentKeys.length === 0 &&
config.rightPanel?.relation?.leftColumn &&
config.rightPanel?.relation?.foreignKey
) {
updateRightPanel({
relation: {
...config.rightPanel?.relation,
keys: [
{
leftColumn: config.rightPanel.relation.leftColumn,
rightColumn: config.rightPanel.relation.foreignKey,
},
{ leftColumn: "", rightColumn: "" },
],
},
});
} else {
updateRightPanel({
relation: {
...config.rightPanel?.relation,
keys: [...currentKeys, { leftColumn: "", rightColumn: "" }],
},
});
}
}}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
<div>
<Label className="text-sm font-semibold"> ( )</Label>
<p className="text-xs text-gray-600"> </p>
</div>
<p className="text-[10px] text-blue-600">복합키: 여러 (: item_code + lot_number)</p>
{/* 복합키가 설정된 경우 */}
{(config.rightPanel?.relation?.keys || []).length > 0 ? (
<>
{(config.rightPanel?.relation?.keys || []).map((key, index) => (
<div key={index} className="space-y-2 rounded-md border bg-white p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium"> {index + 1}</span>
<Button
size="sm"
variant="ghost"
className="text-destructive h-6 w-6 p-0"
onClick={() => {
const newKeys = (config.rightPanel?.relation?.keys || []).filter((_, i) => i !== index);
updateRightPanel({
relation: { ...config.rightPanel?.relation, keys: newKeys },
});
}}
>
<X className="h-3 w-3" />
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs"> </Label>
<Select
value={key.leftColumn || ""}
onValueChange={(value) => {
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
newKeys[index] = { ...newKeys[index], leftColumn: value };
updateRightPanel({
relation: { ...config.rightPanel?.relation, keys: newKeys },
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="좌측 컬럼" />
</SelectTrigger>
<SelectContent>
{leftTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={key.rightColumn || ""}
onValueChange={(value) => {
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
newKeys[index] = { ...newKeys[index], rightColumn: value };
updateRightPanel({
relation: { ...config.rightPanel?.relation, keys: newKeys },
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="우측 컬럼" />
</SelectTrigger>
<SelectContent>
{rightTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{isDetectingRelations ? (
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="h-3 w-3 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
...
</div>
) : autoDetectedRelations.length > 0 ? (
<div className="space-y-2">
{autoDetectedRelations.map((rel, index) => (
<div key={index} className="flex items-center gap-2 rounded-md border border-blue-300 bg-white p-2">
<span className="rounded bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
{leftTableName}.{rel.leftColumn}
</span>
<ArrowRight className="h-3 w-3 text-blue-400" />
<span className="rounded bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">
{rightTableName}.{rel.rightColumn}
</span>
<span className="ml-auto text-[10px] text-gray-500">
{rel.inputType === "entity" ? "엔티티" : "카테고리"}
</span>
</div>
))}
</>
<p className="text-[10px] text-blue-600">
/
</p>
</div>
) : config.rightPanel?.tableName ? (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
<p className="mt-1 text-[10px] text-gray-400">
</p>
</div>
) : (
/* 단일키 (하위 호환성) */
<>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Popover open={leftColumnOpen} onOpenChange={setLeftColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={leftColumnOpen}
className="w-full justify-between"
disabled={!config.leftPanel?.tableName}
>
{config.rightPanel?.relation?.leftColumn || "좌측 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{leftTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
updateRightPanel({
relation: { ...config.rightPanel?.relation, leftColumn: value },
});
setLeftColumnOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.rightPanel?.relation?.leftColumn === column.columnName
? "opacity-100"
: "opacity-0",
)}
/>
{column.columnName}
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center justify-center">
<ArrowRight className="h-4 w-4 text-gray-400" />
</div>
<div className="space-y-2">
<Label className="text-xs"> ()</Label>
<Popover open={rightColumnOpen} onOpenChange={setRightColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={rightColumnOpen}
className="w-full justify-between"
disabled={!config.rightPanel?.tableName}
>
{config.rightPanel?.relation?.foreignKey || "우측 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{rightTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
updateRightPanel({
relation: { ...config.rightPanel?.relation, foreignKey: value },
});
setRightColumnOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.rightPanel?.relation?.foreignKey === column.columnName
? "opacity-100"
: "opacity-0",
)}
/>
{column.columnName}
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
</>
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
</div>
)}
</div>
)}