fix(autocomplete-search-input): 필드 매핑 저장 문제 해결
- types.ts에 targetTable 필드 추가하여 config에 저장되도록 수정 - ConfigPanel에서 targetTable을 localConfig로 관리하여 설정 유지 - Renderer 단순화 (TextInput 패턴 적용) - Component에서 직접 isInteractive 체크 및 필드 매핑 처리 - ComponentRendererProps 상속으로 필수 props 타입 안정성 확보 문제: - ConfigPanel 설정이 초기화되는 문제 - 필드 매핑 데이터가 DB에 저장되지 않는 문제 해결: - 정상 작동하는 TextInput 컴포넌트 패턴 분석 및 적용 - Renderer는 props만 전달, Component가 저장 로직 처리
This commit is contained in:
parent
3aee36515a
commit
95b5e3dc7a
|
|
@ -337,12 +337,22 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||||
formData={formData}
|
formData={formData}
|
||||||
originalData={originalData}
|
originalData={originalData}
|
||||||
onFormDataChange={(fieldName, value) => {
|
onFormDataChange={(fieldName, value) => {
|
||||||
setFormData((prev) => ({
|
console.log("📝 SaveModal - formData 변경:", {
|
||||||
|
fieldName,
|
||||||
|
value,
|
||||||
|
componentType: component.type,
|
||||||
|
componentId: component.id,
|
||||||
|
});
|
||||||
|
setFormData((prev) => {
|
||||||
|
const newData = {
|
||||||
...prev,
|
...prev,
|
||||||
[fieldName]: value,
|
[fieldName]: value,
|
||||||
}));
|
};
|
||||||
|
console.log("📦 새 formData:", newData);
|
||||||
|
return newData;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
mode={initialData ? "edit" : "create"}
|
mode="edit"
|
||||||
isInModal={true}
|
isInModal={true}
|
||||||
isInteractive={true}
|
isInteractive={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,23 @@ import { Button } from "@/components/ui/button";
|
||||||
import { useEntitySearch } from "../entity-search-input/useEntitySearch";
|
import { useEntitySearch } from "../entity-search-input/useEntitySearch";
|
||||||
import { EntitySearchResult } from "../entity-search-input/types";
|
import { EntitySearchResult } from "../entity-search-input/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AutocompleteSearchInputConfig, FieldMapping } from "./types";
|
import { AutocompleteSearchInputConfig } from "./types";
|
||||||
|
import { ComponentRendererProps } from "../../DynamicComponentRenderer";
|
||||||
|
|
||||||
interface AutocompleteSearchInputProps extends Partial<AutocompleteSearchInputConfig> {
|
export interface AutocompleteSearchInputProps extends ComponentRendererProps {
|
||||||
config?: AutocompleteSearchInputConfig;
|
config?: AutocompleteSearchInputConfig;
|
||||||
|
tableName?: string;
|
||||||
|
displayField?: string;
|
||||||
|
valueField?: string;
|
||||||
|
searchFields?: string[];
|
||||||
filterCondition?: Record<string, any>;
|
filterCondition?: Record<string, any>;
|
||||||
disabled?: boolean;
|
placeholder?: string;
|
||||||
value?: any;
|
showAdditionalInfo?: boolean;
|
||||||
onChange?: (value: any, fullData?: any) => void;
|
additionalFields?: string[];
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AutocompleteSearchInputComponent({
|
export function AutocompleteSearchInputComponent({
|
||||||
|
component,
|
||||||
config,
|
config,
|
||||||
tableName: propTableName,
|
tableName: propTableName,
|
||||||
displayField: propDisplayField,
|
displayField: propDisplayField,
|
||||||
|
|
@ -29,9 +34,10 @@ export function AutocompleteSearchInputComponent({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
showAdditionalInfo: propShowAdditionalInfo,
|
|
||||||
additionalFields: propAdditionalFields,
|
|
||||||
className,
|
className,
|
||||||
|
isInteractive = false,
|
||||||
|
onFormDataChange,
|
||||||
|
formData,
|
||||||
}: AutocompleteSearchInputProps) {
|
}: AutocompleteSearchInputProps) {
|
||||||
// config prop 우선, 없으면 개별 prop 사용
|
// config prop 우선, 없으면 개별 prop 사용
|
||||||
const tableName = config?.tableName || propTableName || "";
|
const tableName = config?.tableName || propTableName || "";
|
||||||
|
|
@ -39,8 +45,7 @@ export function AutocompleteSearchInputComponent({
|
||||||
const valueField = config?.valueField || propValueField || "";
|
const valueField = config?.valueField || propValueField || "";
|
||||||
const searchFields = config?.searchFields || propSearchFields || [displayField];
|
const searchFields = config?.searchFields || propSearchFields || [displayField];
|
||||||
const placeholder = config?.placeholder || propPlaceholder || "검색...";
|
const placeholder = config?.placeholder || propPlaceholder || "검색...";
|
||||||
const showAdditionalInfo = config?.showAdditionalInfo ?? propShowAdditionalInfo ?? false;
|
|
||||||
const additionalFields = config?.additionalFields || propAdditionalFields || [];
|
|
||||||
const [inputValue, setInputValue] = useState("");
|
const [inputValue, setInputValue] = useState("");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [selectedData, setSelectedData] = useState<EntitySearchResult | null>(null);
|
const [selectedData, setSelectedData] = useState<EntitySearchResult | null>(null);
|
||||||
|
|
@ -52,15 +57,20 @@ export function AutocompleteSearchInputComponent({
|
||||||
filterCondition,
|
filterCondition,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// formData에서 현재 값 가져오기 (isInteractive 모드)
|
||||||
|
const currentValue = isInteractive && formData && component?.columnName
|
||||||
|
? formData[component.columnName]
|
||||||
|
: value;
|
||||||
|
|
||||||
// value가 변경되면 표시값 업데이트
|
// value가 변경되면 표시값 업데이트
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value && selectedData) {
|
if (currentValue && selectedData) {
|
||||||
setInputValue(selectedData[displayField] || "");
|
setInputValue(selectedData[displayField] || "");
|
||||||
} else if (!value) {
|
} else if (!currentValue) {
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setSelectedData(null);
|
setSelectedData(null);
|
||||||
}
|
}
|
||||||
}, [value, displayField]);
|
}, [currentValue, displayField, selectedData]);
|
||||||
|
|
||||||
// 외부 클릭 감지
|
// 외부 클릭 감지
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -81,45 +91,61 @@ export function AutocompleteSearchInputComponent({
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 필드 자동 매핑 처리
|
|
||||||
const applyFieldMappings = (item: EntitySearchResult) => {
|
|
||||||
if (!config?.enableFieldMapping || !config?.fieldMappings) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.fieldMappings.forEach((mapping: FieldMapping) => {
|
|
||||||
if (!mapping.sourceField || !mapping.targetField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = item[mapping.sourceField];
|
|
||||||
|
|
||||||
// DOM에서 타겟 필드 찾기 (id로 검색)
|
|
||||||
const targetElement = document.getElementById(mapping.targetField);
|
|
||||||
|
|
||||||
if (targetElement) {
|
|
||||||
// input, textarea 등의 값 설정
|
|
||||||
if (
|
|
||||||
targetElement instanceof HTMLInputElement ||
|
|
||||||
targetElement instanceof HTMLTextAreaElement
|
|
||||||
) {
|
|
||||||
targetElement.value = value?.toString() || "";
|
|
||||||
|
|
||||||
// React의 change 이벤트 트리거
|
|
||||||
const event = new Event("input", { bubbles: true });
|
|
||||||
targetElement.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelect = (item: EntitySearchResult) => {
|
const handleSelect = (item: EntitySearchResult) => {
|
||||||
setSelectedData(item);
|
setSelectedData(item);
|
||||||
setInputValue(item[displayField] || "");
|
setInputValue(item[displayField] || "");
|
||||||
onChange?.(item[valueField], item);
|
|
||||||
|
|
||||||
// 필드 자동 매핑 실행
|
console.log("🔍 AutocompleteSearchInput handleSelect:", {
|
||||||
applyFieldMappings(item);
|
item,
|
||||||
|
valueField,
|
||||||
|
value: item[valueField],
|
||||||
|
config,
|
||||||
|
isInteractive,
|
||||||
|
hasOnFormDataChange: !!onFormDataChange,
|
||||||
|
columnName: component?.columnName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// isInteractive 모드에서만 저장
|
||||||
|
if (isInteractive && onFormDataChange) {
|
||||||
|
// 필드 매핑 처리
|
||||||
|
if (config?.fieldMappings && Array.isArray(config.fieldMappings)) {
|
||||||
|
console.log("📋 필드 매핑 처리 시작:", config.fieldMappings);
|
||||||
|
|
||||||
|
config.fieldMappings.forEach((mapping: any, index: number) => {
|
||||||
|
const targetField = mapping.targetField || mapping.targetColumn;
|
||||||
|
|
||||||
|
console.log(` 매핑 ${index + 1}:`, {
|
||||||
|
sourceField: mapping.sourceField,
|
||||||
|
targetField,
|
||||||
|
label: mapping.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mapping.sourceField && targetField) {
|
||||||
|
const sourceValue = item[mapping.sourceField];
|
||||||
|
|
||||||
|
console.log(` 값: ${mapping.sourceField} = ${sourceValue}`);
|
||||||
|
|
||||||
|
if (sourceValue !== undefined) {
|
||||||
|
console.log(` ✅ 저장: ${targetField} = ${sourceValue}`);
|
||||||
|
onFormDataChange(targetField, sourceValue);
|
||||||
|
} else {
|
||||||
|
console.warn(` ⚠️ sourceField "${mapping.sourceField}"의 값이 undefined입니다`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(` ⚠️ 매핑 불완전: sourceField=${mapping.sourceField}, targetField=${targetField}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본 필드 저장 (columnName이 설정된 경우)
|
||||||
|
if (component?.columnName) {
|
||||||
|
console.log(`💾 기본 필드 저장: ${component.columnName} = ${item[valueField]}`);
|
||||||
|
onFormDataChange(component.columnName, item[valueField]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// onChange 콜백 호출 (호환성)
|
||||||
|
onChange?.(item[valueField], item);
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
@ -149,9 +175,9 @@ export function AutocompleteSearchInputComponent({
|
||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm pr-16"
|
className="h-8 pr-16 text-xs sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
<div className="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1">
|
||||||
{loading && (
|
{loading && (
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -172,10 +198,10 @@ export function AutocompleteSearchInputComponent({
|
||||||
|
|
||||||
{/* 드롭다운 결과 */}
|
{/* 드롭다운 결과 */}
|
||||||
{isOpen && (results.length > 0 || loading) && (
|
{isOpen && (results.length > 0 || loading) && (
|
||||||
<div className="absolute z-50 w-full mt-1 bg-background border rounded-md shadow-lg max-h-[300px] overflow-y-auto">
|
<div className="absolute z-50 mt-1 max-h-[300px] w-full overflow-y-auto rounded-md border bg-background shadow-lg">
|
||||||
{loading && results.length === 0 ? (
|
{loading && results.length === 0 ? (
|
||||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
<Loader2 className="h-4 w-4 animate-spin mx-auto mb-2" />
|
<Loader2 className="mx-auto mb-2 h-4 w-4 animate-spin" />
|
||||||
검색 중...
|
검색 중...
|
||||||
</div>
|
</div>
|
||||||
) : results.length === 0 ? (
|
) : results.length === 0 ? (
|
||||||
|
|
@ -189,37 +215,15 @@ export function AutocompleteSearchInputComponent({
|
||||||
key={index}
|
key={index}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelect(item)}
|
onClick={() => handleSelect(item)}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-accent text-xs sm:text-sm transition-colors"
|
className="w-full px-3 py-2 text-left text-xs transition-colors hover:bg-accent sm:text-sm"
|
||||||
>
|
>
|
||||||
<div className="font-medium">{item[displayField]}</div>
|
<div className="font-medium">{item[displayField]}</div>
|
||||||
{additionalFields.length > 0 && (
|
|
||||||
<div className="text-xs text-muted-foreground mt-1 space-y-0.5">
|
|
||||||
{additionalFields.map((field) => (
|
|
||||||
<div key={field}>
|
|
||||||
{field}: {item[field] || "-"}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 추가 정보 표시 */}
|
|
||||||
{showAdditionalInfo && selectedData && additionalFields.length > 0 && (
|
|
||||||
<div className="mt-2 text-xs text-muted-foreground space-y-1 px-2">
|
|
||||||
{additionalFields.map((field) => (
|
|
||||||
<div key={field} className="flex gap-2">
|
|
||||||
<span className="font-medium">{field}:</span>
|
|
||||||
<span>{selectedData[field] || "-"}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,9 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
||||||
import { AutocompleteSearchInputConfig, FieldMapping, ValueFieldStorage } from "./types";
|
import { AutocompleteSearchInputConfig } from "./types";
|
||||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -24,83 +23,14 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
}: AutocompleteSearchInputConfigPanelProps) {
|
}: AutocompleteSearchInputConfigPanelProps) {
|
||||||
const [localConfig, setLocalConfig] = useState(config);
|
const [localConfig, setLocalConfig] = useState(config);
|
||||||
const [allTables, setAllTables] = useState<any[]>([]);
|
const [allTables, setAllTables] = useState<any[]>([]);
|
||||||
const [tableColumns, setTableColumns] = useState<any[]>([]);
|
const [sourceTableColumns, setSourceTableColumns] = useState<any[]>([]);
|
||||||
|
const [targetTableColumns, setTargetTableColumns] = useState<any[]>([]);
|
||||||
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
const [isLoadingTables, setIsLoadingTables] = useState(false);
|
||||||
const [isLoadingColumns, setIsLoadingColumns] = useState(false);
|
const [isLoadingSourceColumns, setIsLoadingSourceColumns] = useState(false);
|
||||||
const [openTableCombo, setOpenTableCombo] = useState(false);
|
const [isLoadingTargetColumns, setIsLoadingTargetColumns] = useState(false);
|
||||||
|
const [openSourceTableCombo, setOpenSourceTableCombo] = useState(false);
|
||||||
|
const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false);
|
||||||
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
||||||
const [openValueFieldCombo, setOpenValueFieldCombo] = useState(false);
|
|
||||||
const [openStorageTableCombo, setOpenStorageTableCombo] = useState(false);
|
|
||||||
const [openStorageColumnCombo, setOpenStorageColumnCombo] = useState(false);
|
|
||||||
const [storageTableColumns, setStorageTableColumns] = useState<any[]>([]);
|
|
||||||
const [isLoadingStorageColumns, setIsLoadingStorageColumns] = useState(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(() => {
|
|
||||||
const loadStorageColumns = async () => {
|
|
||||||
const storageTable = localConfig.valueFieldStorage?.targetTable;
|
|
||||||
if (!storageTable) {
|
|
||||||
setStorageTableColumns([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoadingStorageColumns(true);
|
|
||||||
try {
|
|
||||||
const response = await tableManagementApi.getColumnList(storageTable);
|
|
||||||
if (response.success && response.data) {
|
|
||||||
setStorageTableColumns(response.data.columns);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("저장 테이블 컬럼 로드 실패:", error);
|
|
||||||
setStorageTableColumns([]);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingStorageColumns(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
loadStorageColumns();
|
|
||||||
}, [localConfig.valueFieldStorage?.targetTable]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalConfig(config);
|
setLocalConfig(config);
|
||||||
|
|
@ -112,52 +42,76 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
onConfigChange(newConfig);
|
onConfigChange(newConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSearchField = () => {
|
// 테이블 목록 로드
|
||||||
const fields = localConfig.searchFields || [];
|
useEffect(() => {
|
||||||
updateConfig({ searchFields: [...fields, ""] });
|
const loadTables = async () => {
|
||||||
|
setIsLoadingTables(true);
|
||||||
|
try {
|
||||||
|
const response = await tableManagementApi.getTableList();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setAllTables(response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setAllTables([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingTables(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
loadTables();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const updateSearchField = (index: number, value: string) => {
|
// 외부 테이블 컬럼 로드
|
||||||
const fields = [...(localConfig.searchFields || [])];
|
useEffect(() => {
|
||||||
fields[index] = value;
|
const loadColumns = async () => {
|
||||||
updateConfig({ searchFields: fields });
|
if (!localConfig.tableName) {
|
||||||
|
setSourceTableColumns([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoadingSourceColumns(true);
|
||||||
|
try {
|
||||||
|
const response = await tableManagementApi.getColumnList(localConfig.tableName);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setSourceTableColumns(response.data.columns);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setSourceTableColumns([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingSourceColumns(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
loadColumns();
|
||||||
|
}, [localConfig.tableName]);
|
||||||
|
|
||||||
const removeSearchField = (index: number) => {
|
// 저장 테이블 컬럼 로드
|
||||||
const fields = [...(localConfig.searchFields || [])];
|
useEffect(() => {
|
||||||
fields.splice(index, 1);
|
const loadTargetColumns = async () => {
|
||||||
updateConfig({ searchFields: fields });
|
if (!localConfig.targetTable) {
|
||||||
|
setTargetTableColumns([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoadingTargetColumns(true);
|
||||||
|
try {
|
||||||
|
const response = await tableManagementApi.getColumnList(localConfig.targetTable);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setTargetTableColumns(response.data.columns);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setTargetTableColumns([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingTargetColumns(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
loadTargetColumns();
|
||||||
|
}, [localConfig.targetTable]);
|
||||||
|
|
||||||
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 addFieldMapping = () => {
|
const addFieldMapping = () => {
|
||||||
const mappings = localConfig.fieldMappings || [];
|
const mappings = localConfig.fieldMappings || [];
|
||||||
updateConfig({
|
updateConfig({
|
||||||
fieldMappings: [
|
fieldMappings: [...mappings, { sourceField: "", targetField: "", label: "" }],
|
||||||
...mappings,
|
|
||||||
{ sourceField: "", targetField: "", label: "" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFieldMapping = (index: number, updates: Partial<FieldMapping>) => {
|
const updateFieldMapping = (index: number, updates: any) => {
|
||||||
const mappings = [...(localConfig.fieldMappings || [])];
|
const mappings = [...(localConfig.fieldMappings || [])];
|
||||||
mappings[index] = { ...mappings[index], ...updates };
|
mappings[index] = { ...mappings[index], ...updates };
|
||||||
updateConfig({ fieldMappings: mappings });
|
updateConfig({ fieldMappings: mappings });
|
||||||
|
|
@ -170,21 +124,22 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-6 p-4">
|
||||||
|
{/* 1. 외부 테이블 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm">테이블명 *</Label>
|
<Label className="text-xs font-semibold sm:text-sm">1. 외부 테이블 선택 *</Label>
|
||||||
<Popover open={openTableCombo} onOpenChange={setOpenTableCombo}>
|
<Popover open={openSourceTableCombo} onOpenChange={setOpenSourceTableCombo}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={openTableCombo}
|
aria-expanded={openSourceTableCombo}
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||||
disabled={isLoadingTables}
|
disabled={isLoadingTables}
|
||||||
>
|
>
|
||||||
{localConfig.tableName
|
{localConfig.tableName
|
||||||
? allTables.find((t) => t.tableName === localConfig.tableName)?.displayName || localConfig.tableName
|
? allTables.find((t) => t.tableName === localConfig.tableName)?.displayName || localConfig.tableName
|
||||||
: isLoadingTables ? "로딩 중..." : "테이블 선택"}
|
: isLoadingTables ? "로딩 중..." : "데이터를 가져올 테이블 선택"}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
@ -200,7 +155,7 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
value={table.tableName}
|
value={table.tableName}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
updateConfig({ tableName: table.tableName });
|
updateConfig({ tableName: table.tableName });
|
||||||
setOpenTableCombo(false);
|
setOpenSourceTableCombo(false);
|
||||||
}}
|
}}
|
||||||
className="text-xs sm:text-sm"
|
className="text-xs sm:text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -216,13 +171,11 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
검색할 데이터가 저장된 테이블을 선택하세요
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 2. 표시 필드 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm">표시 필드 *</Label>
|
<Label className="text-xs font-semibold sm:text-sm">2. 표시 필드 *</Label>
|
||||||
<Popover open={openDisplayFieldCombo} onOpenChange={setOpenDisplayFieldCombo}>
|
<Popover open={openDisplayFieldCombo} onOpenChange={setOpenDisplayFieldCombo}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -230,11 +183,11 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={openDisplayFieldCombo}
|
aria-expanded={openDisplayFieldCombo}
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||||
disabled={!localConfig.tableName || isLoadingColumns}
|
disabled={!localConfig.tableName || isLoadingSourceColumns}
|
||||||
>
|
>
|
||||||
{localConfig.displayField
|
{localConfig.displayField
|
||||||
? tableColumns.find((c) => c.columnName === localConfig.displayField)?.displayName || localConfig.displayField
|
? sourceTableColumns.find((c) => c.columnName === localConfig.displayField)?.displayName || localConfig.displayField
|
||||||
: isLoadingColumns ? "로딩 중..." : "필드 선택"}
|
: isLoadingSourceColumns ? "로딩 중..." : "사용자에게 보여줄 필드"}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
@ -244,7 +197,7 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="text-xs sm:text-sm">필드를 찾을 수 없습니다.</CommandEmpty>
|
<CommandEmpty className="text-xs sm:text-sm">필드를 찾을 수 없습니다.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{tableColumns.map((column) => (
|
{sourceTableColumns.map((column) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
value={column.columnName}
|
value={column.columnName}
|
||||||
|
|
@ -266,98 +219,23 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
사용자에게 보여줄 필드 (예: 거래처명)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 3. 저장 대상 테이블 선택 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs sm:text-sm">값 필드 *</Label>
|
<Label className="text-xs font-semibold sm:text-sm">3. 저장 대상 테이블 *</Label>
|
||||||
<Popover open={openValueFieldCombo} onOpenChange={setOpenValueFieldCombo}>
|
<Popover open={openTargetTableCombo} onOpenChange={setOpenTargetTableCombo}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={openValueFieldCombo}
|
aria-expanded={openTargetTableCombo}
|
||||||
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.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 && <span className="text-[10px] text-gray-500">{column.columnName}</span>}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
검색 테이블에서 가져올 값의 컬럼 (예: customer_code)
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* 값 필드 저장 위치 설정 */}
|
|
||||||
<div className="space-y-4 border rounded-lg p-4 bg-card">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold mb-1">값 필드 저장 위치 (고급)</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
위에서 선택한 "값 필드"의 데이터를 어느 테이블/컬럼에 저장할지 지정합니다.
|
|
||||||
<br />
|
|
||||||
미설정 시 화면의 연결 테이블에 컴포넌트의 바인딩 필드로 자동 저장됩니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 저장 테이블 선택 */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs sm:text-sm">저장 테이블</Label>
|
|
||||||
<Popover open={openStorageTableCombo} onOpenChange={setOpenStorageTableCombo}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={openStorageTableCombo}
|
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
||||||
disabled={isLoadingTables}
|
disabled={isLoadingTables}
|
||||||
>
|
>
|
||||||
{localConfig.valueFieldStorage?.targetTable
|
{localConfig.targetTable
|
||||||
? allTables.find((t) => t.tableName === localConfig.valueFieldStorage?.targetTable)?.displayName ||
|
? allTables.find((t) => t.tableName === localConfig.targetTable)?.displayName || localConfig.targetTable
|
||||||
localConfig.valueFieldStorage.targetTable
|
: "데이터를 저장할 테이블 선택"}
|
||||||
: "기본값 (화면 연결 테이블)"}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
@ -367,49 +245,17 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
<CommandEmpty className="text-xs sm:text-sm">테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{/* 기본값 옵션 */}
|
|
||||||
<CommandItem
|
|
||||||
value=""
|
|
||||||
onSelect={() => {
|
|
||||||
updateConfig({
|
|
||||||
valueFieldStorage: {
|
|
||||||
...localConfig.valueFieldStorage,
|
|
||||||
targetTable: undefined,
|
|
||||||
targetColumn: undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setOpenStorageTableCombo(false);
|
|
||||||
}}
|
|
||||||
className="text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
<Check className={cn("mr-2 h-4 w-4", !localConfig.valueFieldStorage?.targetTable ? "opacity-100" : "opacity-0")} />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">기본값</span>
|
|
||||||
<span className="text-[10px] text-gray-500">화면의 연결 테이블 사용</span>
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
{allTables.map((table) => (
|
{allTables.map((table) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={table.tableName}
|
key={table.tableName}
|
||||||
value={table.tableName}
|
value={table.tableName}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
updateConfig({
|
updateConfig({ targetTable: table.tableName });
|
||||||
valueFieldStorage: {
|
setOpenTargetTableCombo(false);
|
||||||
...localConfig.valueFieldStorage,
|
|
||||||
targetTable: table.tableName,
|
|
||||||
targetColumn: undefined, // 테이블 변경 시 컬럼 초기화
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setOpenStorageTableCombo(false);
|
|
||||||
}}
|
}}
|
||||||
className="text-xs sm:text-sm"
|
className="text-xs sm:text-sm"
|
||||||
>
|
>
|
||||||
<Check
|
<Check className={cn("mr-2 h-4 w-4", localConfig.targetTable === table.tableName ? "opacity-100" : "opacity-0")} />
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
localConfig.valueFieldStorage?.targetTable === table.tableName ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||||
{table.displayName && <span className="text-[10px] text-gray-500">{table.tableName}</span>}
|
{table.displayName && <span className="text-[10px] text-gray-500">{table.tableName}</span>}
|
||||||
|
|
@ -421,255 +267,35 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
값을 저장할 테이블 (기본값: 화면 연결 테이블)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 저장 컬럼 선택 */}
|
{/* 4. 필드 매핑 */}
|
||||||
{localConfig.valueFieldStorage?.targetTable && (
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs sm:text-sm">저장 컬럼</Label>
|
|
||||||
<Popover open={openStorageColumnCombo} onOpenChange={setOpenStorageColumnCombo}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={openStorageColumnCombo}
|
|
||||||
className="h-8 w-full justify-between text-xs sm:h-10 sm:text-sm"
|
|
||||||
disabled={isLoadingStorageColumns}
|
|
||||||
>
|
|
||||||
{localConfig.valueFieldStorage?.targetColumn
|
|
||||||
? storageTableColumns.find((c) => c.columnName === localConfig.valueFieldStorage?.targetColumn)
|
|
||||||
?.displayName || localConfig.valueFieldStorage.targetColumn
|
|
||||||
: isLoadingStorageColumns
|
|
||||||
? "로딩 중..."
|
|
||||||
: "컬럼 선택"}
|
|
||||||
<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>
|
|
||||||
{storageTableColumns.map((column) => (
|
|
||||||
<CommandItem
|
|
||||||
key={column.columnName}
|
|
||||||
value={column.columnName}
|
|
||||||
onSelect={() => {
|
|
||||||
updateConfig({
|
|
||||||
valueFieldStorage: {
|
|
||||||
...localConfig.valueFieldStorage,
|
|
||||||
targetColumn: column.columnName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setOpenStorageColumnCombo(false);
|
|
||||||
}}
|
|
||||||
className="text-xs sm:text-sm"
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
localConfig.valueFieldStorage?.targetColumn === column.columnName ? "opacity-100" : "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">{column.displayName || column.columnName}</span>
|
|
||||||
{column.displayName && <span className="text-[10px] text-gray-500">{column.columnName}</span>}
|
|
||||||
</div>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
값을 저장할 컬럼명
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 설명 박스 */}
|
|
||||||
<div className="p-3 bg-blue-50 dark:bg-blue-950 rounded border border-blue-200 dark:border-blue-800">
|
|
||||||
<p className="text-xs font-medium mb-2 text-blue-800 dark:text-blue-200">
|
|
||||||
저장 위치 동작
|
|
||||||
</p>
|
|
||||||
<div className="text-[10px] text-blue-700 dark:text-blue-300 space-y-1">
|
|
||||||
{localConfig.valueFieldStorage?.targetTable ? (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
선택한 값(<code className="font-mono bg-blue-100 dark:bg-blue-900 px-1 rounded">{localConfig.valueField}</code>)을
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<code className="font-mono bg-blue-100 dark:bg-blue-900 px-1 rounded">
|
|
||||||
{localConfig.valueFieldStorage.targetTable}
|
|
||||||
</code>{" "}
|
|
||||||
테이블의{" "}
|
|
||||||
<code className="font-mono bg-blue-100 dark:bg-blue-900 px-1 rounded">
|
|
||||||
{localConfig.valueFieldStorage.targetColumn || "(컬럼 미지정)"}
|
|
||||||
</code>{" "}
|
|
||||||
컬럼에 저장합니다.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>기본값: 화면의 연결 테이블에 컴포넌트의 바인딩 필드로 저장됩니다.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs sm:text-sm">검색 필드</Label>
|
<Label className="text-xs font-semibold sm:text-sm">4. 필드 매핑 *</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 className="space-y-4 border rounded-lg p-4 bg-card">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold mb-1">필드 자동 매핑</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
선택한 항목의 필드를 화면의 다른 입력 필드에 자동으로 채워넣습니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="text-xs sm:text-sm">필드 매핑 활성화</Label>
|
|
||||||
<Switch
|
|
||||||
checked={localConfig.enableFieldMapping || false}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
updateConfig({ enableFieldMapping: checked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
활성화하면 항목 선택 시 설정된 필드들이 자동으로 채워집니다
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{localConfig.enableFieldMapping && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="text-xs sm:text-sm">매핑 필드 목록</Label>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={addFieldMapping}
|
onClick={addFieldMapping}
|
||||||
className="h-7 text-xs"
|
className="h-7 text-xs"
|
||||||
disabled={!localConfig.tableName || isLoadingColumns}
|
disabled={!localConfig.tableName || !localConfig.targetTable || isLoadingSourceColumns || isLoadingTargetColumns}
|
||||||
>
|
>
|
||||||
<Plus className="h-3 w-3 mr-1" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
매핑 추가
|
매핑 추가
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(localConfig.fieldMappings || []).length === 0 && (
|
||||||
|
<div className="rounded-lg border border-dashed p-6 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
매핑 추가 버튼을 눌러 필드 매핑을 설정하세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{(localConfig.fieldMappings || []).map((mapping, index) => (
|
{(localConfig.fieldMappings || []).map((mapping, index) => (
|
||||||
<div key={index} className="border rounded-lg p-3 space-y-3 bg-background">
|
<div key={index} className="rounded-lg border bg-card p-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
매핑 #{index + 1}
|
매핑 #{index + 1}
|
||||||
|
|
@ -684,7 +310,6 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 표시명 */}
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs">표시명</Label>
|
<Label className="text-xs">표시명</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -692,110 +317,106 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateFieldMapping(index, { label: e.target.value })
|
updateFieldMapping(index, { label: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="예: 거래처명"
|
placeholder="예: 거래처 코드"
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
이 매핑의 설명 (선택사항)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 소스 필드 (테이블의 컬럼) */}
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs">
|
<Label className="text-xs">외부 테이블 컬럼 *</Label>
|
||||||
소스 필드 (테이블 컬럼) *
|
|
||||||
</Label>
|
|
||||||
<Select
|
<Select
|
||||||
value={mapping.sourceField}
|
value={mapping.sourceField}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateFieldMapping(index, { sourceField: value })
|
updateFieldMapping(index, { sourceField: value })
|
||||||
}
|
}
|
||||||
disabled={!localConfig.tableName || isLoadingColumns}
|
disabled={!localConfig.tableName || isLoadingSourceColumns}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
<SelectValue placeholder="컬럼 선택" />
|
<SelectValue placeholder="가져올 컬럼 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{tableColumns.map((col) => (
|
{sourceTableColumns.map((col) => (
|
||||||
<SelectItem key={col.columnName} value={col.columnName}>
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium">
|
|
||||||
{col.displayName || col.columnName}
|
{col.displayName || col.columnName}
|
||||||
</span>
|
|
||||||
{col.displayName && (
|
|
||||||
<span className="text-[10px] text-gray-500">
|
|
||||||
{col.columnName}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
|
||||||
가져올 데이터의 컬럼명
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 타겟 필드 (화면의 input ID) */}
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs">
|
<Label className="text-xs">저장 테이블 컬럼 *</Label>
|
||||||
타겟 필드 (화면 컴포넌트 ID) *
|
<Select
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
value={mapping.targetField}
|
value={mapping.targetField}
|
||||||
onChange={(e) =>
|
onValueChange={(value) =>
|
||||||
updateFieldMapping(index, { targetField: e.target.value })
|
updateFieldMapping(index, { targetField: value })
|
||||||
}
|
}
|
||||||
placeholder="예: customer_name_input"
|
disabled={!localConfig.targetTable || isLoadingTargetColumns}
|
||||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
>
|
||||||
/>
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<SelectValue placeholder="저장할 컬럼 선택" />
|
||||||
값을 채울 화면 컴포넌트의 ID (예: input의 id 속성)
|
</SelectTrigger>
|
||||||
</p>
|
<SelectContent>
|
||||||
|
{targetTableColumns.map((col) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
|
{col.displayName || col.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 예시 설명 */}
|
{mapping.sourceField && mapping.targetField && (
|
||||||
<div className="p-2 bg-blue-50 dark:bg-blue-950 rounded border border-blue-200 dark:border-blue-800">
|
<div className="rounded bg-blue-50 p-2 dark:bg-blue-950">
|
||||||
<p className="text-[10px] text-blue-700 dark:text-blue-300">
|
<p className="text-[10px] text-blue-700 dark:text-blue-300">
|
||||||
{mapping.sourceField && mapping.targetField ? (
|
<code className="rounded bg-blue-100 px-1 font-mono dark:bg-blue-900">
|
||||||
<>
|
{localConfig.tableName}.{mapping.sourceField}
|
||||||
<span className="font-semibold">{mapping.label || "이 필드"}</span>: 테이블의{" "}
|
</code>
|
||||||
<code className="font-mono bg-blue-100 dark:bg-blue-900 px-1 rounded">
|
{" → "}
|
||||||
{mapping.sourceField}
|
<code className="rounded bg-blue-100 px-1 font-mono dark:bg-blue-900">
|
||||||
</code>{" "}
|
{localConfig.targetTable}.{mapping.targetField}
|
||||||
값을 화면의{" "}
|
</code>
|
||||||
<code className="font-mono bg-blue-100 dark:bg-blue-900 px-1 rounded">
|
|
||||||
{mapping.targetField}
|
|
||||||
</code>{" "}
|
|
||||||
컴포넌트에 자동으로 채웁니다
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"소스 필드와 타겟 필드를 모두 선택하세요"
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 사용 안내 */}
|
{/* 플레이스홀더 */}
|
||||||
{localConfig.fieldMappings && localConfig.fieldMappings.length > 0 && (
|
<div className="space-y-2">
|
||||||
<div className="p-3 bg-amber-50 dark:bg-amber-950 rounded border border-amber-200 dark:border-amber-800">
|
<Label className="text-xs sm:text-sm">플레이스홀더</Label>
|
||||||
<p className="text-xs font-medium mb-2 text-amber-800 dark:text-amber-200">
|
<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.tableName && localConfig.targetTable && (localConfig.fieldMappings || []).length > 0 && (
|
||||||
|
<div className="rounded-lg border bg-green-50 p-4 dark:bg-green-950">
|
||||||
|
<h3 className="mb-2 text-sm font-semibold text-green-800 dark:text-green-200">
|
||||||
|
설정 요약
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-1 text-xs text-green-700 dark:text-green-300">
|
||||||
|
<p>
|
||||||
|
<strong>외부 테이블:</strong> {localConfig.tableName}
|
||||||
</p>
|
</p>
|
||||||
<ul className="text-[10px] text-amber-700 dark:text-amber-300 space-y-1 list-disc list-inside">
|
<p>
|
||||||
<li>화면에서 이 검색 컴포넌트로 항목을 선택하면</li>
|
<strong>표시 필드:</strong> {localConfig.displayField}
|
||||||
<li>설정된 매핑에 따라 다른 입력 필드들이 자동으로 채워집니다</li>
|
</p>
|
||||||
<li>타겟 필드 ID는 화면 디자이너에서 설정한 컴포넌트 ID와 일치해야 합니다</li>
|
<p>
|
||||||
</ul>
|
<strong>저장 테이블:</strong> {localConfig.targetTable}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>매핑 개수:</strong> {(localConfig.fieldMappings || []).length}개
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,5 +27,7 @@ export interface AutocompleteSearchInputConfig {
|
||||||
// 필드 자동 매핑 설정
|
// 필드 자동 매핑 설정
|
||||||
enableFieldMapping?: boolean; // 필드 자동 매핑 활성화 여부
|
enableFieldMapping?: boolean; // 필드 자동 매핑 활성화 여부
|
||||||
fieldMappings?: FieldMapping[]; // 매핑할 필드 목록
|
fieldMappings?: FieldMapping[]; // 매핑할 필드 목록
|
||||||
|
// 저장 대상 테이블 (간소화 버전)
|
||||||
|
targetTable?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue