Merge branch 'ksh'
This commit is contained in:
commit
30cece9bec
|
|
@ -0,0 +1,141 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { AutocompleteSearchInputComponent } from "@/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
export default function TestAutocompleteMapping() {
|
||||||
|
const [selectedValue, setSelectedValue] = useState("");
|
||||||
|
const [customerName, setCustomerName] = useState("");
|
||||||
|
const [address, setAddress] = useState("");
|
||||||
|
const [phone, setPhone] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>AutocompleteSearchInput 필드 자동 매핑 테스트</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
거래처를 선택하면 아래 입력 필드들이 자동으로 채워집니다
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* 검색 컴포넌트 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>거래처 검색</Label>
|
||||||
|
<AutocompleteSearchInputComponent
|
||||||
|
config={{
|
||||||
|
tableName: "customer_mng",
|
||||||
|
displayField: "customer_name",
|
||||||
|
valueField: "customer_code",
|
||||||
|
searchFields: ["customer_name", "customer_code"],
|
||||||
|
placeholder: "거래처명 또는 코드로 검색",
|
||||||
|
enableFieldMapping: true,
|
||||||
|
fieldMappings: [
|
||||||
|
{
|
||||||
|
sourceField: "customer_name",
|
||||||
|
targetField: "customer_name_input",
|
||||||
|
label: "거래처명",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourceField: "address",
|
||||||
|
targetField: "address_input",
|
||||||
|
label: "주소",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourceField: "phone",
|
||||||
|
targetField: "phone_input",
|
||||||
|
label: "전화번호",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
value={selectedValue}
|
||||||
|
onChange={(value, fullData) => {
|
||||||
|
setSelectedValue(value);
|
||||||
|
console.log("선택된 항목:", fullData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 구분선 */}
|
||||||
|
<div className="border-t pt-6">
|
||||||
|
<h3 className="text-sm font-semibold mb-4">
|
||||||
|
자동으로 채워지는 필드들
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 거래처명 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="customer_name_input">거래처명</Label>
|
||||||
|
<Input
|
||||||
|
id="customer_name_input"
|
||||||
|
value={customerName}
|
||||||
|
onChange={(e) => setCustomerName(e.target.value)}
|
||||||
|
placeholder="자동으로 채워집니다"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 주소 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="address_input">주소</Label>
|
||||||
|
<Input
|
||||||
|
id="address_input"
|
||||||
|
value={address}
|
||||||
|
onChange={(e) => setAddress(e.target.value)}
|
||||||
|
placeholder="자동으로 채워집니다"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 전화번호 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="phone_input">전화번호</Label>
|
||||||
|
<Input
|
||||||
|
id="phone_input"
|
||||||
|
value={phone}
|
||||||
|
onChange={(e) => setPhone(e.target.value)}
|
||||||
|
placeholder="자동으로 채워집니다"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 상태 표시 */}
|
||||||
|
<div className="border-t pt-6">
|
||||||
|
<h3 className="text-sm font-semibold mb-2">현재 상태</h3>
|
||||||
|
<div className="p-4 bg-muted rounded-lg">
|
||||||
|
<pre className="text-xs">
|
||||||
|
{JSON.stringify(
|
||||||
|
{
|
||||||
|
selectedValue,
|
||||||
|
customerName,
|
||||||
|
address,
|
||||||
|
phone,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 사용 안내 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">사용 방법</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm">
|
||||||
|
<ol className="list-decimal list-inside space-y-2">
|
||||||
|
<li>위의 검색 필드에 거래처명이나 코드를 입력하세요</li>
|
||||||
|
<li>드롭다운에서 원하는 거래처를 선택하세요</li>
|
||||||
|
<li>아래 입력 필드들이 자동으로 채워지는 것을 확인하세요</li>
|
||||||
|
<li>필요한 경우 자동으로 채워진 값을 수정할 수 있습니다</li>
|
||||||
|
</ol>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ 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 } from "./types";
|
import { AutocompleteSearchInputConfig, FieldMapping } from "./types";
|
||||||
|
|
||||||
interface AutocompleteSearchInputProps extends Partial<AutocompleteSearchInputConfig> {
|
interface AutocompleteSearchInputProps extends Partial<AutocompleteSearchInputConfig> {
|
||||||
config?: AutocompleteSearchInputConfig;
|
config?: AutocompleteSearchInputConfig;
|
||||||
|
|
@ -81,10 +81,46 @@ 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);
|
onChange?.(item[valueField], item);
|
||||||
|
|
||||||
|
// 필드 자동 매핑 실행
|
||||||
|
applyFieldMappings(item);
|
||||||
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||||
import { Switch } from "@/components/ui/switch";
|
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 } from "./types";
|
import { AutocompleteSearchInputConfig, FieldMapping, ValueFieldStorage } from "./types";
|
||||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -30,6 +30,10 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
const [openTableCombo, setOpenTableCombo] = useState(false);
|
const [openTableCombo, setOpenTableCombo] = useState(false);
|
||||||
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
||||||
const [openValueFieldCombo, setOpenValueFieldCombo] = 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(() => {
|
useEffect(() => {
|
||||||
|
|
@ -73,6 +77,31 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
loadColumns();
|
loadColumns();
|
||||||
}, [localConfig.tableName]);
|
}, [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);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
@ -117,6 +146,29 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
updateConfig({ additionalFields: fields });
|
updateConfig({ additionalFields: fields });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 필드 매핑 관리 함수
|
||||||
|
const addFieldMapping = () => {
|
||||||
|
const mappings = localConfig.fieldMappings || [];
|
||||||
|
updateConfig({
|
||||||
|
fieldMappings: [
|
||||||
|
...mappings,
|
||||||
|
{ sourceField: "", targetField: "", label: "" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFieldMapping = (index: number, updates: Partial<FieldMapping>) => {
|
||||||
|
const mappings = [...(localConfig.fieldMappings || [])];
|
||||||
|
mappings[index] = { ...mappings[index], ...updates };
|
||||||
|
updateConfig({ fieldMappings: mappings });
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFieldMapping = (index: number) => {
|
||||||
|
const mappings = [...(localConfig.fieldMappings || [])];
|
||||||
|
mappings.splice(index, 1);
|
||||||
|
updateConfig({ fieldMappings: mappings });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 p-4">
|
<div className="space-y-4 p-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -164,6 +216,9 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
검색할 데이터가 저장된 테이블을 선택하세요
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -211,6 +266,9 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
사용자에게 보여줄 필드 (예: 거래처명)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -258,6 +316,9 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
검색 테이블에서 가져올 값의 컬럼 (예: customer_code)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -270,6 +331,196 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
/>
|
/>
|
||||||
</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">
|
||||||
|
위에서 선택한 "값 필드"의 데이터를 어느 테이블/컬럼에 저장할지 지정합니다.
|
||||||
|
<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"
|
||||||
|
disabled={isLoadingTables}
|
||||||
|
>
|
||||||
|
{localConfig.valueFieldStorage?.targetTable
|
||||||
|
? allTables.find((t) => t.tableName === localConfig.valueFieldStorage?.targetTable)?.displayName ||
|
||||||
|
localConfig.valueFieldStorage.targetTable
|
||||||
|
: "기본값 (화면 연결 테이블)"}
|
||||||
|
<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>
|
||||||
|
{/* 기본값 옵션 */}
|
||||||
|
<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) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.tableName}
|
||||||
|
value={table.tableName}
|
||||||
|
onSelect={() => {
|
||||||
|
updateConfig({
|
||||||
|
valueFieldStorage: {
|
||||||
|
...localConfig.valueFieldStorage,
|
||||||
|
targetTable: table.tableName,
|
||||||
|
targetColumn: undefined, // 테이블 변경 시 컬럼 초기화
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setOpenStorageTableCombo(false);
|
||||||
|
}}
|
||||||
|
className="text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
localConfig.valueFieldStorage?.targetTable === table.tableName ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||||
|
{table.displayName && <span className="text-[10px] text-gray-500">{table.tableName}</span>}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
값을 저장할 테이블 (기본값: 화면 연결 테이블)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 저장 컬럼 선택 */}
|
||||||
|
{localConfig.valueFieldStorage?.targetTable && (
|
||||||
|
<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="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 sm:text-sm">검색 필드</Label>
|
||||||
|
|
@ -375,6 +626,175 @@ export function AutocompleteSearchInputConfigPanel({
|
||||||
</div>
|
</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
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={addFieldMapping}
|
||||||
|
className="h-7 text-xs"
|
||||||
|
disabled={!localConfig.tableName || isLoadingColumns}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3 mr-1" />
|
||||||
|
매핑 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{(localConfig.fieldMappings || []).map((mapping, index) => (
|
||||||
|
<div key={index} className="border rounded-lg p-3 space-y-3 bg-background">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
|
매핑 #{index + 1}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => removeFieldMapping(index)}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 표시명 */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">표시명</Label>
|
||||||
|
<Input
|
||||||
|
value={mapping.label || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateFieldMapping(index, { label: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="예: 거래처명"
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
이 매핑의 설명 (선택사항)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 소스 필드 (테이블의 컬럼) */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">
|
||||||
|
소스 필드 (테이블 컬럼) *
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={mapping.sourceField}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateFieldMapping(index, { sourceField: value })
|
||||||
|
}
|
||||||
|
disabled={!localConfig.tableName || isLoadingColumns}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
|
||||||
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{tableColumns.map((col) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">
|
||||||
|
{col.displayName || col.columnName}
|
||||||
|
</span>
|
||||||
|
{col.displayName && (
|
||||||
|
<span className="text-[10px] text-gray-500">
|
||||||
|
{col.columnName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
가져올 데이터의 컬럼명
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타겟 필드 (화면의 input ID) */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">
|
||||||
|
타겟 필드 (화면 컴포넌트 ID) *
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={mapping.targetField}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateFieldMapping(index, { targetField: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="예: customer_name_input"
|
||||||
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
값을 채울 화면 컴포넌트의 ID (예: input의 id 속성)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 예시 설명 */}
|
||||||
|
<div className="p-2 bg-blue-50 dark:bg-blue-950 rounded border border-blue-200 dark:border-blue-800">
|
||||||
|
<p className="text-[10px] text-blue-700 dark:text-blue-300">
|
||||||
|
{mapping.sourceField && mapping.targetField ? (
|
||||||
|
<>
|
||||||
|
<span className="font-semibold">{mapping.label || "이 필드"}</span>: 테이블의{" "}
|
||||||
|
<code className="font-mono bg-blue-100 dark:bg-blue-900 px-1 rounded">
|
||||||
|
{mapping.sourceField}
|
||||||
|
</code>{" "}
|
||||||
|
값을 화면의{" "}
|
||||||
|
<code className="font-mono bg-blue-100 dark:bg-blue-900 px-1 rounded">
|
||||||
|
{mapping.targetField}
|
||||||
|
</code>{" "}
|
||||||
|
컴포넌트에 자동으로 채웁니다
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"소스 필드와 타겟 필드를 모두 선택하세요"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 사용 안내 */}
|
||||||
|
{localConfig.fieldMappings && localConfig.fieldMappings.length > 0 && (
|
||||||
|
<div className="p-3 bg-amber-50 dark:bg-amber-950 rounded border border-amber-200 dark:border-amber-800">
|
||||||
|
<p className="text-xs font-medium mb-2 text-amber-800 dark:text-amber-200">
|
||||||
|
사용 방법
|
||||||
|
</p>
|
||||||
|
<ul className="text-[10px] text-amber-700 dark:text-amber-300 space-y-1 list-disc list-inside">
|
||||||
|
<li>화면에서 이 검색 컴포넌트로 항목을 선택하면</li>
|
||||||
|
<li>설정된 매핑에 따라 다른 입력 필드들이 자동으로 채워집니다</li>
|
||||||
|
<li>타겟 필드 ID는 화면 디자이너에서 설정한 컴포넌트 ID와 일치해야 합니다</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@
|
||||||
- 추가 정보 표시 가능
|
- 추가 정보 표시 가능
|
||||||
- X 버튼으로 선택 초기화
|
- X 버튼으로 선택 초기화
|
||||||
- 외부 클릭 시 자동 닫힘
|
- 외부 클릭 시 자동 닫힘
|
||||||
|
- **필드 자동 매핑**: 선택한 항목의 값을 화면의 다른 입력 필드에 자동으로 채움
|
||||||
|
|
||||||
## 사용 예시
|
## 사용 예시
|
||||||
|
|
||||||
|
### 기본 사용
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<AutocompleteSearchInputComponent
|
<AutocompleteSearchInputComponent
|
||||||
tableName="customer_mng"
|
tableName="customer_mng"
|
||||||
|
|
@ -28,8 +31,51 @@
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 필드 자동 매핑 사용
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<AutocompleteSearchInputComponent
|
||||||
|
config={{
|
||||||
|
tableName: "customer_mng",
|
||||||
|
displayField: "customer_name",
|
||||||
|
valueField: "customer_code",
|
||||||
|
searchFields: ["customer_name", "customer_code"],
|
||||||
|
placeholder: "거래처 검색",
|
||||||
|
enableFieldMapping: true,
|
||||||
|
fieldMappings: [
|
||||||
|
{
|
||||||
|
sourceField: "customer_name", // 테이블의 컬럼명
|
||||||
|
targetField: "customer_name_input", // 화면 input의 id
|
||||||
|
label: "거래처명"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourceField: "address",
|
||||||
|
targetField: "address_input",
|
||||||
|
label: "주소"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourceField: "phone",
|
||||||
|
targetField: "phone_input",
|
||||||
|
label: "전화번호"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
onChange={(code, fullData) => {
|
||||||
|
console.log("선택됨:", code, fullData);
|
||||||
|
// 필드 매핑은 자동으로 실행됩니다
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 화면의 다른 곳에 있는 입력 필드들 -->
|
||||||
|
<input id="customer_name_input" placeholder="거래처명" />
|
||||||
|
<input id="address_input" placeholder="주소" />
|
||||||
|
<input id="phone_input" placeholder="전화번호" />
|
||||||
|
```
|
||||||
|
|
||||||
## 설정 옵션
|
## 설정 옵션
|
||||||
|
|
||||||
|
### 기본 설정
|
||||||
|
|
||||||
- `tableName`: 검색할 테이블명
|
- `tableName`: 검색할 테이블명
|
||||||
- `displayField`: 표시할 필드
|
- `displayField`: 표시할 필드
|
||||||
- `valueField`: 값으로 사용할 필드
|
- `valueField`: 값으로 사용할 필드
|
||||||
|
|
@ -38,3 +84,31 @@
|
||||||
- `showAdditionalInfo`: 추가 정보 표시 여부
|
- `showAdditionalInfo`: 추가 정보 표시 여부
|
||||||
- `additionalFields`: 추가로 표시할 필드들
|
- `additionalFields`: 추가로 표시할 필드들
|
||||||
|
|
||||||
|
### 값 필드 저장 위치 설정 (고급)
|
||||||
|
|
||||||
|
- `valueFieldStorage`: 값 필드 저장 위치 지정
|
||||||
|
- `targetTable`: 저장할 테이블명 (미설정 시 화면 연결 테이블)
|
||||||
|
- `targetColumn`: 저장할 컬럼명 (미설정 시 바인딩 필드)
|
||||||
|
|
||||||
|
### 필드 자동 매핑 설정
|
||||||
|
|
||||||
|
- `enableFieldMapping`: 필드 자동 매핑 활성화 여부
|
||||||
|
- `fieldMappings`: 매핑할 필드 목록
|
||||||
|
- `sourceField`: 소스 테이블의 컬럼명 (예: customer_name)
|
||||||
|
- `targetField`: 타겟 필드 ID (예: 화면의 input id 속성)
|
||||||
|
- `label`: 표시명 (선택사항)
|
||||||
|
|
||||||
|
## 필드 자동 매핑 동작 방식
|
||||||
|
|
||||||
|
1. 사용자가 검색 컴포넌트에서 항목을 선택합니다
|
||||||
|
2. 선택된 항목의 데이터에서 `sourceField`에 해당하는 값을 가져옵니다
|
||||||
|
3. 화면에서 `targetField` ID를 가진 컴포넌트를 찾습니다
|
||||||
|
4. 해당 컴포넌트에 값을 자동으로 채워넣습니다
|
||||||
|
5. React의 change 이벤트를 트리거하여 상태 업데이트를 유발합니다
|
||||||
|
|
||||||
|
## 주의사항
|
||||||
|
|
||||||
|
- 타겟 필드 ID는 화면 디자이너에서 설정한 컴포넌트 ID와 정확히 일치해야 합니다
|
||||||
|
- 필드 매핑은 input, textarea 타입의 요소에만 동작합니다
|
||||||
|
- 여러 필드를 한 번에 매핑할 수 있습니다
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
// 값 필드 저장 설정
|
||||||
|
export interface ValueFieldStorage {
|
||||||
|
targetTable?: string; // 저장할 테이블명 (기본값: 화면의 연결 테이블)
|
||||||
|
targetColumn?: string; // 저장할 컬럼명 (기본값: 바인딩 필드)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 필드 매핑 설정
|
||||||
|
export interface FieldMapping {
|
||||||
|
sourceField: string; // 소스 테이블의 컬럼명 (예: customer_name)
|
||||||
|
targetField: string; // 매핑될 타겟 필드 ID (예: 화면의 input ID)
|
||||||
|
label?: string; // 표시명
|
||||||
|
targetTable?: string; // 저장할 테이블 (선택사항, 기본값은 화면 연결 테이블)
|
||||||
|
targetColumn?: string; // 저장할 컬럼명 (선택사항, targetField가 화면 ID가 아닌 경우)
|
||||||
|
}
|
||||||
|
|
||||||
export interface AutocompleteSearchInputConfig {
|
export interface AutocompleteSearchInputConfig {
|
||||||
tableName: string;
|
tableName: string;
|
||||||
displayField: string;
|
displayField: string;
|
||||||
|
|
@ -7,5 +22,10 @@ export interface AutocompleteSearchInputConfig {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
showAdditionalInfo?: boolean;
|
showAdditionalInfo?: boolean;
|
||||||
additionalFields?: string[];
|
additionalFields?: string[];
|
||||||
|
// 값 필드 저장 위치 설정
|
||||||
|
valueFieldStorage?: ValueFieldStorage;
|
||||||
|
// 필드 자동 매핑 설정
|
||||||
|
enableFieldMapping?: boolean; // 필드 자동 매핑 활성화 여부
|
||||||
|
fieldMappings?: FieldMapping[]; // 매핑할 필드 목록
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
# AutocompleteSearchInput 컴포넌트 사용 가이드
|
||||||
|
|
||||||
|
## 📌 이 컴포넌트는 무엇인가요?
|
||||||
|
|
||||||
|
검색 가능한 드롭다운 선택 박스입니다.
|
||||||
|
거래처, 품목, 직원 등을 검색해서 선택할 때 사용합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ 패널 설정 방법
|
||||||
|
|
||||||
|
### 1. 기본 검색 설정 (필수)
|
||||||
|
|
||||||
|
#### 테이블명
|
||||||
|
- **의미**: 어디서 검색할 것인가?
|
||||||
|
- **예시**: `customer_mng` (거래처 테이블)
|
||||||
|
|
||||||
|
#### 표시 필드
|
||||||
|
- **의미**: 사용자에게 무엇을 보여줄 것인가?
|
||||||
|
- **예시**: `customer_name` → 화면에 "삼성전자" 표시
|
||||||
|
|
||||||
|
#### 값 필드
|
||||||
|
- **의미**: 실제로 어떤 값을 가져올 것인가?
|
||||||
|
- **예시**: `customer_code` → "CUST-0001" 가져오기
|
||||||
|
|
||||||
|
#### 검색 필드 (선택)
|
||||||
|
- **의미**: 어떤 컬럼으로 검색할 것인가?
|
||||||
|
- **예시**: `customer_name`, `customer_code` 추가
|
||||||
|
- **동작**: 이름으로도 검색, 코드로도 검색 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 값 필드 저장 위치 (고급, 선택)
|
||||||
|
|
||||||
|
#### 저장 테이블
|
||||||
|
- **기본값**: 화면의 연결 테이블에 자동 저장
|
||||||
|
- **변경 시**: 다른 테이블에 저장 가능
|
||||||
|
|
||||||
|
#### 저장 컬럼
|
||||||
|
- **기본값**: 컴포넌트의 바인딩 필드
|
||||||
|
- **변경 시**: 다른 컬럼에 저장 가능
|
||||||
|
|
||||||
|
> 💡 **대부분은 기본값을 사용하면 됩니다!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 사용 예제
|
||||||
|
|
||||||
|
### 예제 1: 거래처 선택 (가장 일반적)
|
||||||
|
|
||||||
|
#### 패널 설정
|
||||||
|
```
|
||||||
|
테이블명: customer_mng
|
||||||
|
표시 필드: customer_name
|
||||||
|
값 필드: customer_code
|
||||||
|
검색 필드: customer_name, customer_code
|
||||||
|
플레이스홀더: 거래처명 또는 코드 입력
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 동작
|
||||||
|
```
|
||||||
|
사용자 입력: "삼성"
|
||||||
|
드롭다운 표시: "삼성전자", "삼성물산", ...
|
||||||
|
선택: "삼성전자"
|
||||||
|
저장 값: "CUST-0001" (customer_code)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 결과
|
||||||
|
```
|
||||||
|
order_mng 테이블
|
||||||
|
┌───────────┬───────────────┐
|
||||||
|
│ order_id │ customer_code │
|
||||||
|
├───────────┼───────────────┤
|
||||||
|
│ ORD-0001 │ CUST-0001 │ ✅
|
||||||
|
└───────────┴───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 예제 2: 거래처명을 직접 저장
|
||||||
|
|
||||||
|
#### 패널 설정
|
||||||
|
```
|
||||||
|
테이블명: customer_mng
|
||||||
|
표시 필드: customer_name
|
||||||
|
값 필드: customer_name ← 이름을 가져옴
|
||||||
|
플레이스홀더: 거래처명 입력
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 동작
|
||||||
|
```
|
||||||
|
사용자 선택: "삼성전자"
|
||||||
|
저장 값: "삼성전자" (customer_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 결과
|
||||||
|
```
|
||||||
|
order_mng 테이블
|
||||||
|
┌───────────┬───────────────┐
|
||||||
|
│ order_id │ customer_name │
|
||||||
|
├───────────┼───────────────┤
|
||||||
|
│ ORD-0001 │ 삼성전자 │ ✅
|
||||||
|
└───────────┴───────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 예제 3: 품목 선택 (추가 정보 표시)
|
||||||
|
|
||||||
|
#### 패널 설정
|
||||||
|
```
|
||||||
|
테이블명: item_mng
|
||||||
|
표시 필드: item_name
|
||||||
|
값 필드: item_code
|
||||||
|
검색 필드: item_name, item_code, category
|
||||||
|
플레이스홀더: 품목명, 코드, 카테고리로 검색
|
||||||
|
|
||||||
|
추가 정보 표시: ON
|
||||||
|
추가 필드: item_code, unit_price
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 동작
|
||||||
|
```
|
||||||
|
드롭다운:
|
||||||
|
┌────────────────────────────┐
|
||||||
|
│ 삼성 노트북 │
|
||||||
|
│ item_code: ITEM-0123 │
|
||||||
|
│ unit_price: 1,500,000 │
|
||||||
|
├────────────────────────────┤
|
||||||
|
│ LG 그램 노트북 │
|
||||||
|
│ item_code: ITEM-0124 │
|
||||||
|
│ unit_price: 1,800,000 │
|
||||||
|
└────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 필드 선택 가이드
|
||||||
|
|
||||||
|
### 언제 표시 필드 ≠ 값 필드 인가?
|
||||||
|
|
||||||
|
**대부분의 경우 (권장)**
|
||||||
|
```
|
||||||
|
표시 필드: customer_name (이름 - 사람이 읽기 쉬움)
|
||||||
|
값 필드: customer_code (코드 - 데이터베이스에 저장)
|
||||||
|
|
||||||
|
이유:
|
||||||
|
✅ 외래키 관계 유지
|
||||||
|
✅ 데이터 무결성
|
||||||
|
✅ 이름이 바뀌어도 코드는 그대로
|
||||||
|
```
|
||||||
|
|
||||||
|
### 언제 표시 필드 = 값 필드 인가?
|
||||||
|
|
||||||
|
**특수한 경우**
|
||||||
|
```
|
||||||
|
표시 필드: customer_name
|
||||||
|
값 필드: customer_name
|
||||||
|
|
||||||
|
사용 케이스:
|
||||||
|
- 이름 자체를 저장해야 할 때
|
||||||
|
- 외래키가 필요 없을 때
|
||||||
|
- 간단한 참조용 데이터
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 자주 묻는 질문
|
||||||
|
|
||||||
|
### Q1. 저장 위치를 설정하지 않으면?
|
||||||
|
|
||||||
|
**A**: 자동으로 화면의 연결 테이블에 바인딩 필드로 저장됩니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
화면: 수주 등록 (연결 테이블: order_mng)
|
||||||
|
컴포넌트 바인딩 필드: customer_code
|
||||||
|
|
||||||
|
→ order_mng.customer_code에 자동 저장 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q2. 값 필드와 저장 위치의 차이는?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
- **값 필드**: 검색 테이블에서 무엇을 가져올지
|
||||||
|
- **저장 위치**: 가져온 값을 어디에 저장할지
|
||||||
|
|
||||||
|
```
|
||||||
|
값 필드: customer_mng.customer_code (어떤 값?)
|
||||||
|
저장 위치: order_mng.customer_code (어디에?)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q3. 검색 필드는 왜 여러 개 추가하나요?
|
||||||
|
|
||||||
|
**A**: 여러 방법으로 검색할 수 있게 하기 위해서입니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
검색 필드: [customer_name, customer_code]
|
||||||
|
|
||||||
|
사용자가 "삼성" 입력 → customer_name에서 검색
|
||||||
|
사용자가 "CUST" 입력 → customer_code에서 검색
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Q4. 추가 정보 표시는 언제 사용하나요?
|
||||||
|
|
||||||
|
**A**: 선택할 때 참고할 정보를 함께 보여주고 싶을 때 사용합니다.
|
||||||
|
|
||||||
|
```
|
||||||
|
추가 정보 표시: ON
|
||||||
|
추가 필드: [address, phone]
|
||||||
|
|
||||||
|
드롭다운:
|
||||||
|
┌────────────────────────────┐
|
||||||
|
│ 삼성전자 │
|
||||||
|
│ address: 서울시 서초구 │
|
||||||
|
│ phone: 02-1234-5678 │
|
||||||
|
└────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 빠른 시작
|
||||||
|
|
||||||
|
### 1단계: 기본 설정만 입력
|
||||||
|
|
||||||
|
```
|
||||||
|
테이블명: [검색할 테이블]
|
||||||
|
표시 필드: [사용자에게 보여줄 컬럼]
|
||||||
|
값 필드: [저장할 컬럼]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2단계: 화면 디자이너에서 바인딩 필드 설정
|
||||||
|
|
||||||
|
```
|
||||||
|
컴포넌트 ID: customer_search
|
||||||
|
바인딩 필드: customer_code
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3단계: 완료!
|
||||||
|
|
||||||
|
이제 사용자가 선택하면 자동으로 저장됩니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 체크리스트
|
||||||
|
|
||||||
|
설정 전:
|
||||||
|
- [ ] 어느 테이블에서 검색할지 알고 있나요?
|
||||||
|
- [ ] 사용자에게 무엇을 보여줄지 정했나요?
|
||||||
|
- [ ] 어떤 값을 저장할지 정했나요?
|
||||||
|
|
||||||
|
설정 후:
|
||||||
|
- [ ] 검색이 정상적으로 되나요?
|
||||||
|
- [ ] 드롭다운에 원하는 항목이 보이나요?
|
||||||
|
- [ ] 선택 후 값이 저장되나요?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 설정 패턴 비교
|
||||||
|
|
||||||
|
| 패턴 | 표시 필드 | 값 필드 | 사용 케이스 |
|
||||||
|
|------|----------|---------|------------|
|
||||||
|
| 1 | customer_name | customer_code | 이름 표시, 코드 저장 (일반적) |
|
||||||
|
| 2 | customer_name | customer_name | 이름 표시, 이름 저장 (특수) |
|
||||||
|
| 3 | item_name | item_code | 품목명 표시, 품목코드 저장 |
|
||||||
|
| 4 | employee_name | employee_id | 직원명 표시, ID 저장 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 실전 팁
|
||||||
|
|
||||||
|
### 1. 검색 필드는 2-3개가 적당
|
||||||
|
```
|
||||||
|
✅ 좋음: [name, code]
|
||||||
|
✅ 좋음: [name, code, category]
|
||||||
|
❌ 과함: [name, code, address, phone, email, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 플레이스홀더는 구체적으로
|
||||||
|
```
|
||||||
|
❌ "검색..."
|
||||||
|
✅ "거래처명 또는 코드 입력"
|
||||||
|
✅ "품목명, 코드, 카테고리로 검색"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 추가 정보는 선택에 도움되는 것만
|
||||||
|
```
|
||||||
|
✅ 도움됨: 가격, 주소, 전화번호
|
||||||
|
❌ 불필요: 등록일, 수정일, ID
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
이 가이드로 autocomplete-search-input 컴포넌트를 쉽게 사용할 수 있습니다! 🎉
|
||||||
|
|
||||||
|
|
@ -5,14 +5,122 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { ItemSelectionModal } from "./ItemSelectionModal";
|
import { ItemSelectionModal } from "./ItemSelectionModal";
|
||||||
import { RepeaterTable } from "./RepeaterTable";
|
import { RepeaterTable } from "./RepeaterTable";
|
||||||
import { ModalRepeaterTableProps, RepeaterColumnConfig } from "./types";
|
import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./types";
|
||||||
import { useCalculation } from "./useCalculation";
|
import { useCalculation } from "./useCalculation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { apiClient } from "@/lib/api/client";
|
||||||
|
|
||||||
interface ModalRepeaterTableComponentProps extends Partial<ModalRepeaterTableProps> {
|
interface ModalRepeaterTableComponentProps extends Partial<ModalRepeaterTableProps> {
|
||||||
config?: ModalRepeaterTableProps;
|
config?: ModalRepeaterTableProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 외부 테이블에서 참조 값을 조회하는 함수
|
||||||
|
* @param referenceTable 참조 테이블명 (예: "customer_item_mapping")
|
||||||
|
* @param referenceField 참조할 컬럼명 (예: "basic_price")
|
||||||
|
* @param joinConditions 조인 조건 배열
|
||||||
|
* @param sourceItem 소스 데이터 (모달에서 선택한 항목)
|
||||||
|
* @param currentItem 현재 빌드 중인 항목 (이미 설정된 필드들)
|
||||||
|
* @returns 참조된 값 또는 undefined
|
||||||
|
*/
|
||||||
|
async function fetchReferenceValue(
|
||||||
|
referenceTable: string,
|
||||||
|
referenceField: string,
|
||||||
|
joinConditions: JoinCondition[],
|
||||||
|
sourceItem: any,
|
||||||
|
currentItem: any
|
||||||
|
): Promise<any> {
|
||||||
|
if (joinConditions.length === 0) {
|
||||||
|
console.warn("⚠️ 조인 조건이 없습니다. 참조 조회를 건너뜁니다.");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 조인 조건을 WHERE 절로 변환
|
||||||
|
const whereConditions: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const condition of joinConditions) {
|
||||||
|
const { sourceTable = "target", sourceField, targetField, operator = "=" } = condition;
|
||||||
|
|
||||||
|
// 소스 테이블에 따라 값을 가져오기
|
||||||
|
let value: any;
|
||||||
|
if (sourceTable === "source") {
|
||||||
|
// 소스 테이블 (item_info 등): 모달에서 선택한 원본 데이터
|
||||||
|
value = sourceItem[sourceField];
|
||||||
|
console.log(` 📘 소스 테이블에서 값 가져오기: ${sourceField} =`, value);
|
||||||
|
} else {
|
||||||
|
// 저장 테이블 (sales_order_mng 등): 반복 테이블에 이미 복사된 값
|
||||||
|
value = currentItem[sourceField];
|
||||||
|
console.log(` 📗 저장 테이블(반복테이블)에서 값 가져오기: ${sourceField} =`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
console.warn(`⚠️ 조인 조건의 소스 필드 "${sourceField}" 값이 없습니다. (sourceTable: ${sourceTable})`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 연산자가 "=" 인 경우만 지원 (확장 가능)
|
||||||
|
if (operator === "=") {
|
||||||
|
whereConditions[targetField] = value;
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ 연산자 "${operator}"는 아직 지원되지 않습니다.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔍 참조 조회 API 호출:`, {
|
||||||
|
table: referenceTable,
|
||||||
|
field: referenceField,
|
||||||
|
where: whereConditions,
|
||||||
|
});
|
||||||
|
|
||||||
|
// API 호출: 테이블 데이터 조회 (POST 방식)
|
||||||
|
const requestBody = {
|
||||||
|
search: whereConditions, // ✅ filters → search 변경 (백엔드 파라미터명)
|
||||||
|
size: 1, // 첫 번째 결과만 가져오기
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("📤 API 요청 Body:", JSON.stringify(requestBody, null, 2));
|
||||||
|
|
||||||
|
const response = await apiClient.post(
|
||||||
|
`/table-management/tables/${referenceTable}/data`,
|
||||||
|
requestBody
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("📥 API 전체 응답:", {
|
||||||
|
success: response.data.success,
|
||||||
|
dataLength: response.data.data?.data?.length, // ✅ data.data.data 구조
|
||||||
|
total: response.data.data?.total, // ✅ data.data.total
|
||||||
|
firstRow: response.data.data?.data?.[0], // ✅ data.data.data[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data?.data?.length > 0) {
|
||||||
|
const firstRow = response.data.data.data[0]; // ✅ data.data.data[0]
|
||||||
|
const value = firstRow[referenceField];
|
||||||
|
|
||||||
|
console.log(`✅ 참조 조회 성공:`, {
|
||||||
|
table: referenceTable,
|
||||||
|
field: referenceField,
|
||||||
|
value,
|
||||||
|
fullRow: firstRow,
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ 참조 조회 결과 없음:`, {
|
||||||
|
table: referenceTable,
|
||||||
|
where: whereConditions,
|
||||||
|
responseData: response.data.data,
|
||||||
|
total: response.data.total,
|
||||||
|
});
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 참조 조회 API 오류:`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ModalRepeaterTableComponent({
|
export function ModalRepeaterTableComponent({
|
||||||
config,
|
config,
|
||||||
sourceTable: propSourceTable,
|
sourceTable: propSourceTable,
|
||||||
|
|
@ -126,15 +234,31 @@ export function ModalRepeaterTableComponent({
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleAddItems = (items: any[]) => {
|
const handleAddItems = async (items: any[]) => {
|
||||||
console.log("➕ handleAddItems 호출:", items.length, "개 항목");
|
console.log("➕ handleAddItems 호출:", items.length, "개 항목");
|
||||||
console.log("📋 소스 데이터:", items);
|
console.log("📋 소스 데이터:", items);
|
||||||
|
|
||||||
// 매핑 규칙에 따라 데이터 변환
|
// 매핑 규칙에 따라 데이터 변환 (비동기 처리)
|
||||||
const mappedItems = items.map((sourceItem) => {
|
const mappedItems = await Promise.all(items.map(async (sourceItem) => {
|
||||||
const newItem: any = {};
|
const newItem: any = {};
|
||||||
|
|
||||||
columns.forEach((col) => {
|
// ⚠️ 중요: reference 매핑은 다른 컬럼에 의존할 수 있으므로
|
||||||
|
// 1단계: source/manual 매핑을 먼저 처리
|
||||||
|
// 2단계: reference 매핑을 나중에 처리
|
||||||
|
|
||||||
|
const referenceColumns: typeof columns = [];
|
||||||
|
const otherColumns: typeof columns = [];
|
||||||
|
|
||||||
|
for (const col of columns) {
|
||||||
|
if (col.mapping?.type === "reference") {
|
||||||
|
referenceColumns.push(col);
|
||||||
|
} else {
|
||||||
|
otherColumns.push(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1단계: source/manual 컬럼 먼저 처리
|
||||||
|
for (const col of otherColumns) {
|
||||||
console.log(`🔄 컬럼 "${col.field}" 매핑 처리:`, col.mapping);
|
console.log(`🔄 컬럼 "${col.field}" 매핑 처리:`, col.mapping);
|
||||||
|
|
||||||
// 1. 매핑 규칙이 있는 경우
|
// 1. 매핑 규칙이 있는 경우
|
||||||
|
|
@ -148,11 +272,6 @@ export function ModalRepeaterTableComponent({
|
||||||
} else {
|
} else {
|
||||||
console.warn(` ⚠️ 소스 필드 "${sourceField}" 값이 없음`);
|
console.warn(` ⚠️ 소스 필드 "${sourceField}" 값이 없음`);
|
||||||
}
|
}
|
||||||
} else if (col.mapping.type === "reference") {
|
|
||||||
// 외부 테이블 참조 (TODO: API 호출 필요)
|
|
||||||
console.log(` ⏳ 참조 조회 필요: ${col.mapping.referenceTable}.${col.mapping.referenceField}`);
|
|
||||||
// 현재는 빈 값으로 설정 (나중에 API 호출로 구현)
|
|
||||||
newItem[col.field] = undefined;
|
|
||||||
} else if (col.mapping.type === "manual") {
|
} else if (col.mapping.type === "manual") {
|
||||||
// 사용자 입력 (빈 값)
|
// 사용자 입력 (빈 값)
|
||||||
newItem[col.field] = undefined;
|
newItem[col.field] = undefined;
|
||||||
|
|
@ -170,11 +289,47 @@ export function ModalRepeaterTableComponent({
|
||||||
newItem[col.field] = col.defaultValue;
|
newItem[col.field] = col.defaultValue;
|
||||||
console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue);
|
console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// 2단계: reference 컬럼 처리 (다른 컬럼들이 모두 설정된 후)
|
||||||
|
console.log("🔗 2단계: reference 컬럼 처리 시작");
|
||||||
|
for (const col of referenceColumns) {
|
||||||
|
console.log(`🔄 컬럼 "${col.field}" 참조 매핑 처리:`, col.mapping);
|
||||||
|
|
||||||
|
// 외부 테이블 참조 (API 호출)
|
||||||
|
console.log(` ⏳ 참조 조회 시작: ${col.mapping?.referenceTable}.${col.mapping?.referenceField}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const referenceValue = await fetchReferenceValue(
|
||||||
|
col.mapping!.referenceTable!,
|
||||||
|
col.mapping!.referenceField!,
|
||||||
|
col.mapping!.joinCondition || [],
|
||||||
|
sourceItem,
|
||||||
|
newItem
|
||||||
|
);
|
||||||
|
|
||||||
|
if (referenceValue !== null && referenceValue !== undefined) {
|
||||||
|
newItem[col.field] = referenceValue;
|
||||||
|
console.log(` ✅ 참조 조회 성공: ${col.field}:`, referenceValue);
|
||||||
|
} else {
|
||||||
|
newItem[col.field] = undefined;
|
||||||
|
console.warn(` ⚠️ 참조 조회 결과 없음`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(` ❌ 참조 조회 오류:`, error);
|
||||||
|
newItem[col.field] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본값 적용
|
||||||
|
if (col.defaultValue !== undefined && newItem[col.field] === undefined) {
|
||||||
|
newItem[col.field] = col.defaultValue;
|
||||||
|
console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("📦 변환된 항목:", newItem);
|
console.log("📦 변환된 항목:", newItem);
|
||||||
return newItem;
|
return newItem;
|
||||||
});
|
}));
|
||||||
|
|
||||||
// 계산 필드 업데이트
|
// 계산 필드 업데이트
|
||||||
const calculatedItems = calculateAll(mappedItems);
|
const calculatedItems = calculateAll(mappedItems);
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,8 @@ export function ModalRepeaterTableConfigPanel({
|
||||||
// 계산 규칙이 없으면 모든 컬럼의 calculated 속성 제거
|
// 계산 규칙이 없으면 모든 컬럼의 calculated 속성 제거
|
||||||
if (!initialConfig.calculationRules || initialConfig.calculationRules.length === 0) {
|
if (!initialConfig.calculationRules || initialConfig.calculationRules.length === 0) {
|
||||||
const cleanedColumns = (initialConfig.columns || []).map((col) => {
|
const cleanedColumns = (initialConfig.columns || []).map((col) => {
|
||||||
const { calculated: _calc, ...rest } = col;
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { calculated, ...rest } = col;
|
||||||
return { ...rest, editable: true };
|
return { ...rest, editable: true };
|
||||||
});
|
});
|
||||||
return { ...initialConfig, columns: cleanedColumns };
|
return { ...initialConfig, columns: cleanedColumns };
|
||||||
|
|
@ -145,7 +146,8 @@ export function ModalRepeaterTableConfigPanel({
|
||||||
return { ...col, calculated: true, editable: false };
|
return { ...col, calculated: true, editable: false };
|
||||||
} else {
|
} else {
|
||||||
// 나머지 필드는 calculated 제거, editable=true
|
// 나머지 필드는 calculated 제거, editable=true
|
||||||
const { calculated: _calc, ...rest } = col;
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { calculated, ...rest } = col;
|
||||||
return { ...rest, editable: true };
|
return { ...rest, editable: true };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -281,12 +283,12 @@ export function ModalRepeaterTableConfigPanel({
|
||||||
const columns = localConfig.columns || [];
|
const columns = localConfig.columns || [];
|
||||||
|
|
||||||
// 이미 존재하는 컬럼인지 확인
|
// 이미 존재하는 컬럼인지 확인
|
||||||
if (columns.some(col => col.field === columnName)) {
|
if (columns.some((col) => col.field === columnName)) {
|
||||||
alert("이미 추가된 컬럼입니다.");
|
alert("이미 추가된 컬럼입니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetCol = targetTableColumns.find(c => c.columnName === columnName);
|
const targetCol = targetTableColumns.find((c) => c.columnName === columnName);
|
||||||
|
|
||||||
const newColumn: RepeaterColumnConfig = {
|
const newColumn: RepeaterColumnConfig = {
|
||||||
field: columnName,
|
field: columnName,
|
||||||
|
|
@ -340,16 +342,17 @@ export function ModalRepeaterTableConfigPanel({
|
||||||
|
|
||||||
// 이전 결과 필드의 calculated 속성 제거
|
// 이전 결과 필드의 calculated 속성 제거
|
||||||
if (oldRule.result) {
|
if (oldRule.result) {
|
||||||
const oldResultIndex = columns.findIndex(c => c.field === oldRule.result);
|
const oldResultIndex = columns.findIndex((c) => c.field === oldRule.result);
|
||||||
if (oldResultIndex !== -1) {
|
if (oldResultIndex !== -1) {
|
||||||
const { calculated: _calc, ...rest } = columns[oldResultIndex];
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { calculated, ...rest } = columns[oldResultIndex];
|
||||||
columns[oldResultIndex] = { ...rest, editable: true };
|
columns[oldResultIndex] = { ...rest, editable: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 새 결과 필드를 calculated=true, editable=false로 설정
|
// 새 결과 필드를 calculated=true, editable=false로 설정
|
||||||
if (updates.result) {
|
if (updates.result) {
|
||||||
const newResultIndex = columns.findIndex(c => c.field === updates.result);
|
const newResultIndex = columns.findIndex((c) => c.field === updates.result);
|
||||||
if (newResultIndex !== -1) {
|
if (newResultIndex !== -1) {
|
||||||
columns[newResultIndex] = {
|
columns[newResultIndex] = {
|
||||||
...columns[newResultIndex],
|
...columns[newResultIndex],
|
||||||
|
|
@ -375,9 +378,10 @@ export function ModalRepeaterTableConfigPanel({
|
||||||
// 결과 필드의 calculated 속성 제거
|
// 결과 필드의 calculated 속성 제거
|
||||||
if (removedRule.result) {
|
if (removedRule.result) {
|
||||||
const columns = [...(localConfig.columns || [])];
|
const columns = [...(localConfig.columns || [])];
|
||||||
const resultIndex = columns.findIndex(c => c.field === removedRule.result);
|
const resultIndex = columns.findIndex((c) => c.field === removedRule.result);
|
||||||
if (resultIndex !== -1) {
|
if (resultIndex !== -1) {
|
||||||
const { calculated: _calc, ...rest } = columns[resultIndex];
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { calculated, ...rest } = columns[resultIndex];
|
||||||
columns[resultIndex] = { ...rest, editable: true };
|
columns[resultIndex] = { ...rest, editable: true };
|
||||||
}
|
}
|
||||||
rules.splice(index, 1);
|
rules.splice(index, 1);
|
||||||
|
|
@ -978,6 +982,296 @@ export function ModalRepeaterTableConfigPanel({
|
||||||
가져올 컬럼명
|
가져올 컬럼명
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 조인 조건 설정 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-[11px]">조인 조건 *</Label>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
const currentConditions = col.mapping?.joinCondition || [];
|
||||||
|
updateRepeaterColumn(index, {
|
||||||
|
mapping: {
|
||||||
|
...col.mapping,
|
||||||
|
type: "reference",
|
||||||
|
joinCondition: [
|
||||||
|
...currentConditions,
|
||||||
|
{ sourceField: "", targetField: "", operator: "=" }
|
||||||
|
]
|
||||||
|
} as ColumnMapping
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-6 text-[10px] px-2"
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3 mr-1" />
|
||||||
|
조인 조건 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
소스 테이블과 참조 테이블을 어떻게 매칭할지 설정
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 조인 조건 목록 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(col.mapping?.joinCondition || []).map((condition, condIndex) => (
|
||||||
|
<div key={condIndex} className="p-3 border rounded-md bg-background space-y-2">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground">
|
||||||
|
조인 조건 {condIndex + 1}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
||||||
|
currentConditions.splice(condIndex, 1);
|
||||||
|
updateRepeaterColumn(index, {
|
||||||
|
mapping: {
|
||||||
|
...col.mapping,
|
||||||
|
type: "reference",
|
||||||
|
joinCondition: currentConditions
|
||||||
|
} as ColumnMapping
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 소스 테이블 선택 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px] text-muted-foreground">소스 테이블 (어느 테이블)</Label>
|
||||||
|
<Select
|
||||||
|
value={condition.sourceTable || "target"}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
||||||
|
currentConditions[condIndex] = {
|
||||||
|
...currentConditions[condIndex],
|
||||||
|
sourceTable: value as "source" | "target"
|
||||||
|
};
|
||||||
|
updateRepeaterColumn(index, {
|
||||||
|
mapping: {
|
||||||
|
...col.mapping,
|
||||||
|
type: "reference",
|
||||||
|
joinCondition: currentConditions
|
||||||
|
} as ColumnMapping
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 text-xs w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="target">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">저장 테이블 (권장)</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{localConfig.targetTable || "sales_order_mng"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="source">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">소스 테이블</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{localConfig.sourceTable || "item_info"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
반복 테이블 = 저장 테이블 컬럼 사용
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 소스 필드 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px] text-muted-foreground">소스 필드 (어느 컬럼)</Label>
|
||||||
|
<Select
|
||||||
|
value={condition.sourceField || ""}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
||||||
|
currentConditions[condIndex] = {
|
||||||
|
...currentConditions[condIndex],
|
||||||
|
sourceField: value
|
||||||
|
};
|
||||||
|
updateRepeaterColumn(index, {
|
||||||
|
mapping: {
|
||||||
|
...col.mapping,
|
||||||
|
type: "reference",
|
||||||
|
joinCondition: currentConditions
|
||||||
|
} as ColumnMapping
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 text-xs w-full">
|
||||||
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{/* 저장 테이블 선택 시 */}
|
||||||
|
{(!condition.sourceTable || condition.sourceTable === "target") && (
|
||||||
|
<>
|
||||||
|
{/* 반복 테이블에 추가된 컬럼 */}
|
||||||
|
{(localConfig.columns || []).length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="px-2 py-1.5 text-[10px] font-semibold text-muted-foreground">
|
||||||
|
반복 테이블 컬럼 (권장)
|
||||||
|
</div>
|
||||||
|
{localConfig.columns
|
||||||
|
.filter((c) => c.field !== col.field)
|
||||||
|
.map((repeaterCol) => (
|
||||||
|
<SelectItem key={`repeater-${repeaterCol.field}`} value={repeaterCol.field}>
|
||||||
|
<span className="font-medium">{repeaterCol.label}</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground ml-1">({repeaterCol.field})</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 저장 테이블의 모든 컬럼 */}
|
||||||
|
{targetTableColumns.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="px-2 py-1.5 text-[10px] font-semibold text-muted-foreground border-t mt-1 pt-2">
|
||||||
|
저장 테이블 전체 컬럼 ({localConfig.targetTable || "sales_order_mng"})
|
||||||
|
</div>
|
||||||
|
{targetTableColumns.map((targetCol) => (
|
||||||
|
<SelectItem key={`target-${targetCol.columnName}`} value={targetCol.columnName}>
|
||||||
|
{targetCol.displayName || targetCol.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 소스 테이블 선택 시 */}
|
||||||
|
{condition.sourceTable === "source" && (
|
||||||
|
<>
|
||||||
|
{tableColumns.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="px-2 py-1.5 text-[10px] font-semibold text-muted-foreground">
|
||||||
|
소스 테이블 컬럼 ({localConfig.sourceTable || "item_info"})
|
||||||
|
</div>
|
||||||
|
{tableColumns.map((sourceCol) => (
|
||||||
|
<SelectItem key={`source-${sourceCol.columnName}`} value={sourceCol.columnName}>
|
||||||
|
{sourceCol.displayName || sourceCol.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
{(!condition.sourceTable || condition.sourceTable === "target")
|
||||||
|
? "반복 테이블에 이미 추가된 컬럼"
|
||||||
|
: "모달에서 선택한 원본 데이터의 컬럼"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 연산자 */}
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Select
|
||||||
|
value={condition.operator || "="}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
||||||
|
currentConditions[condIndex] = {
|
||||||
|
...currentConditions[condIndex],
|
||||||
|
operator: value as "=" | "!=" | ">" | "<" | ">=" | "<="
|
||||||
|
};
|
||||||
|
updateRepeaterColumn(index, {
|
||||||
|
mapping: {
|
||||||
|
...col.mapping,
|
||||||
|
type: "reference",
|
||||||
|
joinCondition: currentConditions
|
||||||
|
} as ColumnMapping
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9 text-xs w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="=">=</SelectItem>
|
||||||
|
<SelectItem value="!=">!=</SelectItem>
|
||||||
|
<SelectItem value=">">></SelectItem>
|
||||||
|
<SelectItem value="<"><</SelectItem>
|
||||||
|
<SelectItem value=">=">>=</SelectItem>
|
||||||
|
<SelectItem value="<="><=</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 대상 필드 */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px] text-muted-foreground">대상 필드 (참조 테이블)</Label>
|
||||||
|
<ReferenceColumnSelector
|
||||||
|
referenceTable={col.mapping?.referenceTable || ""}
|
||||||
|
value={condition.targetField || ""}
|
||||||
|
onChange={(value) => {
|
||||||
|
const currentConditions = [...(col.mapping?.joinCondition || [])];
|
||||||
|
currentConditions[condIndex] = {
|
||||||
|
...currentConditions[condIndex],
|
||||||
|
targetField: value
|
||||||
|
};
|
||||||
|
updateRepeaterColumn(index, {
|
||||||
|
mapping: {
|
||||||
|
...col.mapping,
|
||||||
|
type: "reference",
|
||||||
|
joinCondition: currentConditions
|
||||||
|
} as ColumnMapping
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 조인 조건 미리보기 */}
|
||||||
|
{condition.sourceField && condition.targetField && (
|
||||||
|
<div className="mt-2 p-2 bg-muted/50 rounded text-[10px] font-mono">
|
||||||
|
<span className="text-blue-600 dark:text-blue-400">
|
||||||
|
{condition.sourceTable === "source"
|
||||||
|
? localConfig.sourceTable
|
||||||
|
: localConfig.targetTable || "저장테이블"}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">.{condition.sourceField}</span>
|
||||||
|
<span className="mx-2 text-purple-600 dark:text-purple-400">{condition.operator || "="}</span>
|
||||||
|
<span className="text-purple-600 dark:text-purple-400">{col.mapping?.referenceTable}</span>
|
||||||
|
<span className="text-muted-foreground">.{condition.targetField}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 조인 조건 없을 때 안내 */}
|
||||||
|
{(!col.mapping?.joinCondition || col.mapping.joinCondition.length === 0) && (
|
||||||
|
<div className="p-4 border-2 border-dashed rounded-lg text-center">
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
조인 조건이 없습니다
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
"조인 조건 추가" 버튼을 클릭하여 매칭 조건을 설정하세요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 조인 조건 예시 */}
|
||||||
|
{col.mapping?.referenceTable && (
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-950 rounded-md border border-blue-200 dark:border-blue-800">
|
||||||
|
<p className="text-xs font-medium mb-2">조인 조건 예시</p>
|
||||||
|
<div className="space-y-1 text-[10px] text-muted-foreground">
|
||||||
|
<p>예) 거래처별 품목 단가 조회:</p>
|
||||||
|
<p className="ml-2 font-mono">• item_code = item_code</p>
|
||||||
|
<p className="ml-2 font-mono">• customer_code = customer_code</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ export interface ColumnMapping {
|
||||||
* 조인 조건 정의
|
* 조인 조건 정의
|
||||||
*/
|
*/
|
||||||
export interface JoinCondition {
|
export interface JoinCondition {
|
||||||
|
/** 소스 테이블 (어느 테이블의 컬럼인지) */
|
||||||
|
sourceTable?: string; // "source" (item_info) 또는 "target" (sales_order_mng)
|
||||||
/** 현재 테이블의 컬럼 (소스 테이블 또는 반복 테이블) */
|
/** 현재 테이블의 컬럼 (소스 테이블 또는 반복 테이블) */
|
||||||
sourceField: string;
|
sourceField: string;
|
||||||
/** 참조 테이블의 컬럼 */
|
/** 참조 테이블의 컬럼 */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue