const SECRET_KEY = "1234"; // 원하는 비밀번호로 변경하세요
const CHUNK_SIZE = 35000; // 시트 셀 분할 저장 크기 (용량 무제한 지원)
function getOrCreateVaultSheet(ss) {
let sheet = ss.getSheetByName("_KEYRING_VAULT_DB");
if (!sheet) {
sheet = ss.insertSheet("_KEYRING_VAULT_DB");
sheet.hideSheet();
}
return sheet;
}
function loadRawDbJson(ss) {
const vault = ss.getSheetByName("_KEYRING_VAULT_DB");
if (vault) {
const chunkCount = Number(vault.getRange("A1").getValue()) || 0;
if (chunkCount > 0) {
const rows = vault.getRange(2, 1, chunkCount, 1).getValues();
return rows.map(function(r) { return String(r[0] || ""); }).join("");
}
}
// 이전 버전(PropertiesService) 호환용 자동 마이그레이션
return PropertiesService.getScriptProperties().getProperty("KEYRING_DB") || "";
}
function saveRawDbJson(ss, jsonStr) {
const vault = getOrCreateVaultSheet(ss);
// 직전 백업본을 B열에 보관하여 실수 덮어쓰기 복구 지원
const prevCount = Number(vault.getRange("A1").getValue()) || 0;
if (prevCount > 0) {
const prevChunks = vault.getRange(2, 1, prevCount, 1).getValues();
vault.getRange("B1").setValue(prevCount);
vault.getRange(2, 2, prevCount, 1).setValues(prevChunks);
}
const chunks = [];
for (let i = 0; i < jsonStr.length; i += CHUNK_SIZE) {
chunks.push([jsonStr.slice(i, i + CHUNK_SIZE)]);
}
vault.getRange("A1:A200").clearContent();
vault.getRange("A1").setValue(chunks.length);
if (chunks.length > 0) {
vault.getRange(2, 1, chunks.length, 1).setValues(chunks);
}
// 8KB 이하일 때는 구버전 속성에도 함께 기록
if (jsonStr.length < 8000) {
PropertiesService.getScriptProperties().setProperty("KEYRING_DB", jsonStr);
}
}
function doGet(e) {
try {
const key = (e && e.parameter && e.parameter.key) ? String(e.parameter.key).trim() : "";
const callback = (e && e.parameter && e.parameter.callback) ? String(e.parameter.callback).replace(/[^a-zA-Z0-9_]/g, "") : "";
let payload;
if (key !== String(SECRET_KEY).trim()) {
payload = { ok: false, msg: "암호 불일치 (2번 칸의 비밀 암호키가 구글 시트 코드의 SECRET_KEY와 다릅니다)" };
} else {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) {
payload = { ok: false, msg: "구글 스프레드시트 안의 [확장 프로그램 -> Apps Script]에서 만든 코드가 아닙니다." };
} else {
const data = loadRawDbJson(ss);
payload = { ok: true, data: data, sheetUrl: ss.getUrl() };
}
}
const jsonOut = JSON.stringify(payload);
if (callback) {
return ContentService.createTextOutput(callback + "(" + jsonOut + ");").setMimeType(ContentService.MimeType.JAVASCRIPT);
}
return ContentService.createTextOutput(jsonOut).setMimeType(ContentService.MimeType.JSON);
} catch (err) {
const errOut = JSON.stringify({ ok: false, msg: "서버 오류: " + err.message });
return ContentService.createTextOutput(errOut).setMimeType(ContentService.MimeType.JSON);
}
}
function doPost(e) {
const lock = LockService.getScriptLock();
try {
lock.waitLock(10000);
const body = JSON.parse(e.postData.contents);
if (String(body.key || "").trim() !== String(SECRET_KEY).trim()) {
return ContentService.createTextOutput(JSON.stringify({ ok: false, msg: "암호 불일치" })).setMimeType(ContentService.MimeType.JSON);
}
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) {
return ContentService.createTextOutput(JSON.stringify({ ok: false, msg: "구글 시트에 연결된 스크립트가 아닙니다." })).setMimeType(ContentService.MimeType.JSON);
}
const jsonStr = JSON.stringify(body.data);
saveRawDbJson(ss, jsonStr);
// 구글 시트 첫 번째 화면에 최종 동기화 시각과 데이터 개수를 자동 기록
const sheet = ss.getSheets()[0];
sheet.getRange("A1:B5").setValues([
["항목", "키링 원가·재고 클라우드 금고 상태"],
["최종 동기화 시각", new Date().toLocaleString("ko-KR", { timeZone: "Asia/Seoul" })],
["등록된 사입 파츠 수", ((body.data && body.data.parts) || []).length + "건"],
["등록된 완제품 수", ((body.data && body.data.products) || []).length + "종"],
["등록된 판매 장부 수", ((body.data && body.data.sales) || []).length + "건"]
]);
return ContentService.createTextOutput(JSON.stringify({ ok: true, sheetUrl: ss.getUrl() })).setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ ok: false, msg: "저장 오류: " + err.message })).setMimeType(ContentService.MimeType.JSON);
} finally {
try { lock.releaseLock(); } catch (ignore) {}
}
}