commit a777e711a20d33550907e0fa346969119f6a9dff Author: minsung Date: Mon Apr 13 11:47:52 2026 +0900 Add Apps Script for syncing 판매용SW Package data to DB sheet - clasp project bound to existing Google Sheet - syncPackageToDB: maps columns between 판매용SW and DB sheets, updates matching rows by 보여질순서 key - Handles merged 분야 column fill-down, 금액/연 calculation (단가×copy수) - Highlights updated cells in yellow - Includes listSheets utility and onOpen menu Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.clasp.json b/.clasp.json new file mode 100644 index 0000000..ea7e0ff --- /dev/null +++ b/.clasp.json @@ -0,0 +1,17 @@ +{ + "scriptId": "1hdzYOiAg3WSvr3lzIX4ebgFro5zunXhMxcEqmugBWB7SegK3Xn7kKR6H", + "rootDir": "", + "parentId": "1FFoOU20EFhOBucfC3RoX6EPUHVdxRnkA4v1-DQtXnzU", + "scriptExtensions": [ + ".js", + ".gs" + ], + "htmlExtensions": [ + ".html" + ], + "jsonExtensions": [ + ".json" + ], + "filePushOrder": [], + "skipSubdirectories": false +} \ No newline at end of file diff --git a/Code.gs b/Code.gs new file mode 100644 index 0000000..7c5717b --- /dev/null +++ b/Code.gs @@ -0,0 +1,200 @@ +/** 모든 시트 이름 출력 */ +function listSheets() { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var sheets = ss.getSheets(); + var names = []; + for (var i = 0; i < sheets.length; i++) { + names.push(sheets[i].getName()); + } + Logger.log("시트 목록: " + JSON.stringify(names)); + SpreadsheetApp.getUi().alert("시트 목록:\n" + names.join("\n")); +} + +/** + * "판매용 SW" 시트의 Pakage 데이터(Row 7~12)를 "DB" 시트로 동기화 + * - 매칭 키: 보여질순서 (A01, B01, C02 등) + * - 업데이트된 셀은 노란색 배경으로 표시 + * - DB의 데이터 유효성 검사가 있는 셀은 유효한 값만 업데이트 + */ +function syncPackageToDB() { + var ss = SpreadsheetApp.getActiveSpreadsheet(); + var swSheet = ss.getSheetByName("판매용 SW"); + var dbSheet = ss.getSheetByName("DB"); + + if (!swSheet || !dbSheet) { + SpreadsheetApp.getUi().alert("시트를 찾을 수 없습니다."); + return; + } + + // 판매용 SW 시트에서 Row 7~12 Pakage 데이터 읽기 + var swData = swSheet.getRange("A1:X12").getValues(); // Row 1~12만 읽기 + var swRows = extractPackageRows(swData, 6, 11); // 0-indexed: Row 7=index 6, Row 12=index 11 + + // DB 시트 데이터 읽기 (헤더: Row 1, 데이터: Row 2~) + var dbData = dbSheet.getDataRange().getValues(); + + // DB에서 보여질순서 → 행번호(1-indexed) 매핑 구축 + var dbKeyCol = 4; // DB의 "보여질순서" 컬럼 인덱스 (0-based) + var dbTypeCol = 3; // DB의 "Type" 컬럼 인덱스 + var dbRowMap = {}; + for (var i = 1; i < dbData.length; i++) { + var key = String(dbData[i][dbKeyCol]).trim(); + var type = String(dbData[i][dbTypeCol]).trim(); + if (key && type === "Pakage") { + dbRowMap[key] = i + 1; // 시트의 실제 행번호 (1-indexed) + } + } + + // SW → DB 컬럼 매핑 (SW컬럼인덱스 → DB컬럼인덱스) + var colMap = { + 0: 0, // Lv1 → Lv1 + 1: 1, // 배포범위 → 배포범위 + 2: 2, // 서비스형태 → 서비스형태 + 3: 4, // 보여질순서 → 보여질순서 + // 4: skip (빈 컬럼) + 5: 5, // 분야 → 분야 + 6: 6, // 명칭 → 명칭 + 7: 3, // Type → Type + 8: 7, // 사용 목적 → 사용 목적 + 9: 9, // 기획팀(협업) → 기획팀(협업) + 10: 8, // 기획/개발/영업 → 담당자 + 11: 10, // 기획 → 기획담당자 + 12: 11, // 개발팀 → 개발팀 + 13: 12, // 개발 → 개발담당자 + 14: 13, // 영업 → 영업담당자 + 15: 14, // 진행상태 → 진행상태 + 16: 15, // 판매단가/연 → 판매단가/연 + 17: 16, // copy수 → copy수 + // 18: 금액/연은 별도 계산 (판매단가 × copy수) + 19: 18, // 매입 or 사용료 → 매입 or 사용료 + 20: 19, // 프로젝트상태 → 프로젝트상태 + 21: 20, // 버전 → 버전 + 22: 21, // PM → PM + 23: 22 // 참조 → 참조 + }; + + var dbTotalCols = 23; + var updatedCount = 0; + var addedCount = 0; + var skippedCells = []; + var yellow = "#FFFF00"; + + for (var r = 0; r < swRows.length; r++) { + var swRow = swRows[r]; + var orderKey = String(swRow[3]).trim(); // 보여질순서 + + if (!orderKey) continue; + + var dbRowNum = dbRowMap[orderKey]; + + if (dbRowNum) { + // 기존 행 업데이트 + var dbRowData = dbSheet.getRange(dbRowNum, 1, 1, dbTotalCols).getValues()[0]; + var updatedCells = []; + + for (var swCol in colMap) { + var dbCol = colMap[swCol]; + var newVal = swRow[parseInt(swCol)]; + var oldVal = dbRowData[dbCol]; + + if (String(newVal).trim() !== String(oldVal).trim()) { + try { + dbSheet.getRange(dbRowNum, dbCol + 1).setValue(newVal); + updatedCells.push(dbCol + 1); + } catch (e) { + skippedCells.push(orderKey + " 컬럼" + (dbCol + 1) + ": \"" + newVal + "\" → 유효성 검사 위반"); + Logger.log("SKIP: " + orderKey + " DB컬럼" + (dbCol + 1) + " 값: " + newVal + " (" + e.message + ")"); + } + } + } + + // 금액/연 계산 (판매단가 × copy수) + var unitPrice = swRow[16] || 0; + var copies = swRow[17] || 0; + var calcAmount = unitPrice * copies; + var dbAmountCol = 17; // DB의 금액/연 (0-indexed) + if (calcAmount !== dbRowData[dbAmountCol]) { + dbSheet.getRange(dbRowNum, dbAmountCol + 1).setValue(calcAmount); + updatedCells.push(dbAmountCol + 1); + } + + // 업데이트된 셀 노란색 표시 + if (updatedCells.length > 0) { + for (var c = 0; c < updatedCells.length; c++) { + dbSheet.getRange(dbRowNum, updatedCells[c]).setBackground(yellow); + } + updatedCount++; + } + } else { + // 새 행 추가 + var newRow = new Array(dbTotalCols).fill(""); + for (var swCol in colMap) { + newRow[colMap[swCol]] = swRow[parseInt(swCol)]; + } + newRow[17] = (swRow[16] || 0) * (swRow[17] || 0); + + dbSheet.appendRow(newRow); + var lastRow = dbSheet.getLastRow(); + dbSheet.getRange(lastRow, 1, 1, dbTotalCols).setBackground(yellow); + addedCount++; + } + } + + var msg = "동기화 완료!\n업데이트: " + updatedCount + "건\n새로 추가: " + addedCount + "건"; + if (skippedCells.length > 0) { + msg += "\n\n⚠ 유효성 검사로 건너뛴 셀:\n" + skippedCells.join("\n"); + } + Logger.log(msg); + SpreadsheetApp.getUi().alert(msg); +} + +/** + * 판매용 SW 시트에서 지정 범위의 Pakage 타입 행만 추출 + * 머지된 분야 컬럼의 빈값을 이전 값으로 채움 + */ +function extractPackageRows(swData, startIdx, endIdx) { + var typeCol = 7; + var fieldCol = 5; + + var rows = []; + var lastField = ""; + + // startIdx 이전 행들에서 분야 값 미리 추적 (머지 대비) + for (var i = 0; i < startIdx; i++) { + if (String(swData[i][fieldCol]).trim()) { + lastField = swData[i][fieldCol]; + } + } + + // 지정 범위 행만 처리 + for (var i = startIdx; i <= endIdx && i < swData.length; i++) { + var row = swData[i]; + + if (String(row[fieldCol]).trim()) { + lastField = row[fieldCol]; + } + + if (String(row[typeCol]).trim() === "Pakage") { + var rowCopy = row.slice(); + if (!String(rowCopy[fieldCol]).trim()) { + rowCopy[fieldCol] = lastField; + } + rows.push(rowCopy); + } + } + + Logger.log("추출된 Pakage 행 수: " + rows.length); + for (var j = 0; j < rows.length; j++) { + Logger.log(" " + rows[j][3] + " | " + rows[j][5] + " | " + rows[j][6] + " | " + rows[j][7]); + } + + return rows; +} + +function onOpen() { + var ui = SpreadsheetApp.getUi(); + ui.createMenu("DB 동기화") + .addItem("Pakage → DB 업데이트", "syncPackageToDB") + .addItem("시트 목록 보기", "listSheets") + .addToUi(); +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/appsscript.json b/appsscript.json new file mode 100644 index 0000000..01c0bda --- /dev/null +++ b/appsscript.json @@ -0,0 +1,10 @@ +{ + "timeZone": "Asia/Seoul", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "executionApi": { + "access": "MYSELF" + } +} \ No newline at end of file