diff --git a/.cursor/rules/component-development-guide.mdc b/.cursor/rules/component-development-guide.mdc index f39e70ca..f9a807a7 100644 --- a/.cursor/rules/component-development-guide.mdc +++ b/.cursor/rules/component-development-guide.mdc @@ -13,15 +13,238 @@ alwaysApply: false ## 목차 -1. [엔티티 조인 컬럼 활용 (필수)](#1-엔티티-조인-컬럼-활용-필수) -2. [폼 데이터 관리](#2-폼-데이터-관리) -3. [다국어 지원](#3-다국어-지원) -4. [컬럼 설정 패널 구현](#4-컬럼-설정-패널-구현) -5. [체크리스트](#5-체크리스트) +1. [컴포넌트별 테이블 설정 (핵심 원칙)](#1-컴포넌트별-테이블-설정-핵심-원칙) +2. [엔티티 조인 컬럼 활용 (필수)](#2-엔티티-조인-컬럼-활용-필수) +3. [폼 데이터 관리](#3-폼-데이터-관리) +4. [다국어 지원](#4-다국어-지원) +5. [컬럼 설정 패널 구현](#5-컬럼-설정-패널-구현) +6. [체크리스트](#6-체크리스트) --- -## 1. 엔티티 조인 컬럼 활용 (필수) +## 1. 컴포넌트별 테이블 설정 (핵심 원칙) + +### 핵심 원칙 + +**하나의 화면에서 여러 테이블을 다룰 수 있습니다.** + +화면 생성 시 "메인 테이블"을 필수로 지정하지 않으며, 컴포넌트별로 사용할 테이블을 지정할 수 있습니다. + +### 왜 필요한가? + +일반적인 ERP 화면에서는 여러 테이블이 동시에 필요합니다: + +| 예시: 입고 화면 | 테이블 | 용도 | +| --------------- | ----------------------- | ------------------------------- | +| 메인 폼 | `receiving_mng` | 입고 마스터 정보 입력/저장 | +| 조회 리스트 | `purchase_order_detail` | 발주 상세 목록 조회 (읽기 전용) | +| 입력 리피터 | `receiving_detail` | 입고 상세 항목 입력/저장 | + +### 컴포넌트 설정 패턴 + +#### 1. 테이블 리스트 (조회용) + +```typescript +interface TableListConfig { + // 조회용 테이블 (화면 메인 테이블과 다를 수 있음) + customTableName?: string; // 사용할 테이블명 + useCustomTable?: boolean; // true: customTableName 사용 + isReadOnly?: boolean; // true: 조회만, 저장 안 함 +} +``` + +#### 2. 리피터 (입력/저장용) + +```typescript +interface UnifiedRepeaterConfig { + // 저장 대상 테이블 (화면 메인 테이블과 다를 수 있음) + mainTableName?: string; // 저장할 테이블명 + useCustomTable?: boolean; // true: mainTableName 사용 + + // FK 자동 연결 (마스터-디테일 관계) + foreignKeyColumn?: string; // 이 테이블의 FK 컬럼 (예: receiving_id) + foreignKeySourceColumn?: string; // 마스터 테이블의 PK 컬럼 (예: id) +} +``` + +### 저장 테이블 설정 UI 표준 + +리피터 등 저장 기능이 있는 컴포넌트의 ConfigPanel에서: + +```tsx +// 1. 테이블 선택 Combobox + + + + + + + + + {/* 그룹 1: 현재 화면 테이블 (기본) */} + + + + {currentTableName} + + + + {/* 그룹 2: 연관 테이블 (FK 자동 설정) */} + {relatedTables.length > 0 && ( + + {relatedTables.map((table) => ( + + + {table.tableName} + + FK: {table.foreignKeyColumn} + + + ))} + + )} + + {/* 그룹 3: 전체 테이블 */} + + {allTables.map((table) => ( + + {table.displayName || table.tableName} + + ))} + + + + +; + +// 2. 연관 테이블 선택 시 FK/PK 자동 설정 +const handleSaveTableSelect = (tableName: string) => { + const relation = relatedTables.find((r) => r.tableName === tableName); + + if (relation) { + // 엔티티 관계에서 자동으로 FK/PK 가져옴 + updateConfig({ + useCustomTable: true, + mainTableName: tableName, + foreignKeyColumn: relation.foreignKeyColumn, + foreignKeySourceColumn: relation.referenceColumn, + }); + } else { + // 연관 테이블이 아니면 수동 입력 필요 + updateConfig({ + useCustomTable: true, + mainTableName: tableName, + foreignKeyColumn: undefined, + foreignKeySourceColumn: undefined, + }); + } +}; +``` + +### 연관 테이블 조회 API + +엔티티 관계에서 현재 테이블을 참조하는 테이블 목록을 조회합니다: + +```typescript +// API 호출 +const response = await apiClient.get( + `/api/table-management/columns/${currentTableName}/referenced-by` +); + +// 응답 +{ + success: true, + data: [ + { + tableName: "receiving_detail", // 참조하는 테이블 + columnName: "receiving_id", // FK 컬럼 + referenceColumn: "id", // 참조되는 컬럼 (PK) + }, + // ... + ] +} +``` + +### FK 자동 연결 동작 + +마스터 저장 후 디테일 저장 시 FK가 자동으로 설정됩니다: + +```typescript +// 1. 마스터 저장 이벤트 발생 (ButtonConfigPanel에서) +window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + masterRecordId: savedId, // 마스터 테이블에 저장된 ID + tableName: "receiving_mng", + mainFormData: formData, + }, + }) +); + +// 2. 리피터에서 이벤트 수신 및 FK 설정 +useEffect(() => { + const handleSaveEvent = (event: CustomEvent) => { + const { masterRecordId } = event.detail; + + if (config.foreignKeyColumn && masterRecordId) { + // 모든 행에 FK 값 자동 설정 + const updatedRows = rows.map((row) => ({ + ...row, + [config.foreignKeyColumn]: masterRecordId, + })); + + // 저장 실행 + saveRows(updatedRows); + } + }; + + window.addEventListener("repeaterSave", handleSaveEvent); + return () => window.removeEventListener("repeaterSave", handleSaveEvent); +}, [config.foreignKeyColumn, rows]); +``` + +### 저장 테이블 변경 시 컬럼 자동 로드 + +저장 테이블이 변경되면 해당 테이블의 컬럼이 자동으로 로드됩니다: + +```typescript +// 저장 테이블 또는 화면 테이블 기준으로 컬럼 로드 +const targetTableForColumns = + config.useCustomTable && config.mainTableName + ? config.mainTableName + : currentTableName; + +useEffect(() => { + const loadColumns = async () => { + if (!targetTableForColumns) return; + + const columnData = await tableTypeApi.getColumns(targetTableForColumns); + setCurrentTableColumns(columnData); + }; + + loadColumns(); +}, [targetTableForColumns]); +``` + +### 요약 + +| 상황 | 처리 방법 | +| ------------------------------------- | ----------------------------------- | +| 화면과 같은 테이블에 저장 | `useCustomTable: false` (기본값) | +| 다른 테이블에 저장 + 엔티티 관계 있음 | 연관 테이블 선택 → FK/PK 자동 설정 | +| 다른 테이블에 저장 + 엔티티 관계 없음 | 전체 테이블에서 선택 → FK 수동 입력 | +| 조회만 (저장 안 함) | `isReadOnly: true` 설정 | + +--- + +## 2. 엔티티 조인 컬럼 활용 (필수) ### 핵심 원칙 @@ -283,7 +506,7 @@ const getEntityJoinValue = (item: any, columnName: string): any => { --- -## 2. 폼 데이터 관리 +## 3. 폼 데이터 관리 ### 통합 폼 시스템 (UnifiedFormContext) @@ -368,7 +591,7 @@ const handleChange = useCallback( --- -## 3. 다국어 지원 +## 4. 다국어 지원 ### 타입 정의 시 다국어 필드 추가 @@ -534,7 +757,7 @@ if (comp.componentType === "my-new-component") { --- -## 4. 컬럼 설정 패널 구현 +## 5. 컬럼 설정 패널 구현 ### 필수 구조 @@ -639,10 +862,19 @@ export const MyComponentConfigPanel: React.FC = ({ --- -## 5. 체크리스트 +## 6. 체크리스트 새 컴포넌트 개발 시 다음 항목을 확인하세요: +### 컴포넌트별 테이블 설정 (핵심) + +- [ ] 화면 메인 테이블과 다른 테이블을 사용할 수 있는지 확인 +- [ ] `useCustomTable`, `mainTableName` (또는 `customTableName`) 설정 지원 +- [ ] 연관 테이블 선택 시 FK/PK 자동 설정 (`/api/table-management/columns/:tableName/referenced-by` API 활용) +- [ ] 저장 테이블 변경 시 해당 테이블의 컬럼 자동 로드 +- [ ] 테이블 선택 UI는 Combobox 형태로 그룹별 표시 (기본/연관/전체) +- [ ] FK 자동 연결: `repeaterSave` 이벤트에서 `masterRecordId` 수신 및 적용 + ### 엔티티 조인 (필수) - [ ] `entityJoinApi.getEntityJoinColumns()` 호출하여 조인 컬럼 로드 diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index cb28c873..e57e8295 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -97,10 +97,10 @@ export async function getColumnList( } const tableManagementService = new TableManagementService(); - + // 🔥 캐시 버스팅: _t 파라미터가 있으면 캐시 무시 const bustCache = !!req.query._t; - + const result = await tableManagementService.getColumnList( tableName, parseInt(page as string), @@ -2376,3 +2376,90 @@ export async function getTableEntityRelations( res.status(500).json(response); } } + +/** + * 현재 테이블을 참조(FK로 연결)하는 테이블 목록 조회 + * GET /api/table-management/columns/:tableName/referenced-by + * + * column_labels에서 reference_table이 현재 테이블인 레코드를 찾아서 + * 해당 테이블과 FK 컬럼 정보를 반환합니다. + */ +export async function getReferencedByTables( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName } = req.params; + + logger.info( + `=== 테이블 참조 관계 조회 시작: ${tableName} 을 참조하는 테이블 ===` + ); + + if (!tableName) { + const response: ApiResponse = { + success: false, + message: "tableName 파라미터가 필요합니다.", + error: { + code: "MISSING_PARAMETERS", + details: "tableName 경로 파라미터가 필요합니다.", + }, + }; + res.status(400).json(response); + return; + } + + // column_labels에서 reference_table이 현재 테이블인 레코드 조회 + // input_type이 'entity'인 것만 조회 (실제 FK 관계) + const sqlQuery = ` + SELECT DISTINCT + cl.table_name, + cl.column_name, + cl.column_label, + cl.reference_table, + cl.reference_column, + cl.display_column, + cl.table_name as table_label + FROM column_labels cl + WHERE cl.reference_table = $1 + AND cl.input_type = 'entity' + ORDER BY cl.table_name, cl.column_name + `; + + const result = await query(sqlQuery, [tableName]); + + const referencedByTables = result.map((row: any) => ({ + tableName: row.table_name, + tableLabel: row.table_label, + columnName: row.column_name, + columnLabel: row.column_label, + referenceTable: row.reference_table, + referenceColumn: row.reference_column || "id", + displayColumn: row.display_column, + })); + + logger.info( + `테이블 참조 관계 조회 완료: ${referencedByTables.length}개 발견` + ); + + const response: ApiResponse = { + success: true, + message: `${referencedByTables.length}개의 테이블이 ${tableName}을 참조합니다.`, + data: referencedByTables, + }; + + res.status(200).json(response); + } catch (error) { + logger.error("테이블 참조 관계 조회 중 오류 발생:", error); + + const response: ApiResponse = { + success: false, + message: "테이블 참조 관계 조회 중 오류가 발생했습니다.", + error: { + code: "REFERENCED_BY_ERROR", + details: error instanceof Error ? error.message : "Unknown error", + }, + }; + + res.status(500).json(response); + } +} diff --git a/backend-node/src/routes/tableManagementRoutes.ts b/backend-node/src/routes/tableManagementRoutes.ts index fa7832ee..1af6d87d 100644 --- a/backend-node/src/routes/tableManagementRoutes.ts +++ b/backend-node/src/routes/tableManagementRoutes.ts @@ -26,6 +26,7 @@ import { getCategoryColumnsByMenu, // 🆕 메뉴별 카테고리 컬럼 조회 multiTableSave, // 🆕 범용 다중 테이블 저장 getTableEntityRelations, // 🆕 두 테이블 간 엔티티 관계 조회 + getReferencedByTables, // 🆕 현재 테이블을 참조하는 테이블 조회 } from "../controllers/tableManagementController"; const router = express.Router(); @@ -54,6 +55,14 @@ router.get("/tables/entity-relations", getTableEntityRelations); */ router.get("/tables/:tableName/columns", getColumnList); +/** + * 현재 테이블을 참조하는 테이블 목록 조회 + * GET /api/table-management/columns/:tableName/referenced-by + * + * 리피터 컴포넌트에서 저장 테이블 선택 시 FK 관계를 자동으로 가져오기 위해 사용 + */ +router.get("/columns/:tableName/referenced-by", getReferencedByTables); + /** * 테이블 라벨 설정 * PUT /api/table-management/tables/:tableName/label diff --git a/frontend/components/screen/CreateScreenModal.tsx b/frontend/components/screen/CreateScreenModal.tsx index fc39140d..e26a86d7 100644 --- a/frontend/components/screen/CreateScreenModal.tsx +++ b/frontend/components/screen/CreateScreenModal.tsx @@ -185,16 +185,18 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre } }, [open, screenCode]); + // 테이블 선택은 선택 사항 - 컴포넌트별로 테이블을 설정할 수 있음 const isValid = useMemo(() => { const baseValid = screenName.trim().length > 0 && screenCode.trim().length > 0; if (dataSourceType === "database") { - return baseValid && tableName.trim().length > 0; + // 테이블 선택은 선택 사항 (비워두면 컴포넌트별로 테이블 설정) + return baseValid; } else { // REST API: 연결 선택 필수 return baseValid && selectedRestApiId !== null; } - }, [screenName, screenCode, tableName, dataSourceType, selectedRestApiId]); + }, [screenName, screenCode, dataSourceType, selectedRestApiId]); // 테이블 필터링 (내부 DB용) const filteredTables = useMemo(() => { @@ -230,8 +232,8 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre }; if (dataSourceType === "database") { - // 데이터베이스 소스 - createData.tableName = tableName.trim(); + // 데이터베이스 소스 - 테이블 선택은 선택 사항 + createData.tableName = tableName.trim() || null; // 비어있으면 null createData.dbSourceType = selectedDbSource === "internal" ? "internal" : "external"; createData.dbConnectionId = selectedDbSource === "internal" ? undefined : Number(selectedDbSource); } else { @@ -507,7 +509,10 @@ export default function CreateScreenModal({ open, onOpenChange, onCreated }: Cre {/* 테이블 선택 (데이터베이스 모드일 때만) */} {dataSourceType === "database" && (
- + +

