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