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) <noreply@anthropic.com>
This commit is contained in:
minsung
2026-04-13 11:47:52 +09:00
commit a777e711a2
4 changed files with 227 additions and 0 deletions

200
Code.gs Normal file
View File

@@ -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();
}