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:
17
.clasp.json
Normal file
17
.clasp.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"scriptId": "1hdzYOiAg3WSvr3lzIX4ebgFro5zunXhMxcEqmugBWB7SegK3Xn7kKR6H",
|
||||
"rootDir": "",
|
||||
"parentId": "1FFoOU20EFhOBucfC3RoX6EPUHVdxRnkA4v1-DQtXnzU",
|
||||
"scriptExtensions": [
|
||||
".js",
|
||||
".gs"
|
||||
],
|
||||
"htmlExtensions": [
|
||||
".html"
|
||||
],
|
||||
"jsonExtensions": [
|
||||
".json"
|
||||
],
|
||||
"filePushOrder": [],
|
||||
"skipSubdirectories": false
|
||||
}
|
||||
200
Code.gs
Normal file
200
Code.gs
Normal 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();
|
||||
}
|
||||
10
appsscript.json
Normal file
10
appsscript.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"timeZone": "Asia/Seoul",
|
||||
"dependencies": {
|
||||
},
|
||||
"exceptionLogging": "STACKDRIVER",
|
||||
"runtimeVersion": "V8",
|
||||
"executionApi": {
|
||||
"access": "MYSELF"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user