From 4787a8b177c8e8d173e9ac021aac401e7931e77b Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 2 Dec 2025 14:02:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(repeat-screen-modal):=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=EC=98=81=EC=97=AD=20=EB=8F=85=EB=A6=BD=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TableCrudConfig에 allowSave, saveButtonLabel 속성 추가 - CRUD 설정 패널에 저장 스위치 추가 - saveTableAreaData 함수: editable 컬럼 + 조인키만 필터링하여 저장 - 날짜 필드 ISO 8601 -> YYYY-MM-DD 형식 변환 - 백엔드: company_code 자동 주입 로직 추가 - tableManagementService에 hasColumn 메서드 추가 --- .../controllers/tableManagementController.ts | 11 + .../src/services/tableManagementService.ts | 18 ++ .../RepeatScreenModalComponent.tsx | 229 +++++++++++++++--- .../RepeatScreenModalConfigPanel.tsx | 20 +- .../components/repeat-screen-modal/types.ts | 4 + 5 files changed, 250 insertions(+), 32 deletions(-) diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index f552124f..4a80b007 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -870,6 +870,17 @@ export async function addTableData( const tableManagementService = new TableManagementService(); + // 🆕 멀티테넌시: company_code 자동 추가 (테이블에 company_code 컬럼이 있는 경우) + const companyCode = req.user?.companyCode; + if (companyCode && !data.company_code) { + // 테이블에 company_code 컬럼이 있는지 확인 + const hasCompanyCodeColumn = await tableManagementService.hasColumn(tableName, "company_code"); + if (hasCompanyCodeColumn) { + data.company_code = companyCode; + logger.info(`🔒 멀티테넌시: company_code 자동 추가 - ${companyCode}`); + } + } + // 데이터 추가 await tableManagementService.addTableData(tableName, data); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index c1748123..dabe41da 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -4076,4 +4076,22 @@ export class TableManagementService { throw error; } } + + /** + * 테이블에 특정 컬럼이 존재하는지 확인 + */ + async hasColumn(tableName: string, columnName: string): Promise { + try { + const result = await query( + `SELECT column_name + FROM information_schema.columns + WHERE table_name = $1 AND column_name = $2`, + [tableName, columnName] + ); + return result.length > 0; + } catch (error) { + logger.error(`컬럼 존재 여부 확인 실패: ${tableName}.${columnName}`, error); + return false; + } + } } diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx index 25807607..2484f1d7 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx @@ -295,6 +295,8 @@ export function RepeatScreenModalComponent({ rowCount: tableData.length, sampleRow: tableData[0] ? Object.keys(tableData[0]) : [], firstRowData: tableData[0], + // 디버그: plan_date 필드 확인 + plan_date_value: tableData[0]?.plan_date, }); // 🆕 v3.3: 추가 조인 테이블 데이터 로드 및 병합 @@ -344,6 +346,14 @@ export function RepeatScreenModalComponent({ _isNew: false, ...row, })); + + // 디버그: 저장된 외부 테이블 데이터 확인 + console.log(`[RepeatScreenModal] 외부 테이블 데이터 저장:`, { + key, + rowCount: newExternalData[key].length, + firstRow: newExternalData[key][0], + plan_date_in_firstRow: newExternalData[key][0]?.plan_date, + }); } } catch (error) { console.error(`[RepeatScreenModal] 외부 테이블 데이터 로드 실패:`, error); @@ -599,6 +609,159 @@ export function RepeatScreenModalComponent({ }); }; + // 🆕 v3.6: 테이블 영역 저장 기능 + const saveTableAreaData = async (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => { + const key = `${cardId}-${contentRowId}`; + const rows = externalTableData[key] || []; + + console.log("[RepeatScreenModal] saveTableAreaData 시작:", { + key, + rowsCount: rows.length, + contentRowId, + tableDataSource: contentRow?.tableDataSource, + tableCrud: contentRow?.tableCrud, + }); + + if (!contentRow?.tableDataSource?.enabled) { + console.warn("[RepeatScreenModal] 외부 테이블 데이터 소스가 설정되지 않음"); + return { success: false, message: "데이터 소스가 설정되지 않았습니다." }; + } + + const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable; + const dirtyRows = rows.filter((row) => row._isDirty); + + console.log("[RepeatScreenModal] 저장 대상:", { + targetTable, + dirtyRowsCount: dirtyRows.length, + dirtyRows: dirtyRows.map(r => ({ _isNew: r._isNew, _isDirty: r._isDirty, data: r })), + }); + + if (dirtyRows.length === 0) { + return { success: true, message: "저장할 변경사항이 없습니다.", savedCount: 0 }; + } + + const savePromises: Promise[] = []; + const savedIds: number[] = []; + + // 🆕 v3.6: editable한 컬럼 + 조인 키만 추출 (읽기 전용 컬럼은 제외) + const allowedFields = new Set(); + + // tableColumns에서 editable: true인 필드만 추가 (읽기 전용 컬럼 제외) + if (contentRow.tableColumns) { + contentRow.tableColumns.forEach((col) => { + // editable이 명시적으로 true이거나, editable이 undefined가 아니고 false가 아닌 경우 + // 또는 inputType이 있는 경우 (입력 가능한 컬럼) + if (col.field && (col.editable === true || col.inputType)) { + allowedFields.add(col.field); + } + }); + } + + // 조인 조건의 sourceKey 추가 (예: sales_order_id) - 이건 항상 필요 + if (contentRow.tableDataSource?.joinConditions) { + contentRow.tableDataSource.joinConditions.forEach((cond) => { + if (cond.sourceKey) allowedFields.add(cond.sourceKey); + }); + } + + console.log("[RepeatScreenModal] 저장 허용 필드 (editable + 조인키):", Array.from(allowedFields)); + console.log("[RepeatScreenModal] tableColumns 정보:", contentRow.tableColumns?.map(c => ({ + field: c.field, + editable: c.editable, + inputType: c.inputType + }))); + + for (const row of dirtyRows) { + const { _rowId, _originalData, _isDirty, _isNew, ...allData } = row; + + // 허용된 필드만 필터링 + const dataToSave: Record = {}; + for (const field of allowedFields) { + if (allData[field] !== undefined) { + dataToSave[field] = allData[field]; + } + } + + console.log("[RepeatScreenModal] 저장할 데이터:", { + _isNew, + _originalData, + allData, + dataToSave, + }); + + if (_isNew) { + // INSERT - /add 엔드포인트 사용 + console.log(`[RepeatScreenModal] INSERT 요청: /table-management/tables/${targetTable}/add`, dataToSave); + savePromises.push( + apiClient.post(`/table-management/tables/${targetTable}/add`, dataToSave).then((res) => { + console.log("[RepeatScreenModal] INSERT 응답:", res.data); + if (res.data?.data?.id) { + savedIds.push(res.data.data.id); + } + return res; + }).catch((err) => { + console.error("[RepeatScreenModal] INSERT 실패:", err.response?.data || err.message); + throw err; + }) + ); + } else if (_originalData?.id) { + // UPDATE - /edit 엔드포인트 사용 (id를 body에 포함) + const updateData = { ...dataToSave, id: _originalData.id }; + console.log(`[RepeatScreenModal] UPDATE 요청: /table-management/tables/${targetTable}/edit`, updateData); + savePromises.push( + apiClient.put(`/table-management/tables/${targetTable}/edit`, updateData).then((res) => { + console.log("[RepeatScreenModal] UPDATE 응답:", res.data); + savedIds.push(_originalData.id); + return res; + }).catch((err) => { + console.error("[RepeatScreenModal] UPDATE 실패:", err.response?.data || err.message); + throw err; + }) + ); + } + } + + try { + await Promise.all(savePromises); + + // 저장 후 해당 키의 dirty 플래그만 초기화 + setExternalTableData((prev) => { + const updated = { ...prev }; + if (updated[key]) { + updated[key] = updated[key].map((row) => ({ + ...row, + _isDirty: false, + _isNew: false, + _originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined }, + })); + } + return updated; + }); + + return { success: true, message: `${dirtyRows.length}건 저장 완료`, savedCount: dirtyRows.length, savedIds }; + } catch (error: any) { + console.error("[RepeatScreenModal] 테이블 영역 저장 실패:", error); + return { success: false, message: error.message || "저장 중 오류가 발생했습니다." }; + } + }; + + // 🆕 v3.6: 테이블 영역 저장 핸들러 + const handleTableAreaSave = async (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => { + setIsSaving(true); + try { + const result = await saveTableAreaData(cardId, contentRowId, contentRow); + if (result.success) { + console.log("[RepeatScreenModal] 테이블 영역 저장 성공:", result); + // 성공 알림 (필요 시 toast 추가) + } else { + console.error("[RepeatScreenModal] 테이블 영역 저장 실패:", result.message); + // 실패 알림 (필요 시 toast 추가) + } + } finally { + setIsSaving(false); + } + }; + // 🆕 v3.1: 외부 테이블 행 삭제 요청 const handleDeleteExternalRowRequest = (cardId: string, rowId: string, contentRowId: string, contentRow: CardContentRowConfig) => { if (contentRow.tableCrud?.deleteConfirm?.enabled !== false) { @@ -1467,33 +1630,41 @@ export function RepeatScreenModalComponent({ {contentRow.type === "table" && contentRow.tableDataSource?.enabled ? ( // 🆕 v3.1: 외부 테이블 데이터 소스 사용
- {contentRow.tableTitle && ( + {/* 테이블 헤더 영역: 제목 + 버튼들 */} + {(contentRow.tableTitle || contentRow.tableCrud?.allowSave || contentRow.tableCrud?.allowCreate) && (
- {contentRow.tableTitle} - {contentRow.tableCrud?.allowCreate && ( - - )} -
- )} - {!contentRow.tableTitle && contentRow.tableCrud?.allowCreate && ( -
- + {contentRow.tableTitle || ""} +
+ {/* 저장 버튼 - allowSave가 true일 때만 표시 */} + {contentRow.tableCrud?.allowSave && ( + + )} + {/* 추가 버튼 */} + {contentRow.tableCrud?.allowCreate && ( + + )} +
)} @@ -2243,10 +2414,12 @@ function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (va /> ); case "date": + // ISO 8601 형식('2025-12-02T00:00:00.000Z')을 'YYYY-MM-DD' 형식으로 변환 + const dateValue = value ? (typeof value === 'string' && value.includes('T') ? value.split('T')[0] : value) : ""; return ( onChange(e.target.value)} className="h-8 text-sm" /> @@ -2298,7 +2471,7 @@ function renderColumn(col: CardColumnConfig, card: CardData, onChange: (value: a {col.type === "date" && ( onChange(e.target.value)} className="h-10 text-sm" /> diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx index da7088a9..54949627 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx @@ -2568,13 +2568,13 @@ function ContentRowConfigSection({ {/* CRUD 설정 */}
-
+
onUpdateRow({ - tableCrud: { ...row.tableCrud, allowCreate: checked, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false }, + tableCrud: { ...row.tableCrud, allowCreate: checked, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false, allowSave: row.tableCrud?.allowSave || false }, }) } className="scale-[0.5]" @@ -2586,7 +2586,7 @@ function ContentRowConfigSection({ checked={row.tableCrud?.allowUpdate || false} onCheckedChange={(checked) => onUpdateRow({ - tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: checked, allowDelete: row.tableCrud?.allowDelete || false }, + tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: checked, allowDelete: row.tableCrud?.allowDelete || false, allowSave: row.tableCrud?.allowSave || false }, }) } className="scale-[0.5]" @@ -2598,13 +2598,25 @@ function ContentRowConfigSection({ checked={row.tableCrud?.allowDelete || false} onCheckedChange={(checked) => onUpdateRow({ - tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: checked }, + tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: checked, allowSave: row.tableCrud?.allowSave || false }, }) } className="scale-[0.5]" />
+
+ + onUpdateRow({ + tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false, allowSave: checked }, + }) + } + className="scale-[0.5]" + /> + +
{row.tableCrud?.allowDelete && (
diff --git a/frontend/lib/registry/components/repeat-screen-modal/types.ts b/frontend/lib/registry/components/repeat-screen-modal/types.ts index 81a36366..7226503e 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/types.ts +++ b/frontend/lib/registry/components/repeat-screen-modal/types.ts @@ -188,6 +188,10 @@ export interface TableCrudConfig { allowUpdate: boolean; // 행 수정 허용 allowDelete: boolean; // 행 삭제 허용 + // 🆕 v3.5: 테이블 영역 저장 버튼 + allowSave?: boolean; // 테이블 영역에 저장 버튼 표시 + saveButtonLabel?: string; // 저장 버튼 라벨 (기본: "저장") + // 신규 행 기본값 newRowDefaults?: Record; // 기본값 (예: { status: "READY", sales_order_id: "{id}" })