diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index d9b13475..fd85248d 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -1227,18 +1227,24 @@ class DataService { // 새 레코드 처리 (INSERT or UPDATE) for (const newRecord of records) { + console.log(`🔍 처리할 새 레코드:`, newRecord); + // 날짜 필드 정규화 const normalizedRecord: Record = {}; for (const [key, value] of Object.entries(newRecord)) { normalizedRecord[key] = normalizeDateValue(value); } + console.log(`🔄 정규화된 레코드:`, normalizedRecord); + // 전체 레코드 데이터 (parentKeys + normalizedRecord) const fullRecord = { ...parentKeys, ...normalizedRecord }; // 고유 키: parentKeys 제외한 나머지 필드들 const uniqueFields = Object.keys(normalizedRecord); + console.log(`🔑 고유 필드들:`, uniqueFields); + // 기존 레코드에서 일치하는 것 찾기 const existingRecord = existingRecords.rows.find((existing) => { return uniqueFields.every((field) => { diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index 3283ea09..a8f6c482 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -134,23 +134,32 @@ export class EntityJoinService { `🔧 기존 display_column 사용: ${column.column_name} → ${displayColumn}` ); } else { - // display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정 - let defaultDisplayColumn = referenceColumn; - if (referenceTable === "dept_info") { - defaultDisplayColumn = "dept_name"; - } else if (referenceTable === "company_info") { - defaultDisplayColumn = "company_name"; - } else if (referenceTable === "user_info") { - defaultDisplayColumn = "user_name"; - } else if (referenceTable === "category_values") { - defaultDisplayColumn = "category_name"; - } + // display_column이 "none"이거나 없는 경우 참조 테이블의 모든 컬럼 가져오기 + logger.info(`🔍 ${referenceTable}의 모든 컬럼 조회 중...`); - displayColumns = [defaultDisplayColumn]; - logger.info( - `🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${referenceTable})` + // 참조 테이블의 모든 컬럼 이름 가져오기 + const tableColumnsResult = await query<{ column_name: string }>( + `SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 + AND table_schema = 'public' + ORDER BY ordinal_position`, + [referenceTable] ); - logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns); + + if (tableColumnsResult.length > 0) { + displayColumns = tableColumnsResult.map((col) => col.column_name); + logger.info( + `✅ ${referenceTable}의 모든 컬럼 자동 포함 (${displayColumns.length}개):`, + displayColumns.join(", ") + ); + } else { + // 테이블 컬럼을 못 찾으면 기본값 사용 + displayColumns = [referenceColumn]; + logger.warn( + `⚠️ ${referenceTable}의 컬럼 조회 실패, 기본값 사용: ${referenceColumn}` + ); + } } // 별칭 컬럼명 생성 (writer -> writer_name) @@ -346,25 +355,26 @@ export class EntityJoinService { ); } } else { - // 여러 컬럼인 경우 CONCAT으로 연결 - // 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리 - const concatParts = displayColumns - .map((col) => { - // ✅ 개선: referenceTable이 설정되어 있으면 조인 테이블에서 가져옴 - const isJoinTableColumn = - config.referenceTable && config.referenceTable !== tableName; + // 🆕 여러 컬럼인 경우 각 컬럼을 개별 alias로 반환 (합치지 않음) + // 예: item_info.standard_price → sourceColumn_standard_price (item_id_standard_price) + displayColumns.forEach((col) => { + const isJoinTableColumn = + config.referenceTable && config.referenceTable !== tableName; - if (isJoinTableColumn) { - // 조인 테이블 컬럼은 조인 별칭 사용 - return `COALESCE(${alias}.${col}::TEXT, '')`; - } else { - // 기본 테이블 컬럼은 main 별칭 사용 - return `COALESCE(main.${col}::TEXT, '')`; - } - }) - .join(` || '${separator}' || `); + const individualAlias = `${config.sourceColumn}_${col}`; - resultColumns.push(`(${concatParts}) AS ${config.aliasColumn}`); + if (isJoinTableColumn) { + // 조인 테이블 컬럼은 조인 별칭 사용 + resultColumns.push( + `COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}` + ); + } else { + // 기본 테이블 컬럼은 main 별칭 사용 + resultColumns.push( + `COALESCE(main.${col}::TEXT, '') AS ${individualAlias}` + ); + } + }); // 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용) const isJoinTableColumn = diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index cf0a5edb..f44e2227 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -31,7 +31,7 @@ interface ScreenModalProps { } export const ScreenModal: React.FC = ({ className }) => { - const { userId } = useAuth(); + const { userId, userName, user } = useAuth(); const [modalState, setModalState] = useState({ isOpen: false, @@ -262,7 +262,7 @@ export const ScreenModal: React.FC = ({ className }) => { // 🆕 apiClient를 named import로 가져오기 const { apiClient } = await import("@/lib/api/client"); const params: any = { - enableEntityJoin: true, + enableEntityJoin: true, // 엔티티 조인 활성화 (모든 엔티티 컬럼 자동 포함) }; if (groupByColumns.length > 0) { params.groupByColumns = JSON.stringify(groupByColumns); @@ -325,7 +325,14 @@ export const ScreenModal: React.FC = ({ className }) => { console.log("📥 [ScreenModal] API 응답 원본:", JSON.stringify(response.data, null, 2)); const normalizedData = normalizeDates(response.data); console.log("📥 [ScreenModal] 정규화 후:", JSON.stringify(normalizedData, null, 2)); - setFormData(normalizedData); + + // 🔧 배열 데이터는 formData로 설정하지 않음 (SelectedItemsDetailInput만 사용) + if (Array.isArray(normalizedData)) { + console.log("⚠️ [ScreenModal] 그룹 레코드(배열)는 formData로 설정하지 않음. SelectedItemsDetailInput만 사용합니다."); + setFormData(normalizedData); // SelectedItemsDetailInput이 직접 사용 + } else { + setFormData(normalizedData); + } // setFormData 직후 확인 console.log("🔄 setFormData 호출 완료 (날짜 정규화됨)"); @@ -580,6 +587,9 @@ export const ScreenModal: React.FC = ({ className }) => { id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} + userId={userId} + userName={userName} + companyCode={user?.companyCode} /> ); })} diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index ba27c94e..df134685 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -42,6 +42,10 @@ interface InteractiveScreenViewerProps { onSave?: () => Promise; onRefresh?: () => void; onFlowRefresh?: () => void; + // 🆕 외부에서 전달받는 사용자 정보 (ScreenModal 등에서 사용) + userId?: string; + userName?: string; + companyCode?: string; } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -54,9 +58,24 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 - const { userName, user } = useAuth(); + const { userName: authUserName, user: authUser } = useAuth(); + + // 외부에서 전달받은 사용자 정보가 있으면 우선 사용 (ScreenModal 등에서) + const userName = externalUserName || authUserName; + const user = + externalUserId && externalUserId !== authUser?.userId + ? { + userId: externalUserId, + userName: externalUserName || authUserName || "", + companyCode: externalCompanyCode || authUser?.companyCode || "", + isAdmin: authUser?.isAdmin || false, + } + : authUser; const [localFormData, setLocalFormData] = useState>({}); const [dateValues, setDateValues] = useState>({}); @@ -130,59 +149,55 @@ export const InteractiveScreenViewerDynamic: React.FC { if (e.key === "Enter" && !e.shiftKey) { const target = e.target as HTMLElement; - + // 한글 조합 중이면 무시 (한글 입력 문제 방지) if ((e as any).isComposing || e.keyCode === 229) { return; } - + // textarea는 제외 (여러 줄 입력) if (target.tagName === "TEXTAREA") { return; } - + // input, select 등의 폼 요소에서만 작동 - if ( - target.tagName === "INPUT" || - target.tagName === "SELECT" || - target.getAttribute("role") === "combobox" - ) { + if (target.tagName === "INPUT" || target.tagName === "SELECT" || target.getAttribute("role") === "combobox") { e.preventDefault(); - + // 모든 포커스 가능한 요소 찾기 const focusableElements = document.querySelectorAll( - 'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])' + 'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [role="combobox"]:not([disabled])', ); - + // 화면에 보이는 순서(Y 좌표 → X 좌표)대로 정렬 const focusableArray = Array.from(focusableElements).sort((a, b) => { const rectA = a.getBoundingClientRect(); const rectB = b.getBoundingClientRect(); - + // Y 좌표 차이가 10px 이상이면 Y 좌표로 정렬 (위에서 아래로) if (Math.abs(rectA.top - rectB.top) > 10) { return rectA.top - rectB.top; } - + // 같은 줄이면 X 좌표로 정렬 (왼쪽에서 오른쪽으로) return rectA.left - rectB.left; }); - + const currentIndex = focusableArray.indexOf(target); - + if (currentIndex !== -1 && currentIndex < focusableArray.length - 1) { // 다음 요소로 포커스 이동 const nextElement = focusableArray[currentIndex + 1]; nextElement.focus(); - + // select() 제거: 한글 입력 시 이전 필드의 마지막 글자가 복사되는 버그 방지 } } } }; - + document.addEventListener("keydown", handleEnterKey); - + return () => { document.removeEventListener("keydown", handleEnterKey); }; @@ -193,31 +208,26 @@ export const InteractiveScreenViewerDynamic: React.FC { for (const comp of allComponents) { // type: "component" 또는 type: "widget" 모두 처리 - if (comp.type === 'widget' || comp.type === 'component') { + if (comp.type === "widget" || comp.type === "component") { const widget = comp as any; const fieldName = widget.columnName || widget.id; - + // autoFill 처리 (테이블 조회 기반 자동 입력) if (widget.autoFill?.enabled || (comp as any).autoFill?.enabled) { const autoFillConfig = widget.autoFill || (comp as any).autoFill; const currentValue = formData[fieldName]; - - if (currentValue === undefined || currentValue === '') { + + if (currentValue === undefined || currentValue === "") { const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig; - + // 사용자 정보에서 필터 값 가져오기 const userValue = user?.[userField]; - + if (userValue && sourceTable && filterColumn && displayColumn) { try { const { tableTypeApi } = await import("@/lib/api/screen"); - const result = await tableTypeApi.getTableRecord( - sourceTable, - filterColumn, - userValue, - displayColumn - ); - + const result = await tableTypeApi.getTableRecord(sourceTable, filterColumn, userValue, displayColumn); + updateFormData(fieldName, result.value); } catch (error) { console.error(`autoFill 조회 실패: ${fieldName}`, error); @@ -329,10 +339,13 @@ export const InteractiveScreenViewerDynamic: React.FC { - // 부모로부터 전달받은 onRefresh 또는 기본 동작 - console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출"); - })} + onRefresh={ + onRefresh || + (() => { + // 부모로부터 전달받은 onRefresh 또는 기본 동작 + console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출"); + }) + } onFlowRefresh={onFlowRefresh} onClose={() => { // buttonActions.ts가 이미 처리함 @@ -357,7 +370,7 @@ export const InteractiveScreenViewerDynamic: React.FC {label || "버튼"} @@ -689,18 +702,18 @@ export const InteractiveScreenViewerDynamic: React.FC([]); + const [loadingTables, setLoadingTables] = useState(false); // 미리보기 관련 상태 const [previewDialogOpen, setPreviewDialogOpen] = useState(false); @@ -260,14 +263,31 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr onScreenSelect(screen); }; - const handleEdit = (screen: ScreenDefinition) => { + const handleEdit = async (screen: ScreenDefinition) => { setScreenToEdit(screen); setEditFormData({ screenName: screen.screenName, description: screen.description || "", isActive: screen.isActive, + tableName: screen.tableName || "", }); setEditDialogOpen(true); + + // 테이블 목록 로드 + try { + setLoadingTables(true); + const { tableManagementApi } = await import("@/lib/api/tableManagement"); + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + // tableName만 추출 (camelCase) + const tableNames = response.data.map((table: any) => table.tableName); + setTables(tableNames); + } + } catch (error) { + console.error("테이블 목록 조회 실패:", error); + } finally { + setLoadingTables(false); + } }; const handleEditSave = async () => { @@ -1180,6 +1200,25 @@ export default function ScreenList({ onScreenSelect, selectedScreen, onDesignScr placeholder="화면명을 입력하세요" /> +
+ + +