929 lines
37 KiB
TypeScript
929 lines
37 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react";
|
|
// allComponents는 현재 사용되지 않지만 향후 확장을 위해 props에 유지
|
|
import { EntitySearchInputConfig } from "./config";
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
|
import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation";
|
|
import { cn } from "@/lib/utils";
|
|
import Link from "next/link";
|
|
|
|
interface EntitySearchInputConfigPanelProps {
|
|
config: EntitySearchInputConfig;
|
|
onConfigChange: (config: EntitySearchInputConfig) => void;
|
|
currentComponent?: any; // 테이블 패널에서 드래그한 컴포넌트 정보
|
|
allComponents?: any[]; // 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용)
|
|
}
|
|
|
|
export function EntitySearchInputConfigPanel({
|
|
config,
|
|
onConfigChange,
|
|
currentComponent,
|
|
allComponents = [],
|
|
}: EntitySearchInputConfigPanelProps) {
|
|
const [localConfig, setLocalConfig] = useState(config);
|
|
const [allTables, setAllTables] = useState<any[]>([]);
|
|
const [tableColumns, setTableColumns] = useState<any[]>([]);
|
|
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
|
const [isLoadingColumns, setIsLoadingColumns] = useState(false);
|
|
const [openTableCombo, setOpenTableCombo] = useState(false);
|
|
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
|
const [openValueFieldCombo, setOpenValueFieldCombo] = useState(false);
|
|
|
|
// 연쇄 드롭다운 설정 상태 (SelectBasicConfigPanel과 동일)
|
|
const [cascadingEnabled, setCascadingEnabled] = useState(!!config.cascadingRelationCode);
|
|
|
|
// 연쇄관계 목록
|
|
const [relationList, setRelationList] = useState<CascadingRelation[]>([]);
|
|
const [loadingRelations, setLoadingRelations] = useState(false);
|
|
|
|
// 테이블 타입 관리에서 설정된 참조 테이블 정보
|
|
const [referenceInfo, setReferenceInfo] = useState<{
|
|
referenceTable: string;
|
|
referenceColumn: string;
|
|
displayColumn: string;
|
|
isLoading: boolean;
|
|
isAutoLoaded: boolean; // 자동 로드되었는지 여부
|
|
error: string | null;
|
|
}>({
|
|
referenceTable: "",
|
|
referenceColumn: "",
|
|
displayColumn: "",
|
|
isLoading: false,
|
|
isAutoLoaded: false,
|
|
error: null,
|
|
});
|
|
|
|
// 자동 설정 완료 여부 (중복 방지)
|
|
const autoConfigApplied = useRef(false);
|
|
|
|
// 테이블 패널에서 드래그한 컴포넌트인 경우, 참조 테이블 정보 자동 로드
|
|
useEffect(() => {
|
|
const loadReferenceInfo = async () => {
|
|
// currentComponent에서 소스 테이블/컬럼 정보 추출
|
|
const sourceTableName = currentComponent?.tableName || currentComponent?.sourceTableName;
|
|
const sourceColumnName = currentComponent?.columnName || currentComponent?.sourceColumnName;
|
|
|
|
if (!sourceTableName || !sourceColumnName) {
|
|
return;
|
|
}
|
|
|
|
// 이미 config에 테이블명이 설정되어 있고, 자동 로드가 완료되었다면 스킵
|
|
if (config.tableName && autoConfigApplied.current) {
|
|
return;
|
|
}
|
|
|
|
setReferenceInfo(prev => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
// 테이블 타입 관리에서 컬럼 정보 조회
|
|
const columns = await tableTypeApi.getColumns(sourceTableName);
|
|
const columnInfo = columns.find((col: any) =>
|
|
(col.columnName || col.column_name) === sourceColumnName
|
|
);
|
|
|
|
if (columnInfo) {
|
|
const refTable = columnInfo.referenceTable || columnInfo.reference_table || "";
|
|
const refColumn = columnInfo.referenceColumn || columnInfo.reference_column || "";
|
|
const dispColumn = columnInfo.displayColumn || columnInfo.display_column || "";
|
|
|
|
// detailSettings에서도 정보 확인 (JSON 파싱)
|
|
let detailSettings: any = {};
|
|
if (columnInfo.detailSettings) {
|
|
try {
|
|
if (typeof columnInfo.detailSettings === 'string') {
|
|
detailSettings = JSON.parse(columnInfo.detailSettings);
|
|
} else {
|
|
detailSettings = columnInfo.detailSettings;
|
|
}
|
|
} catch {
|
|
// JSON 파싱 실패 시 무시
|
|
}
|
|
}
|
|
|
|
const finalRefTable = refTable || detailSettings.referenceTable || "";
|
|
const finalRefColumn = refColumn || detailSettings.referenceColumn || "id";
|
|
const finalDispColumn = dispColumn || detailSettings.displayColumn || "name";
|
|
|
|
setReferenceInfo({
|
|
referenceTable: finalRefTable,
|
|
referenceColumn: finalRefColumn,
|
|
displayColumn: finalDispColumn,
|
|
isLoading: false,
|
|
isAutoLoaded: true,
|
|
error: null,
|
|
});
|
|
|
|
// 참조 테이블 정보로 config 자동 설정 (config에 아직 설정이 없는 경우만)
|
|
if (finalRefTable && !config.tableName) {
|
|
autoConfigApplied.current = true;
|
|
const newConfig: EntitySearchInputConfig = {
|
|
...localConfig,
|
|
tableName: finalRefTable,
|
|
valueField: finalRefColumn,
|
|
displayField: finalDispColumn,
|
|
};
|
|
setLocalConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
}
|
|
} else {
|
|
setReferenceInfo({
|
|
referenceTable: "",
|
|
referenceColumn: "",
|
|
displayColumn: "",
|
|
isLoading: false,
|
|
isAutoLoaded: false,
|
|
error: "컬럼 정보를 찾을 수 없습니다.",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("참조 테이블 정보 로드 실패:", error);
|
|
setReferenceInfo({
|
|
referenceTable: "",
|
|
referenceColumn: "",
|
|
displayColumn: "",
|
|
isLoading: false,
|
|
isAutoLoaded: false,
|
|
error: "참조 테이블 정보 로드 실패",
|
|
});
|
|
}
|
|
};
|
|
|
|
loadReferenceInfo();
|
|
}, [currentComponent?.tableName, currentComponent?.columnName, currentComponent?.sourceTableName, currentComponent?.sourceColumnName]);
|
|
|
|
// 연쇄 관계 목록 로드
|
|
useEffect(() => {
|
|
if (cascadingEnabled && relationList.length === 0) {
|
|
loadRelationList();
|
|
}
|
|
}, [cascadingEnabled]);
|
|
|
|
// 연쇄 관계 목록 로드 함수
|
|
const loadRelationList = async () => {
|
|
setLoadingRelations(true);
|
|
try {
|
|
const response = await cascadingRelationApi.getList("Y");
|
|
if (response.success && response.data) {
|
|
setRelationList(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("연쇄 관계 목록 로드 실패:", error);
|
|
} finally {
|
|
setLoadingRelations(false);
|
|
}
|
|
};
|
|
|
|
// 전체 테이블 목록 로드 (수동 선택을 위해)
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
setIsLoadingTables(true);
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setAllTables(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
} finally {
|
|
setIsLoadingTables(false);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
// 선택된 테이블의 컬럼 목록 로드
|
|
useEffect(() => {
|
|
const loadColumns = async () => {
|
|
if (!localConfig.tableName) {
|
|
setTableColumns([]);
|
|
return;
|
|
}
|
|
|
|
setIsLoadingColumns(true);
|
|
try {
|
|
const response = await tableManagementApi.getColumnList(localConfig.tableName);
|
|
if (response.success && response.data) {
|
|
setTableColumns(response.data.columns);
|
|
}
|
|
} catch (error) {
|
|
console.error("컬럼 목록 로드 실패:", error);
|
|
setTableColumns([]);
|
|
} finally {
|
|
setIsLoadingColumns(false);
|
|
}
|
|
};
|
|
loadColumns();
|
|
}, [localConfig.tableName]);
|
|
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
useEffect(() => {
|
|
setLocalConfig(config);
|
|
// 연쇄 드롭다운 설정 동기화
|
|
setCascadingEnabled(!!config.cascadingRelationCode);
|
|
}, [config]);
|
|
|
|
const updateConfig = (updates: Partial<EntitySearchInputConfig>) => {
|
|
const newConfig = { ...localConfig, ...updates };
|
|
setLocalConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
};
|
|
|
|
// 연쇄 드롭다운 활성화/비활성화
|
|
const handleCascadingToggle = (enabled: boolean) => {
|
|
setCascadingEnabled(enabled);
|
|
|
|
if (!enabled) {
|
|
// 비활성화 시 관계 설정 제거
|
|
const newConfig = {
|
|
...localConfig,
|
|
cascadingRelationCode: undefined,
|
|
cascadingRole: undefined,
|
|
cascadingParentField: undefined,
|
|
};
|
|
setLocalConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
} else {
|
|
// 활성화 시 관계 목록 로드
|
|
loadRelationList();
|
|
}
|
|
};
|
|
|
|
// 연쇄 관계 선택 (역할은 별도 선택)
|
|
const handleRelationSelect = (code: string) => {
|
|
const newConfig = {
|
|
...localConfig,
|
|
cascadingRelationCode: code || undefined,
|
|
cascadingRole: undefined, // 역할은 별도로 선택
|
|
cascadingParentField: undefined,
|
|
};
|
|
setLocalConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
};
|
|
|
|
// 역할 변경 핸들러
|
|
const handleRoleChange = (role: "parent" | "child") => {
|
|
const selectedRel = relationList.find(r => r.relation_code === localConfig.cascadingRelationCode);
|
|
|
|
if (role === "parent" && selectedRel) {
|
|
// 부모 역할: 부모 테이블 정보로 설정
|
|
const newConfig = {
|
|
...localConfig,
|
|
cascadingRole: role,
|
|
tableName: selectedRel.parent_table,
|
|
valueField: selectedRel.parent_value_column,
|
|
displayField: selectedRel.parent_label_column || selectedRel.parent_value_column,
|
|
cascadingParentField: undefined, // 부모 역할이면 부모 필드 필요 없음
|
|
};
|
|
setLocalConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
} else if (role === "child" && selectedRel) {
|
|
// 자식 역할: 자식 테이블 정보로 설정
|
|
const newConfig = {
|
|
...localConfig,
|
|
cascadingRole: role,
|
|
tableName: selectedRel.child_table,
|
|
valueField: selectedRel.child_value_column,
|
|
displayField: selectedRel.child_label_column || selectedRel.child_value_column,
|
|
};
|
|
setLocalConfig(newConfig);
|
|
onConfigChange(newConfig);
|
|
}
|
|
};
|
|
|
|
// 선택된 관계 정보
|
|
const selectedRelation = relationList.find(r => r.relation_code === localConfig.cascadingRelationCode);
|
|
|
|
const addSearchField = () => {
|
|
const fields = localConfig.searchFields || [];
|
|
updateConfig({ searchFields: [...fields, ""] });
|
|
};
|
|
|
|
const updateSearchField = (index: number, value: string) => {
|
|
const fields = [...(localConfig.searchFields || [])];
|
|
fields[index] = value;
|
|
updateConfig({ searchFields: fields });
|
|
};
|
|
|
|
const removeSearchField = (index: number) => {
|
|
const fields = [...(localConfig.searchFields || [])];
|
|
fields.splice(index, 1);
|
|
updateConfig({ searchFields: fields });
|
|
};
|
|
|
|
const addModalColumn = () => {
|
|
const columns = localConfig.modalColumns || [];
|
|
updateConfig({ modalColumns: [...columns, ""] });
|
|
};
|
|
|
|
const updateModalColumn = (index: number, value: string) => {
|
|
const columns = [...(localConfig.modalColumns || [])];
|
|
columns[index] = value;
|
|
updateConfig({ modalColumns: columns });
|
|
};
|
|
|
|
const removeModalColumn = (index: number) => {
|
|
const columns = [...(localConfig.modalColumns || [])];
|
|
columns.splice(index, 1);
|
|
updateConfig({ modalColumns: columns });
|
|
};
|
|
|
|
const addAdditionalField = () => {
|
|
const fields = localConfig.additionalFields || [];
|
|
updateConfig({ additionalFields: [...fields, ""] });
|
|
};
|
|
|
|
const updateAdditionalField = (index: number, value: string) => {
|
|
const fields = [...(localConfig.additionalFields || [])];
|
|
fields[index] = value;
|
|
updateConfig({ additionalFields: fields });
|
|
};
|
|
|
|
const removeAdditionalField = (index: number) => {
|
|
const fields = [...(localConfig.additionalFields || [])];
|
|
fields.splice(index, 1);
|
|
updateConfig({ additionalFields: fields });
|
|
};
|
|
|
|
// 자동 로드된 참조 테이블 정보가 있는지 확인
|
|
const hasAutoReference = referenceInfo.isAutoLoaded && referenceInfo.referenceTable;
|
|
|
|
return (
|
|
<div className="space-y-4 p-4">
|
|
{/* 연쇄 드롭다운 설정 - SelectConfigPanel과 동일한 패턴 */}
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<Link2 className="h-4 w-4" />
|
|
<h4 className="text-sm font-medium">연쇄 드롭다운</h4>
|
|
</div>
|
|
<Switch
|
|
checked={cascadingEnabled}
|
|
onCheckedChange={handleCascadingToggle}
|
|
/>
|
|
</div>
|
|
<p className="text-muted-foreground text-xs">
|
|
다른 필드의 값에 따라 옵션이 동적으로 변경됩니다. (예: 창고 선택 → 해당 창고의 위치만 표시)
|
|
</p>
|
|
|
|
{cascadingEnabled && (
|
|
<div className="space-y-3 rounded-md border p-3 bg-muted/30">
|
|
{/* 관계 선택 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">연쇄 관계 선택</Label>
|
|
<Select
|
|
value={localConfig.cascadingRelationCode || ""}
|
|
onValueChange={handleRelationSelect}
|
|
>
|
|
<SelectTrigger className="text-xs">
|
|
<SelectValue placeholder={loadingRelations ? "로딩 중..." : "관계 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{relationList.map((relation) => (
|
|
<SelectItem key={relation.relation_code} value={relation.relation_code}>
|
|
<div className="flex flex-col">
|
|
<span>{relation.relation_name}</span>
|
|
<span className="text-muted-foreground text-xs">
|
|
{relation.parent_table} → {relation.child_table}
|
|
</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 역할 선택 */}
|
|
{localConfig.cascadingRelationCode && (
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">역할 선택</Label>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant={localConfig.cascadingRole === "parent" ? "default" : "outline"}
|
|
className="flex-1 text-xs"
|
|
onClick={() => handleRoleChange("parent")}
|
|
>
|
|
부모 (상위 선택)
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant={localConfig.cascadingRole === "child" ? "default" : "outline"}
|
|
className="flex-1 text-xs"
|
|
onClick={() => handleRoleChange("child")}
|
|
>
|
|
자식 (하위 선택)
|
|
</Button>
|
|
</div>
|
|
<p className="text-muted-foreground text-xs">
|
|
{localConfig.cascadingRole === "parent"
|
|
? "이 필드가 상위 선택 역할을 합니다. (예: 창고 선택)"
|
|
: localConfig.cascadingRole === "child"
|
|
? "이 필드는 상위 필드 값에 따라 옵션이 변경됩니다. (예: 위치 선택)"
|
|
: "이 필드의 역할을 선택하세요."}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 부모 필드 설정 (자식 역할일 때만) */}
|
|
{localConfig.cascadingRelationCode && localConfig.cascadingRole === "child" && (
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">부모 필드명</Label>
|
|
<Input
|
|
value={localConfig.cascadingParentField || ""}
|
|
onChange={(e) => updateConfig({ cascadingParentField: e.target.value || undefined })}
|
|
placeholder="예: warehouse_code"
|
|
className="text-xs"
|
|
/>
|
|
<p className="text-muted-foreground text-xs">
|
|
이 드롭다운의 옵션을 결정할 부모 필드의 컬럼명을 입력하세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 선택된 관계 정보 표시 */}
|
|
{selectedRelation && localConfig.cascadingRole && (
|
|
<div className="bg-background space-y-1 rounded-md p-2 text-xs">
|
|
{localConfig.cascadingRole === "parent" ? (
|
|
<>
|
|
<div className="font-medium text-blue-600">부모 역할 (상위 선택)</div>
|
|
<div>
|
|
<span className="text-muted-foreground">데이터 소스:</span>{" "}
|
|
<span className="font-medium">{selectedRelation.parent_table}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">저장 값:</span>{" "}
|
|
<span className="font-medium">{selectedRelation.parent_value_column}</span>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="font-medium text-green-600">자식 역할 (하위 선택)</div>
|
|
<div>
|
|
<span className="text-muted-foreground">데이터 소스:</span>{" "}
|
|
<span className="font-medium">{selectedRelation.child_table}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">저장 값:</span>{" "}
|
|
<span className="font-medium">{selectedRelation.child_value_column}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">필터 컬럼:</span>{" "}
|
|
<span className="font-medium">{selectedRelation.child_filter_column}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 관계 관리 페이지 링크 */}
|
|
<div className="flex justify-end">
|
|
<Link href="/admin/cascading-relations" target="_blank">
|
|
<Button variant="link" size="sm" className="h-auto p-0 text-xs">
|
|
<ExternalLink className="mr-1 h-3 w-3" />
|
|
관계 관리
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 구분선 - 연쇄 드롭다운 비활성화 시에만 표시 */}
|
|
{!cascadingEnabled && (
|
|
<div className="border-t pt-4">
|
|
<p className="text-[10px] text-muted-foreground mb-4">
|
|
아래에서 직접 테이블/필드를 설정하세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 참조 테이블 자동 로드 정보 표시 */}
|
|
{referenceInfo.isLoading && (
|
|
<div className="bg-muted/50 rounded-md border p-3">
|
|
<p className="text-xs text-muted-foreground">참조 테이블 정보 로딩 중...</p>
|
|
</div>
|
|
)}
|
|
|
|
{hasAutoReference && !cascadingEnabled && (
|
|
<div className="bg-primary/5 rounded-md border border-primary/20 p-3 space-y-2">
|
|
<div className="flex items-center gap-2">
|
|
<Database className="h-4 w-4 text-primary" />
|
|
<span className="text-xs font-medium text-primary">테이블 타입에서 자동 설정됨</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
|
<div>
|
|
<span className="text-muted-foreground">참조 테이블:</span>
|
|
<div className="font-medium">{referenceInfo.referenceTable}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">값 필드:</span>
|
|
<div className="font-medium">{referenceInfo.referenceColumn || "id"}</div>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">표시 필드:</span>
|
|
<div className="font-medium">{referenceInfo.displayColumn || "name"}</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
소스: {currentComponent?.tableName}.{currentComponent?.columnName}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{referenceInfo.error && !hasAutoReference && !cascadingEnabled && (
|
|
<div className="bg-amber-500/10 rounded-md border border-amber-500/20 p-3">
|
|
<p className="text-xs text-amber-700 flex items-center gap-1">
|
|
<Info className="h-3 w-3" />
|
|
{referenceInfo.error}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground mt-1">
|
|
테이블을 수동으로 선택하세요.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">
|
|
테이블명 *
|
|
{hasAutoReference && (
|
|
<span className="text-[10px] text-muted-foreground ml-2">(자동 설정됨)</span>
|
|
)}
|
|
</Label>
|
|
<Popover open={openTableCombo} onOpenChange={setOpenTableCombo}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={openTableCombo}
|
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
disabled={isLoadingTables}
|
|
>
|
|
{localConfig.tableName
|
|
? allTables.find((t) => t.tableName === localConfig.tableName)?.displayName || localConfig.tableName
|
|
: isLoadingTables ? "로딩 중..." : "테이블 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 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 sm:text-sm" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{allTables.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.displayName || table.tableName}-${table.tableName}`}
|
|
onSelect={() => {
|
|
updateConfig({ tableName: table.tableName });
|
|
setOpenTableCombo(false);
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
localConfig.tableName === table.tableName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
|
{table.displayName && table.displayName !== table.tableName && (
|
|
<span className="text-[10px] text-gray-500">{table.tableName}</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">표시 필드 *</Label>
|
|
<Popover open={openDisplayFieldCombo} onOpenChange={setOpenDisplayFieldCombo}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={openDisplayFieldCombo}
|
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
{localConfig.displayField
|
|
? tableColumns.find((c) => c.columnName === localConfig.displayField)?.displayName || localConfig.displayField
|
|
: isLoadingColumns ? "로딩 중..." : "필드 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 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 sm:text-sm" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs sm:text-sm">필드를 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{tableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={`${column.displayName || column.columnName}-${column.columnName}`}
|
|
onSelect={() => {
|
|
updateConfig({ displayField: column.columnName });
|
|
setOpenDisplayFieldCombo(false);
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
localConfig.displayField === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{column.displayName || column.columnName}</span>
|
|
{column.displayName && column.displayName !== column.columnName && (
|
|
<span className="text-[10px] text-gray-500">{column.columnName}</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">값 필드 *</Label>
|
|
<Popover open={openValueFieldCombo} onOpenChange={setOpenValueFieldCombo}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={openValueFieldCombo}
|
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
{localConfig.valueField
|
|
? tableColumns.find((c) => c.columnName === localConfig.valueField)?.displayName || localConfig.valueField
|
|
: isLoadingColumns ? "로딩 중..." : "필드 선택"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 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 sm:text-sm" />
|
|
<CommandList>
|
|
<CommandEmpty className="text-xs sm:text-sm">필드를 찾을 수 없습니다.</CommandEmpty>
|
|
<CommandGroup>
|
|
{tableColumns.map((column) => (
|
|
<CommandItem
|
|
key={column.columnName}
|
|
value={`${column.displayName || column.columnName}-${column.columnName}`}
|
|
onSelect={() => {
|
|
updateConfig({ valueField: column.columnName });
|
|
setOpenValueFieldCombo(false);
|
|
}}
|
|
className="text-xs sm:text-sm"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-4 w-4",
|
|
localConfig.valueField === column.columnName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<span className="font-medium">{column.displayName || column.columnName}</span>
|
|
{column.displayName && column.displayName !== column.columnName && (
|
|
<span className="text-[10px] text-gray-500">{column.columnName}</span>
|
|
)}
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">UI 모드</Label>
|
|
<Select
|
|
value={localConfig.mode || "combo"}
|
|
onValueChange={(value: "select" | "autocomplete" | "modal" | "combo") =>
|
|
updateConfig({ mode: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="select">드롭다운 (검색 가능)</SelectItem>
|
|
<SelectItem value="combo">콤보 (입력 + 모달)</SelectItem>
|
|
<SelectItem value="modal">모달만</SelectItem>
|
|
<SelectItem value="autocomplete">자동완성만</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{localConfig.mode === "select" && "검색 가능한 드롭다운 형태로 표시됩니다."}
|
|
{localConfig.mode === "modal" && "모달 팝업에서 데이터를 검색하고 선택합니다."}
|
|
{(localConfig.mode === "combo" || !localConfig.mode) && "입력 필드와 검색 버튼이 함께 표시됩니다."}
|
|
{localConfig.mode === "autocomplete" && "입력하면서 자동완성 목록이 표시됩니다."}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">플레이스홀더</Label>
|
|
<Input
|
|
value={localConfig.placeholder || ""}
|
|
onChange={(e) => updateConfig({ placeholder: e.target.value })}
|
|
placeholder="검색..."
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{(localConfig.mode === "modal" || localConfig.mode === "combo") && (
|
|
<>
|
|
<div className="space-y-2">
|
|
<Label className="text-xs sm:text-sm">모달 제목</Label>
|
|
<Input
|
|
value={localConfig.modalTitle || ""}
|
|
onChange={(e) => updateConfig({ modalTitle: e.target.value })}
|
|
placeholder="검색 및 선택"
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs sm:text-sm">모달 컬럼</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addModalColumn}
|
|
className="h-7 text-xs"
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{(localConfig.modalColumns || []).map((column, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<Select
|
|
value={column}
|
|
onValueChange={(value) => updateModalColumn(index, value)}
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm flex-1">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeModalColumn(index)}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs sm:text-sm">검색 필드</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addSearchField}
|
|
className="h-7 text-xs"
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{(localConfig.searchFields || []).map((field, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<Select
|
|
value={field}
|
|
onValueChange={(value) => updateSearchField(index, value)}
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm flex-1">
|
|
<SelectValue placeholder="필드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeSearchField(index)}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs sm:text-sm">추가 정보 표시</Label>
|
|
<Switch
|
|
checked={localConfig.showAdditionalInfo || false}
|
|
onCheckedChange={(checked) =>
|
|
updateConfig({ showAdditionalInfo: checked })
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{localConfig.showAdditionalInfo && (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs sm:text-sm">추가 필드</Label>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={addAdditionalField}
|
|
className="h-7 text-xs"
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{(localConfig.additionalFields || []).map((field, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<Select
|
|
value={field}
|
|
onValueChange={(value) => updateAdditionalField(index, value)}
|
|
disabled={!localConfig.tableName || isLoadingColumns}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm flex-1">
|
|
<SelectValue placeholder="필드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{tableColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
{col.displayName || col.columnName}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => removeAdditionalField(index)}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|