리피터 케이블 설정 구현

This commit is contained in:
kjs
2026-01-15 15:17:52 +09:00
parent bed7f5f5c4
commit e168753d87
8 changed files with 893 additions and 116 deletions

View File

@@ -29,8 +29,24 @@ import {
Eye,
EyeOff,
Wand2,
Check,
ChevronsUpDown,
} from "lucide-react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { tableTypeApi } from "@/lib/api/screen";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
import { NumberingRuleConfig } from "@/types/numbering-rule";
import { cn } from "@/lib/utils";
@@ -42,6 +58,14 @@ import {
MODAL_SIZE_OPTIONS,
} from "@/types/unified-repeater";
// 테이블 엔티티 관계 정보
interface TableRelation {
tableName: string;
tableLabel: string;
foreignKeyColumn: string; // 저장 테이블의 FK 컬럼
referenceColumn: string; // 마스터 테이블의 PK 컬럼
}
interface UnifiedRepeaterConfigPanelProps {
config: UnifiedRepeaterConfig;
onChange: (config: UnifiedRepeaterConfig) => void;
@@ -117,6 +141,13 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
const [loadingColumns, setLoadingColumns] = useState(false);
const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
// 저장 테이블 관련 상태
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [relatedTables, setRelatedTables] = useState<TableRelation[]>([]); // 현재 테이블과 연관된 테이블 목록
const [loadingRelations, setLoadingRelations] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 테이블 Combobox 열림 상태
// 🆕 확장된 컬럼 (상세 설정 표시용)
const [expandedColumn, setExpandedColumn] = useState<string | null>(null);
@@ -199,6 +230,60 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
loadNumberingRules();
}, [selectedMenuObjid]);
// 전체 테이블 목록 로드
useEffect(() => {
const loadTables = async () => {
setLoadingTables(true);
try {
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
setAllTables(response.data.map((t: any) => ({
tableName: t.tableName || t.table_name,
displayName: t.displayName || t.table_label || t.tableName || t.table_name,
})));
}
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
} finally {
setLoadingTables(false);
}
};
loadTables();
}, []);
// 현재 테이블과 연관된 테이블 목록 로드 (엔티티 관계 기반)
useEffect(() => {
const loadRelatedTables = async () => {
if (!currentTableName) {
setRelatedTables([]);
return;
}
setLoadingRelations(true);
try {
// column_labels에서 현재 테이블을 reference_table로 참조하는 테이블 찾기
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
if (response.data.success && response.data.data) {
const relations: TableRelation[] = response.data.data.map((rel: any) => ({
tableName: rel.tableName || rel.table_name,
tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼
referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼
}));
setRelatedTables(relations);
}
} catch (error) {
console.error("연관 테이블 로드 실패:", error);
setRelatedTables([]);
} finally {
setLoadingRelations(false);
}
};
loadRelatedTables();
}, [currentTableName]);
// 설정 업데이트 헬퍼
const updateConfig = useCallback(
(updates: Partial<UnifiedRepeaterConfig>) => {
@@ -234,10 +319,50 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
[config.features, updateConfig],
);
// 현재 화면 테이블 컬럼 로드 + 엔티티 컬럼 감지
// 저장 테이블 선택 핸들러 - 엔티티 관계에서 FK/PK 자동 설정
const handleSaveTableSelect = useCallback((tableName: string) => {
// 빈 값 선택 시 (현재 테이블로 복원)
if (!tableName || tableName === currentTableName) {
updateConfig({
useCustomTable: false,
mainTableName: undefined,
foreignKeyColumn: undefined,
foreignKeySourceColumn: undefined,
});
return;
}
// 연관 테이블에서 FK 관계 찾기
const relation = relatedTables.find(r => r.tableName === tableName);
if (relation) {
// 엔티티 관계가 있으면 자동으로 FK/PK 설정
updateConfig({
useCustomTable: true,
mainTableName: tableName,
foreignKeyColumn: relation.foreignKeyColumn,
foreignKeySourceColumn: relation.referenceColumn,
});
} else {
// 엔티티 관계가 없으면 직접 입력 필요
updateConfig({
useCustomTable: true,
mainTableName: tableName,
foreignKeyColumn: undefined,
foreignKeySourceColumn: "id",
});
}
}, [currentTableName, relatedTables, updateConfig]);
// 저장 테이블 컬럼 로드 (저장 테이블이 설정되면 해당 테이블, 아니면 현재 화면 테이블)
// 실제 저장할 테이블의 컬럼을 보여줘야 함
const targetTableForColumns = config.useCustomTable && config.mainTableName
? config.mainTableName
: currentTableName;
useEffect(() => {
const loadCurrentTableColumns = async () => {
if (!currentTableName) {
if (!targetTableForColumns) {
setCurrentTableColumns([]);
setEntityColumns([]);
return;
@@ -245,7 +370,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
setLoadingColumns(true);
try {
const columnData = await tableTypeApi.getColumns(currentTableName);
const columnData = await tableTypeApi.getColumns(targetTableForColumns);
const cols: ColumnOption[] = [];
const entityCols: EntityColumnOption[] = [];
@@ -297,7 +422,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
setCurrentTableColumns(cols);
setEntityColumns(entityCols);
} catch (error) {
console.error("현재 테이블 컬럼 로드 실패:", error);
console.error("저장 테이블 컬럼 로드 실패:", error);
setCurrentTableColumns([]);
setEntityColumns([]);
} finally {
@@ -305,7 +430,7 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
}
};
loadCurrentTableColumns();
}, [currentTableName]);
}, [targetTableForColumns]);
// 소스(엔티티) 테이블 컬럼 로드 (모달 모드일 때)
useEffect(() => {
@@ -529,97 +654,185 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
<Separator />
{/* 저장 대상 테이블 설정 */}
{/* 저장 대상 테이블 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<p className="text-[10px] text-muted-foreground">
</p>
<Label className="text-xs font-medium"> </Label>
<div className="flex items-center space-x-2">
<Checkbox
id="useCustomTable"
checked={config.useCustomTable || false}
onCheckedChange={(checked) => {
if (!checked) {
updateConfig({
useCustomTable: false,
mainTableName: undefined,
foreignKeyColumn: undefined,
foreignKeySourceColumn: undefined,
});
} else {
updateConfig({ useCustomTable: true });
}
}}
/>
<label htmlFor="useCustomTable" className="text-xs"> </label>
{/* 현재 선택된 테이블 표시 (기존 테이블 UI와 동일한 스타일) */}
<div className={cn(
"rounded-lg border p-3",
config.useCustomTable && config.mainTableName
? "border-orange-300 bg-orange-50"
: "border-blue-300 bg-blue-50"
)}>
<div className="flex items-center gap-2">
<Database className={cn(
"h-4 w-4",
config.useCustomTable && config.mainTableName
? "text-orange-600"
: "text-blue-600"
)} />
<div className="flex-1">
<p className={cn(
"text-sm font-medium",
config.useCustomTable && config.mainTableName
? "text-orange-700"
: "text-blue-700"
)}>
{config.useCustomTable && config.mainTableName
? (allTables.find(t => t.tableName === config.mainTableName)?.displayName || config.mainTableName)
: (currentTableName || "미설정")
}
</p>
{config.useCustomTable && config.mainTableName && config.foreignKeyColumn && (
<p className="text-[10px] text-orange-600 mt-0.5">
FK: {config.foreignKeyColumn} {currentTableName}.{config.foreignKeySourceColumn || "id"}
</p>
)}
{!config.useCustomTable && currentTableName && (
<p className="text-[10px] text-blue-600 mt-0.5"> </p>
)}
</div>
</div>
</div>
{config.useCustomTable && (
<div className="space-y-2 rounded border border-orange-200 bg-orange-50 p-2">
{/* 저장 테이블 선택 */}
<div className="space-y-1">
<Label className="text-[10px]"> *</Label>
<Select
value={config.mainTableName || ""}
onValueChange={(value) => updateConfig({ mainTableName: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="테이블 선택..." />
</SelectTrigger>
<SelectContent>
{currentTableColumns.length === 0 ? (
<div className="p-2 text-xs text-gray-500"> ...</div>
) : (
<SelectItem value={currentTableName || ""} disabled>
<span className="text-gray-400">( - )</span>
</SelectItem>
)}
</SelectContent>
</Select>
<Input
value={config.mainTableName || ""}
onChange={(e) => updateConfig({ mainTableName: e.target.value })}
placeholder="테이블명 직접 입력 (예: receiving_detail)"
className="h-7 text-xs"
/>
</div>
{/* FK 컬럼 설정 */}
{/* 테이블 변경 Combobox */}
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={tableComboboxOpen}
disabled={loadingTables || loadingRelations}
className="h-8 w-full justify-between text-xs"
>
{loadingTables ? "로딩 중..." : "다른 테이블 선택..."}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
style={{ width: "var(--radix-popover-trigger-width)" }}
align="start"
>
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandList className="max-h-60">
<CommandEmpty className="text-xs py-3 text-center">
.
</CommandEmpty>
{/* 현재 테이블 (기본) */}
{currentTableName && (
<CommandGroup heading="기본">
<CommandItem
value={currentTableName}
onSelect={() => {
handleSaveTableSelect(currentTableName);
setTableComboboxOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
!config.useCustomTable || !config.mainTableName ? "opacity-100" : "opacity-0"
)}
/>
<Database className="mr-2 h-3 w-3 text-blue-500" />
<span>{currentTableName}</span>
<span className="ml-1 text-[10px] text-muted-foreground">()</span>
</CommandItem>
</CommandGroup>
)}
{/* 연관 테이블 (엔티티 관계) */}
{relatedTables.length > 0 && (
<CommandGroup heading="연관 테이블 (FK 자동 설정)">
{relatedTables.map((rel) => (
<CommandItem
key={rel.tableName}
value={`${rel.tableName} ${rel.tableLabel}`}
onSelect={() => {
handleSaveTableSelect(rel.tableName);
setTableComboboxOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.mainTableName === rel.tableName ? "opacity-100" : "opacity-0"
)}
/>
<Link2 className="mr-2 h-3 w-3 text-orange-500" />
<span>{rel.tableLabel}</span>
<span className="ml-1 text-[10px] text-muted-foreground">
({rel.foreignKeyColumn})
</span>
</CommandItem>
))}
</CommandGroup>
)}
{/* 전체 테이블 목록 */}
<CommandGroup heading="전체 테이블 (FK 직접 입력)">
{allTables
.filter(t => t.tableName !== currentTableName && !relatedTables.some(r => r.tableName === t.tableName))
.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.tableName} ${table.displayName}`}
onSelect={() => {
handleSaveTableSelect(table.tableName);
setTableComboboxOpen(false);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.mainTableName === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
<Database className="mr-2 h-3 w-3 text-gray-400" />
<span>{table.displayName}</span>
</CommandItem>
))
}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* FK 직접 입력 (연관 테이블이 아닌 경우만) */}
{config.useCustomTable && config.mainTableName &&
!relatedTables.some(r => r.tableName === config.mainTableName) && (
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-2">
<p className="text-[10px] text-amber-700">
. FK .
</p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[10px]">FK ( )</Label>
<Label className="text-[10px]">FK </Label>
<Input
value={config.foreignKeyColumn || ""}
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
placeholder="예: receiving_id"
placeholder="예: master_id"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]">PK ( )</Label>
<Label className="text-[10px]">PK </Label>
<Input
value={config.foreignKeySourceColumn || "id"}
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
placeholder="예: id"
placeholder="id"
className="h-7 text-xs"
/>
</div>
</div>
{config.mainTableName && config.foreignKeyColumn && (
<div className="text-[10px] text-orange-700">
<strong> :</strong> {config.mainTableName}.{config.foreignKeyColumn}
{currentTableName}.{config.foreignKeySourceColumn || "id"} ( )
</div>
)}
</div>
)}
{!config.useCustomTable && (
<div className="text-[10px] text-muted-foreground">
현재: 화면 ({currentTableName || "미설정"})
</div>
)}
</div>
@@ -809,10 +1022,10 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
</>
)}
{/* 현재 테이블 컬럼 (입력용) */}
{/* 저장 테이블 컬럼 (입력용) */}
<div className="text-[10px] font-medium text-gray-600 mt-3 mb-1 flex items-center gap-1">
<Database className="h-3 w-3" />
({currentTableName || "미선택"}) -
({targetTableForColumns || "미선택"}) -
</div>
{loadingColumns ? (
<p className="text-muted-foreground py-2 text-xs"> ...</p>