/** * 탑씰(company_7) 버튼 스타일 일괄 변경 스크립트 * * 사용법: * npx ts-node scripts/btn-bulk-update-company7.ts --test # 1건만 테스트 (ROLLBACK) * npx ts-node scripts/btn-bulk-update-company7.ts --run # 전체 실행 (COMMIT) * npx ts-node scripts/btn-bulk-update-company7.ts --backup # 백업 테이블만 생성 * npx ts-node scripts/btn-bulk-update-company7.ts --restore # 백업에서 원복 */ import { Pool } from "pg"; // ── 배포 DB 연결 ── const pool = new Pool({ connectionString: "postgresql://postgres:vexplor0909!!@211.115.91.141:11134/vexplor", }); const COMPANY_CODE = "COMPANY_7"; const BACKUP_TABLE = "screen_layouts_v2_backup_20260313"; // ── 액션별 기본 아이콘 매핑 (frontend/lib/button-icon-map.tsx 기준) ── const actionIconMap: Record = { save: "Check", delete: "Trash2", edit: "Pencil", navigate: "ArrowRight", modal: "Maximize2", transferData: "SendHorizontal", excel_download: "Download", excel_upload: "Upload", quickInsert: "Zap", control: "Settings", barcode_scan: "ScanLine", operation_control: "Truck", event: "Send", copy: "Copy", }; const FALLBACK_ICON = "SquareMousePointer"; function getIconForAction(actionType?: string): string { if (actionType && actionIconMap[actionType]) { return actionIconMap[actionType]; } return FALLBACK_ICON; } // ── 버튼 컴포넌트인지 판별 (최상위 + 탭 내부 둘 다 지원) ── function isTopLevelButton(comp: any): boolean { return ( comp.url?.includes("v2-button-primary") || comp.overrides?.type === "v2-button-primary" ); } function isTabChildButton(comp: any): boolean { return comp.componentType === "v2-button-primary"; } function isButtonComponent(comp: any): boolean { return isTopLevelButton(comp) || isTabChildButton(comp); } // ── 탭 위젯인지 판별 ── function isTabsWidget(comp: any): boolean { return ( comp.url?.includes("v2-tabs-widget") || comp.overrides?.type === "v2-tabs-widget" ); } // ── 버튼 스타일 변경 (최상위 버튼용: overrides 사용) ── function applyButtonStyle(config: any, actionType: string | undefined) { const iconName = getIconForAction(actionType); config.displayMode = "icon-text"; config.icon = { name: iconName, type: "lucide", size: "보통", ...(config.icon?.color ? { color: config.icon.color } : {}), }; config.iconTextPosition = "right"; config.iconGap = 6; if (!config.style) config.style = {}; delete config.style.width; // 레거시 하드코딩 너비 제거 (size.width만 사용) config.style.borderRadius = "8px"; config.style.labelColor = "#FFFFFF"; config.style.fontSize = "12px"; config.style.fontWeight = "normal"; config.style.labelTextAlign = "left"; if (actionType === "delete") { config.style.backgroundColor = "#F04544"; } else if (actionType === "excel_upload" || actionType === "excel_download") { config.style.backgroundColor = "#212121"; } else { config.style.backgroundColor = "#3B83F6"; } } function updateButtonStyle(comp: any): boolean { if (isTopLevelButton(comp)) { const overrides = comp.overrides || {}; const actionType = overrides.action?.type; if (!comp.size) comp.size = {}; comp.size.height = 40; applyButtonStyle(overrides, actionType); comp.overrides = overrides; return true; } if (isTabChildButton(comp)) { const config = comp.componentConfig || {}; const actionType = config.action?.type; if (!comp.size) comp.size = {}; comp.size.height = 40; applyButtonStyle(config, actionType); comp.componentConfig = config; // 탭 내부 버튼은 렌더러가 comp.style (최상위)에서 스타일을 읽음 if (!comp.style) comp.style = {}; comp.style.borderRadius = "8px"; comp.style.labelColor = "#FFFFFF"; comp.style.fontSize = "12px"; comp.style.fontWeight = "normal"; comp.style.labelTextAlign = "left"; comp.style.backgroundColor = config.style.backgroundColor; return true; } return false; } // ── 백업 테이블 생성 ── async function createBackup() { console.log(`\n=== 백업 테이블 생성: ${BACKUP_TABLE} ===`); const exists = await pool.query( `SELECT to_regclass($1) AS tbl`, [BACKUP_TABLE], ); if (exists.rows[0].tbl) { console.log(`백업 테이블이 이미 존재합니다: ${BACKUP_TABLE}`); const count = await pool.query(`SELECT COUNT(*) FROM ${BACKUP_TABLE}`); console.log(`기존 백업 레코드 수: ${count.rows[0].count}`); return; } await pool.query( `CREATE TABLE ${BACKUP_TABLE} AS SELECT * FROM screen_layouts_v2 WHERE company_code = $1`, [COMPANY_CODE], ); const count = await pool.query(`SELECT COUNT(*) FROM ${BACKUP_TABLE}`); console.log(`백업 완료. 레코드 수: ${count.rows[0].count}`); } // ── 백업에서 원복 ── async function restoreFromBackup() { console.log(`\n=== 백업에서 원복: ${BACKUP_TABLE} ===`); const result = await pool.query( `UPDATE screen_layouts_v2 AS target SET layout_data = backup.layout_data, updated_at = backup.updated_at FROM ${BACKUP_TABLE} AS backup WHERE target.screen_id = backup.screen_id AND target.company_code = backup.company_code AND target.layer_id = backup.layer_id`, ); console.log(`원복 완료. 변경된 레코드 수: ${result.rowCount}`); } // ── 메인: 버튼 일괄 변경 ── async function updateButtons(testMode: boolean) { const modeLabel = testMode ? "테스트 (1건, ROLLBACK)" : "전체 실행 (COMMIT)"; console.log(`\n=== 버튼 일괄 변경 시작 [${modeLabel}] ===`); // company_7 레코드 조회 const rows = await pool.query( `SELECT screen_id, layer_id, company_code, layout_data FROM screen_layouts_v2 WHERE company_code = $1 ORDER BY screen_id, layer_id`, [COMPANY_CODE], ); console.log(`대상 레코드 수: ${rows.rowCount}`); if (!rows.rowCount) { console.log("변경할 레코드가 없습니다."); return; } const client = await pool.connect(); try { await client.query("BEGIN"); let totalUpdated = 0; let totalButtons = 0; const targetRows = testMode ? [rows.rows[0]] : rows.rows; for (const row of targetRows) { const layoutData = row.layout_data; if (!layoutData?.components || !Array.isArray(layoutData.components)) { continue; } let buttonsInRow = 0; for (const comp of layoutData.components) { // 최상위 버튼 처리 if (updateButtonStyle(comp)) { buttonsInRow++; } // 탭 위젯 내부 버튼 처리 if (isTabsWidget(comp)) { const tabs = comp.overrides?.tabs || []; for (const tab of tabs) { const tabComps = tab.components || []; for (const tabComp of tabComps) { if (updateButtonStyle(tabComp)) { buttonsInRow++; } } } } } if (buttonsInRow > 0) { await client.query( `UPDATE screen_layouts_v2 SET layout_data = $1, updated_at = NOW() WHERE screen_id = $2 AND company_code = $3 AND layer_id = $4`, [JSON.stringify(layoutData), row.screen_id, row.company_code, row.layer_id], ); totalUpdated++; totalButtons += buttonsInRow; console.log( ` screen_id=${row.screen_id}, layer_id=${row.layer_id} → 버튼 ${buttonsInRow}개 변경`, ); // 테스트 모드: 변경 전후 비교를 위해 첫 번째 버튼 출력 if (testMode) { const sampleBtn = layoutData.components.find(isButtonComponent); if (sampleBtn) { console.log("\n--- 변경 후 샘플 버튼 ---"); console.log(JSON.stringify(sampleBtn, null, 2)); } } } } console.log(`\n--- 결과 ---`); console.log(`변경된 레코드: ${totalUpdated}개`); console.log(`변경된 버튼: ${totalButtons}개`); if (testMode) { await client.query("ROLLBACK"); console.log("\n[테스트 모드] ROLLBACK 완료. 실제 DB 변경 없음."); } else { await client.query("COMMIT"); console.log("\nCOMMIT 완료."); } } catch (err) { await client.query("ROLLBACK"); console.error("\n에러 발생. ROLLBACK 완료.", err); throw err; } finally { client.release(); } } // ── CLI 진입점 ── async function main() { const arg = process.argv[2]; if (!arg || !["--test", "--run", "--backup", "--restore"].includes(arg)) { console.log("사용법:"); console.log(" --test : 1건 테스트 (ROLLBACK, DB 변경 없음)"); console.log(" --run : 전체 실행 (COMMIT)"); console.log(" --backup : 백업 테이블 생성"); console.log(" --restore : 백업에서 원복"); process.exit(1); } try { if (arg === "--backup") { await createBackup(); } else if (arg === "--restore") { await restoreFromBackup(); } else if (arg === "--test") { await createBackup(); await updateButtons(true); } else if (arg === "--run") { await createBackup(); await updateButtons(false); } } catch (err) { console.error("스크립트 실행 실패:", err); process.exit(1); } finally { await pool.end(); } } main();