리피터 케이블 설정 구현
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user