From 2bbb5d701342f335faf83e388c2fda533acd232a Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 11 Feb 2026 15:43:50 +0900 Subject: [PATCH] feat: Enhance Excel upload functionality with automatic numbering column detection - Implemented automatic detection of numbering columns in the Excel upload modal, improving user experience by streamlining the upload process. - Updated the master-detail Excel upload configuration to reflect changes in how numbering rules are applied, ensuring consistency across uploads. - Refactored related components to remove deprecated properties and improve clarity in the configuration settings. - Enhanced error handling and logging for better debugging during the upload process. --- .../src/services/masterDetailExcelService.ts | 288 +++++++++++++----- .../components/common/ExcelUploadModal.tsx | 90 ++++-- .../config-panels/ButtonConfigPanel.tsx | 193 +----------- frontend/lib/utils/buttonActions.ts | 96 ++++-- 4 files changed, 362 insertions(+), 305 deletions(-) diff --git a/backend-node/src/services/masterDetailExcelService.ts b/backend-node/src/services/masterDetailExcelService.ts index a3eecb61..fa19c0a0 100644 --- a/backend-node/src/services/masterDetailExcelService.ts +++ b/backend-node/src/services/masterDetailExcelService.ts @@ -413,6 +413,16 @@ class MasterDetailExcelService { ? `WHERE ${whereConditions.join(" AND ")}` : ""; + // 디테일 테이블의 id 컬럼 존재 여부 확인 (user_info 등 id가 없는 테이블 대응) + const detailIdCheck = await queryOne<{ exists: boolean }>( + `SELECT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = $1 AND column_name = 'id' + ) as exists`, + [detailTable] + ); + const detailOrderColumn = detailIdCheck?.exists ? `d."id"` : `d."${detailFkColumn}"`; + // JOIN 쿼리 실행 const sql = ` SELECT ${selectClause} @@ -422,7 +432,7 @@ class MasterDetailExcelService { AND m.company_code = d.company_code ${entityJoinClauses} ${whereClause} - ORDER BY m."${masterKeyColumn}", d.id + ORDER BY m."${masterKeyColumn}", ${detailOrderColumn} `; logger.info(`마스터-디테일 JOIN 쿼리:`, { sql, params }); @@ -481,14 +491,67 @@ class MasterDetailExcelService { } } + /** + * 특정 테이블의 특정 컬럼이 채번 타입인지 확인하고, 채번 규칙 ID를 반환 + * 회사별 설정을 우선 조회하고, 없으면 공통(*) 설정으로 fallback + */ + private async detectNumberingRuleForColumn( + tableName: string, + columnName: string, + companyCode?: string + ): Promise<{ numberingRuleId: string } | null> { + try { + // 회사별 설정 우선, 공통 설정 fallback (company_code DESC로 회사별이 먼저) + const companyCondition = companyCode && companyCode !== "*" + ? `AND company_code IN ($3, '*')` + : `AND company_code = '*'`; + const params = companyCode && companyCode !== "*" + ? [tableName, columnName, companyCode] + : [tableName, columnName]; + + const result = await query( + `SELECT input_type, detail_settings, company_code + FROM table_type_columns + WHERE table_name = $1 AND column_name = $2 ${companyCondition} + ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END`, + params + ); + + logger.info(`채번 컬럼 조회 결과: ${tableName}.${columnName}`, { + rowCount: result.length, + rows: result.map((r: any) => ({ input_type: r.input_type, company_code: r.company_code })), + }); + + // 채번 타입인 행 찾기 (회사별 우선) + for (const row of result) { + if (row.input_type === "numbering") { + const settings = typeof row.detail_settings === "string" + ? JSON.parse(row.detail_settings || "{}") + : row.detail_settings; + + if (settings?.numberingRuleId) { + logger.info(`채번 컬럼 감지: ${tableName}.${columnName} → 규칙 ID: ${settings.numberingRuleId} (company: ${row.company_code})`); + return { numberingRuleId: settings.numberingRuleId }; + } + } + } + + logger.info(`채번 컬럼 아님: ${tableName}.${columnName}`); + return null; + } catch (error) { + logger.error(`채번 컬럼 감지 실패: ${tableName}.${columnName}`, error); + return null; + } + } + /** * 마스터-디테일 데이터 업로드 (엑셀 업로드용) * * 처리 로직: - * 1. 엑셀 데이터를 마스터 키로 그룹화 - * 2. 각 그룹의 첫 번째 행에서 마스터 데이터 추출 → UPSERT - * 3. 해당 마스터 키의 기존 디테일 삭제 - * 4. 새 디테일 데이터 INSERT + * 1. 마스터 키 컬럼이 채번 타입인지 확인 + * 2-A. 채번인 경우: 다른 마스터 컬럼 값으로 그룹화 → 키 자동 생성 → INSERT + * 2-B. 채번 아닌 경우: 마스터 키 값으로 그룹화 → UPSERT + * 3. 디테일 데이터 INSERT */ async uploadJoinedData( relation: MasterDetailRelation, @@ -513,94 +576,164 @@ class MasterDetailExcelService { const { masterTable, detailTable, masterKeyColumn, detailFkColumn, masterColumns, detailColumns } = relation; - // 1. 데이터를 마스터 키로 그룹화 - const groupedData = new Map[]>(); - - for (const row of data) { - const masterKey = row[masterKeyColumn]; - if (!masterKey) { - result.errors.push(`마스터 키(${masterKeyColumn}) 값이 없는 행이 있습니다.`); - continue; - } + // 마스터/디테일 테이블의 실제 컬럼 존재 여부 확인 (writer, created_date 등 하드코딩 방지) + const masterColsResult = await client.query( + `SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, + [masterTable] + ); + const masterExistingCols = new Set(masterColsResult.rows.map((r: any) => r.column_name)); - if (!groupedData.has(masterKey)) { - groupedData.set(masterKey, []); + const detailColsResult = await client.query( + `SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1`, + [detailTable] + ); + const detailExistingCols = new Set(detailColsResult.rows.map((r: any) => r.column_name)); + + // 마스터 키 컬럼의 채번 규칙 자동 감지 (회사별 설정 우선) + const numberingInfo = await this.detectNumberingRuleForColumn(masterTable, masterKeyColumn, companyCode); + const isAutoNumbering = !!numberingInfo; + + logger.info(`마스터 키 채번 감지:`, { + masterKeyColumn, + isAutoNumbering, + numberingRuleId: numberingInfo?.numberingRuleId + }); + + // 데이터 그룹화 + const groupedData = new Map[]>(); + + if (isAutoNumbering) { + // 채번 모드: 마스터 키 제외한 다른 마스터 컬럼 값으로 그룹화 + const otherMasterCols = masterColumns.filter(c => c.name !== masterKeyColumn).map(c => c.name); + + for (const row of data) { + // 다른 마스터 컬럼 값들을 조합해 그룹 키 생성 + const groupKey = otherMasterCols.map(col => row[col] ?? "").join("|||"); + if (!groupedData.has(groupKey)) { + groupedData.set(groupKey, []); + } + groupedData.get(groupKey)!.push(row); } - groupedData.get(masterKey)!.push(row); + + logger.info(`채번 모드 그룹화 완료: ${groupedData.size}개 그룹 (기준: ${otherMasterCols.join(", ")})`); + } else { + // 일반 모드: 마스터 키 값으로 그룹화 + for (const row of data) { + const masterKey = row[masterKeyColumn]; + if (!masterKey) { + result.errors.push(`마스터 키(${masterKeyColumn}) 값이 없는 행이 있습니다.`); + continue; + } + if (!groupedData.has(masterKey)) { + groupedData.set(masterKey, []); + } + groupedData.get(masterKey)!.push(row); + } + + logger.info(`일반 모드 그룹화 완료: ${groupedData.size}개 마스터 그룹`); } - logger.info(`데이터 그룹화 완료: ${groupedData.size}개 마스터 그룹`); - - // 2. 각 그룹 처리 - for (const [masterKey, rows] of groupedData.entries()) { + // 각 그룹 처리 + for (const [groupKey, rows] of groupedData.entries()) { try { - // 2a. 마스터 데이터 추출 (첫 번째 행에서) + // 마스터 키 결정 (채번이면 자동 생성, 아니면 그룹 키 자체가 마스터 키) + let masterKey: string; + + if (isAutoNumbering) { + // 채번 규칙으로 마스터 키 자동 생성 + masterKey = await this.generateNumberWithRule(client, numberingInfo!.numberingRuleId, companyCode); + logger.info(`채번 생성: ${masterKey}`); + } else { + masterKey = groupKey; + } + + // 마스터 데이터 추출 (첫 번째 행에서) const masterData: Record = {}; + // 마스터 키 컬럼은 항상 설정 (분할패널 컬럼 목록에 없어도) + masterData[masterKeyColumn] = masterKey; for (const col of masterColumns) { + if (col.name === masterKeyColumn) continue; // 이미 위에서 설정 if (rows[0][col.name] !== undefined) { masterData[col.name] = rows[0][col.name]; } } - // 회사 코드, 작성자 추가 - masterData.company_code = companyCode; - if (userId) { + // 회사 코드, 작성자 추가 (테이블에 해당 컬럼이 있을 때만) + if (masterExistingCols.has("company_code")) { + masterData.company_code = companyCode; + } + if (userId && masterExistingCols.has("writer")) { masterData.writer = userId; } - // 2b. 마스터 UPSERT - const existingMaster = await client.query( - `SELECT id FROM "${masterTable}" WHERE "${masterKeyColumn}" = $1 AND company_code = $2`, - [masterKey, companyCode] - ); + // INSERT SQL 생성 헬퍼 (created_date 존재 시만 추가) + const buildInsertSQL = (table: string, data: Record, existingCols: Set) => { + const cols = Object.keys(data); + const hasCreatedDate = existingCols.has("created_date"); + const colList = hasCreatedDate ? [...cols, "created_date"] : cols; + const placeholders = cols.map((_, i) => `$${i + 1}`); + const valList = hasCreatedDate ? [...placeholders, "NOW()"] : placeholders; + const values = cols.map(k => data[k]); + return { + sql: `INSERT INTO "${table}" (${colList.map(c => `"${c}"`).join(", ")}) VALUES (${valList.join(", ")})`, + values, + }; + }; - if (existingMaster.rows.length > 0) { - // UPDATE - const updateCols = Object.keys(masterData) - .filter(k => k !== masterKeyColumn && k !== "id") - .map((k, i) => `"${k}" = $${i + 1}`); - const updateValues = Object.keys(masterData) - .filter(k => k !== masterKeyColumn && k !== "id") - .map(k => masterData[k]); - - if (updateCols.length > 0) { - await client.query( - `UPDATE "${masterTable}" - SET ${updateCols.join(", ")}, updated_date = NOW() - WHERE "${masterKeyColumn}" = $${updateValues.length + 1} AND company_code = $${updateValues.length + 2}`, - [...updateValues, masterKey, companyCode] - ); - } - result.masterUpdated++; - } else { - // INSERT - const insertCols = Object.keys(masterData); - const insertPlaceholders = insertCols.map((_, i) => `$${i + 1}`); - const insertValues = insertCols.map(k => masterData[k]); - - await client.query( - `INSERT INTO "${masterTable}" (${insertCols.map(c => `"${c}"`).join(", ")}, created_date) - VALUES (${insertPlaceholders.join(", ")}, NOW())`, - insertValues - ); + if (isAutoNumbering) { + // 채번 모드: 항상 INSERT (새 마스터 생성) + const { sql, values } = buildInsertSQL(masterTable, masterData, masterExistingCols); + await client.query(sql, values); result.masterInserted++; + } else { + // 일반 모드: UPSERT (있으면 UPDATE, 없으면 INSERT) + const existingMaster = await client.query( + `SELECT 1 FROM "${masterTable}" WHERE "${masterKeyColumn}" = $1 AND company_code = $2`, + [masterKey, companyCode] + ); + + if (existingMaster.rows.length > 0) { + const updateCols = Object.keys(masterData) + .filter(k => k !== masterKeyColumn && k !== "id") + .map((k, i) => `"${k}" = $${i + 1}`); + const updateValues = Object.keys(masterData) + .filter(k => k !== masterKeyColumn && k !== "id") + .map(k => masterData[k]); + + if (updateCols.length > 0) { + const updatedDateClause = masterExistingCols.has("updated_date") ? `, updated_date = NOW()` : ""; + await client.query( + `UPDATE "${masterTable}" + SET ${updateCols.join(", ")}${updatedDateClause} + WHERE "${masterKeyColumn}" = $${updateValues.length + 1} AND company_code = $${updateValues.length + 2}`, + [...updateValues, masterKey, companyCode] + ); + } + result.masterUpdated++; + } else { + const { sql, values } = buildInsertSQL(masterTable, masterData, masterExistingCols); + await client.query(sql, values); + result.masterInserted++; + } + + // 일반 모드에서만 기존 디테일 삭제 (채번 모드는 새 마스터이므로 삭제할 디테일 없음) + const deleteResult = await client.query( + `DELETE FROM "${detailTable}" WHERE "${detailFkColumn}" = $1 AND company_code = $2`, + [masterKey, companyCode] + ); + result.detailDeleted += deleteResult.rowCount || 0; } - // 2c. 기존 디테일 삭제 - const deleteResult = await client.query( - `DELETE FROM "${detailTable}" WHERE "${detailFkColumn}" = $1 AND company_code = $2`, - [masterKey, companyCode] - ); - result.detailDeleted += deleteResult.rowCount || 0; - - // 2d. 새 디테일 INSERT + // 디테일 INSERT for (const row of rows) { const detailData: Record = {}; - // FK 컬럼 추가 + // FK 컬럼에 마스터 키 주입 detailData[detailFkColumn] = masterKey; - detailData.company_code = companyCode; - if (userId) { + if (detailExistingCols.has("company_code")) { + detailData.company_code = companyCode; + } + if (userId && detailExistingCols.has("writer")) { detailData.writer = userId; } @@ -611,20 +744,13 @@ class MasterDetailExcelService { } } - const insertCols = Object.keys(detailData); - const insertPlaceholders = insertCols.map((_, i) => `$${i + 1}`); - const insertValues = insertCols.map(k => detailData[k]); - - await client.query( - `INSERT INTO "${detailTable}" (${insertCols.map(c => `"${c}"`).join(", ")}, created_date) - VALUES (${insertPlaceholders.join(", ")}, NOW())`, - insertValues - ); + const { sql, values } = buildInsertSQL(detailTable, detailData, detailExistingCols); + await client.query(sql, values); result.detailInserted++; } } catch (error: any) { - result.errors.push(`마스터 키 ${masterKey} 처리 실패: ${error.message}`); - logger.error(`마스터 키 ${masterKey} 처리 실패:`, error); + result.errors.push(`그룹 처리 실패: ${error.message}`); + logger.error(`그룹 처리 실패:`, error); } } diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index f6429a09..067d3a45 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -84,12 +84,9 @@ export interface ExcelUploadModalProps { masterColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>; detailColumns: Array<{ name: string; label: string; inputType: string; isFromMaster: boolean }>; }; - // 🆕 마스터-디테일 엑셀 업로드 설정 + // 마스터-디테일 엑셀 업로드 설정 masterDetailExcelConfig?: MasterDetailExcelConfig; - // 🆕 단일 테이블 채번 설정 - numberingRuleId?: string; - numberingTargetColumn?: string; - // 🆕 업로드 후 제어 실행 설정 + // 업로드 후 제어 실행 설정 afterUploadFlows?: Array<{ flowId: string; order: number }>; } @@ -112,9 +109,6 @@ export const ExcelUploadModal: React.FC = ({ isMasterDetail = false, masterDetailRelation, masterDetailExcelConfig, - // 단일 테이블 채번 설정 - numberingRuleId, - numberingTargetColumn, // 업로드 후 제어 실행 설정 afterUploadFlows, }) => { @@ -627,6 +621,44 @@ export const ExcelUploadModal: React.FC = ({ setCurrentStep((prev) => Math.max(prev - 1, 1)); }; + // 테이블 타입 관리에서 채번 컬럼 자동 감지 + const detectNumberingColumn = async ( + targetTableName: string + ): Promise<{ columnName: string; numberingRuleId: string } | null> => { + try { + const { getTableColumns } = await import("@/lib/api/tableManagement"); + const response = await getTableColumns(targetTableName); + + if (response.success && response.data?.columns) { + for (const col of response.data.columns) { + if (col.inputType === "numbering") { + try { + const settings = + typeof col.detailSettings === "string" + ? JSON.parse(col.detailSettings) + : col.detailSettings; + if (settings?.numberingRuleId) { + console.log( + `✅ 채번 컬럼 자동 감지: ${col.columnName} → 규칙 ID: ${settings.numberingRuleId}` + ); + return { + columnName: col.columnName, + numberingRuleId: settings.numberingRuleId, + }; + } + } catch { + // detailSettings 파싱 실패 시 무시 + } + } + } + } + return null; + } catch (error) { + console.error("채번 컬럼 감지 실패:", error); + return null; + } + }; + // 업로드 핸들러 const handleUpload = async () => { if (!file || !tableName) { @@ -667,19 +699,24 @@ export const ExcelUploadModal: React.FC = ({ `📊 엑셀 업로드: 전체 ${mappedData.length}행 중 유효한 ${filteredData.length}행` ); - // 🆕 마스터-디테일 간단 모드 처리 (마스터 필드 선택 + 채번) + // 마스터-디테일 간단 모드 처리 (마스터 필드 선택 + 채번 자동 감지) if (isSimpleMasterDetailMode && screenId && masterDetailRelation) { + // 마스터 테이블에서 채번 컬럼 자동 감지 + const masterNumberingInfo = await detectNumberingColumn(masterDetailRelation.masterTable); + const detectedNumberingRuleId = masterNumberingInfo?.numberingRuleId || masterDetailExcelConfig?.numberingRuleId; + console.log("📊 마스터-디테일 간단 모드 업로드:", { masterDetailRelation, masterFieldValues, - numberingRuleId: masterDetailExcelConfig?.numberingRuleId, + detectedNumberingRuleId, + autoDetected: !!masterNumberingInfo, }); const uploadResult = await DynamicFormApi.uploadMasterDetailSimple( screenId, filteredData, masterFieldValues, - masterDetailExcelConfig?.numberingRuleId || undefined, + detectedNumberingRuleId || undefined, masterDetailExcelConfig?.afterUploadFlowId || undefined, // 하위 호환성 masterDetailExcelConfig?.afterUploadFlows || undefined // 다중 제어 ); @@ -704,6 +741,24 @@ export const ExcelUploadModal: React.FC = ({ else if (isMasterDetail && screenId && masterDetailRelation) { console.log("📊 마스터-디테일 업로드 모드:", masterDetailRelation); + // 마스터 키 컬럼 매핑 검증 (채번 타입이면 자동 생성되므로 검증 생략) + const masterKeyCol = masterDetailRelation.masterKeyColumn; + const hasMasterKey = filteredData.length > 0 && filteredData[0][masterKeyCol] !== undefined && filteredData[0][masterKeyCol] !== null && filteredData[0][masterKeyCol] !== ""; + if (!hasMasterKey) { + // 채번 여부 확인 - 채번이면 백엔드에서 자동 생성하므로 통과 + const numberingInfo = await detectNumberingColumn(masterDetailRelation.masterTable); + const isMasterKeyAutoNumbering = numberingInfo && numberingInfo.columnName === masterKeyCol; + + if (!isMasterKeyAutoNumbering) { + toast.error( + `마스터 키 컬럼(${masterKeyCol})이 매핑되지 않았습니다. 컬럼 매핑에서 [마스터] 항목을 확인해주세요.` + ); + setIsUploading(false); + return; + } + console.log(`✅ 마스터 키(${masterKeyCol})는 채번 타입 → 백엔드에서 자동 생성`); + } + const uploadResult = await DynamicFormApi.uploadMasterDetailData( screenId, filteredData @@ -731,8 +786,9 @@ export const ExcelUploadModal: React.FC = ({ let skipCount = 0; let overwriteCount = 0; - // 단일 테이블 채번 설정 확인 - const hasNumbering = numberingRuleId && numberingTargetColumn; + // 단일 테이블 채번 자동 감지 (테이블 타입 관리에서 input_type = 'numbering' 컬럼) + const numberingInfo = await detectNumberingColumn(tableName); + const hasNumbering = !!numberingInfo; // 중복 체크 설정 확인 const duplicateCheckMappings = columnMappings.filter( @@ -816,14 +872,14 @@ export const ExcelUploadModal: React.FC = ({ continue; } - // 채번 적용: 각 행마다 채번 API 호출 (신규 등록 시에만) - if (hasNumbering && uploadMode === "insert" && !shouldUpdate) { + // 채번 적용: 각 행마다 채번 API 호출 (신규 등록 시에만, 자동 감지된 채번 규칙 사용) + if (hasNumbering && numberingInfo && uploadMode === "insert" && !shouldUpdate) { try { const { apiClient } = await import("@/lib/api/client"); - const numberingResponse = await apiClient.post(`/numbering-rules/${numberingRuleId}/allocate`); + const numberingResponse = await apiClient.post(`/numbering-rules/${numberingInfo.numberingRuleId}/allocate`); const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code; if (numberingResponse.data?.success && generatedCode) { - dataToSave[numberingTargetColumn] = generatedCode; + dataToSave[numberingInfo.columnName] = generatedCode; } } catch (numError) { console.error("채번 오류:", numError); diff --git a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx index 8d6df989..ea2febb1 100644 --- a/frontend/components/screen/config-panels/ButtonConfigPanel.tsx +++ b/frontend/components/screen/config-panels/ButtonConfigPanel.tsx @@ -3777,7 +3777,7 @@ export const ButtonConfigPanel: React.FC = ({ /** * 마스터-디테일 엑셀 업로드 설정 컴포넌트 - * 분할 패널 + column_labels에서 관계를 자동 감지하고, 사용자는 채번 규칙만 선택 + * 분할 패널 + column_labels에서 관계를 자동 감지 (채번은 테이블 타입 관리에서 자동 감지) */ const MasterDetailExcelUploadConfig: React.FC<{ config: any; @@ -4005,7 +4005,7 @@ const MasterDetailExcelUploadConfig: React.FC<{ {/* 마스터 키 자동 생성 안내 */} {relationInfo && (

- 마스터 테이블의 {relationInfo.masterKeyColumn} 값은 위에서 설정한 채번 규칙으로 자동 + 마스터 테이블의 {relationInfo.masterKeyColumn} 값은 테이블 타입 관리에서 설정된 채번 규칙으로 자동 생성됩니다.

)} @@ -4114,165 +4114,15 @@ const MasterDetailExcelUploadConfig: React.FC<{ }; /** - * 엑셀 업로드 채번 규칙 설정 (단일 테이블/마스터-디테일 모두 사용 가능) + * 엑셀 업로드 채번 규칙 안내 (테이블 타입 관리에서 자동 감지) */ -const ExcelNumberingRuleConfig: React.FC<{ - config: { numberingRuleId?: string; numberingTargetColumn?: string }; - updateConfig: (updates: { numberingRuleId?: string; numberingTargetColumn?: string }) => void; - tableName?: string; // 단일 테이블인 경우 테이블명 - hasSplitPanel?: boolean; // 분할 패널 여부 (마스터-디테일) -}> = ({ config, updateConfig, tableName, hasSplitPanel }) => { - const [numberingRules, setNumberingRules] = useState([]); - const [ruleSelectOpen, setRuleSelectOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [tableColumns, setTableColumns] = useState>([]); - const [columnsLoading, setColumnsLoading] = useState(false); - - // 채번 규칙 목록 로드 - useEffect(() => { - const loadNumberingRules = async () => { - setIsLoading(true); - try { - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get("/numbering-rules"); - if (response.data?.success && response.data?.data) { - setNumberingRules(response.data.data); - } - } catch (error) { - console.error("채번 규칙 목록 로드 실패:", error); - } finally { - setIsLoading(false); - } - }; - - loadNumberingRules(); - }, []); - - // 단일 테이블인 경우 컬럼 목록 로드 - useEffect(() => { - if (!tableName || hasSplitPanel) { - setTableColumns([]); - return; - } - - const loadColumns = async () => { - setColumnsLoading(true); - try { - const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); - if (response.data?.success && response.data?.data?.columns) { - const cols = response.data.data.columns.map((col: any) => ({ - columnName: col.columnName || col.column_name, - columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name, - })); - setTableColumns(cols); - } - } catch (error) { - console.error("컬럼 목록 로드 실패:", error); - } finally { - setColumnsLoading(false); - } - }; - - loadColumns(); - }, [tableName, hasSplitPanel]); - - const selectedRule = numberingRules.find((r) => String(r.rule_id || r.ruleId) === String(config.numberingRuleId)); - +const ExcelNumberingRuleInfo: React.FC = () => { return (
-

- 업로드 시 자동으로 생성할 코드/번호의 채번 규칙을 선택하세요. +

+ 테이블 타입 관리에서 "채번" 타입으로 설정된 컬럼의 채번 규칙이 업로드 시 자동으로 적용됩니다.

- - - - - - - - - - 검색 결과 없음 - - { - updateConfig({ numberingRuleId: undefined, numberingTargetColumn: undefined }); - setRuleSelectOpen(false); - }} - className="text-xs" - > - - 채번 없음 - - {numberingRules.map((rule, idx) => { - const ruleId = String(rule.rule_id || rule.ruleId || `rule-${idx}`); - const ruleName = rule.rule_name || rule.ruleName || "(이름 없음)"; - return ( - { - updateConfig({ numberingRuleId: ruleId }); - setRuleSelectOpen(false); - }} - className="text-xs" - > - - {ruleName} - - ); - })} - - - - - - - {/* 단일 테이블이고 채번 규칙이 선택된 경우, 적용할 컬럼 선택 */} - {config.numberingRuleId && !hasSplitPanel && tableName && ( -
- - -

채번 값이 입력될 컬럼을 선택하세요.

-
- )} - - {/* 분할 패널인 경우 안내 메시지 */} - {config.numberingRuleId && hasSplitPanel && ( -

마스터-디테일 구조에서는 마스터 키 컬럼에 자동 적용됩니다.

- )}
); }; @@ -4440,14 +4290,10 @@ const ExcelUploadConfigSection: React.FC<{ allComponents: ComponentData[]; currentTableName?: string; // 현재 화면의 테이블명 (ButtonConfigPanel에서 전달) }> = ({ config, onUpdateProperty, allComponents, currentTableName: propTableName }) => { - // 엑셀 업로드 설정 상태 관리 + // 엑셀 업로드 설정 상태 관리 (채번은 테이블 타입 관리에서 자동 감지) const [excelUploadConfig, setExcelUploadConfig] = useState<{ - numberingRuleId?: string; - numberingTargetColumn?: string; afterUploadFlows?: Array<{ flowId: string; order: number }>; }>({ - numberingRuleId: config.action?.excelNumberingRuleId, - numberingTargetColumn: config.action?.excelNumberingTargetColumn, afterUploadFlows: config.action?.excelAfterUploadFlows || [], }); @@ -4529,17 +4375,11 @@ const ExcelUploadConfigSection: React.FC<{ ); }, [hasSplitPanel, singleTableName, propTableName]); - // 설정 업데이트 함수 + // 설정 업데이트 함수 (채번은 테이블 타입 관리에서 자동 감지되므로 제어 실행만 관리) const updateExcelUploadConfig = (updates: Partial) => { const newConfig = { ...excelUploadConfig, ...updates }; setExcelUploadConfig(newConfig); - if (updates.numberingRuleId !== undefined) { - onUpdateProperty("componentConfig.action.excelNumberingRuleId", updates.numberingRuleId); - } - if (updates.numberingTargetColumn !== undefined) { - onUpdateProperty("componentConfig.action.excelNumberingTargetColumn", updates.numberingTargetColumn); - } if (updates.afterUploadFlows !== undefined) { onUpdateProperty("componentConfig.action.excelAfterUploadFlows", updates.afterUploadFlows); } @@ -4548,15 +4388,9 @@ const ExcelUploadConfigSection: React.FC<{ // config 변경 시 로컬 상태 동기화 useEffect(() => { setExcelUploadConfig({ - numberingRuleId: config.action?.excelNumberingRuleId, - numberingTargetColumn: config.action?.excelNumberingTargetColumn, afterUploadFlows: config.action?.excelAfterUploadFlows || [], }); - }, [ - config.action?.excelNumberingRuleId, - config.action?.excelNumberingTargetColumn, - config.action?.excelAfterUploadFlows, - ]); + }, [config.action?.excelAfterUploadFlows]); return (
@@ -4595,13 +4429,8 @@ const ExcelUploadConfigSection: React.FC<{
)} - {/* 채번 규칙 설정 (항상 표시) */} - + {/* 채번 규칙 안내 (테이블 타입 관리에서 자동 감지) */} + {/* 업로드 후 제어 실행 (항상 표시) */} diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index e1abcb25..358b0df1 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4984,7 +4984,7 @@ export class ButtonActionExecutor { // visible이 true인 컬럼만 추출 visibleColumns = columns.filter((col: any) => col.visible !== false).map((col: any) => col.columnName); - // 🎯 column_labels 테이블에서 실제 라벨 가져오기 + // column_labels 테이블에서 실제 라벨 가져오기 try { const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, { params: { page: 1, size: 9999 }, @@ -5021,19 +5021,77 @@ export class ButtonActionExecutor { } }); } - } else { - console.warn("⚠️ 화면 레이아웃에서 테이블 리스트 컴포넌트를 찾을 수 없습니다."); } } } catch (error) { console.error("❌ 화면 레이아웃 조회 실패:", error); } - // 🎨 카테고리 값들 조회 (한 번만) + // Fallback: 레이아웃에서 컬럼 정보를 못 가져온 경우, table_type_columns에서 직접 조회 + // 시스템 컬럼 제외 + 라벨 적용으로 raw 컬럼명 노출 방지 + const SYSTEM_COLUMNS = ["id", "company_code", "created_date", "updated_date", "writer"]; + if ((!visibleColumns || visibleColumns.length === 0) && context.tableName && dataToExport.length > 0) { + console.log("⚠️ 레이아웃에서 컬럼 설정을 찾지 못함 → table_type_columns에서 fallback 조회"); + try { + const { apiClient } = await import("@/lib/api/client"); + const columnsResponse = await apiClient.get(`/table-management/tables/${context.tableName}/columns`, { + params: { page: 1, size: 9999 }, + }); + + if (columnsResponse.data?.success && columnsResponse.data?.data) { + let columnData = columnsResponse.data.data; + if (columnData.columns && Array.isArray(columnData.columns)) { + columnData = columnData.columns; + } + + if (Array.isArray(columnData) && columnData.length > 0) { + // visible이 false가 아닌 컬럼만 + 시스템 컬럼 제외 + const filteredCols = columnData.filter((col: any) => { + const colName = (col.column_name || col.columnName || "").toLowerCase(); + if (SYSTEM_COLUMNS.includes(colName)) return false; + if (col.isVisible === false || col.is_visible === false) return false; + return true; + }); + + visibleColumns = filteredCols.map((col: any) => col.column_name || col.columnName); + columnLabels = {}; + filteredCols.forEach((col: any) => { + const colName = col.column_name || col.columnName; + const labelValue = col.column_label || col.label || col.displayName || colName; + if (colName) { + columnLabels![colName] = labelValue; + } + }); + + console.log(`✅ Fallback 컬럼 ${visibleColumns.length}개 로드 완료`); + } + } + } catch (fallbackError) { + console.error("❌ Fallback 컬럼 조회 실패:", fallbackError); + } + } + + // 최종 안전장치: 여전히 컬럼 정보가 없으면 데이터의 키에서 시스템 컬럼만 제외 + if ((!visibleColumns || visibleColumns.length === 0) && dataToExport.length > 0) { + console.log("⚠️ 최종 fallback: 데이터 키에서 시스템 컬럼 제외"); + const allKeys = Object.keys(dataToExport[0]); + visibleColumns = allKeys.filter((key) => { + const lowerKey = key.toLowerCase(); + // 시스템 컬럼 제외 + if (SYSTEM_COLUMNS.includes(lowerKey)) return false; + // _name, _label 등 조인된 보조 필드 제외 + if (lowerKey.endsWith("_name") || lowerKey.endsWith("_label") || lowerKey.endsWith("_value_label")) return false; + return true; + }); + // 라벨이 없으므로 최소한 column_labels 비워두지 않음 (컬럼명 그대로 표시되지만 시스템 컬럼은 제외됨) + if (!columnLabels) { + columnLabels = {}; + } + } + + // 카테고리 값들 조회 (한 번만) const categoryMap: Record> = {}; let categoryColumns: string[] = []; - - // 백엔드에서 카테고리 컬럼 정보 가져오기 if (context.tableName) { try { const { getCategoryColumns, getCategoryValues } = await import("@/lib/api/tableCategoryValue"); @@ -5072,7 +5130,7 @@ export class ButtonActionExecutor { } } - // 🎨 컬럼 필터링 및 라벨 적용 (항상 실행) + // 컬럼 필터링 및 라벨 적용 if (visibleColumns && visibleColumns.length > 0 && dataToExport.length > 0) { dataToExport = dataToExport.map((row: any) => { const filteredRow: Record = {}; @@ -5165,6 +5223,8 @@ export class ButtonActionExecutor { ? config.excelAfterUploadFlows : config.masterDetailExcel?.afterUploadFlows; + // masterDetailExcel 설정이 명시적으로 있을 때만 간단 모드 (디테일만 업로드) + // 설정이 없으면 기본 모드 (마스터+디테일 둘 다 업로드) if (config.masterDetailExcel) { masterDetailExcelConfig = { ...config.masterDetailExcel, @@ -5173,25 +5233,13 @@ export class ButtonActionExecutor { detailTable: relationResponse.data.detailTable, masterKeyColumn: relationResponse.data.masterKeyColumn, detailFkColumn: relationResponse.data.detailFkColumn, - // 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑) - numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId, - // 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선) - afterUploadFlows, - }; - } else { - // 버튼 설정이 없으면 분할 패널 정보만 사용 - masterDetailExcelConfig = { - masterTable: relationResponse.data.masterTable, - detailTable: relationResponse.data.detailTable, - masterKeyColumn: relationResponse.data.masterKeyColumn, - detailFkColumn: relationResponse.data.detailFkColumn, - simpleMode: true, // 기본값으로 간단 모드 사용 - // 채번 규칙 ID 추가 (excelNumberingRuleId 사용) - numberingRuleId: config.excelNumberingRuleId, + // 채번은 ExcelUploadModal에서 마스터 테이블 기반 자동 감지 // 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선) afterUploadFlows, }; } + // masterDetailExcel 설정 없으면 masterDetailExcelConfig는 undefined 유지 + // → ExcelUploadModal에서 기본 모드로 동작 (마스터+디테일 둘 다 매핑/업로드) } } @@ -5233,9 +5281,7 @@ export class ButtonActionExecutor { isMasterDetail, masterDetailRelation, masterDetailExcelConfig, - // 🆕 단일 테이블 채번 설정 - numberingRuleId: config.excelNumberingRuleId, - numberingTargetColumn: config.excelNumberingTargetColumn, + // 채번은 ExcelUploadModal에서 테이블 타입 관리 기반 자동 감지 // 🆕 업로드 후 제어 실행 설정 afterUploadFlows: config.excelAfterUploadFlows, onSuccess: () => {