Files
googlesheetauto/Code.gs
minsung a777e711a2 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>
2026-04-13 11:47:52 +09:00

201 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 모든 시트 이름 출력 */
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();
}