From 585febfb52a310031725444c74beafb0cee5f25f Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 19 Jan 2026 18:58:23 +0900 Subject: [PATCH] =?UTF-8?q?make:=20RepeaterFieldGroup=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20-=20=ED=95=98=EC=9C=84=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0=20-=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EC=9D=98=20=EB=A0=88=EB=B2=A8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20subDataSource=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=ED=95=84=EB=93=9C=EB=B3=84=20=EC=88=A8?= =?UTF-8?q?=EA=B9=80(isHidden)=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20-=20=EA=B8=B0=EC=A1=B4=20fieldMappings=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0,=20=ED=95=84=EB=93=9C=EB=B3=84=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=5F?= =?UTF-8?q?repeaterFieldsConfig=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EC=84=A4=EC=A0=95=20=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?:=20"=EC=9D=B4=20=ED=95=84=EB=93=9C=EB=93=A4=EC=9D=98=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=EC=A1=B0=ED=9A=8C=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B0=92=20=EA=B0=80=EC=A0=B8=EC=99=80?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=B4=EC=A4=98"=EB=9D=BC=EB=8A=94=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=97=AD=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/webtypes/RepeaterInput.tsx | 18 +- .../webtypes/config/RepeaterConfigPanel.tsx | 185 ++++++++++-------- .../RepeaterFieldGroupRenderer.tsx | 18 ++ frontend/lib/utils/buttonActions.ts | 38 +++- frontend/types/repeater.ts | 14 +- 5 files changed, 182 insertions(+), 91 deletions(-) diff --git a/frontend/components/webtypes/RepeaterInput.tsx b/frontend/components/webtypes/RepeaterInput.tsx index 49751699..050b386b 100644 --- a/frontend/components/webtypes/RepeaterInput.tsx +++ b/frontend/components/webtypes/RepeaterInput.tsx @@ -907,6 +907,10 @@ export const RepeaterInput: React.FC = ({ const renderGridLayout = () => { // 하위 데이터 조회 설정이 있으면 연결 컬럼 찾기 const linkColumn = subDataLookup?.lookup?.linkColumn; + + // hidden이 아닌 필드만 표시 + // isHidden이 true이거나 displayMode가 hidden인 필드는 제외 (하위 호환성 유지) + const visibleFields = fields.filter((f) => !f.isHidden && f.displayMode !== "hidden"); return (
@@ -919,7 +923,7 @@ export const RepeaterInput: React.FC = ({ {allowReorder && ( )} - {fields.map((field) => ( + {visibleFields.map((field) => ( {field.label} {field.required && *} @@ -958,8 +962,8 @@ export const RepeaterInput: React.FC = ({ )} - {/* 필드들 */} - {fields.map((field) => ( + {/* 필드들 (hidden 제외) */} + {visibleFields.map((field) => ( {renderField(field, itemIndex, item[field.name])} @@ -987,7 +991,7 @@ export const RepeaterInput: React.FC = ({ @@ -1016,6 +1020,10 @@ export const RepeaterInput: React.FC = ({ const renderCardLayout = () => { // 하위 데이터 조회 설정이 있으면 연결 컬럼 찾기 const linkColumn = subDataLookup?.lookup?.linkColumn; + + // hidden이 아닌 필드만 표시 + // isHidden이 true이거나 displayMode가 hidden인 필드는 제외 (하위 호환성 유지) + const visibleFields = fields.filter((f) => !f.isHidden && f.displayMode !== "hidden"); return ( <> @@ -1084,7 +1092,7 @@ export const RepeaterInput: React.FC = ({ {!isCollapsed && (
- {fields.map((field) => ( + {visibleFields.map((field) => (
); })}
- {config.targetTable && ( -

- * 저장 대상: {config.targetTable} -

- )} +

+ * 저장 설정은 필드 정의에서 "하위 데이터 조회에서 값 가져오기"로 설정하세요 +

)} @@ -1545,35 +1491,106 @@ export const RepeaterConfigPanel: React.FC = ({ {/* 카테고리 타입이 아닐 때만 표시 모드 선택 */} {field.type !== "category" && ( -
-
- - -
+
+
+
+ + +
-
-
- updateField(index, { required: checked as boolean })} - /> - +
+
+ updateField(index, { required: checked as boolean })} + /> + +
+ + {/* 숨김 체크박스 */} +
+ updateField(index, { isHidden: checked as boolean })} + /> + +
+ + {/* 하위 데이터 조회에서 값 가져오기 */} + {config.subDataLookup?.enabled && ( +
+
+ { + updateField(index, { + subDataSource: { + enabled: checked as boolean, + sourceColumn: field.subDataSource?.sourceColumn || "", + }, + }); + }} + /> + +
+ + {field.subDataSource?.enabled && ( +
+ + +

+ 재고 조회 결과에서 이 컬럼의 값을 가져옵니다 +

+
+ )} +
+ )}
)} diff --git a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx index 2aefb047..63e1cbb9 100644 --- a/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx +++ b/frontend/lib/registry/components/repeater-field-group/RepeaterFieldGroupRenderer.tsx @@ -287,12 +287,18 @@ const RepeaterFieldGroupComponent: React.FC = (props) => if (onChange && items.length > 0) { // 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출 const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // 🆕 subDataSource 설정이 있는 필드 목록 (하위 데이터 조회 연동) + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: targetTable, _originalItemIds: itemIds, // 🆕 원본 ID 목록도 함께 전달 _existingRecord: !!item.id, // 🆕 기존 레코드 플래그 (id가 있으면 기존 레코드) _repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록 + _repeaterFieldsConfig: fieldsConfig, // 🆕 필드 설정 (subDataSource 등) })); onChange(dataWithMeta); } @@ -393,11 +399,17 @@ const RepeaterFieldGroupComponent: React.FC = (props) => if (items.length > 0) { // 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출 const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // 🆕 subDataSource 설정이 있는 필드 목록 + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); const dataWithMeta = items.map((item: any) => ({ ...item, _targetTable: effectiveTargetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록 + _repeaterFieldsConfig: fieldsConfig, // 🆕 필드 설정 (subDataSource 등) })); onChange(dataWithMeta); } else { @@ -681,6 +693,11 @@ const RepeaterFieldGroupComponent: React.FC = (props) => (newValue: any[]) => { // 🆕 RepeaterFieldGroup이 관리하는 필드 목록 추출 const repeaterFieldNames = (configRef.current.fields || []).map((f: any) => f.name); + // 🆕 subDataSource 설정이 있는 필드 목록 + const fieldsConfig = (configRef.current.fields || []).map((f: any) => ({ + name: f.name, + subDataSource: f.subDataSource, + })); // 🆕 모든 항목에 메타데이터 추가 let valueWithMeta = newValue.map((item: any) => ({ @@ -688,6 +705,7 @@ const RepeaterFieldGroupComponent: React.FC = (props) => _targetTable: effectiveTargetTable || targetTable, _existingRecord: !!item.id, _repeaterFields: repeaterFieldNames, // 🆕 품목 고유 필드 목록 + _repeaterFieldsConfig: fieldsConfig, // 🆕 필드 설정 (subDataSource 등) })); // 🆕 분할 패널에서 우측인 경우, FK 값 추가 diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index debf58b2..b0ac2ba9 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -803,7 +803,7 @@ export class ButtonActionExecutor { for (const item of parsedData) { // 메타 필드 제거 - const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, ...itemData } = item; + const { _targetTable, _isNewItem, _existingRecord, _originalItemIds, _deletedItemIds, _repeaterFields, _subDataSelection, _subDataMaxValue, ...itemData } = item; // 🔧 품목 고유 필드만 추출 (RepeaterFieldGroup 설정 기반) const itemOnlyData: Record = {}; @@ -812,6 +812,42 @@ export class ButtonActionExecutor { itemOnlyData[field] = itemData[field]; } }); + + // 🆕 하위 데이터 선택에서 값 추출 (subDataSource 설정 기반) + // 필드 정의에서 subDataSource.enabled가 true이고 sourceColumn이 설정된 필드만 처리 + if (_subDataSelection && typeof _subDataSelection === 'object') { + // _repeaterFieldsConfig에서 subDataSource 설정 확인 + const fieldsConfig = item._repeaterFieldsConfig as Array<{ + name: string; + subDataSource?: { enabled: boolean; sourceColumn: string }; + }> | undefined; + + if (fieldsConfig && Array.isArray(fieldsConfig)) { + fieldsConfig.forEach((fieldConfig) => { + if (fieldConfig.subDataSource?.enabled && fieldConfig.subDataSource?.sourceColumn) { + const targetField = fieldConfig.name; // 필드명 = 저장할 컬럼명 + const sourceColumn = fieldConfig.subDataSource.sourceColumn; + const sourceValue = _subDataSelection[sourceColumn]; + + if (sourceValue !== undefined && sourceValue !== null) { + itemOnlyData[targetField] = sourceValue; + console.log(`📋 [handleSave] 하위 데이터 값 매핑: ${sourceColumn} → ${targetField} = ${sourceValue}`); + } + } + }); + } else { + // 하위 호환성: fieldsConfig가 없으면 기존 방식 사용 + Object.keys(_subDataSelection).forEach((subDataKey) => { + if (itemOnlyData[subDataKey] === undefined || itemOnlyData[subDataKey] === null || itemOnlyData[subDataKey] === '') { + const subDataValue = _subDataSelection[subDataKey]; + if (subDataValue !== undefined && subDataValue !== null) { + itemOnlyData[subDataKey] = subDataValue; + console.log(`📋 [handleSave] 하위 데이터 선택 값 추가 (레거시): ${subDataKey} = ${subDataValue}`); + } + } + }); + } + } // 🔧 마스터 정보 + 품목 고유 정보 병합 // masterFields: 상단 폼에서 수정한 최신 마스터 정보 diff --git a/frontend/types/repeater.ts b/frontend/types/repeater.ts index bbdc8727..c7f0fa98 100644 --- a/frontend/types/repeater.ts +++ b/frontend/types/repeater.ts @@ -43,9 +43,19 @@ export interface CalculationFormula { * 필드 표시 모드 * - input: 입력 필드로 표시 (편집 가능) * - readonly: 읽기 전용 텍스트로 표시 + * - hidden: 숨김 (UI에 표시되지 않지만 데이터에 포함됨) * - (카테고리 타입은 자동으로 배지로 표시됨) */ -export type RepeaterFieldDisplayMode = "input" | "readonly"; +export type RepeaterFieldDisplayMode = "input" | "readonly" | "hidden"; + +/** + * 하위 데이터 조회 소스 설정 + * 필드 값을 하위 데이터 조회 결과에서 가져올 때 사용 + */ +export interface SubDataSourceConfig { + enabled: boolean; // 활성화 여부 + sourceColumn: string; // 하위 데이터 조회 테이블의 소스 컬럼 (예: lot_number) +} /** * 반복 그룹 내 개별 필드 정의 @@ -60,6 +70,8 @@ export interface RepeaterFieldDefinition { options?: Array<{ label: string; value: string }>; // select용 width?: string; // 필드 너비 (예: "200px", "50%") displayMode?: RepeaterFieldDisplayMode; // 표시 모드: input(입력), readonly(읽기전용) + isHidden?: boolean; // 숨김 여부 (true면 테이블에 표시 안 함, 데이터는 저장) + subDataSource?: SubDataSourceConfig; // 하위 데이터 조회에서 값 가져오기 설정 categoryCode?: string; // category 타입일 때 사용할 카테고리 코드 formula?: CalculationFormula; // 계산식 (type이 "calculated"일 때 사용) numberFormat?: {