+ 비워두면 화면 디자이너에서 컴포넌트별로 테이블을 설정할 수 있습니다. +

setTableSearchTerm(e.target.value)} + autoFocus + className="w-full rounded-md border px-8 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + {tableSearchTerm && ( + + )} +
+ + + {/* 테이블 목록 */} +
+ {loadingAllTables ? ( +
로드 중...
+ ) : filteredAllTables.length === 0 ? ( +
+ {tableSearchTerm ? "검색 결과 없음" : "테이블 없음"} +
+ ) : ( + filteredAllTables.map((t) => ( + + )) + )} +
+ + )} + + + {/* 현재 테이블 정보 */} + {tables.length > 0 && ( +
+ 현재: {tables[0]?.tableLabel || tables[0]?.tableName} +
+ )} + + )} + {/* 테이블과 컬럼 평면 목록 */}
diff --git a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx index 6bfdc27c..85015604 100644 --- a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx +++ b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx @@ -29,8 +29,24 @@ import { Eye, EyeOff, Wand2, + Check, + ChevronsUpDown, } from "lucide-react"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { tableTypeApi } from "@/lib/api/screen"; +import { tableManagementApi } from "@/lib/api/tableManagement"; import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule"; import { NumberingRuleConfig } from "@/types/numbering-rule"; import { cn } from "@/lib/utils"; @@ -42,6 +58,14 @@ import { MODAL_SIZE_OPTIONS, } from "@/types/unified-repeater"; +// 테이블 엔티티 관계 정보 +interface TableRelation { + tableName: string; + tableLabel: string; + foreignKeyColumn: string; // 저장 테이블의 FK 컬럼 + referenceColumn: string; // 마스터 테이블의 PK 컬럼 +} + interface UnifiedRepeaterConfigPanelProps { config: UnifiedRepeaterConfig; onChange: (config: UnifiedRepeaterConfig) => void; @@ -117,6 +141,13 @@ export const UnifiedRepeaterConfigPanel: React.FC>([]); + const [loadingTables, setLoadingTables] = useState(false); + const [relatedTables, setRelatedTables] = useState([]); // 현재 테이블과 연관된 테이블 목록 + const [loadingRelations, setLoadingRelations] = useState(false); + const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 테이블 Combobox 열림 상태 + // 🆕 확장된 컬럼 (상세 설정 표시용) const [expandedColumn, setExpandedColumn] = useState(null); @@ -199,6 +230,60 @@ export const UnifiedRepeaterConfigPanel: React.FC { + const loadTables = async () => { + setLoadingTables(true); + try { + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + setAllTables(response.data.map((t: any) => ({ + tableName: t.tableName || t.table_name, + displayName: t.displayName || t.table_label || t.tableName || t.table_name, + }))); + } + } catch (error) { + console.error("테이블 목록 로드 실패:", error); + } finally { + setLoadingTables(false); + } + }; + loadTables(); + }, []); + + // 현재 테이블과 연관된 테이블 목록 로드 (엔티티 관계 기반) + useEffect(() => { + const loadRelatedTables = async () => { + if (!currentTableName) { + setRelatedTables([]); + return; + } + + setLoadingRelations(true); + try { + // column_labels에서 현재 테이블을 reference_table로 참조하는 테이블 찾기 + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`); + + if (response.data.success && response.data.data) { + const relations: TableRelation[] = response.data.data.map((rel: any) => ({ + tableName: rel.tableName || rel.table_name, + tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name, + foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼 + referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼 + })); + setRelatedTables(relations); + } + } catch (error) { + console.error("연관 테이블 로드 실패:", error); + setRelatedTables([]); + } finally { + setLoadingRelations(false); + } + }; + loadRelatedTables(); + }, [currentTableName]); + // 설정 업데이트 헬퍼 const updateConfig = useCallback( (updates: Partial) => { @@ -234,10 +319,50 @@ export const UnifiedRepeaterConfigPanel: React.FC { + // 빈 값 선택 시 (현재 테이블로 복원) + if (!tableName || tableName === currentTableName) { + updateConfig({ + useCustomTable: false, + mainTableName: undefined, + foreignKeyColumn: undefined, + foreignKeySourceColumn: undefined, + }); + return; + } + + // 연관 테이블에서 FK 관계 찾기 + const relation = relatedTables.find(r => r.tableName === tableName); + + if (relation) { + // 엔티티 관계가 있으면 자동으로 FK/PK 설정 + updateConfig({ + useCustomTable: true, + mainTableName: tableName, + foreignKeyColumn: relation.foreignKeyColumn, + foreignKeySourceColumn: relation.referenceColumn, + }); + } else { + // 엔티티 관계가 없으면 직접 입력 필요 + updateConfig({ + useCustomTable: true, + mainTableName: tableName, + foreignKeyColumn: undefined, + foreignKeySourceColumn: "id", + }); + } + }, [currentTableName, relatedTables, updateConfig]); + + // 저장 테이블 컬럼 로드 (저장 테이블이 설정되면 해당 테이블, 아니면 현재 화면 테이블) + // 실제 저장할 테이블의 컬럼을 보여줘야 함 + const targetTableForColumns = config.useCustomTable && config.mainTableName + ? config.mainTableName + : currentTableName; + useEffect(() => { const loadCurrentTableColumns = async () => { - if (!currentTableName) { + if (!targetTableForColumns) { setCurrentTableColumns([]); setEntityColumns([]); return; @@ -245,7 +370,7 @@ export const UnifiedRepeaterConfigPanel: React.FC { @@ -529,97 +654,185 @@ export const UnifiedRepeaterConfigPanel: React.FC - {/* 저장 대상 테이블 설정 */} + {/* 저장 대상 테이블 */}
- -

