구글시트 Apps Script로 창고 관리 UI 만들기
구글시트 창고 관리 UI 만들기를 찾고 있다면, 핵심은 단순합니다. 시트 4개(INBOUND·LOCATIONS·FLOOR_MAP·SETTINGS)와 Apps Script 사이드바 하나로 입고 확인, putaway 위치 배정, floor map 시각화를 한 번에 처리하는 구조를 만드는 겁니다. 별도 WMS 없이, 구글시트와 Apps Script만으로 무료에 가깝게 구성할 수 있습니다.
현장에서는 아직도 “컨테이너별 입고 확인 → 어디에 둘지 결정 → 나중에 어디에 있는지 찾기”를 엑셀과 구두로 해결하는 경우가 많습니다. 이 글은 제가 실제로 써본 방식 기준으로, 재현 가능한 최소 구현 흐름과 코드 뼈대를 정리한 것입니다.
1. 시트 4개로 기본 뼈대 잡기
먼저 구글시트에 아래 4개 시트를 만듭니다.
1) INBOUND – 입고 예정·검수·putaway 결과
- A: ETA_DATE (YYYY-MM-DD)
- B: CONTAINER (예: CTR-00001)
- C: MODEL (예: MODEL-A)
- D: QTY
- E: PUTAWAY (예:
B22-4) - F: STATUS (PLANNED / RECEIVED / PUTAWAY_DONE 등)
→ 매일 입고 예정 컨테이너를 여기 올려두고, “오늘 ETA” 기준으로만 사이드바에 노출합니다.
2) LOCATIONS – 실제 재고 테이블
- A: LOCATION (예: B22-4)
- B: CONTAINER
- C: MODEL
- D: QTY
- E: TIMESTAMP
→ 입고 확정 시 이 시트에 한 줄 추가하며, 이게 로케이션별 재고의 기준이 됩니다.
3) FLOOR_MAP – 랙·슬롯 시각화
- 예: B18~G24 범위를 창고 랙으로 사용
- 열(B~G): 랙 열
- 행(18~24): 랙 단
- 한 셀 안에서 슬롯 1~6을 줄바꿈으로 표시
- Apps Script가 색과 텍스트를 이 영역에 그려 넣습니다.
4) SETTINGS – 파라미터 모음
- FLOOR_MAP_RANGE, MAX_SLOTS, 색 단계 등 상수를 나중에 여기로 빼서 관리합니다.
2. Apps Script: 메뉴·사이드바 뼈대 코드
스프레드시트에서 확장 프로그램 → Apps Script를 열고, Code.gs에 기본 골격을 넣습니다.
```javascript
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('창고')
.addItem('입고 UI 열기', 'showInboundSidebar')
.addToUi();
}
function showInboundSidebar() {
const html = HtmlService.createHtmlOutputFromFile('Sidebar')
.setTitle('입고 / Putaway');
SpreadsheetApp.getUi().showSidebar(html);
}
function getTodayInbound() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('INBOUND');
const values = sh.getDataRange().getValues();
const today = Utilities.formatDate(new Date(), ss.getSpreadsheetTimeZone(), 'yyyy-MM-dd');
const header = values[0];
const etaIdx = header.indexOf('ETA_DATE');
const result = values.filter((row, i) =>
i > 0 && row[etaIdx] && row[etaIdx].toString().slice(0,10) === today
);
return result; // Sidebar에서 드롭다운 데이터로 사용
}
function savePutaway(data) {
// data: {container, model, qty, location}
const ss = SpreadsheetApp.getActive();
const inbound = ss.getSheetByName('INBOUND');
const locations = ss.getSheetByName('LOCATIONS');
// 1) LOCATIONS에 기록
locations.appendRow([
data.location,
data.container,
data.model,
data.qty,
new Date()
]);
// 2) INBOUND 업데이트 (간단히 컨테이너 첫 행만 찾는 예시)
const range = inbound.getDataRange();
const values = range.getValues();
const header = values[0];
const contIdx = header.indexOf('CONTAINER');
const putIdx = header.indexOf('PUTAWAY');
const statIdx = header.indexOf('STATUS');
for (let i = 1; i < values.length; i++) {
if (values[i][contIdx] === data.container) {
values[i][putIdx] = data.location;
values[i][statIdx] = 'PUTAWAY_DONE';
break;
}
}
range.setValues(values);
refreshFloorMap();
}
function refreshFloorMap() {
const ss = SpreadsheetApp.getActive();
const locSh = ss.getSheetByName('LOCATIONS');
const mapSh = ss.getSheetByName('FLOOR_MAP');
const setSh = ss.getSheetByName('SETTINGS');
// 예시: SETTINGS!A1에 FLOOR_MAP_RANGE, A2에 MAX_SLOTS
const floorRangeA1 = setSh.getRange('A1').getValue() || 'B18:G24';
const maxSlots = setSh.getRange('A2').getValue() || 6;
const mapRange = mapSh.getRange(floorRangeA1);
const colors = [];
const texts = [];
// 기본값 초기화
for (let r = 0; r < mapRange.getNumRows(); r++) {
colors[r] = [];
texts[r] = [];
for (let c = 0; c < mapRange.getNumColumns(); c++) {
colors[r][c] = '#ffffff';
texts[r][c] = '';
}
}
// LOCATIONS를 읽어 cell별 슬롯 수/컨테이너 텍스트 구성
// (실무에 맞게 LOCATION → 셀 좌표 매핑 로직을 추가)
// ...
mapRange.setBackgrounds(colors);
mapRange.setValues(texts);
mapRange.setNumberFormat('@'); // 텍스트로 고정
}
```
위 함수들은 “메뉴 → 사이드바 열기 → 오늘 입고 조회 → putaway 저장 → floor map 갱신”까지의 기본 흐름을 담당합니다.
3. Sidebar.html: google.script.run으로 입고 UI 만들기
Sidebar.html 파일을 추가해 사이드바 UI를 구성합니다. 핵심은 버튼 클릭 시 google.script.run.함수명()으로 Apps Script 함수를 부르는 구조입니다.
```html
<!DOCTYPE html>
<html>
<body>
<h3>입고 / Putaway</h3>
<label>컨테이너 선택</label>
<select id="containerSelect" onchange="onContainerChange()"></select>
<div id="info"></div>
<h4>로케이션 선택</h4>
<div id="loc-ui">
<!-- B~G / 18~24 / 1~6 선택용 버튼을 JS로 생성 -->
</div>
<button onclick="save()">저장</button>
<script>
let inboundRows = [];
let selected = { container: '', model: '', qty: 0, location: '' };
function loadInbound() {
google.script.run
.withSuccessHandler(function(rows){
inboundRows = rows;
const sel = document.getElementById('containerSelect');
rows.forEach(function(r){
const opt = document.createElement('option');
opt.value = r[1]; // CONTAINER
opt.text = r[1];
sel.appendChild(opt);
});
})
.getTodayInbound();
}
function onContainerChange() {
const cont = document.getElementById('containerSelect').value;
const row = inboundRows.find(r => r[1] === cont);
if (!row)