diff --git a/backend-node/src/controllers/entitySearchController.ts b/backend-node/src/controllers/entitySearchController.ts index 4d911c57..5f198c3f 100644 --- a/backend-node/src/controllers/entitySearchController.ts +++ b/backend-node/src/controllers/entitySearchController.ts @@ -107,14 +107,88 @@ export async function searchEntity(req: AuthenticatedRequest, res: Response) { } // 추가 필터 조건 (존재하는 컬럼만) + // 지원 연산자: =, !=, >, <, >=, <=, in, notIn, like + // 특수 키 형식: column__operator (예: division__in, name__like) const additionalFilter = JSON.parse(filterCondition as string); for (const [key, value] of Object.entries(additionalFilter)) { - if (existingColumns.has(key)) { - whereConditions.push(`${key} = $${paramIndex}`); - params.push(value); - paramIndex++; - } else { - logger.warn("엔티티 검색: 필터 조건에 존재하지 않는 컬럼 제외", { tableName, key }); + // 특수 키 형식 파싱: column__operator + let columnName = key; + let operator = "="; + + if (key.includes("__")) { + const parts = key.split("__"); + columnName = parts[0]; + operator = parts[1] || "="; + } + + if (!existingColumns.has(columnName)) { + logger.warn("엔티티 검색: 필터 조건에 존재하지 않는 컬럼 제외", { tableName, key, columnName }); + continue; + } + + // 연산자별 WHERE 조건 생성 + switch (operator) { + case "=": + whereConditions.push(`"${columnName}" = $${paramIndex}`); + params.push(value); + paramIndex++; + break; + case "!=": + whereConditions.push(`"${columnName}" != $${paramIndex}`); + params.push(value); + paramIndex++; + break; + case ">": + whereConditions.push(`"${columnName}" > $${paramIndex}`); + params.push(value); + paramIndex++; + break; + case "<": + whereConditions.push(`"${columnName}" < $${paramIndex}`); + params.push(value); + paramIndex++; + break; + case ">=": + whereConditions.push(`"${columnName}" >= $${paramIndex}`); + params.push(value); + paramIndex++; + break; + case "<=": + whereConditions.push(`"${columnName}" <= $${paramIndex}`); + params.push(value); + paramIndex++; + break; + case "in": + // IN 연산자: 값이 배열이거나 쉼표로 구분된 문자열 + const inValues = Array.isArray(value) ? value : String(value).split(",").map(v => v.trim()); + if (inValues.length > 0) { + const placeholders = inValues.map((_, i) => `$${paramIndex + i}`).join(", "); + whereConditions.push(`"${columnName}" IN (${placeholders})`); + params.push(...inValues); + paramIndex += inValues.length; + } + break; + case "notIn": + // NOT IN 연산자 + const notInValues = Array.isArray(value) ? value : String(value).split(",").map(v => v.trim()); + if (notInValues.length > 0) { + const placeholders = notInValues.map((_, i) => `$${paramIndex + i}`).join(", "); + whereConditions.push(`"${columnName}" NOT IN (${placeholders})`); + params.push(...notInValues); + paramIndex += notInValues.length; + } + break; + case "like": + whereConditions.push(`"${columnName}"::text ILIKE $${paramIndex}`); + params.push(`%${value}%`); + paramIndex++; + break; + default: + // 알 수 없는 연산자는 등호로 처리 + whereConditions.push(`"${columnName}" = $${paramIndex}`); + params.push(value); + paramIndex++; + break; } } diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 7c84898b..83384be6 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -775,7 +775,8 @@ export async function getTableData( const userField = autoFilter?.userField || "companyCode"; const userValue = (req.user as any)[userField]; - if (userValue) { + // 🆕 최고 관리자(company_code = '*')는 모든 회사 데이터 조회 가능 + if (userValue && userValue !== "*") { enhancedSearch[filterColumn] = userValue; logger.info("🔍 현재 사용자 필터 적용:", { @@ -784,6 +785,10 @@ export async function getTableData( userValue, tableName, }); + } else if (userValue === "*") { + logger.info("🔓 최고 관리자 - 회사 필터 미적용 (모든 회사 데이터 조회)", { + tableName, + }); } else { logger.warn("⚠️ 사용자 정보 필드 값 없음:", { userField, @@ -792,6 +797,9 @@ export async function getTableData( } } + // 🆕 최종 검색 조건 로그 + logger.info(`🔍 최종 검색 조건 (enhancedSearch):`, JSON.stringify(enhancedSearch)); + // 데이터 조회 const result = await tableManagementService.getTableData(tableName, { page: parseInt(page), diff --git a/backend-node/src/routes/cascadingAutoFillRoutes.ts b/backend-node/src/routes/cascadingAutoFillRoutes.ts index a5107448..c1d69e9f 100644 --- a/backend-node/src/routes/cascadingAutoFillRoutes.ts +++ b/backend-node/src/routes/cascadingAutoFillRoutes.ts @@ -55,3 +55,4 @@ export default router; + diff --git a/backend-node/src/routes/cascadingConditionRoutes.ts b/backend-node/src/routes/cascadingConditionRoutes.ts index 22cd2d2b..bbc9384d 100644 --- a/backend-node/src/routes/cascadingConditionRoutes.ts +++ b/backend-node/src/routes/cascadingConditionRoutes.ts @@ -51,3 +51,4 @@ export default router; + diff --git a/backend-node/src/routes/cascadingHierarchyRoutes.ts b/backend-node/src/routes/cascadingHierarchyRoutes.ts index 79a1c6e8..35ced071 100644 --- a/backend-node/src/routes/cascadingHierarchyRoutes.ts +++ b/backend-node/src/routes/cascadingHierarchyRoutes.ts @@ -67,3 +67,4 @@ export default router; + diff --git a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts index 352a05b5..29ac8ee4 100644 --- a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts +++ b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts @@ -55,3 +55,4 @@ export default router; + diff --git a/backend-node/src/services/adminService.ts b/backend-node/src/services/adminService.ts index 1b9280db..95d8befa 100644 --- a/backend-node/src/services/adminService.ts +++ b/backend-node/src/services/adminService.ts @@ -65,6 +65,13 @@ export class AdminService { } ); + // [임시 비활성화] 메뉴 권한 그룹 체크 - 모든 사용자에게 전체 메뉴 표시 + // TODO: 권한 체크 다시 활성화 필요 + logger.info( + `⚠️ [임시 비활성화] 권한 그룹 체크 스킵 - 사용자 ${userId}(${userType})에게 전체 메뉴 표시` + ); + + /* [원본 코드 - 권한 그룹 체크] if (userType === "COMPANY_ADMIN") { // 회사 관리자: 권한 그룹 기반 필터링 적용 if (userRoleGroups.length > 0) { @@ -141,6 +148,7 @@ export class AdminService { return []; } } + */ } else if ( menuType !== undefined && userType === "SUPER_ADMIN" && @@ -412,6 +420,15 @@ export class AdminService { let queryParams: any[] = [userLang]; let paramIndex = 2; + // [임시 비활성화] 메뉴 권한 그룹 체크 - 모든 사용자에게 전체 메뉴 표시 + // TODO: 권한 체크 다시 활성화 필요 + logger.info( + `⚠️ [임시 비활성화] getUserMenuList 권한 그룹 체크 스킵 - 사용자 ${userId}(${userType})에게 전체 메뉴 표시` + ); + authFilter = ""; + unionFilter = ""; + + /* [원본 코드 - getUserMenuList 권한 그룹 체크] if (userType === "SUPER_ADMIN") { // SUPER_ADMIN: 권한 그룹 체크 없이 해당 회사의 모든 메뉴 표시 logger.info(`✅ 좌측 사이드바 (SUPER_ADMIN): 회사 ${userCompanyCode}의 모든 메뉴 표시`); @@ -471,6 +488,7 @@ export class AdminService { return []; } } + */ // 2. 회사별 필터링 조건 생성 let companyFilter = ""; diff --git a/docs/노드플로우_개선사항.md b/docs/노드플로우_개선사항.md index c9349b94..32757807 100644 --- a/docs/노드플로우_개선사항.md +++ b/docs/노드플로우_개선사항.md @@ -587,3 +587,4 @@ const result = await executeNodeFlow(flowId, { + diff --git a/docs/메일발송_기능_사용_가이드.md b/docs/메일발송_기능_사용_가이드.md index 42900211..8bfe484e 100644 --- a/docs/메일발송_기능_사용_가이드.md +++ b/docs/메일발송_기능_사용_가이드.md @@ -360,3 +360,4 @@ + diff --git a/docs/즉시저장_버튼_액션_구현_계획서.md b/docs/즉시저장_버튼_액션_구현_계획서.md index c392eece..8d8fb497 100644 --- a/docs/즉시저장_버튼_액션_구현_계획서.md +++ b/docs/즉시저장_버튼_액션_구현_계획서.md @@ -346,3 +346,4 @@ const getComponentValue = (componentId: string) => { + diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 3145d9d3..0327e122 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -127,3 +127,4 @@ export default function ScreenManagementPage() { ); } + diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index dffbd75b..9e92bf2b 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -104,7 +104,7 @@ function ScreenViewPage() { // 편집 모달 이벤트 리스너 등록 useEffect(() => { const handleOpenEditModal = (event: CustomEvent) => { - console.log("🎭 편집 모달 열기 이벤트 수신:", event.detail); + // console.log("🎭 편집 모달 열기 이벤트 수신:", event.detail); setEditModalConfig({ screenId: event.detail.screenId, diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index cceadae9..f41c62af 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -261,7 +261,7 @@ export const ScreenModal: React.FC = ({ className }) => { // dataSourceId 파라미터 제거 currentUrl.searchParams.delete("dataSourceId"); window.history.pushState({}, "", currentUrl.toString()); - console.log("🧹 URL 파라미터 제거"); + // console.log("🧹 URL 파라미터 제거"); } setModalState({ @@ -277,7 +277,7 @@ export const ScreenModal: React.FC = ({ className }) => { setSelectedData([]); // 🆕 선택된 데이터 초기화 setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장 - console.log("🔄 연속 모드 초기화: false"); + // console.log("🔄 연속 모드 초기화: false"); }; // 저장 성공 이벤트 처리 (연속 등록 모드 지원) @@ -285,36 +285,36 @@ export const ScreenModal: React.FC = ({ className }) => { // 🆕 모달이 열린 후 500ms 이내의 저장 성공 이벤트는 무시 (이전 이벤트 방지) const timeSinceOpen = Date.now() - modalOpenedAtRef.current; if (timeSinceOpen < 500) { - console.log("⏭️ [ScreenModal] 모달 열린 직후 저장 성공 이벤트 무시:", { timeSinceOpen }); + // console.log("⏭️ [ScreenModal] 모달 열린 직후 저장 성공 이벤트 무시:", { timeSinceOpen }); return; } const isContinuousMode = continuousMode; - console.log("💾 저장 성공 이벤트 수신"); - console.log("📌 현재 연속 모드 상태:", isContinuousMode); - console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode")); + // console.log("💾 저장 성공 이벤트 수신"); + // console.log("📌 현재 연속 모드 상태:", isContinuousMode); + // console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode")); if (isContinuousMode) { // 연속 모드: 폼만 초기화하고 모달은 유지 - console.log("✅ 연속 모드 활성화 - 폼 초기화 및 화면 리셋"); + // console.log("✅ 연속 모드 활성화 - 폼 초기화 및 화면 리셋"); // 1. 폼 데이터 초기화 setFormData({}); // 2. 리셋 키 변경 (컴포넌트 강제 리마운트) setResetKey((prev) => prev + 1); - console.log("🔄 resetKey 증가 - 컴포넌트 리마운트"); + // console.log("🔄 resetKey 증가 - 컴포넌트 리마운트"); // 3. 화면 데이터 다시 로드 (채번 규칙 새로 생성) if (modalState.screenId) { - console.log("🔄 화면 데이터 다시 로드:", modalState.screenId); + // console.log("🔄 화면 데이터 다시 로드:", modalState.screenId); loadScreenData(modalState.screenId); } toast.success("저장되었습니다. 계속 입력하세요."); } else { // 일반 모드: 모달 닫기 - console.log("❌ 일반 모드 - 모달 닫기"); + // console.log("❌ 일반 모드 - 모달 닫기"); handleCloseModal(); } }; diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 58149088..32451d18 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -190,14 +190,14 @@ export const EditModal: React.FC = ({ className }) => { const innerLayoutData = await screenApi.getLayout(section.screenId); saveButton = findSaveButtonInComponents(innerLayoutData?.components || []); if (saveButton) { - console.log("[EditModal] 조건부 컨테이너 내부에서 저장 버튼 발견:", { - sectionScreenId: section.screenId, - sectionLabel: section.label, - }); + // console.log("[EditModal] 조건부 컨테이너 내부에서 저장 버튼 발견:", { + // sectionScreenId: section.screenId, + // sectionLabel: section.label, + // }); break; } } catch (innerError) { - console.warn("[EditModal] 내부 화면 레이아웃 조회 실패:", section.screenId); + // console.warn("[EditModal] 내부 화면 레이아웃 조회 실패:", section.screenId); } } } @@ -207,7 +207,7 @@ export const EditModal: React.FC = ({ className }) => { } if (!saveButton) { - console.log("[EditModal] 저장 버튼을 찾을 수 없음:", targetScreenId); + // console.log("[EditModal] 저장 버튼을 찾을 수 없음:", targetScreenId); return null; } @@ -219,14 +219,14 @@ export const EditModal: React.FC = ({ className }) => { dataflowConfig: webTypeConfig.dataflowConfig, dataflowTiming: webTypeConfig.dataflowConfig?.flowConfig?.executionTiming || "after", }; - console.log("[EditModal] 저장 버튼 제어로직 설정 발견:", config); + // console.log("[EditModal] 저장 버튼 제어로직 설정 발견:", config); return config; } - console.log("[EditModal] 저장 버튼에 제어로직 설정 없음"); + // console.log("[EditModal] 저장 버튼에 제어로직 설정 없음"); return null; } catch (error) { - console.warn("[EditModal] 저장 버튼 설정 조회 실패:", error); + // console.warn("[EditModal] 저장 버튼 설정 조회 실패:", error); return null; } }; @@ -309,16 +309,16 @@ export const EditModal: React.FC = ({ className }) => { // 🆕 그룹 데이터 조회 함수 const loadGroupData = async () => { if (!modalState.tableName || !modalState.groupByColumns || modalState.groupByColumns.length === 0) { - console.warn("테이블명 또는 그룹핑 컬럼이 없습니다."); + // console.warn("테이블명 또는 그룹핑 컬럼이 없습니다."); return; } try { - console.log("🔍 그룹 데이터 조회 시작:", { - tableName: modalState.tableName, - groupByColumns: modalState.groupByColumns, - editData: modalState.editData, - }); + // console.log("🔍 그룹 데이터 조회 시작:", { + // tableName: modalState.tableName, + // groupByColumns: modalState.groupByColumns, + // editData: modalState.editData, + // }); // 그룹핑 컬럼 값 추출 (예: order_no = "ORD-20251124-001") const groupValues: Record = {}; @@ -329,14 +329,14 @@ export const EditModal: React.FC = ({ className }) => { }); if (Object.keys(groupValues).length === 0) { - console.warn("그룹핑 컬럼 값이 없습니다:", modalState.groupByColumns); + // console.warn("그룹핑 컬럼 값이 없습니다:", modalState.groupByColumns); return; } - console.log("🔍 그룹 조회 요청:", { - tableName: modalState.tableName, - groupValues, - }); + // console.log("🔍 그룹 조회 요청:", { + // tableName: modalState.tableName, + // groupValues, + // }); // 같은 그룹의 모든 레코드 조회 (entityJoinApi 사용) const { entityJoinApi } = await import("@/lib/api/entityJoin"); @@ -347,13 +347,13 @@ export const EditModal: React.FC = ({ className }) => { enableEntityJoin: true, }); - console.log("🔍 그룹 조회 응답:", response); + // console.log("🔍 그룹 조회 응답:", response); // entityJoinApi는 배열 또는 { data: [] } 형식으로 반환 const dataArray = Array.isArray(response) ? response : response?.data || []; if (dataArray.length > 0) { - console.log("✅ 그룹 데이터 조회 성공:", dataArray.length, "건"); + // console.log("✅ 그룹 데이터 조회 성공:", dataArray.length, "건"); setGroupData(dataArray); setOriginalGroupData(JSON.parse(JSON.stringify(dataArray))); // Deep copy toast.info(`${dataArray.length}개의 관련 품목을 불러왔습니다.`); @@ -374,7 +374,7 @@ export const EditModal: React.FC = ({ className }) => { try { setLoading(true); - console.log("화면 데이터 로딩 시작:", screenId); + // console.log("화면 데이터 로딩 시작:", screenId); // 화면 정보와 레이아웃 데이터 로딩 const [screenInfo, layoutData] = await Promise.all([ @@ -382,7 +382,7 @@ export const EditModal: React.FC = ({ className }) => { screenApi.getLayout(screenId), ]); - console.log("API 응답:", { screenInfo, layoutData }); + // console.log("API 응답:", { screenInfo, layoutData }); if (screenInfo && layoutData) { const components = layoutData.components || []; @@ -395,11 +395,11 @@ export const EditModal: React.FC = ({ className }) => { components, screenInfo: screenInfo, }); - console.log("화면 데이터 설정 완료:", { - componentsCount: components.length, - dimensions, - screenInfo, - }); + // console.log("화면 데이터 설정 완료:", { + // componentsCount: components.length, + // dimensions, + // screenInfo, + // }); } else { throw new Error("화면 데이터가 없습니다"); } diff --git a/frontend/contexts/ActiveTabContext.tsx b/frontend/contexts/ActiveTabContext.tsx index 228dc990..35081225 100644 --- a/frontend/contexts/ActiveTabContext.tsx +++ b/frontend/contexts/ActiveTabContext.tsx @@ -140,3 +140,4 @@ export const useActiveTabOptional = () => { + diff --git a/frontend/hooks/useAutoFill.ts b/frontend/hooks/useAutoFill.ts index caa1e826..7d78322b 100644 --- a/frontend/hooks/useAutoFill.ts +++ b/frontend/hooks/useAutoFill.ts @@ -197,3 +197,4 @@ export function applyAutoFillToFormData( + diff --git a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx index f311c035..86155bd6 100644 --- a/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx +++ b/frontend/lib/registry/components/button-primary/ButtonPrimaryComponent.tsx @@ -299,6 +299,20 @@ export const ButtonPrimaryComponent: React.FC = ({ // 🆕 modalDataStore에서 선택된 데이터 확인 (분할 패널 등에서 저장됨) const [modalStoreData, setModalStoreData] = useState>({}); + // 🆕 splitPanelContext?.selectedLeftData를 로컬 상태로 추적 (리렌더링 보장) + const [trackedSelectedLeftData, setTrackedSelectedLeftData] = useState | null>(null); + + // splitPanelContext?.selectedLeftData 변경 감지 및 로컬 상태 동기화 + useEffect(() => { + const newData = splitPanelContext?.selectedLeftData ?? null; + setTrackedSelectedLeftData(newData); + // console.log("🔄 [ButtonPrimary] selectedLeftData 변경 감지:", { + // label: component.label, + // hasData: !!newData, + // dataKeys: newData ? Object.keys(newData) : [], + // }); + }, [splitPanelContext?.selectedLeftData, component.label]); + // modalDataStore 상태 구독 (실시간 업데이트) useEffect(() => { const actionConfig = component.componentConfig?.action; @@ -357,8 +371,8 @@ export const ButtonPrimaryComponent: React.FC = ({ // 2. 분할 패널 좌측 선택 데이터 확인 if (rowSelectionSource === "auto" || rowSelectionSource === "splitPanelLeft") { - // SplitPanelContext에서 확인 - if (splitPanelContext?.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0) { + // SplitPanelContext에서 확인 (trackedSelectedLeftData 사용으로 리렌더링 보장) + if (trackedSelectedLeftData && Object.keys(trackedSelectedLeftData).length > 0) { if (!hasSelection) { hasSelection = true; selectionCount = 1; @@ -397,7 +411,7 @@ export const ButtonPrimaryComponent: React.FC = ({ selectionCount, selectionSource, hasSplitPanelContext: !!splitPanelContext, - selectedLeftData: splitPanelContext?.selectedLeftData, + trackedSelectedLeftData: trackedSelectedLeftData, selectedRowsData: selectedRowsData?.length, selectedRows: selectedRows?.length, flowSelectedData: flowSelectedData?.length, @@ -429,7 +443,7 @@ export const ButtonPrimaryComponent: React.FC = ({ component.label, selectedRows, selectedRowsData, - splitPanelContext?.selectedLeftData, + trackedSelectedLeftData, flowSelectedData, splitPanelContext, modalStoreData, diff --git a/frontend/lib/registry/components/entity-search-input/useEntitySearch.ts b/frontend/lib/registry/components/entity-search-input/useEntitySearch.ts index 1fac26d6..a118cffe 100644 --- a/frontend/lib/registry/components/entity-search-input/useEntitySearch.ts +++ b/frontend/lib/registry/components/entity-search-input/useEntitySearch.ts @@ -53,6 +53,12 @@ export function useEntitySearch({ limit: pagination.limit.toString(), }); + // console.log("[useEntitySearch] 검색 실행:", { + // tableName, + // filterCondition: filterConditionRef.current, + // searchText: text, + // }); + const response = await apiClient.get( `/entity-search/${tableName}?${params.toString()}` ); diff --git a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx index ad73c317..1eca9fab 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx @@ -32,6 +32,7 @@ export function ItemSelectionModal({ onSelect, columnLabels = {}, modalFilters = [], + categoryColumns = [], }: ItemSelectionModalProps) { const [localSearchText, setLocalSearchText] = useState(""); const [selectedItems, setSelectedItems] = useState([]); @@ -41,6 +42,9 @@ export function ItemSelectionModal({ // 카테고리 옵션 상태 (categoryRef별로 로드된 옵션) const [categoryOptions, setCategoryOptions] = useState>({}); + + // 카테고리 코드 → 라벨 매핑 (테이블 데이터 표시용) + const [categoryLabelMap, setCategoryLabelMap] = useState>({}); // 모달 필터 값과 기본 filterCondition을 합친 최종 필터 조건 const combinedFilterCondition = useMemo(() => { @@ -152,6 +156,54 @@ export function ItemSelectionModal({ } }, [modalFilterValues]); + // 검색 결과가 변경되면 카테고리 값들의 라벨 조회 + useEffect(() => { + const loadCategoryLabels = async () => { + if (!open || categoryColumns.length === 0 || results.length === 0) { + return; + } + + // 현재 결과에서 카테고리 컬럼의 모든 고유한 값 수집 + // 쉼표로 구분된 다중 값도 개별적으로 수집 + const allCodes = new Set(); + for (const row of results) { + for (const col of categoryColumns) { + const val = row[col]; + if (val && typeof val === "string") { + // 쉼표로 구분된 다중 값 처리 + const codes = val.split(",").map((c) => c.trim()).filter(Boolean); + for (const code of codes) { + if (!categoryLabelMap[code]) { + allCodes.add(code); + } + } + } + } + } + + if (allCodes.size === 0) { + return; + } + + try { + const response = await apiClient.post("/table-categories/labels-by-codes", { + valueCodes: Array.from(allCodes), + }); + + if (response.data?.success && response.data.data) { + setCategoryLabelMap((prev) => ({ + ...prev, + ...response.data.data, + })); + } + } catch (error) { + console.error("카테고리 라벨 조회 실패:", error); + } + }; + + loadCategoryLabels(); + }, [open, results, categoryColumns]); + // 모달 필터 값 변경 핸들러 const handleModalFilterChange = (column: string, value: any) => { setModalFilterValues((prev) => ({ @@ -450,11 +502,25 @@ export function ItemSelectionModal({ )} - {validColumns.map((col) => ( - - {item[col] || "-"} - - ))} + {validColumns.map((col) => { + const rawValue = item[col]; + // 카테고리 컬럼이면 라벨로 변환 + const isCategory = categoryColumns.includes(col); + let displayValue = rawValue; + + if (isCategory && rawValue && typeof rawValue === "string") { + // 쉼표로 구분된 다중 값 처리 + const codes = rawValue.split(",").map((c) => c.trim()).filter(Boolean); + const labels = codes.map((code) => categoryLabelMap[code] || code); + displayValue = labels.join(", "); + } + + return ( + + {displayValue || "-"} + + ); + })} ); }) diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index ad373200..ba23c60e 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -202,4 +202,7 @@ export interface ItemSelectionModalProps { // 모달 내부 필터 (사용자 선택 가능) modalFilters?: ModalFilterConfig[]; + + // 카테고리 타입 컬럼 목록 (해당 컬럼은 코드 → 라벨로 변환하여 표시) + categoryColumns?: string[]; } diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalRenderer.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalRenderer.tsx index b43722d3..814e3545 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalRenderer.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalRenderer.tsx @@ -6,7 +6,7 @@ import { RepeatScreenModalDefinition } from "./index"; // 컴포넌트 자동 등록 if (typeof window !== "undefined") { ComponentRegistry.registerComponent(RepeatScreenModalDefinition); - console.log("✅ RepeatScreenModal 컴포넌트 등록 완료"); + // console.log("✅ RepeatScreenModal 컴포넌트 등록 완료"); } export {}; diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index bfb26c90..ad7f5302 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -205,12 +205,12 @@ export const SplitPanelLayoutComponent: React.FC const splitPanelId = `split-panel-${component.id}`; // 디버깅: Context 연결 상태 확인 - console.log("🔗 [SplitPanelLayout] Context 연결 상태:", { - componentId: component.id, - splitPanelId, - hasRegisterFunc: typeof ctxRegisterSplitPanel === "function", - splitPanelsSize: splitPanelContext.splitPanels?.size ?? "없음", - }); + // console.log("🔗 [SplitPanelLayout] Context 연결 상태:", { + // componentId: component.id, + // splitPanelId, + // hasRegisterFunc: typeof ctxRegisterSplitPanel === "function", + // splitPanelsSize: splitPanelContext.splitPanels?.size ?? "없음", + // }); // Context에 분할 패널 등록 (좌표 정보 포함) - 마운트 시 1회만 실행 const ctxRegisterRef = useRef(ctxRegisterSplitPanel); @@ -235,15 +235,15 @@ export const SplitPanelLayoutComponent: React.FC isDragging: false, }; - console.log("📦 [SplitPanelLayout] Context에 분할 패널 등록:", { - splitPanelId, - panelInfo, - }); + // console.log("📦 [SplitPanelLayout] Context에 분할 패널 등록:", { + // splitPanelId, + // panelInfo, + // }); ctxRegisterRef.current(splitPanelId, panelInfo); return () => { - console.log("📦 [SplitPanelLayout] Context에서 분할 패널 해제:", splitPanelId); + // console.log("📦 [SplitPanelLayout] Context에서 분할 패널 해제:", splitPanelId); ctxUnregisterRef.current(splitPanelId); }; // 마운트/언마운트 시에만 실행, 위치/크기 변경은 별도 업데이트로 처리 @@ -311,11 +311,11 @@ export const SplitPanelLayoutComponent: React.FC // 🆕 그룹별 합산된 데이터 계산 const summedLeftData = useMemo(() => { - console.log("🔍 [그룹합산] leftGroupSumConfig:", leftGroupSumConfig); + // console.log("🔍 [그룹합산] leftGroupSumConfig:", leftGroupSumConfig); // 그룹핑이 비활성화되었거나 그룹 기준 컬럼이 없으면 원본 데이터 반환 if (!leftGroupSumConfig?.enabled || !leftGroupSumConfig?.groupByColumn) { - console.log("🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환"); + // console.log("🔍 [그룹합산] 그룹핑 비활성화 - 원본 데이터 반환"); return leftData; } @@ -756,8 +756,8 @@ export const SplitPanelLayoutComponent: React.FC } }); - console.log("🔗 [분할패널] additionalJoinColumns:", additionalJoinColumns); - console.log("🔗 [분할패널] configuredColumns:", configuredColumns); + // console.log("🔗 [분할패널] additionalJoinColumns:", additionalJoinColumns); + // console.log("🔗 [분할패널] configuredColumns:", configuredColumns); const result = await entityJoinApi.getTableDataWithJoins(leftTableName, { page: 1, @@ -769,10 +769,10 @@ export const SplitPanelLayoutComponent: React.FC }); // 🔍 디버깅: API 응답 데이터의 키 확인 - if (result.data && result.data.length > 0) { - console.log("🔗 [분할패널] API 응답 첫 번째 데이터 키:", Object.keys(result.data[0])); - console.log("🔗 [분할패널] API 응답 첫 번째 데이터:", result.data[0]); - } + // if (result.data && result.data.length > 0) { + // console.log("🔗 [분할패널] API 응답 첫 번째 데이터 키:", Object.keys(result.data[0])); + // console.log("🔗 [분할패널] API 응답 첫 번째 데이터:", result.data[0]); + // } // 가나다순 정렬 (좌측 패널의 표시 컬럼 기준) const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; @@ -1000,7 +1000,7 @@ export const SplitPanelLayoutComponent: React.FC if (leftTableName && !isDesignMode) { import("@/stores/modalDataStore").then(({ useModalDataStore }) => { useModalDataStore.getState().setData(leftTableName, [item]); - console.log(`✅ 분할 패널 좌측 선택: ${leftTableName}`, item); + // console.log(`✅ 분할 패널 좌측 선택: ${leftTableName}`, item); }); } }, @@ -1198,7 +1198,7 @@ export const SplitPanelLayoutComponent: React.FC } }); setLeftColumnLabels(labels); - console.log("✅ 좌측 컬럼 라벨 로드:", labels); + // console.log("✅ 좌측 컬럼 라벨 로드:", labels); } catch (error) { console.error("좌측 테이블 컬럼 라벨 로드 실패:", error); } @@ -1227,7 +1227,7 @@ export const SplitPanelLayoutComponent: React.FC } }); setRightColumnLabels(labels); - console.log("✅ 우측 컬럼 라벨 로드:", labels); + // console.log("✅ 우측 컬럼 라벨 로드:", labels); } catch (error) { console.error("우측 테이블 컬럼 정보 로드 실패:", error); } @@ -1269,7 +1269,7 @@ export const SplitPanelLayoutComponent: React.FC }; }); mappings[columnName] = valueMap; - console.log(`✅ 좌측 카테고리 매핑 로드 [${columnName}]:`, valueMap); + // console.log(`✅ 좌측 카테고리 매핑 로드 [${columnName}]:`, valueMap); } } catch (error) { console.error(`좌측 카테고리 값 조회 실패 [${columnName}]:`, error); @@ -1307,7 +1307,7 @@ export const SplitPanelLayoutComponent: React.FC } }); - console.log("🔍 우측 패널 카테고리 로드 대상 테이블:", Array.from(tablesToLoad)); + // console.log("🔍 우측 패널 카테고리 로드 대상 테이블:", Array.from(tablesToLoad)); // 각 테이블에 대해 카테고리 매핑 로드 for (const tableName of tablesToLoad) { @@ -1940,7 +1940,7 @@ export const SplitPanelLayoutComponent: React.FC useEffect(() => { const handleRefreshTable = () => { if (!isDesignMode) { - console.log("🔄 [SplitPanel] refreshTable 이벤트 수신 - 데이터 새로고침"); + // console.log("🔄 [SplitPanel] refreshTable 이벤트 수신 - 데이터 새로고침"); loadLeftData(); // 선택된 항목이 있으면 우측 패널도 새로고침 if (selectedLeftItem) { @@ -2104,12 +2104,12 @@ export const SplitPanelLayoutComponent: React.FC (() => { // 🆕 그룹별 합산된 데이터 사용 const dataSource = summedLeftData; - console.log( - "🔍 [테이블모드 렌더링] dataSource 개수:", - dataSource.length, - "leftGroupSumConfig:", - leftGroupSumConfig, - ); + // console.log( + // "🔍 [테이블모드 렌더링] dataSource 개수:", + // dataSource.length, + // "leftGroupSumConfig:", + // leftGroupSumConfig, + // ); // 🔧 로컬 검색 필터 적용 const filteredData = leftSearchQuery diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 5f3f7c8d..24a93af8 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2043,7 +2043,7 @@ export const TableListComponent: React.FC = ({ return row.id || row.uuid || `row-${index}`; }; - const handleRowSelection = (rowKey: string, checked: boolean) => { + const handleRowSelection = (rowKey: string, checked: boolean, rowData?: any) => { const newSelectedRows = new Set(selectedRows); if (checked) { newSelectedRows.add(rowKey); @@ -2086,6 +2086,31 @@ export const TableListComponent: React.FC = ({ }); } + // 🆕 분할 패널 컨텍스트에 선택된 데이터 저장/해제 (체크박스 선택 시에도 작동) + const effectiveSplitPosition = splitPanelPosition || currentSplitPosition; + if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) { + if (checked && selectedRowsData.length > 0) { + // 선택된 경우: 첫 번째 선택된 데이터 저장 (또는 전달된 rowData) + const dataToStore = rowData || selectedRowsData[selectedRowsData.length - 1]; + splitPanelContext.setSelectedLeftData(dataToStore); + console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 저장:", { + rowKey, + dataToStore, + }); + } else if (!checked && selectedRowsData.length === 0) { + // 모든 선택이 해제된 경우: 데이터 초기화 + splitPanelContext.setSelectedLeftData(null); + console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 초기화"); + } else if (selectedRowsData.length > 0) { + // 일부 선택 해제된 경우: 남은 첫 번째 데이터로 업데이트 + splitPanelContext.setSelectedLeftData(selectedRowsData[0]); + console.log("🔗 [TableList] handleRowSelection - 분할 패널 좌측 데이터 업데이트:", { + remainingCount: selectedRowsData.length, + firstData: selectedRowsData[0], + }); + } + } + const allRowsSelected = filteredData.every((row, index) => newSelectedRows.has(getRowKey(row, index))); setIsAllSelected(allRowsSelected && filteredData.length > 0); }; @@ -2155,35 +2180,8 @@ export const TableListComponent: React.FC = ({ const rowKey = getRowKey(row, index); const isCurrentlySelected = selectedRows.has(rowKey); - handleRowSelection(rowKey, !isCurrentlySelected); - - // 🆕 분할 패널 컨텍스트에 선택된 데이터 저장 (좌측 화면인 경우) - // disableAutoDataTransfer가 true이면 자동 전달 비활성화 (버튼 클릭으로만 전달) - // currentSplitPosition을 사용하여 정확한 위치 확인 (splitPanelPosition이 없을 수 있음) - const effectiveSplitPosition = splitPanelPosition || currentSplitPosition; - - console.log("🔗 [TableList] 행 클릭 - 분할 패널 위치 확인:", { - splitPanelPosition, - currentSplitPosition, - effectiveSplitPosition, - hasSplitPanelContext: !!splitPanelContext, - disableAutoDataTransfer: splitPanelContext?.disableAutoDataTransfer, - }); - - if (splitPanelContext && effectiveSplitPosition === "left" && !splitPanelContext.disableAutoDataTransfer) { - if (!isCurrentlySelected) { - // 선택된 경우: 데이터 저장 - splitPanelContext.setSelectedLeftData(row); - console.log("🔗 [TableList] 분할 패널 좌측 데이터 저장:", { - row, - parentDataMapping: splitPanelContext.parentDataMapping, - }); - } else { - // 선택 해제된 경우: 데이터 초기화 - splitPanelContext.setSelectedLeftData(null); - console.log("🔗 [TableList] 분할 패널 좌측 데이터 초기화"); - } - } + // handleRowSelection에서 분할 패널 데이터 처리도 함께 수행됨 + handleRowSelection(rowKey, !isCurrentlySelected, row); console.log("행 클릭:", { row, index, isSelected: !isCurrentlySelected }); }; @@ -3936,7 +3934,7 @@ export const TableListComponent: React.FC = ({ if (enterRow) { const rowKey = getRowKey(enterRow, rowIndex); const isCurrentlySelected = selectedRows.has(rowKey); - handleRowSelection(rowKey, !isCurrentlySelected); + handleRowSelection(rowKey, !isCurrentlySelected, enterRow); } break; case " ": // Space @@ -3946,7 +3944,7 @@ export const TableListComponent: React.FC = ({ if (spaceRow) { const currentRowKey = getRowKey(spaceRow, rowIndex); const isChecked = selectedRows.has(currentRowKey); - handleRowSelection(currentRowKey, !isChecked); + handleRowSelection(currentRowKey, !isChecked, spaceRow); } break; case "F2": @@ -4160,7 +4158,7 @@ export const TableListComponent: React.FC = ({ return ( handleRowSelection(rowKey, checked as boolean)} + onCheckedChange={(checked) => handleRowSelection(rowKey, checked as boolean, row)} aria-label={`행 ${index + 1} 선택`} /> ); diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index 405e2abf..4f872bc1 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -381,6 +381,75 @@ export function TableSectionRenderer({ const [dynamicOptions, setDynamicOptions] = useState<{ id: string; value: string; label: string }[]>([]); const [dynamicOptionsLoading, setDynamicOptionsLoading] = useState(false); const dynamicOptionsLoadedRef = React.useRef(false); + + // 소스 테이블의 카테고리 타입 컬럼 목록 + const [sourceCategoryColumns, setSourceCategoryColumns] = useState([]); + + // 소스 테이블의 컬럼 라벨 (API에서 동적 로드) + const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); + + // 소스 테이블의 카테고리 타입 컬럼 목록 로드 + useEffect(() => { + const loadCategoryColumns = async () => { + if (!tableConfig.source.tableName) return; + + try { + const response = await apiClient.get( + `/table-categories/${tableConfig.source.tableName}/columns` + ); + + if (response.data?.success && Array.isArray(response.data.data)) { + const categoryColNames = response.data.data.map( + (col: { columnName?: string; column_name?: string }) => + col.columnName || col.column_name || "" + ).filter(Boolean); + setSourceCategoryColumns(categoryColNames); + } + } catch (error) { + console.error("카테고리 컬럼 목록 조회 실패:", error); + } + }; + + loadCategoryColumns(); + }, [tableConfig.source.tableName]); + + // 소스 테이블의 컬럼 라벨 로드 (source.columnLabels가 비어있을 때만) + useEffect(() => { + const loadColumnLabels = async () => { + const sourceTableName = tableConfig.source.tableName; + if (!sourceTableName) return; + + // 이미 source.columnLabels가 설정되어 있으면 스킵 + if (tableConfig.source.columnLabels && Object.keys(tableConfig.source.columnLabels).length > 0) { + return; + } + + try { + const response = await apiClient.get(`/table-management/tables/${sourceTableName}/columns`); + + if (response.data?.success && response.data.data) { + const columnsData = response.data.data.columns || response.data.data || []; + const labels: Record = {}; + + for (const col of columnsData) { + const colName = col.column_name || col.columnName; + // displayName: API에서 반환하는 라벨 (COALESCE(cl.column_label, c.column_name)) + const colLabel = col.displayName || col.column_label || col.columnLabel || col.comment; + // 라벨이 컬럼명과 다를 때만 저장 (의미있는 라벨인 경우) + if (colName && colLabel && colLabel !== colName) { + labels[colName] = colLabel; + } + } + + setSourceColumnLabels(labels); + } + } catch (error) { + console.error("소스 테이블 컬럼 라벨 조회 실패:", error); + } + }; + + loadColumnLabels(); + }, [tableConfig.source.tableName, tableConfig.source.columnLabels]); // 조건부 테이블: 동적 옵션 로드 (optionSource 설정이 있는 경우) useEffect(() => { @@ -788,13 +857,22 @@ export function TableSectionRenderer({ // 이미 초기화되었으면 스킵 if (initialDataLoadedRef.current) return; - const tableSectionKey = `_tableSection_${sectionId}`; + const tableSectionKey = `__tableSection_${sectionId}`; const initialData = formData[tableSectionKey]; + console.log("[TableSectionRenderer] 초기 데이터 확인:", { + sectionId, + tableSectionKey, + hasInitialData: !!initialData, + initialDataLength: Array.isArray(initialData) ? initialData.length : 0, + formDataKeys: Object.keys(formData).filter(k => k.startsWith("__tableSection_")), + }); + if (Array.isArray(initialData) && initialData.length > 0) { console.log("[TableSectionRenderer] 초기 데이터 로드:", { sectionId, itemCount: initialData.length, + firstItem: initialData[0], }); setTableData(initialData); initialDataLoadedRef.current = true; @@ -1268,7 +1346,12 @@ export function TableSectionRenderer({ const sourceTable = source.tableName; const sourceColumns = source.displayColumns; const sourceSearchFields = source.searchColumns; - const columnLabels = source.columnLabels || {}; + // 컬럼 라벨: source.columnLabels가 있으면 우선 사용, 없으면 동적 로드된 라벨 사용 + const columnLabels = useMemo(() => { + const configLabels = source.columnLabels || {}; + // 설정된 라벨이 있으면 설정 우선, 없으면 API에서 로드한 라벨 사용 + return { ...sourceColumnLabels, ...configLabels }; + }, [source.columnLabels, sourceColumnLabels]); const modalTitle = uiConfig?.modalTitle || "항목 검색 및 선택"; const multiSelect = uiConfig?.multiSelect ?? true; @@ -1281,16 +1364,25 @@ export function TableSectionRenderer({ const addRowButtonText = uiConfig?.addRowButtonText || "직접 입력"; // 기본 필터 조건 생성 (사전 필터만 - 모달 필터는 ItemSelectionModal에서 처리) + // 연산자별로 특수 키 형식 사용: column__operator (예: division__in) const baseFilterCondition: Record = useMemo(() => { const condition: Record = {}; if (filters?.preFilters) { for (const filter of filters.preFilters) { - // 간단한 "=" 연산자만 처리 (확장 가능) - if (filter.operator === "=") { + if (!filter.column || filter.value === undefined || filter.value === "") continue; + + const operator = filter.operator || "="; + + if (operator === "=") { + // 기본 등호 연산자는 그대로 전달 condition[filter.column] = filter.value; + } else { + // 다른 연산자는 특수 키 형식 사용: column__operator + condition[`${filter.column}__${operator}`] = filter.value; } } } + // console.log("[TableSectionRenderer] baseFilterCondition:", condition, "preFilters:", filters?.preFilters); return condition; }, [filters?.preFilters]); @@ -1892,6 +1984,7 @@ export function TableSectionRenderer({ onSelect={handleConditionalAddItems} columnLabels={columnLabels} modalFilters={modalFiltersForModal} + categoryColumns={sourceCategoryColumns} /> ); @@ -2000,6 +2093,7 @@ export function TableSectionRenderer({ onSelect={handleAddItems} columnLabels={columnLabels} modalFilters={modalFiltersForModal} + categoryColumns={sourceCategoryColumns} /> ); diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 5f087b71..cebd8aa6 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -197,6 +197,10 @@ export function UniversalFormModalComponent({ // 로딩 상태 const [saving, setSaving] = useState(false); + // 채번규칙 원본 값 추적 (수동 모드 감지용) + // key: columnName, value: 자동 생성된 원본 값 + const [numberingOriginalValues, setNumberingOriginalValues] = useState>({}); + // 🆕 수정 모드: 원본 그룹 데이터 (INSERT/UPDATE/DELETE 추적용) const [originalGroupedData, setOriginalGroupedData] = useState([]); const groupedDataInitializedRef = useRef(false); @@ -216,47 +220,46 @@ export function UniversalFormModalComponent({ // 초기화 - 최초 마운트 시 또는 initialData가 변경되었을 때 실행 useEffect(() => { - console.log("[UniversalFormModal] useEffect 시작", { - initialData, - hasInitialized: hasInitialized.current, - lastInitializedId: lastInitializedId.current, - }); - + // console.log("[UniversalFormModal] useEffect 시작", { + // initialData, + // hasInitialized: hasInitialized.current, + // lastInitializedId: lastInitializedId.current, + // }); + // initialData에서 ID 값 추출 (id, ID, objid 등) const currentId = initialData?.id || initialData?.ID || initialData?.objid; const currentIdString = currentId !== undefined ? String(currentId) : undefined; - + // 생성 모드에서 부모로부터 전달받은 데이터 해시 (ID가 없을 때만) - const createModeDataHash = !currentIdString && initialData && Object.keys(initialData).length > 0 - ? JSON.stringify(initialData) - : undefined; + const createModeDataHash = + !currentIdString && initialData && Object.keys(initialData).length > 0 ? JSON.stringify(initialData) : undefined; // 이미 초기화되었고, ID가 동일하고, 생성 모드 데이터도 동일하면 스킵 if (hasInitialized.current && lastInitializedId.current === currentIdString) { // 생성 모드에서 데이터가 새로 전달된 경우는 재초기화 필요 if (!createModeDataHash || capturedInitialData.current) { - console.log("[UniversalFormModal] 초기화 스킵 - 이미 초기화됨"); + // console.log("[UniversalFormModal] 초기화 스킵 - 이미 초기화됨"); // 🆕 채번 플래그가 true인데 formData에 값이 없으면 재생성 필요 // (컴포넌트 remount로 인해 state가 초기화된 경우) return; } } - + // 🆕 컴포넌트 remount 감지: hasInitialized가 true인데 formData가 비어있으면 재초기화 // (React의 Strict Mode나 EmbeddedScreen 리렌더링으로 인한 remount) if (hasInitialized.current && !currentIdString) { - console.log("[UniversalFormModal] 컴포넌트 remount 감지 - 채번 플래그 초기화"); + // console.log("[UniversalFormModal] 컴포넌트 remount 감지 - 채번 플래그 초기화"); numberingGeneratedRef.current = false; isGeneratingRef.current = false; } // 🆕 수정 모드: initialData에 데이터가 있으면서 ID가 변경된 경우 재초기화 if (hasInitialized.current && currentIdString && lastInitializedId.current !== currentIdString) { - console.log("[UniversalFormModal] ID 변경 감지 - 재초기화:", { - prevId: lastInitializedId.current, - newId: currentIdString, - initialData: initialData, - }); + // console.log("[UniversalFormModal] ID 변경 감지 - 재초기화:", { + // prevId: lastInitializedId.current, + // newId: currentIdString, + // initialData: initialData, + // }); // 채번 플래그 초기화 (새 항목이므로) numberingGeneratedRef.current = false; isGeneratingRef.current = false; @@ -266,10 +269,10 @@ export function UniversalFormModalComponent({ if (initialData && Object.keys(initialData).length > 0) { capturedInitialData.current = JSON.parse(JSON.stringify(initialData)); // 깊은 복사 lastInitializedId.current = currentIdString; - console.log("[UniversalFormModal] 초기 데이터 캡처:", capturedInitialData.current); + // console.log("[UniversalFormModal] 초기 데이터 캡처:", capturedInitialData.current); } - console.log("[UniversalFormModal] initializeForm 호출 예정"); + // console.log("[UniversalFormModal] initializeForm 호출 예정"); hasInitialized.current = true; initializeForm(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -279,7 +282,7 @@ export function UniversalFormModalComponent({ useEffect(() => { if (!hasInitialized.current) return; // 최초 초기화 전이면 스킵 - console.log("[useEffect config 변경] 재초기화 스킵 (채번 중복 방지)"); + // console.log("[useEffect config 변경] 재초기화 스킵 (채번 중복 방지)"); // initializeForm(); // 주석 처리 - config 변경 시 재초기화 안 함 (채번 중복 방지) // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); @@ -287,7 +290,7 @@ export function UniversalFormModalComponent({ // 컴포넌트 unmount 시 채번 플래그 초기화 useEffect(() => { return () => { - console.log("[채번] 컴포넌트 unmount - 플래그 초기화"); + // console.log("[채번] 컴포넌트 unmount - 플래그 초기화"); numberingGeneratedRef.current = false; isGeneratingRef.current = false; }; @@ -359,14 +362,14 @@ export function UniversalFormModalComponent({ // 테이블 타입 섹션 찾기 const tableSection = config.sections.find((s) => s.type === "table"); if (!tableSection) { - console.log("[UniversalFormModal] 테이블 섹션 없음 - _groupedData 무시"); + // console.log("[UniversalFormModal] 테이블 섹션 없음 - _groupedData 무시"); return; } - console.log("[UniversalFormModal] 수정 모드 - 테이블 섹션 초기화:", { - sectionId: tableSection.id, - itemCount: _groupedData.length, - }); + // console.log("[UniversalFormModal] 수정 모드 - 테이블 섹션 초기화:", { + // sectionId: tableSection.id, + // itemCount: _groupedData.length, + // }); // 원본 데이터 저장 (수정/삭제 추적용) setOriginalGroupedData(JSON.parse(JSON.stringify(_groupedData))); @@ -421,31 +424,31 @@ export function UniversalFormModalComponent({ } if (isGeneratingRef.current) { - console.log("[채번] 생성 진행 중 - 스킵"); + // console.log("[채번] 생성 진행 중 - 스킵"); return; } isGeneratingRef.current = true; // 진행 중 표시 - console.log("[채번] 생성 시작", { sectionsCount: config.sections.length }); + // console.log("[채번] 생성 시작", { sectionsCount: config.sections.length }); const updatedData = { ...currentFormData }; let hasChanges = false; for (const section of config.sections) { - console.log("[채번] 섹션 검사:", section.title, { type: section.type, repeatable: section.repeatable, fieldsCount: section.fields?.length }); + // console.log("[채번] 섹션 검사:", section.title, { type: section.type, repeatable: section.repeatable, fieldsCount: section.fields?.length }); if (section.repeatable || section.type === "table") continue; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { // generateOnOpen은 기본값 true (undefined일 경우 true로 처리) const shouldGenerateOnOpen = field.numberingRule?.generateOnOpen !== false; - console.log("[채번] 필드 검사:", field.columnName, { - hasNumberingRule: !!field.numberingRule, - enabled: field.numberingRule?.enabled, - generateOnOpen: field.numberingRule?.generateOnOpen, - shouldGenerateOnOpen, - ruleId: field.numberingRule?.ruleId, - currentValue: updatedData[field.columnName], - }); + // console.log("[채번] 필드 검사:", field.columnName, { + // hasNumberingRule: !!field.numberingRule, + // enabled: field.numberingRule?.enabled, + // generateOnOpen: field.numberingRule?.generateOnOpen, + // shouldGenerateOnOpen, + // ruleId: field.numberingRule?.ruleId, + // currentValue: updatedData[field.columnName], + // }); if ( field.numberingRule?.enabled && shouldGenerateOnOpen && @@ -453,22 +456,29 @@ export function UniversalFormModalComponent({ !updatedData[field.columnName] ) { try { - console.log(`[채번 미리보기 API 호출] ${field.columnName}, ruleId: ${field.numberingRule.ruleId}`); + // console.log(`[채번 미리보기 API 호출] ${field.columnName}, ruleId: ${field.numberingRule.ruleId}`); // generateOnOpen: 미리보기만 표시 (DB 시퀀스 증가 안 함) const response = await previewNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { - updatedData[field.columnName] = response.data.generatedCode; + const generatedCode = response.data.generatedCode; + updatedData[field.columnName] = generatedCode; // 저장 시 실제 할당을 위해 ruleId 저장 (TextInput과 동일한 키 형식) const ruleIdKey = `${field.columnName}_numberingRuleId`; updatedData[ruleIdKey] = field.numberingRule.ruleId; + // 원본 채번 값 저장 (수동 모드 감지용) + setNumberingOriginalValues((prev) => ({ + ...prev, + [field.columnName]: generatedCode, + })); + hasChanges = true; numberingGeneratedRef.current = true; // 생성 완료 표시 - console.log( - `[채번 미리보기 완료] ${field.columnName} = ${response.data.generatedCode} (저장 시 실제 할당)`, - ); - console.log(`[채번 규칙 ID 저장] ${ruleIdKey} = ${field.numberingRule.ruleId}`); + // console.log( + // `[채번 미리보기 완료] ${field.columnName} = ${response.data.generatedCode} (저장 시 실제 할당)`, + // ); + // console.log(`[채번 규칙 ID 저장] ${ruleIdKey} = ${field.numberingRule.ruleId}`); // 부모 컴포넌트에도 ruleId 전달 (ModalRepeaterTable → ScreenModal) if (onChange) { @@ -476,7 +486,7 @@ export function UniversalFormModalComponent({ ...updatedData, [ruleIdKey]: field.numberingRule.ruleId, }); - console.log(`[채번] 부모에게 ruleId 전달: ${ruleIdKey}`); + // console.log(`[채번] 부모에게 ruleId 전달: ${ruleIdKey}`); } } } catch (error) { @@ -497,17 +507,17 @@ export function UniversalFormModalComponent({ // 폼 초기화 const initializeForm = useCallback(async () => { - console.log("[initializeForm] 시작"); + // console.log("[initializeForm] 시작"); // 캡처된 initialData 사용 (props로 전달된 initialData가 아닌) const effectiveInitialData = capturedInitialData.current || initialData; - console.log("[initializeForm] 초기 데이터:", { - capturedInitialData: capturedInitialData.current, - initialData: initialData, - effectiveInitialData: effectiveInitialData, - hasData: effectiveInitialData && Object.keys(effectiveInitialData).length > 0, - }); + // console.log("[initializeForm] 초기 데이터:", { + // capturedInitialData: capturedInitialData.current, + // initialData: initialData, + // effectiveInitialData: effectiveInitialData, + // hasData: effectiveInitialData && Object.keys(effectiveInitialData).length > 0, + // }); const newFormData: FormDataState = {}; const newRepeatSections: { [sectionId: string]: RepeatSectionItem[] } = {}; @@ -534,7 +544,7 @@ export function UniversalFormModalComponent({ continue; } else { // 일반 섹션 필드 초기화 - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { // 기본값 설정 let value = field.defaultValue ?? ""; @@ -556,14 +566,16 @@ export function UniversalFormModalComponent({ if (section.optionalFieldGroups) { for (const group of section.optionalFieldGroups) { const key = `${section.id}-${group.id}`; - + // 수정 모드: triggerField 값이 triggerValueOnAdd와 일치하면 그룹 자동 활성화 if (effectiveInitialData && group.triggerField && group.triggerValueOnAdd !== undefined) { const triggerValue = effectiveInitialData[group.triggerField]; if (triggerValue === group.triggerValueOnAdd) { newActivatedGroups.add(key); - console.log(`[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`); - + console.log( + `[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`, + ); + // 활성화된 그룹의 필드값도 초기화 for (const field of group.fields || []) { let value = field.defaultValue ?? ""; @@ -575,7 +587,7 @@ export function UniversalFormModalComponent({ } } } - + // 신규 등록 모드: triggerValueOnRemove를 기본값으로 설정 if (group.triggerField && group.triggerValueOnRemove !== undefined) { // effectiveInitialData에 해당 값이 없는 경우에만 기본값 설정 @@ -588,6 +600,244 @@ export function UniversalFormModalComponent({ } } + // 🆕 테이블 섹션(type: "table") 디테일 데이터 로드 (마스터-디테일 구조) + // 수정 모드일 때 디테일 테이블에서 데이터 가져오기 + if (effectiveInitialData) { + console.log("[initializeForm] 테이블 섹션 디테일 로드 시작", { + sectionsCount: config.sections.length, + effectiveInitialDataKeys: Object.keys(effectiveInitialData), + }); + + for (const section of config.sections) { + if (section.type !== "table" || !section.tableConfig) { + continue; + } + + const tableConfig = section.tableConfig; + const editConfig = tableConfig.editConfig; + const saveConfig = tableConfig.saveConfig; + + console.log(`[initializeForm] 테이블 섹션 ${section.id} 검사:`, { + hasEditConfig: !!editConfig, + loadOnEdit: editConfig?.loadOnEdit, + hasSaveConfig: !!saveConfig, + targetTable: saveConfig?.targetTable, + linkColumn: editConfig?.linkColumn, + }); + + // 수정 모드 로드 설정 확인 (기본값: true) + if (editConfig?.loadOnEdit === false) { + console.log(`[initializeForm] 테이블 섹션 ${section.id}: loadOnEdit=false, 스킵`); + continue; + } + + // 디테일 테이블과 연결 정보 확인 + const detailTable = saveConfig?.targetTable; + let linkColumn = editConfig?.linkColumn; + + if (!detailTable) { + console.log(`[initializeForm] 테이블 섹션 ${section.id}: saveConfig.targetTable 미설정, 스킵`); + continue; + } + + // linkColumn이 설정되지 않았으면, 디테일 테이블 컬럼 정보 조회하여 자동 감지 + if (!linkColumn?.masterField || !linkColumn?.detailField) { + try { + // 마스터 테이블명 확인 (saveConfig에서) + // 1. customApiSave.multiTable.mainTable.tableName (다중 테이블 저장) + // 2. saveConfig.tableName (단일 테이블 저장) + const masterTable = + config.saveConfig?.customApiSave?.multiTable?.mainTable?.tableName || config.saveConfig?.tableName; + + // 디테일 테이블의 컬럼 목록 조회 + const columnsResponse = await apiClient.get(`/table-management/tables/${detailTable}/columns`); + + if (columnsResponse.data?.success && columnsResponse.data?.data) { + // API 응답 구조: { success, data: { columns: [...], total, page, ... } } + const columnsArray = columnsResponse.data.data.columns || columnsResponse.data.data || []; + const detailColumnsData = Array.isArray(columnsArray) ? columnsArray : []; + const detailColumns = detailColumnsData.map((col: any) => col.column_name || col.columnName); + const masterKeys = Object.keys(effectiveInitialData); + + console.log(`[initializeForm] 테이블 섹션 ${section.id}: 연결 필드 자동 감지`, { + masterTable, + detailTable, + detailColumnsCount: detailColumnsData.length, + }); + + // 방법 1: 엔티티 관계 기반 감지 (정확) + // 디테일 테이블에서 마스터 테이블을 참조하는 엔티티 컬럼 찾기 + if (masterTable) { + for (const col of detailColumnsData) { + const colName = col.column_name || col.columnName; + const inputType = col.input_type || col.inputType; + + // 엔티티 타입 컬럼 확인 + if (inputType === "entity") { + // reference_table 또는 detail_settings에서 참조 테이블 확인 + let refTable = col.reference_table || col.referenceTable; + + // detail_settings에서 referenceTable 확인 + if (!refTable && col.detail_settings) { + try { + const settings = + typeof col.detail_settings === "string" + ? JSON.parse(col.detail_settings) + : col.detail_settings; + refTable = settings.referenceTable; + } catch { + // JSON 파싱 실패 무시 + } + } + + // 마스터 테이블을 참조하는 컬럼 발견 + if (refTable === masterTable) { + // 참조 컬럼 확인 (마스터 테이블의 어떤 컬럼을 참조하는지) + let refColumn = col.reference_column || col.referenceColumn; + if (!refColumn && col.detail_settings) { + try { + const settings = + typeof col.detail_settings === "string" + ? JSON.parse(col.detail_settings) + : col.detail_settings; + refColumn = settings.referenceColumn; + } catch { + // JSON 파싱 실패 무시 + } + } + + // 마스터 데이터에 해당 컬럼 값이 있는지 확인 + if (refColumn && effectiveInitialData[refColumn]) { + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: 엔티티 관계 감지 - ${colName} → ${masterTable}.${refColumn}`, + ); + linkColumn = { masterField: refColumn, detailField: colName }; + break; + } + } + } + } + } + + // 방법 2: 공통 컬럼 패턴 기반 감지 (폴백) + // 엔티티 관계가 없으면 공통 컬럼명 패턴으로 찾기 + if (!linkColumn) { + const priorityPatterns = ["_no", "_number", "_code", "_id"]; + + for (const pattern of priorityPatterns) { + for (const masterKey of masterKeys) { + if ( + masterKey.endsWith(pattern) && + detailColumns.includes(masterKey) && + effectiveInitialData[masterKey] && + masterKey !== "id" && + masterKey !== "company_code" + ) { + console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 패턴 감지 - ${masterKey}`); + linkColumn = { masterField: masterKey, detailField: masterKey }; + break; + } + } + if (linkColumn) break; + } + } + + // 방법 3: 일반 공통 컬럼 (마지막 폴백) + if (!linkColumn) { + for (const masterKey of masterKeys) { + if ( + detailColumns.includes(masterKey) && + effectiveInitialData[masterKey] && + masterKey !== "id" && + masterKey !== "company_code" && + !masterKey.startsWith("__") + ) { + console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 감지 - ${masterKey}`); + linkColumn = { masterField: masterKey, detailField: masterKey }; + break; + } + } + } + } + } catch (error) { + console.warn(`[initializeForm] 테이블 섹션 ${section.id}: 컬럼 정보 조회 실패`, error); + } + } + + if (!linkColumn?.masterField || !linkColumn?.detailField) { + console.log(`[initializeForm] 테이블 섹션 ${section.id}: linkColumn 미설정 및 자동 감지 실패, 스킵`); + continue; + } + + // 마스터 테이블의 연결 필드 값 가져오기 + const masterValue = effectiveInitialData[linkColumn.masterField]; + if (!masterValue) { + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: masterField(${linkColumn.masterField}) 값 없음, 스킵`, + ); + continue; + } + + try { + console.log(`[initializeForm] 테이블 섹션 ${section.id}: 디테일 데이터 로드 시작`, { + detailTable, + linkColumn, + masterValue, + }); + + // 디테일 테이블에서 데이터 조회 + // operator: "equals"를 사용하여 정확히 일치하는 값만 검색 (엔티티 타입 컬럼에서 중요) + const searchCondition: Record = { + [linkColumn.detailField]: { value: masterValue, operator: "equals" }, + }; + + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: API 요청 - URL: /table-management/tables/${detailTable}/data`, + ); + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: API 요청 - search:`, + JSON.stringify(searchCondition), + ); + + const response = await apiClient.post(`/table-management/tables/${detailTable}/data`, { + search: searchCondition, // filters가 아닌 search로 전달 + page: 1, + size: 1000, // pageSize가 아닌 size로 전달 + autoFilter: { enabled: true }, // 멀티테넌시 필터 적용 + }); + + console.log( + `[initializeForm] 테이블 섹션 ${section.id}: API 응답 - success: ${response.data?.success}, total: ${response.data?.data?.total}, dataLength: ${response.data?.data?.data?.length}`, + ); + + if (response.data?.success) { + // 다양한 응답 구조 처리 + let items: any[] = []; + const data = response.data.data; + + if (Array.isArray(data)) { + items = data; + } else if (data?.items && Array.isArray(data.items)) { + items = data.items; + } else if (data?.rows && Array.isArray(data.rows)) { + items = data.rows; + } else if (data?.data && Array.isArray(data.data)) { + items = data.data; + } + + console.log(`[initializeForm] 테이블 섹션 ${section.id}: ${items.length}건 로드됨`, items); + + // 테이블 섹션 데이터를 formData에 저장 (TableSectionRenderer에서 사용) + const tableSectionKey = `__tableSection_${section.id}`; + newFormData[tableSectionKey] = items; + console.log(`[initializeForm] 테이블 섹션 ${section.id}: formData[${tableSectionKey}]에 저장됨`); + } + } catch (error) { + console.error(`[initializeForm] 테이블 섹션 ${section.id}: 디테일 데이터 로드 실패`, error); + } + } + } + setFormData(newFormData); setRepeatSections(newRepeatSections); setCollapsedSections(newCollapsed); @@ -599,35 +849,35 @@ export function UniversalFormModalComponent({ if (multiTable && effectiveInitialData) { const pkColumn = multiTable.mainTable?.primaryKeyColumn; const pkValue = effectiveInitialData[pkColumn]; - + // PK 값이 있으면 수정 모드로 판단 if (pkValue) { console.log("[initializeForm] 수정 모드 - 서브 테이블 데이터 로드 시작"); - + for (const subTableConfig of multiTable.subTables || []) { // loadOnEdit 옵션이 활성화된 경우에만 로드 if (!subTableConfig.enabled || !subTableConfig.options?.loadOnEdit) { continue; } - + const { tableName, linkColumn, repeatSectionId, fieldMappings, options } = subTableConfig; if (!tableName || !linkColumn?.subColumn || !repeatSectionId) { continue; } - + try { // 서브 테이블에서 데이터 조회 const filters: Record = { [linkColumn.subColumn]: pkValue, }; - + // 서브 항목만 로드 (메인 항목 제외) if (options?.loadOnlySubItems && options?.mainMarkerColumn) { filters[options.mainMarkerColumn] = options.subMarkerValue ?? false; } - + console.log(`[initializeForm] 서브 테이블 ${tableName} 조회:`, filters); - + const response = await apiClient.get(`/table-management/tables/${tableName}/data`, { params: { filters: JSON.stringify(filters), @@ -635,11 +885,11 @@ export function UniversalFormModalComponent({ pageSize: 100, }, }); - + if (response.data?.success && response.data?.data?.items) { const subItems = response.data.data.items; console.log(`[initializeForm] 서브 테이블 ${tableName} 데이터 ${subItems.length}건 로드됨`); - + // 역매핑: 서브 테이블 데이터 → 반복 섹션 데이터 const repeatItems: RepeatSectionItem[] = subItems.map((item: any, index: number) => { const repeatItem: RepeatSectionItem = { @@ -647,17 +897,17 @@ export function UniversalFormModalComponent({ _index: index, _originalData: item, // 원본 데이터 보관 (수정 시 필요) }; - + // 필드 매핑 역변환 (targetColumn → formField) for (const mapping of fieldMappings || []) { if (mapping.formField && mapping.targetColumn) { repeatItem[mapping.formField] = item[mapping.targetColumn]; } } - + return repeatItem; }); - + // 반복 섹션에 데이터 설정 newRepeatSections[repeatSectionId] = repeatItems; setRepeatSections({ ...newRepeatSections }); @@ -671,9 +921,9 @@ export function UniversalFormModalComponent({ } // 채번규칙 자동 생성 - console.log("[initializeForm] generateNumberingValues 호출"); + // console.log("[initializeForm] generateNumberingValues 호출"); await generateNumberingValues(newFormData); - console.log("[initializeForm] 완료"); + // console.log("[initializeForm] 완료"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); // initialData는 의존성에서 제거 (capturedInitialData.current 사용) @@ -684,7 +934,7 @@ export function UniversalFormModalComponent({ _index: index, }; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { item[field.columnName] = field.defaultValue ?? ""; } @@ -694,8 +944,42 @@ export function UniversalFormModalComponent({ // 필드 값 변경 핸들러 const handleFieldChange = useCallback( (columnName: string, value: any) => { + // 채번규칙 필드의 수동 모드 감지 + const originalNumberingValue = numberingOriginalValues[columnName]; + const ruleIdKey = `${columnName}_numberingRuleId`; + + // 해당 필드의 채번규칙 설정 찾기 + let fieldConfig: FormFieldConfig | undefined; + for (const section of config.sections) { + if (section.type === "table" || section.repeatable) continue; + fieldConfig = section.fields?.find((f) => f.columnName === columnName); + if (fieldConfig) break; + // 옵셔널 필드 그룹에서도 찾기 + for (const group of section.optionalFieldGroups || []) { + fieldConfig = group.fields?.find((f) => f.columnName === columnName); + if (fieldConfig) break; + } + if (fieldConfig) break; + } + setFormData((prev) => { const newData = { ...prev, [columnName]: value }; + + // 채번규칙이 활성화된 필드이고, "사용자 수정 가능"이 ON인 경우 + if (fieldConfig?.numberingRule?.enabled && fieldConfig?.numberingRule?.editable && originalNumberingValue) { + // 사용자가 값을 수정했으면 (원본과 다르면) ruleId 제거 → 수동 모드 + if (value !== originalNumberingValue) { + delete newData[ruleIdKey]; + console.log(`[채번 수동 모드] ${columnName}: 사용자가 값 수정 → ruleId 제거`); + } else { + // 원본 값으로 복구하면 ruleId 복구 → 자동 모드 + if (fieldConfig.numberingRule.ruleId) { + newData[ruleIdKey] = fieldConfig.numberingRule.ruleId; + console.log(`[채번 자동 모드] ${columnName}: 원본 값 복구 → ruleId 복구`); + } + } + } + // onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용) if (onChange) { setTimeout(() => onChange(newData), 0); @@ -703,7 +987,7 @@ export function UniversalFormModalComponent({ return newData; }); }, - [onChange], + [onChange, numberingOriginalValues, config.sections], ); // 반복 섹션 필드 값 변경 핸들러 @@ -776,47 +1060,53 @@ export function UniversalFormModalComponent({ }, []); // 옵셔널 필드 그룹 활성화 - const activateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => { - const section = config.sections.find((s) => s.id === sectionId); - const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); - if (!group) return; + const activateOptionalFieldGroup = useCallback( + (sectionId: string, groupId: string) => { + const section = config.sections.find((s) => s.id === sectionId); + const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); + if (!group) return; - const key = `${sectionId}-${groupId}`; - setActivatedOptionalFieldGroups((prev) => { - const newSet = new Set(prev); - newSet.add(key); - return newSet; - }); + const key = `${sectionId}-${groupId}`; + setActivatedOptionalFieldGroups((prev) => { + const newSet = new Set(prev); + newSet.add(key); + return newSet; + }); - // 연동 필드 값 변경 (추가 시) - if (group.triggerField && group.triggerValueOnAdd !== undefined) { - handleFieldChange(group.triggerField, group.triggerValueOnAdd); - } - }, [config, handleFieldChange]); + // 연동 필드 값 변경 (추가 시) + if (group.triggerField && group.triggerValueOnAdd !== undefined) { + handleFieldChange(group.triggerField, group.triggerValueOnAdd); + } + }, + [config, handleFieldChange], + ); // 옵셔널 필드 그룹 비활성화 - const deactivateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => { - const section = config.sections.find((s) => s.id === sectionId); - const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); - if (!group) return; + const deactivateOptionalFieldGroup = useCallback( + (sectionId: string, groupId: string) => { + const section = config.sections.find((s) => s.id === sectionId); + const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); + if (!group) return; - const key = `${sectionId}-${groupId}`; - setActivatedOptionalFieldGroups((prev) => { - const newSet = new Set(prev); - newSet.delete(key); - return newSet; - }); + const key = `${sectionId}-${groupId}`; + setActivatedOptionalFieldGroups((prev) => { + const newSet = new Set(prev); + newSet.delete(key); + return newSet; + }); - // 연동 필드 값 변경 (제거 시) - if (group.triggerField && group.triggerValueOnRemove !== undefined) { - handleFieldChange(group.triggerField, group.triggerValueOnRemove); - } + // 연동 필드 값 변경 (제거 시) + if (group.triggerField && group.triggerValueOnRemove !== undefined) { + handleFieldChange(group.triggerField, group.triggerValueOnRemove); + } - // 옵셔널 필드 그룹 필드 값 초기화 - (group.fields || []).forEach((field) => { - handleFieldChange(field.columnName, field.defaultValue || ""); - }); - }, [config, handleFieldChange]); + // 옵셔널 필드 그룹 필드 값 초기화 + (group.fields || []).forEach((field) => { + handleFieldChange(field.columnName, field.defaultValue || ""); + }); + }, + [config, handleFieldChange], + ); // Select 옵션 로드 const loadSelectOptions = useCallback( @@ -862,13 +1152,11 @@ export function UniversalFormModalComponent({ // categoryKey 형식: "tableName.columnName" const [categoryTable, categoryColumn] = optionConfig.categoryKey.split("."); if (categoryTable && categoryColumn) { - const response = await apiClient.get( - `/table-categories/${categoryTable}/${categoryColumn}/values` - ); + const response = await apiClient.get(`/table-categories/${categoryTable}/${categoryColumn}/values`); if (response.data?.success && response.data?.data) { - // 라벨값을 DB에 저장 (화면에 표시되는 값 그대로 저장) + // 코드값을 DB에 저장하고 라벨값을 화면에 표시 options = response.data.data.map((item: any) => ({ - value: item.valueLabel || item.value_label, + value: item.valueCode || item.value_code, label: item.valueLabel || item.value_label, })); } @@ -943,7 +1231,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; // 반복 섹션 및 테이블 섹션은 별도 검증 - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { if (field.required && !field.hidden && !field.numberingRule?.hidden) { const value = formData[field.columnName]; if (value === undefined || value === null || value === "") { @@ -959,7 +1247,7 @@ export function UniversalFormModalComponent({ // 단일 행 저장 const saveSingleRow = useCallback(async () => { const dataToSave = { ...formData }; - + // 테이블 섹션 데이터 추출 (별도 저장용) const tableSectionData: Record = {}; @@ -975,19 +1263,45 @@ export function UniversalFormModalComponent({ } }); - // 저장 시점 채번규칙 처리 (generateOnSave만 처리) + // 저장 시점 채번규칙 처리 for (const section of config.sections) { // 테이블 타입 섹션은 건너뛰기 if (section.type === "table") continue; - - for (const field of (section.fields || [])) { - if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) { - const response = await allocateNumberingCode(field.numberingRule.ruleId); - if (response.success && response.data?.generatedCode) { - dataToSave[field.columnName] = response.data.generatedCode; - console.log(`[채번 할당] ${field.columnName} = ${response.data.generatedCode}`); + + for (const field of section.fields || []) { + if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { + const ruleIdKey = `${field.columnName}_numberingRuleId`; + const hasRuleId = dataToSave[ruleIdKey]; // 사용자가 수정하지 않았으면 ruleId 유지됨 + + // 채번 규칙 할당 조건 + const shouldAllocate = + // 1. generateOnSave가 ON인 경우: 항상 저장 시점에 할당 + field.numberingRule.generateOnSave || + // 2. editable이 OFF인 경우: 사용자 입력 무시하고 채번 규칙으로 덮어씌움 + !field.numberingRule.editable || + // 3. editable이 ON이고 사용자가 수정하지 않은 경우 (ruleId 유지됨): 실제 번호 할당 + (field.numberingRule.editable && hasRuleId); + + if (shouldAllocate) { + const response = await allocateNumberingCode(field.numberingRule.ruleId); + if (response.success && response.data?.generatedCode) { + dataToSave[field.columnName] = response.data.generatedCode; + let reason = "(알 수 없음)"; + if (field.numberingRule.generateOnSave) { + reason = "(generateOnSave)"; + } else if (!field.numberingRule.editable) { + reason = "(editable=OFF, 강제 덮어씌움)"; + } else if (hasRuleId) { + reason = "(editable=ON, 사용자 미수정)"; + } + console.log(`[채번 할당] ${field.columnName} = ${response.data.generatedCode} ${reason}`); + } else { + console.error(`[채번 실패] ${field.columnName}:`, response.error); + } } else { - console.error(`[채번 실패] ${field.columnName}:`, response.error); + console.log( + `[채번 스킵] ${field.columnName}: 사용자가 직접 입력한 값 유지 = ${dataToSave[field.columnName]}`, + ); } } } @@ -995,22 +1309,30 @@ export function UniversalFormModalComponent({ // 별도 테이블에 저장해야 하는 테이블 섹션 목록 const tableSectionsForSeparateTable = config.sections.filter( - (s) => s.type === "table" && - s.tableConfig?.saveConfig?.targetTable && - s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName + (s) => + s.type === "table" && + s.tableConfig?.saveConfig?.targetTable && + s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName, ); - + // 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장) // targetTable이 없거나 메인 테이블과 같은 경우 const tableSectionsForMainTable = config.sections.filter( - (s) => s.type === "table" && - (!s.tableConfig?.saveConfig?.targetTable || - s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName) + (s) => + s.type === "table" && + (!s.tableConfig?.saveConfig?.targetTable || + s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName), ); console.log("[saveSingleRow] 메인 테이블:", config.saveConfig.tableName); - console.log("[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", tableSectionsForMainTable.map(s => s.id)); - console.log("[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", tableSectionsForSeparateTable.map(s => s.id)); + console.log( + "[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", + tableSectionsForMainTable.map((s) => s.id), + ); + console.log( + "[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", + tableSectionsForSeparateTable.map((s) => s.id), + ); console.log("[saveSingleRow] 테이블 섹션 데이터 키:", Object.keys(tableSectionData)); console.log("[saveSingleRow] dataToSave 키:", Object.keys(dataToSave)); @@ -1018,58 +1340,58 @@ export function UniversalFormModalComponent({ // 공통 저장 필드 수집 (sectionSaveModes 설정에 따라) const commonFieldsData: Record = {}; const { sectionSaveModes } = config.saveConfig; - + // 필드 타입 섹션에서 공통 저장 필드 수집 for (const section of config.sections) { if (section.type === "table") continue; - + const sectionMode = sectionSaveModes?.find((s) => s.sectionId === section.id); const defaultMode = "common"; // 필드 타입 섹션의 기본값은 공통 저장 const sectionSaveMode = sectionMode?.saveMode || defaultMode; - + if (section.fields) { for (const field of section.fields) { const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; - + if (fieldSaveMode === "common" && dataToSave[field.columnName] !== undefined) { commonFieldsData[field.columnName] = dataToSave[field.columnName]; } } } } - + // 각 테이블 섹션의 품목 데이터에 공통 필드 병합하여 저장 for (const tableSection of tableSectionsForMainTable) { const sectionData = tableSectionData[tableSection.id] || []; - + if (sectionData.length > 0) { // 품목별로 행 저장 for (const item of sectionData) { const rowToSave = { ...commonFieldsData, ...item }; - + // _sourceData 등 내부 메타데이터 제거 Object.keys(rowToSave).forEach((key) => { if (key.startsWith("_")) { delete rowToSave[key]; } }); - + const response = await apiClient.post( `/table-management/tables/${config.saveConfig.tableName}/add`, - rowToSave + rowToSave, ); - + if (!response.data?.success) { throw new Error(response.data?.message || "품목 저장 실패"); } } - + // 이미 저장했으므로 아래 로직에서 다시 저장하지 않도록 제거 delete tableSectionData[tableSection.id]; } } - + // 품목이 없으면 공통 데이터만 저장하지 않음 (품목이 필요한 화면이므로) // 다른 테이블 섹션이 있는 경우에만 메인 데이터 저장 const hasOtherTableSections = Object.keys(tableSectionData).length > 0; @@ -1084,7 +1406,7 @@ export function UniversalFormModalComponent({ if (!response.data?.success) { throw new Error(response.data?.message || "저장 실패"); } - + // 테이블 섹션 데이터 저장 (별도 테이블에) for (const section of config.sections) { if (section.type === "table" && section.tableConfig?.saveConfig?.targetTable) { @@ -1092,35 +1414,35 @@ export function UniversalFormModalComponent({ if (sectionData && sectionData.length > 0) { // 메인 레코드 ID가 필요한 경우 (response.data에서 가져오기) const mainRecordId = response.data?.data?.id; - + // 공통 저장 필드 수집: 다른 섹션(필드 타입)에서 공통 저장으로 설정된 필드 값 // 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual' const commonFieldsData: Record = {}; const { sectionSaveModes } = config.saveConfig; - + // 다른 섹션에서 공통 저장으로 설정된 필드 값 수집 for (const otherSection of config.sections) { if (otherSection.id === section.id) continue; // 현재 테이블 섹션은 건너뛰기 - + const sectionMode = sectionSaveModes?.find((s) => s.sectionId === otherSection.id); // 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual' const defaultMode = otherSection.type === "table" ? "individual" : "common"; const sectionSaveMode = sectionMode?.saveMode || defaultMode; - + // 필드 타입 섹션의 필드들 처리 if (otherSection.type !== "table" && otherSection.fields) { for (const field of otherSection.fields) { // 필드별 오버라이드 확인 const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; - + // 공통 저장이면 formData에서 값을 가져와 모든 품목에 적용 if (fieldSaveMode === "common" && formData[field.columnName] !== undefined) { commonFieldsData[field.columnName] = formData[field.columnName]; } } } - + // 🆕 선택적 필드 그룹 (optionalFieldGroups)도 처리 if (otherSection.optionalFieldGroups && otherSection.optionalFieldGroups.length > 0) { for (const optGroup of otherSection.optionalFieldGroups) { @@ -1135,13 +1457,13 @@ export function UniversalFormModalComponent({ } } } - + console.log("[saveSingleRow] 별도 테이블 저장 - 공통 필드:", Object.keys(commonFieldsData)); - + for (const item of sectionData) { // 공통 필드 병합 + 개별 품목 데이터 const itemToSave = { ...commonFieldsData, ...item }; - + // saveToTarget: false인 컬럼은 저장에서 제외 const columns = section.tableConfig?.columns || []; for (const col of columns) { @@ -1149,24 +1471,24 @@ export function UniversalFormModalComponent({ delete itemToSave[col.field]; } } - + // _sourceData 등 내부 메타데이터 제거 Object.keys(itemToSave).forEach((key) => { if (key.startsWith("_")) { delete itemToSave[key]; } }); - + // 메인 레코드와 연결이 필요한 경우 if (mainRecordId && config.saveConfig.primaryKeyColumn) { itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId; } - + const saveResponse = await apiClient.post( `/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`, - itemToSave + itemToSave, ); - + if (!saveResponse.data?.success) { throw new Error(saveResponse.data?.message || `${section.title || "테이블 섹션"} 저장 실패`); } @@ -1174,7 +1496,13 @@ export function UniversalFormModalComponent({ } } } - }, [config.sections, config.saveConfig.tableName, config.saveConfig.primaryKeyColumn, config.saveConfig.sectionSaveModes, formData]); + }, [ + config.sections, + config.saveConfig.tableName, + config.saveConfig.primaryKeyColumn, + config.saveConfig.sectionSaveModes, + formData, + ]); // 다중 행 저장 (겸직 등) const saveMultipleRows = useCallback(async () => { @@ -1250,7 +1578,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당 const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen; @@ -1306,7 +1634,7 @@ export function UniversalFormModalComponent({ } }); }); - + // 1-0. receiveFromParent 필드 값도 mainData에 추가 (서브 테이블 저장용) // 이 필드들은 메인 테이블에는 저장되지 않지만, 서브 테이블 저장 시 필요할 수 있음 config.sections.forEach((section) => { @@ -1325,7 +1653,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; - for (const field of (section.fields || [])) { + for (const field of section.fields || []) { // 채번규칙이 활성화된 필드 처리 if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // 신규 생성이거나 값이 없는 경우에만 채번 @@ -1370,7 +1698,7 @@ export function UniversalFormModalComponent({ } const subItems: Record[] = []; - + // 반복 섹션이 있는 경우에만 반복 데이터 처리 if (subTableConfig.repeatSectionId) { const repeatData = repeatSections[subTableConfig.repeatSectionId] || []; @@ -1683,10 +2011,8 @@ export function UniversalFormModalComponent({ // 메인 표시 컬럼 (displayColumn) const mainDisplayVal = row[lfg.displayColumn || ""] || ""; // 서브 표시 컬럼 (subDisplayColumn이 있으면 사용, 없으면 valueColumn 사용) - const subDisplayVal = lfg.subDisplayColumn - ? (row[lfg.subDisplayColumn] || "") - : (row[valueColumn] || ""); - + const subDisplayVal = lfg.subDisplayColumn ? row[lfg.subDisplayColumn] || "" : row[valueColumn] || ""; + switch (lfg.displayFormat) { case "code_name": // 서브 - 메인 형식 @@ -1704,7 +2030,10 @@ export function UniversalFormModalComponent({ matches.forEach((match) => { const columnName = match.slice(1, -1); // { } 제거 const columnValue = row[columnName]; - result = result.replace(match, columnValue !== undefined && columnValue !== null ? String(columnValue) : ""); + result = result.replace( + match, + columnValue !== undefined && columnValue !== null ? String(columnValue) : "", + ); }); } return result; @@ -1761,7 +2090,12 @@ export function UniversalFormModalComponent({ {sourceData.length > 0 ? ( sourceData - .filter((row) => row[valueColumn] !== null && row[valueColumn] !== undefined && String(row[valueColumn]) !== "") + .filter( + (row) => + row[valueColumn] !== null && + row[valueColumn] !== undefined && + String(row[valueColumn]) !== "", + ) .map((row, index) => ( {getDisplayText(row)} @@ -2021,13 +2355,11 @@ export function UniversalFormModalComponent({ ), )} - + {/* 옵셔널 필드 그룹 렌더링 */} {section.optionalFieldGroups && section.optionalFieldGroups.length > 0 && (
- {section.optionalFieldGroups.map((group) => - renderOptionalFieldGroup(section, group, sectionColumns) - )} + {section.optionalFieldGroups.map((group) => renderOptionalFieldGroup(section, group, sectionColumns))}
)} @@ -2055,7 +2387,7 @@ export function UniversalFormModalComponent({ const renderOptionalFieldGroup = ( section: FormSectionConfig, group: OptionalFieldGroupConfig, - sectionColumns: number + sectionColumns: number, ) => { const key = `${section.id}-${group.id}`; const isActivated = activatedOptionalFieldGroups.has(key); @@ -2074,9 +2406,7 @@ export function UniversalFormModalComponent({

{group.title}

- {group.description && ( -

{group.description}

- )} + {group.description &&

{group.description}

}
@@ -2154,8 +2478,8 @@ export function UniversalFormModalComponent({ formData[field.columnName], (value) => handleFieldChange(field.columnName, value), `${section.id}-${group.id}-${field.id}`, - groupColumns - ) + groupColumns, + ), )}
@@ -2169,9 +2493,7 @@ export function UniversalFormModalComponent({

{group.title}

- {group.description && ( -

{group.description}

- )} + {group.description &&

{group.description}

}
@@ -2327,7 +2649,8 @@ export function UniversalFormModalComponent({

{config.modal.title || "범용 폼 모달"}

- {config.sections.length}개 섹션 |{config.sections.reduce((acc, s) => acc + (s.fields?.length || 0), 0)}개 필드 + {config.sections.length}개 섹션 |{config.sections.reduce((acc, s) => acc + (s.fields?.length || 0), 0)}개 + 필드

저장 테이블: {config.saveConfig.tableName || "(미설정)"}

diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx index 27af68f1..7186ca7e 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPanel.tsx @@ -9,17 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; -import { - Plus, - Trash2, - GripVertical, - ChevronUp, - ChevronDown, - Settings, - Database, - Layout, - Table, -} from "lucide-react"; +import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Database, Layout, Table } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { getNumberingRules } from "@/lib/api/numberingRule"; @@ -31,11 +21,7 @@ import { MODAL_SIZE_OPTIONS, SECTION_TYPE_OPTIONS, } from "./types"; -import { - defaultSectionConfig, - defaultTableSectionConfig, - generateSectionId, -} from "./config"; +import { defaultSectionConfig, defaultTableSectionConfig, generateSectionId } from "./config"; // 모달 import import { FieldDetailSettingsModal } from "./modals/FieldDetailSettingsModal"; @@ -45,22 +31,26 @@ import { TableSectionSettingsModal } from "./modals/TableSectionSettingsModal"; // 도움말 텍스트 컴포넌트 const HelpText = ({ children }: { children: React.ReactNode }) => ( -

{children}

+

{children}

); // 부모 화면에서 전달 가능한 필드 타입 interface AvailableParentField { - name: string; // 필드명 (columnName) - label: string; // 표시 라벨 + name: string; // 필드명 (columnName) + label: string; // 표시 라벨 sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2") - sourceTable?: string; // 출처 테이블명 + sourceTable?: string; // 출처 테이블명 } -export function UniversalFormModalConfigPanel({ config, onChange, allComponents = [] }: UniversalFormModalConfigPanelProps) { +export function UniversalFormModalConfigPanel({ + config, + onChange, + allComponents = [], +}: UniversalFormModalConfigPanelProps) { // 테이블 목록 const [tables, setTables] = useState<{ name: string; label: string }[]>([]); const [tableColumns, setTableColumns] = useState<{ - [tableName: string]: { name: string; type: string; label: string }[]; + [tableName: string]: { name: string; type: string; label: string; inputType?: string }[]; }>({}); // 부모 화면에서 전달 가능한 필드 목록 @@ -140,7 +130,7 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents } }); } - + // 좌측 패널 테이블 컬럼도 추출 const leftTableName = compConfig.leftPanel?.tableName; if (leftTableName) { @@ -152,7 +142,7 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents const colName = col.columnName || col.column_name; const colLabel = col.displayName || col.columnComment || col.column_comment || colName; // 중복 방지 - if (!fields.some(f => f.name === colName && f.sourceTable === leftTableName)) { + if (!fields.some((f) => f.name === colName && f.sourceTable === leftTableName)) { fields.push({ name: colName, label: colLabel, @@ -179,7 +169,7 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents columns.forEach((col: any) => { const colName = col.columnName || col.column_name; const colLabel = col.displayName || col.columnComment || col.column_comment || colName; - if (!fields.some(f => f.name === colName && f.sourceTable === tableName)) { + if (!fields.some((f) => f.name === colName && f.sourceTable === tableName)) { fields.push({ name: colName, label: colLabel, @@ -198,11 +188,11 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents // 4. 버튼 컴포넌트 - openModalWithData의 fieldMappings/dataMapping에서 소스 컬럼 추출 if (compType === "button-primary" || compType === "button" || compType === "button-secondary") { const action = compConfig.action || {}; - + // fieldMappings에서 소스 컬럼 추출 const fieldMappings = action.fieldMappings || []; fieldMappings.forEach((mapping: any) => { - if (mapping.sourceColumn && !fields.some(f => f.name === mapping.sourceColumn)) { + if (mapping.sourceColumn && !fields.some((f) => f.name === mapping.sourceColumn)) { fields.push({ name: mapping.sourceColumn, label: mapping.sourceColumn, @@ -211,11 +201,11 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents }); } }); - + // dataMapping에서 소스 컬럼 추출 const dataMapping = action.dataMapping || []; dataMapping.forEach((mapping: any) => { - if (mapping.sourceColumn && !fields.some(f => f.name === mapping.sourceColumn)) { + if (mapping.sourceColumn && !fields.some((f) => f.name === mapping.sourceColumn)) { fields.push({ name: mapping.sourceColumn, label: mapping.sourceColumn, @@ -237,7 +227,7 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents columns.forEach((col: any) => { const colName = col.columnName || col.column_name; const colLabel = col.displayName || col.columnComment || col.column_comment || colName; - if (!fields.some(f => f.name === colName)) { + if (!fields.some((f) => f.name === colName)) { fields.push({ name: colName, label: colLabel, @@ -253,8 +243,8 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents } // 중복 제거 (같은 name이면 첫 번째만 유지) - const uniqueFields = fields.filter((field, index, self) => - index === self.findIndex(f => f.name === field.name) + const uniqueFields = fields.filter( + (field, index, self) => index === self.findIndex((f) => f.name === field.name), ); setAvailableParentFields(uniqueFields); @@ -276,11 +266,19 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents const data = response.data?.data; if (response.data?.success && Array.isArray(data)) { setTables( - data.map((t: { tableName?: string; table_name?: string; displayName?: string; tableLabel?: string; table_label?: string }) => ({ - name: t.tableName || t.table_name || "", - // displayName 우선, 없으면 tableLabel, 그것도 없으면 테이블명 - label: t.displayName || t.tableLabel || t.table_label || "", - })), + data.map( + (t: { + tableName?: string; + table_name?: string; + displayName?: string; + tableLabel?: string; + table_label?: string; + }) => ({ + name: t.tableName || t.table_name || "", + // displayName 우선, 없으면 tableLabel, 그것도 없으면 테이블명 + label: t.displayName || t.tableLabel || t.table_label || "", + }), + ), ); } } catch (error) { @@ -308,10 +306,13 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents displayName?: string; columnComment?: string; column_comment?: string; + inputType?: string; + input_type?: string; }) => ({ name: c.columnName || c.column_name || "", type: c.dataType || c.data_type || "text", label: c.displayName || c.columnComment || c.column_comment || c.columnName || c.column_name || "", + inputType: c.inputType || c.input_type || "text", }), ), })); @@ -359,21 +360,24 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents ); // 섹션 관리 - const addSection = useCallback((type: "fields" | "table" = "fields") => { - const newSection: FormSectionConfig = { - ...defaultSectionConfig, - id: generateSectionId(), - title: type === "table" ? `테이블 섹션 ${config.sections.length + 1}` : `섹션 ${config.sections.length + 1}`, - type, - fields: type === "fields" ? [] : undefined, - tableConfig: type === "table" ? { ...defaultTableSectionConfig } : undefined, - }; - onChange({ - ...config, - sections: [...config.sections, newSection], - }); - }, [config, onChange]); - + const addSection = useCallback( + (type: "fields" | "table" = "fields") => { + const newSection: FormSectionConfig = { + ...defaultSectionConfig, + id: generateSectionId(), + title: type === "table" ? `테이블 섹션 ${config.sections.length + 1}` : `섹션 ${config.sections.length + 1}`, + type, + fields: type === "fields" ? [] : undefined, + tableConfig: type === "table" ? { ...defaultTableSectionConfig } : undefined, + }; + onChange({ + ...config, + sections: [...config.sections, newSection], + }); + }, + [config, onChange], + ); + // 섹션 타입 변경 const changeSectionType = useCallback( (sectionId: string, newType: "fields" | "table") => { @@ -381,7 +385,7 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents ...config, sections: config.sections.map((s) => { if (s.id !== sectionId) return s; - + if (newType === "table") { return { ...s, @@ -400,9 +404,9 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents }), }); }, - [config, onChange] + [config, onChange], ); - + // 테이블 섹션 설정 모달 열기 const handleOpenTableSectionSettings = (section: FormSectionConfig) => { setSelectedSection(section); @@ -487,293 +491,310 @@ export function UniversalFormModalConfigPanel({ config, onChange, allComponents }; return ( -
-
-
- {/* 모달 기본 설정 */} - - - -
- - 모달 기본 설정 -
-
- -
- - updateModalConfig({ title: e.target.value })} - className="h-9 text-sm w-full max-w-full" - /> - 모달 상단에 표시될 제목입니다 -
- -
- - - 모달 창의 크기를 선택하세요 -
- - {/* 저장 버튼 표시 설정 */} -
-
- updateModalConfig({ showSaveButton: checked === true })} - /> - +
+
+
+ {/* 모달 기본 설정 */} + + + +
+ + 모달 기본 설정
- 체크 해제 시 모달 하단의 저장 버튼이 숨겨집니다 -
- -
+ +
- + updateModalConfig({ saveButtonText: e.target.value })} - className="h-9 text-sm w-full max-w-full" + value={config.modal.title} + onChange={(e) => updateModalConfig({ title: e.target.value })} + className="h-9 w-full max-w-full text-sm" /> + 모달 상단에 표시될 제목입니다
+
- - updateModalConfig({ cancelButtonText: e.target.value })} - className="h-9 text-sm w-full max-w-full" - /> + + + 모달 창의 크기를 선택하세요
-
- - - - {/* 저장 설정 */} - - - -
- - 저장 설정 -
-
- -
-
- -

- {config.saveConfig.tableName || "(미설정)"} -

- {config.saveConfig.customApiSave?.enabled && config.saveConfig.customApiSave?.multiTable?.enabled && ( - - 다중 테이블 모드 - - )} + {/* 저장 버튼 표시 설정 */} +
+
+ updateModalConfig({ showSaveButton: checked === true })} + /> + +
+ 체크 해제 시 모달 하단의 저장 버튼이 숨겨집니다
- -
- - 데이터를 저장할 테이블과 방식을 설정합니다. -
- "저장 설정 열기"를 클릭하여 상세 설정을 변경하세요. -
- - - - {/* 섹션 구성 */} - - - -
- - 섹션 구성 - - {config.sections.length}개 - -
-
- - {/* 섹션 추가 버튼들 */} -
- -