feat(autocomplete-search-input): 필드 자동 매핑 및 저장 위치 선택 기능 추가

- 필드 자동 매핑 기능 구현
  * FieldMapping 타입 추가 (sourceField → targetField)
  * applyFieldMappings() 함수로 선택 시 자동 입력
  * 여러 필드를 한 번에 자동으로 채움 (거래처 선택 → 주소/전화 자동 입력)

- 값 필드 저장 위치 선택 기능 추가
  * ValueFieldStorage 타입 추가 (targetTable, targetColumn)
  * 기본값(화면 연결 테이블) 또는 명시적 테이블/컬럼 지정 가능
  * 중간 테이블, 이력 테이블 등 다중 테이블 저장 지원

- UI/UX 개선
  * 모든 선택 필드를 Combobox 스타일로 통일
  * 각 필드 아래 간략한 사용 설명 추가
  * 저장 위치 동작 미리보기 박스 추가

- 문서 작성
  * 사용_가이드.md 신규 작성 (실전 예제 3개 포함)
  * 빠른 시작 가이드, FAQ, 체크리스트 제공
This commit is contained in:
SeongHyun Kim 2025-11-20 13:47:21 +09:00
parent 6d0acdd1ec
commit 68f79db6ed
6 changed files with 993 additions and 2 deletions

View File

@ -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>
);
}

View File

@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
import { useEntitySearch } from "../entity-search-input/useEntitySearch";
import { EntitySearchResult } from "../entity-search-input/types";
import { cn } from "@/lib/utils";
import { AutocompleteSearchInputConfig } from "./types";
import { AutocompleteSearchInputConfig, FieldMapping } from "./types";
interface AutocompleteSearchInputProps extends Partial<AutocompleteSearchInputConfig> {
config?: AutocompleteSearchInputConfig;
@ -81,10 +81,46 @@ export function AutocompleteSearchInputComponent({
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) => {
setSelectedData(item);
setInputValue(item[displayField] || "");
onChange?.(item[valueField], item);
// 필드 자동 매핑 실행
applyFieldMappings(item);
setIsOpen(false);
};

View File

@ -9,7 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
import { AutocompleteSearchInputConfig } from "./types";
import { AutocompleteSearchInputConfig, FieldMapping, ValueFieldStorage } from "./types";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { cn } from "@/lib/utils";
@ -30,6 +30,10 @@ export function AutocompleteSearchInputConfigPanel({
const [openTableCombo, setOpenTableCombo] = 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(() => {
@ -73,6 +77,31 @@ export function AutocompleteSearchInputConfigPanel({
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(() => {
setLocalConfig(config);
}, [config]);
@ -117,6 +146,29 @@ export function AutocompleteSearchInputConfigPanel({
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 (
<div className="space-y-4 p-4">
<div className="space-y-2">
@ -164,6 +216,9 @@ export function AutocompleteSearchInputConfigPanel({
</Command>
</PopoverContent>
</Popover>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
<div className="space-y-2">
@ -211,6 +266,9 @@ export function AutocompleteSearchInputConfigPanel({
</Command>
</PopoverContent>
</Popover>
<p className="text-[10px] text-muted-foreground">
(: 거래처명)
</p>
</div>
<div className="space-y-2">
@ -258,6 +316,9 @@ export function AutocompleteSearchInputConfigPanel({
</Command>
</PopoverContent>
</Popover>
<p className="text-[10px] text-muted-foreground">
(: customer_code)
</p>
</div>
<div className="space-y-2">
@ -270,6 +331,196 @@ export function AutocompleteSearchInputConfigPanel({
/>
</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="flex items-center justify-between">
<Label className="text-xs sm:text-sm"> </Label>
@ -375,6 +626,175 @@ 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">
</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>
);
}

View File

@ -9,9 +9,12 @@
- 추가 정보 표시 가능
- X 버튼으로 선택 초기화
- 외부 클릭 시 자동 닫힘
- **필드 자동 매핑**: 선택한 항목의 값을 화면의 다른 입력 필드에 자동으로 채움
## 사용 예시
### 기본 사용
```tsx
<AutocompleteSearchInputComponent
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`: 검색할 테이블명
- `displayField`: 표시할 필드
- `valueField`: 값으로 사용할 필드
@ -38,3 +84,31 @@
- `showAdditionalInfo`: 추가 정보 표시 여부
- `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 타입의 요소에만 동작합니다
- 여러 필드를 한 번에 매핑할 수 있습니다

View File

@ -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 {
tableName: string;
displayField: string;
@ -7,5 +22,10 @@ export interface AutocompleteSearchInputConfig {
placeholder?: string;
showAdditionalInfo?: boolean;
additionalFields?: string[];
// 값 필드 저장 위치 설정
valueFieldStorage?: ValueFieldStorage;
// 필드 자동 매핑 설정
enableFieldMapping?: boolean; // 필드 자동 매핑 활성화 여부
fieldMappings?: FieldMapping[]; // 매핑할 필드 목록
}

View File

@ -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 컴포넌트를 쉽게 사용할 수 있습니다! 🎉