- 화면 메인 테이블과 다른 테이블에 저장할 경우 설정 -

+ -
- { - if (!checked) { - updateConfig({ - useCustomTable: false, - mainTableName: undefined, - foreignKeyColumn: undefined, - foreignKeySourceColumn: undefined, - }); - } else { - updateConfig({ useCustomTable: true }); - } - }} - /> - + {/* 현재 선택된 테이블 표시 (기존 테이블 UI와 동일한 스타일) */} +
+
+ +
+

+ {config.useCustomTable && config.mainTableName + ? (allTables.find(t => t.tableName === config.mainTableName)?.displayName || config.mainTableName) + : (currentTableName || "미설정") + } +

+ {config.useCustomTable && config.mainTableName && config.foreignKeyColumn && ( +

+ FK: {config.foreignKeyColumn} → {currentTableName}.{config.foreignKeySourceColumn || "id"} +

+ )} + {!config.useCustomTable && currentTableName && ( +

화면 메인 테이블

+ )} +
+
- {config.useCustomTable && ( -
- {/* 저장 테이블 선택 */} -
- - - updateConfig({ mainTableName: e.target.value })} - placeholder="테이블명 직접 입력 (예: receiving_detail)" - className="h-7 text-xs" - /> -
- - {/* FK 컬럼 설정 */} + {/* 테이블 변경 Combobox */} + + + + + + + + + + 테이블을 찾을 수 없습니다. + + + {/* 현재 테이블 (기본) */} + {currentTableName && ( + + { + handleSaveTableSelect(currentTableName); + setTableComboboxOpen(false); + }} + className="text-xs" + > + + + {currentTableName} + (기본) + + + )} + + {/* 연관 테이블 (엔티티 관계) */} + {relatedTables.length > 0 && ( + + {relatedTables.map((rel) => ( + { + handleSaveTableSelect(rel.tableName); + setTableComboboxOpen(false); + }} + className="text-xs" + > + + + {rel.tableLabel} + + ({rel.foreignKeyColumn}) + + + ))} + + )} + + {/* 전체 테이블 목록 */} + + {allTables + .filter(t => t.tableName !== currentTableName && !relatedTables.some(r => r.tableName === t.tableName)) + .map((table) => ( + { + handleSaveTableSelect(table.tableName); + setTableComboboxOpen(false); + }} + className="text-xs" + > + + + {table.displayName} + + )) + } + + + + + + + {/* FK 직접 입력 (연관 테이블이 아닌 경우만) */} + {config.useCustomTable && config.mainTableName && + !relatedTables.some(r => r.tableName === config.mainTableName) && ( +
+

+ 엔티티 관계가 설정되지 않은 테이블입니다. FK 컬럼을 직접 입력하세요. +

- + updateConfig({ foreignKeyColumn: e.target.value })} - placeholder="예: receiving_id" + placeholder="예: master_id" className="h-7 text-xs" />
- + updateConfig({ foreignKeySourceColumn: e.target.value })} - placeholder="예: id" + placeholder="id" className="h-7 text-xs" />
- - {config.mainTableName && config.foreignKeyColumn && ( -
- 저장 흐름: {config.mainTableName}.{config.foreignKeyColumn} → - {currentTableName}.{config.foreignKeySourceColumn || "id"} (자동 연결) -
- )} -
- )} - - {!config.useCustomTable && ( -
- 현재: 화면 메인 테이블 ({currentTableName || "미설정"})에 저장
)}
@@ -809,10 +1022,10 @@ export const UnifiedRepeaterConfigPanel: React.FC )} - {/* 현재 테이블 컬럼 (입력용) */} + {/* 저장 테이블 컬럼 (입력용) */}
- 현재 테이블 ({currentTableName || "미선택"}) - 입력용 + 저장 테이블 ({targetTableForColumns || "미선택"}) - 입력용
{loadingColumns ? (

로딩 중...