From 6d0acdd1ec6edd8bb7b212bae003845d70aa8557 Mon Sep 17 00:00:00 2001
From: SeongHyun Kim
Date: Thu, 20 Nov 2025 10:16:49 +0900
Subject: [PATCH 1/2] =?UTF-8?q?fix:=20ModalRepeaterTable=20reference=20?=
=?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=B2=98=EB=A6=AC=20=EC=88=9C=EC=84=9C=20?=
=?UTF-8?q?=EB=B0=8F=20API=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
문제:
- reference 매핑 시 조인 조건의 소스 필드 값이 undefined
- API 호출 시 filters 파라미터를 백엔드가 인식 못함
해결:
- 컬럼 처리를 2단계로 분리 (source/manual → reference)
- API 파라미터 변경 (filters→search, limit/offset→size/page)
- 응답 경로 수정 (data.data → data.data.data)
결과:
- 외부 테이블 참조 매핑 정상 작동
- 품목 선택 시 customer_item_mapping에서 단가 자동 조회 성공
---
.../ModalRepeaterTableComponent.tsx | 179 +++++++++-
.../ModalRepeaterTableConfigPanel.tsx | 312 +++++++++++++++++-
.../components/modal-repeater-table/types.ts | 2 +
3 files changed, 472 insertions(+), 21 deletions(-)
diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx
index e387d50e..2003c5ef 100644
--- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx
+++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx
@@ -5,14 +5,122 @@ import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { ItemSelectionModal } from "./ItemSelectionModal";
import { RepeaterTable } from "./RepeaterTable";
-import { ModalRepeaterTableProps, RepeaterColumnConfig } from "./types";
+import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./types";
import { useCalculation } from "./useCalculation";
import { cn } from "@/lib/utils";
+import { apiClient } from "@/lib/api/client";
interface ModalRepeaterTableComponentProps extends Partial {
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 {
+ if (joinConditions.length === 0) {
+ console.warn("⚠️ 조인 조건이 없습니다. 참조 조회를 건너뜁니다.");
+ return undefined;
+ }
+
+ try {
+ // 조인 조건을 WHERE 절로 변환
+ const whereConditions: Record = {};
+
+ 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({
config,
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("📋 소스 데이터:", items);
- // 매핑 규칙에 따라 데이터 변환
- const mappedItems = items.map((sourceItem) => {
+ // 매핑 규칙에 따라 데이터 변환 (비동기 처리)
+ const mappedItems = await Promise.all(items.map(async (sourceItem) => {
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);
// 1. 매핑 규칙이 있는 경우
@@ -148,11 +272,6 @@ export function ModalRepeaterTableComponent({
} else {
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") {
// 사용자 입력 (빈 값)
newItem[col.field] = undefined;
@@ -170,11 +289,47 @@ export function ModalRepeaterTableComponent({
newItem[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);
return newItem;
- });
+ }));
// 계산 필드 업데이트
const calculatedItems = calculateAll(mappedItems);
diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx
index a952d845..a8068c92 100644
--- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx
+++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx
@@ -131,7 +131,8 @@ export function ModalRepeaterTableConfigPanel({
// 계산 규칙이 없으면 모든 컬럼의 calculated 속성 제거
if (!initialConfig.calculationRules || initialConfig.calculationRules.length === 0) {
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 { ...initialConfig, columns: cleanedColumns };
@@ -145,7 +146,8 @@ export function ModalRepeaterTableConfigPanel({
return { ...col, calculated: true, editable: false };
} else {
// 나머지 필드는 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 };
}
});
@@ -281,12 +283,12 @@ export function ModalRepeaterTableConfigPanel({
const columns = localConfig.columns || [];
// 이미 존재하는 컬럼인지 확인
- if (columns.some(col => col.field === columnName)) {
+ if (columns.some((col) => col.field === columnName)) {
alert("이미 추가된 컬럼입니다.");
return;
}
- const targetCol = targetTableColumns.find(c => c.columnName === columnName);
+ const targetCol = targetTableColumns.find((c) => c.columnName === columnName);
const newColumn: RepeaterColumnConfig = {
field: columnName,
@@ -340,16 +342,17 @@ export function ModalRepeaterTableConfigPanel({
// 이전 결과 필드의 calculated 속성 제거
if (oldRule.result) {
- const oldResultIndex = columns.findIndex(c => c.field === oldRule.result);
+ const oldResultIndex = columns.findIndex((c) => c.field === oldRule.result);
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 };
}
}
// 새 결과 필드를 calculated=true, editable=false로 설정
if (updates.result) {
- const newResultIndex = columns.findIndex(c => c.field === updates.result);
+ const newResultIndex = columns.findIndex((c) => c.field === updates.result);
if (newResultIndex !== -1) {
columns[newResultIndex] = {
...columns[newResultIndex],
@@ -375,9 +378,10 @@ export function ModalRepeaterTableConfigPanel({
// 결과 필드의 calculated 속성 제거
if (removedRule.result) {
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) {
- 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 };
}
rules.splice(index, 1);
@@ -978,6 +982,296 @@ export function ModalRepeaterTableConfigPanel({
가져올 컬럼명
+
+ {/* 조인 조건 설정 */}
+
+
+
+
+
+
+ 소스 테이블과 참조 테이블을 어떻게 매칭할지 설정
+
+
+ {/* 조인 조건 목록 */}
+
+ {(col.mapping?.joinCondition || []).map((condition, condIndex) => (
+
+
+
+ 조인 조건 {condIndex + 1}
+
+
+
+
+ {/* 소스 테이블 선택 */}
+
+
+
+
+ 반복 테이블 = 저장 테이블 컬럼 사용
+
+
+
+ {/* 소스 필드 */}
+
+
+
+
+ {(!condition.sourceTable || condition.sourceTable === "target")
+ ? "반복 테이블에 이미 추가된 컬럼"
+ : "모달에서 선택한 원본 데이터의 컬럼"}
+
+
+
+ {/* 연산자 */}
+
+
+
+
+ {/* 대상 필드 */}
+
+
+ {
+ const currentConditions = [...(col.mapping?.joinCondition || [])];
+ currentConditions[condIndex] = {
+ ...currentConditions[condIndex],
+ targetField: value
+ };
+ updateRepeaterColumn(index, {
+ mapping: {
+ ...col.mapping,
+ type: "reference",
+ joinCondition: currentConditions
+ } as ColumnMapping
+ });
+ }}
+ />
+
+
+ {/* 조인 조건 미리보기 */}
+ {condition.sourceField && condition.targetField && (
+
+
+ {condition.sourceTable === "source"
+ ? localConfig.sourceTable
+ : localConfig.targetTable || "저장테이블"}
+
+ .{condition.sourceField}
+ {condition.operator || "="}
+ {col.mapping?.referenceTable}
+ .{condition.targetField}
+
+ )}
+
+ ))}
+
+ {/* 조인 조건 없을 때 안내 */}
+ {(!col.mapping?.joinCondition || col.mapping.joinCondition.length === 0) && (
+
+
+ 조인 조건이 없습니다
+
+
+ "조인 조건 추가" 버튼을 클릭하여 매칭 조건을 설정하세요
+
+
+ )}
+
+
+ {/* 조인 조건 예시 */}
+ {col.mapping?.referenceTable && (
+
+
조인 조건 예시
+
+
예) 거래처별 품목 단가 조회:
+
• item_code = item_code
+
• customer_code = customer_code
+
+
+ )}
+
)}
diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts
index 047dddc8..180830ee 100644
--- a/frontend/lib/registry/components/modal-repeater-table/types.ts
+++ b/frontend/lib/registry/components/modal-repeater-table/types.ts
@@ -77,6 +77,8 @@ export interface ColumnMapping {
* 조인 조건 정의
*/
export interface JoinCondition {
+ /** 소스 테이블 (어느 테이블의 컬럼인지) */
+ sourceTable?: string; // "source" (item_info) 또는 "target" (sales_order_mng)
/** 현재 테이블의 컬럼 (소스 테이블 또는 반복 테이블) */
sourceField: string;
/** 참조 테이블의 컬럼 */
From 68f79db6ed1893dcdb91618ba6e2db79b2c5de18 Mon Sep 17 00:00:00 2001
From: SeongHyun Kim
Date: Thu, 20 Nov 2025 13:47:21 +0900
Subject: [PATCH 2/2] =?UTF-8?q?feat(autocomplete-search-input):=20?=
=?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=9E=90=EB=8F=99=20=EB=A7=A4=ED=95=91=20?=
=?UTF-8?q?=EB=B0=8F=20=EC=A0=80=EC=9E=A5=20=EC=9C=84=EC=B9=98=20=EC=84=A0?=
=?UTF-8?q?=ED=83=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 필드 자동 매핑 기능 구현
* FieldMapping 타입 추가 (sourceField → targetField)
* applyFieldMappings() 함수로 선택 시 자동 입력
* 여러 필드를 한 번에 자동으로 채움 (거래처 선택 → 주소/전화 자동 입력)
- 값 필드 저장 위치 선택 기능 추가
* ValueFieldStorage 타입 추가 (targetTable, targetColumn)
* 기본값(화면 연결 테이블) 또는 명시적 테이블/컬럼 지정 가능
* 중간 테이블, 이력 테이블 등 다중 테이블 저장 지원
- UI/UX 개선
* 모든 선택 필드를 Combobox 스타일로 통일
* 각 필드 아래 간략한 사용 설명 추가
* 저장 위치 동작 미리보기 박스 추가
- 문서 작성
* 사용_가이드.md 신규 작성 (실전 예제 3개 포함)
* 빠른 시작 가이드, FAQ, 체크리스트 제공
---
.../app/test-autocomplete-mapping/page.tsx | 141 ++++++
.../AutocompleteSearchInputComponent.tsx | 38 +-
.../AutocompleteSearchInputConfigPanel.tsx | 422 +++++++++++++++++-
.../autocomplete-search-input/README.md | 74 +++
.../autocomplete-search-input/types.ts | 20 +
.../autocomplete-search-input/사용_가이드.md | 300 +++++++++++++
6 files changed, 993 insertions(+), 2 deletions(-)
create mode 100644 frontend/app/test-autocomplete-mapping/page.tsx
create mode 100644 frontend/lib/registry/components/autocomplete-search-input/사용_가이드.md
diff --git a/frontend/app/test-autocomplete-mapping/page.tsx b/frontend/app/test-autocomplete-mapping/page.tsx
new file mode 100644
index 00000000..234c75f6
--- /dev/null
+++ b/frontend/app/test-autocomplete-mapping/page.tsx
@@ -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 (
+
+
+
+ AutocompleteSearchInput 필드 자동 매핑 테스트
+
+ 거래처를 선택하면 아래 입력 필드들이 자동으로 채워집니다
+
+
+
+ {/* 검색 컴포넌트 */}
+
+
+
{
+ setSelectedValue(value);
+ console.log("선택된 항목:", fullData);
+ }}
+ />
+
+
+ {/* 구분선 */}
+
+
+ {/* 상태 표시 */}
+
+
현재 상태
+
+
+ {JSON.stringify(
+ {
+ selectedValue,
+ customerName,
+ address,
+ phone,
+ },
+ null,
+ 2
+ )}
+
+
+
+
+
+
+ {/* 사용 안내 */}
+
+
+ 사용 방법
+
+
+
+ - 위의 검색 필드에 거래처명이나 코드를 입력하세요
+ - 드롭다운에서 원하는 거래처를 선택하세요
+ - 아래 입력 필드들이 자동으로 채워지는 것을 확인하세요
+ - 필요한 경우 자동으로 채워진 값을 수정할 수 있습니다
+
+
+
+
+ );
+}
+
diff --git a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx
index 0bb78db2..42baabdc 100644
--- a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx
+++ b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputComponent.tsx
@@ -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 {
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);
};
diff --git a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx
index dadcdb3c..96a212b1 100644
--- a/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx
+++ b/frontend/lib/registry/components/autocomplete-search-input/AutocompleteSearchInputConfigPanel.tsx
@@ -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([]);
+ 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) => {
+ 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 (
@@ -164,6 +216,9 @@ export function AutocompleteSearchInputConfigPanel({
+
+ 검색할 데이터가 저장된 테이블을 선택하세요
+
@@ -211,6 +266,9 @@ export function AutocompleteSearchInputConfigPanel({
+
+ 사용자에게 보여줄 필드 (예: 거래처명)
+
@@ -258,6 +316,9 @@ export function AutocompleteSearchInputConfigPanel({
+
+ 검색 테이블에서 가져올 값의 컬럼 (예: customer_code)
+
@@ -270,6 +331,196 @@ export function AutocompleteSearchInputConfigPanel({
/>
+ {/* 값 필드 저장 위치 설정 */}
+
+
+
값 필드 저장 위치 (고급)
+
+ 위에서 선택한 "값 필드"의 데이터를 어느 테이블/컬럼에 저장할지 지정합니다.
+
+ 미설정 시 화면의 연결 테이블에 컴포넌트의 바인딩 필드로 자동 저장됩니다.
+
+
+
+ {/* 저장 테이블 선택 */}
+
+
+
+
+
+
+
+
+
+
+ 테이블을 찾을 수 없습니다.
+
+ {/* 기본값 옵션 */}
+ {
+ updateConfig({
+ valueFieldStorage: {
+ ...localConfig.valueFieldStorage,
+ targetTable: undefined,
+ targetColumn: undefined,
+ },
+ });
+ setOpenStorageTableCombo(false);
+ }}
+ className="text-xs sm:text-sm"
+ >
+
+
+ 기본값
+ 화면의 연결 테이블 사용
+
+
+ {allTables.map((table) => (
+ {
+ updateConfig({
+ valueFieldStorage: {
+ ...localConfig.valueFieldStorage,
+ targetTable: table.tableName,
+ targetColumn: undefined, // 테이블 변경 시 컬럼 초기화
+ },
+ });
+ setOpenStorageTableCombo(false);
+ }}
+ className="text-xs sm:text-sm"
+ >
+
+
+ {table.displayName || table.tableName}
+ {table.displayName && {table.tableName}}
+
+
+ ))}
+
+
+
+
+
+
+ 값을 저장할 테이블 (기본값: 화면 연결 테이블)
+
+
+
+ {/* 저장 컬럼 선택 */}
+ {localConfig.valueFieldStorage?.targetTable && (
+
+
+
+
+
+
+
+
+
+
+ 컬럼을 찾을 수 없습니다.
+
+ {storageTableColumns.map((column) => (
+ {
+ updateConfig({
+ valueFieldStorage: {
+ ...localConfig.valueFieldStorage,
+ targetColumn: column.columnName,
+ },
+ });
+ setOpenStorageColumnCombo(false);
+ }}
+ className="text-xs sm:text-sm"
+ >
+
+
+ {column.displayName || column.columnName}
+ {column.displayName && {column.columnName}}
+
+
+ ))}
+
+
+
+
+
+
+ 값을 저장할 컬럼명
+
+
+ )}
+
+ {/* 설명 박스 */}
+
+
+ 저장 위치 동작
+
+
+ {localConfig.valueFieldStorage?.targetTable ? (
+ <>
+
+ 선택한 값({localConfig.valueField})을
+
+
+
+ {localConfig.valueFieldStorage.targetTable}
+ {" "}
+ 테이블의{" "}
+
+ {localConfig.valueFieldStorage.targetColumn || "(컬럼 미지정)"}
+ {" "}
+ 컬럼에 저장합니다.
+
+ >
+ ) : (
+
기본값: 화면의 연결 테이블에 컴포넌트의 바인딩 필드로 저장됩니다.
+ )}
+
+
+
+
@@ -375,6 +626,175 @@ export function AutocompleteSearchInputConfigPanel({
)}
+
+ {/* 필드 자동 매핑 설정 */}
+
+
+
필드 자동 매핑
+
+ 선택한 항목의 필드를 화면의 다른 입력 필드에 자동으로 채워넣습니다
+
+
+
+
+
+
+
+ updateConfig({ enableFieldMapping: checked })
+ }
+ />
+
+
+ 활성화하면 항목 선택 시 설정된 필드들이 자동으로 채워집니다
+
+
+
+ {localConfig.enableFieldMapping && (
+
+
+
+
+
+
+
+ {(localConfig.fieldMappings || []).map((mapping, index) => (
+
+
+
+ 매핑 #{index + 1}
+
+
+
+
+ {/* 표시명 */}
+
+
+
+ updateFieldMapping(index, { label: e.target.value })
+ }
+ placeholder="예: 거래처명"
+ className="h-8 text-xs sm:h-10 sm:text-sm"
+ />
+
+ 이 매핑의 설명 (선택사항)
+
+
+
+ {/* 소스 필드 (테이블의 컬럼) */}
+
+
+
+
+ 가져올 데이터의 컬럼명
+
+
+
+ {/* 타겟 필드 (화면의 input ID) */}
+
+
+
+ updateFieldMapping(index, { targetField: e.target.value })
+ }
+ placeholder="예: customer_name_input"
+ className="h-8 text-xs sm:h-10 sm:text-sm"
+ />
+
+ 값을 채울 화면 컴포넌트의 ID (예: input의 id 속성)
+
+
+
+ {/* 예시 설명 */}
+
+
+ {mapping.sourceField && mapping.targetField ? (
+ <>
+ {mapping.label || "이 필드"}: 테이블의{" "}
+
+ {mapping.sourceField}
+ {" "}
+ 값을 화면의{" "}
+
+ {mapping.targetField}
+ {" "}
+ 컴포넌트에 자동으로 채웁니다
+ >
+ ) : (
+ "소스 필드와 타겟 필드를 모두 선택하세요"
+ )}
+
+
+
+ ))}
+
+
+ {/* 사용 안내 */}
+ {localConfig.fieldMappings && localConfig.fieldMappings.length > 0 && (
+
+
+ 사용 방법
+
+
+ - 화면에서 이 검색 컴포넌트로 항목을 선택하면
+ - 설정된 매핑에 따라 다른 입력 필드들이 자동으로 채워집니다
+ - 타겟 필드 ID는 화면 디자이너에서 설정한 컴포넌트 ID와 일치해야 합니다
+
+
+ )}
+
+ )}
+
);
}
diff --git a/frontend/lib/registry/components/autocomplete-search-input/README.md b/frontend/lib/registry/components/autocomplete-search-input/README.md
index 3018d5c6..688d0c3a 100644
--- a/frontend/lib/registry/components/autocomplete-search-input/README.md
+++ b/frontend/lib/registry/components/autocomplete-search-input/README.md
@@ -9,9 +9,12 @@
- 추가 정보 표시 가능
- X 버튼으로 선택 초기화
- 외부 클릭 시 자동 닫힘
+- **필드 자동 매핑**: 선택한 항목의 값을 화면의 다른 입력 필드에 자동으로 채움
## 사용 예시
+### 기본 사용
+
```tsx
```
+### 필드 자동 매핑 사용
+
+```tsx
+ {
+ console.log("선택됨:", code, fullData);
+ // 필드 매핑은 자동으로 실행됩니다
+ }}
+/>
+
+
+
+
+
+```
+
## 설정 옵션
+### 기본 설정
+
- `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 타입의 요소에만 동작합니다
+- 여러 필드를 한 번에 매핑할 수 있습니다
+
diff --git a/frontend/lib/registry/components/autocomplete-search-input/types.ts b/frontend/lib/registry/components/autocomplete-search-input/types.ts
index 4e006062..802f27c7 100644
--- a/frontend/lib/registry/components/autocomplete-search-input/types.ts
+++ b/frontend/lib/registry/components/autocomplete-search-input/types.ts
@@ -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[]; // 매핑할 필드 목록
}
diff --git a/frontend/lib/registry/components/autocomplete-search-input/사용_가이드.md b/frontend/lib/registry/components/autocomplete-search-input/사용_가이드.md
new file mode 100644
index 00000000..d8261e8f
--- /dev/null
+++ b/frontend/lib/registry/components/autocomplete-search-input/사용_가이드.md
@@ -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 컴포넌트를 쉽게 사용할 수 있습니다! 🎉
+