
구글 시트의 현재재고가 안전재고 이하로 떨어지면 담당자 이메일로 부족 상품을 알리고, Gemini가 공급처에 보낼 발주 문의 초안까지 작성하는 자동화입니다.

안녕하세요.
IT세상입니다. 😊
상품이나 사무용품을 관리하다 보면 꼭 필요한 물건이 모두 떨어진 뒤에야 재고 부족을 발견하는 경우가 있습니다.
이번 글에서는 구글 시트에 입력한 현재재고가 안전재고 이하로 떨어지면 담당자에게 이메일을 보내는 자동화를 만들어 보겠습니다.
부족한 상품이 발견되면 Gemini가 공급처에 보낼 발주 문의 초안도 작성합니다. 다만 잘못된 수량으로 주문되는 일을 막기 위해 공급처에는 자동으로 보내지 않습니다. 담당자가 수량과 조건을 확인할 수 있도록 검토용 초안만 전달합니다.
복사용지, 포장 테이프, 택배 상자, 프린터 토너처럼 자주 사용하는 물품은 하루만 늦게 주문해도 업무가 멈출 수 있습니다.
그렇다고 매일 재고관리 시트를 열고 모든 상품 수량을 하나씩 확인하는 것도 번거롭습니다.
구글 시트 재고 부족 알림 자동화를 만들어 두면 현재재고가 기준 이하로 내려간 상품만 자동으로 찾을 수 있습니다.
재고가 완전히 소진된 상품은 긴급으로 표시하고, 아직 조금 남아 있지만 안전재고의 절반 이하인 상품은 높은 우선순위로 구분할 수 있습니다.
이번 자동화에서 AI는 재고 부족 여부를 판단하지 않습니다. 현재재고와 안전재고의 숫자를 비교하는 일은 코드가 담당합니다. AI는 이미 부족하다고 확인된 상품을 바탕으로 발주 문의 문장을 작성하는 역할만 합니다.
자동화가 완성되면 다음 순서로 작동합니다.
상품 가격, 납기일, 최소주문수량과 실제 공급 가능 여부는 AI가 확인할 수 없습니다. 이번 자동화는 담당자에게 검토용 문장만 전달합니다.
Gemini API 키를 입력하지 않아도 재고 부족 이메일은 받을 수 있습니다.
API 키가 없거나 Gemini 요청에 실패하면 코드에 미리 작성된 기본 발주 문의 초안이 사용됩니다. 따라서 AI 서비스에 일시적인 문제가 생겨도 재고 부족 사실은 확인할 수 있습니다.
| 항목 | 입력하거나 표시되는 내용 |
|---|---|
| 상품코드 | 같은 이름의 상품을 구분할 고유 코드 |
| 상품명 | 관리할 상품이나 소모품 이름 |
| 현재재고 | 현재 실제로 남아 있는 수량 |
| 안전재고 | 발주 검토를 시작할 기준 수량 |
| 권장발주수량 | 담당자가 미리 정한 주문 검토 수량 |
| 공급처 | 상품을 구매하는 업체 이름 |
| 알림상태 | 정상, 알림완료 또는 확인필요 |
| 우선순위 | 긴급, 높음 또는 보통 |
| AI발주초안 | 공급처에 보내기 전에 검토할 문의 문장 |
| 마지막알림 | 담당자에게 이메일을 보낸 시간 |
스프레드시트 왼쪽 위에 ‘AI 재고 부족 알림’이라는 파일 이름이 표시됩니다.
Apps Script 편집기에 비어 있는 코드 입력 화면이 표시됩니다.
아래 코드를 처음부터 끝까지 복사해 Apps Script 편집기에 붙여 넣습니다.
const CONFIG = Object.freeze({
MODEL: 'gemini-3.5-flash-lite',
TIMEZONE: 'Asia/Seoul',
SHEET_NAME: '재고관리',
SPREADSHEET_ID_KEY: 'INVENTORY_SPREADSHEET_ID',
API_KEY: 'GEMINI_API_KEY',
NOTIFY_EMAIL: 'NOTIFY_EMAIL',
MAX_ITEMS_PER_EMAIL: 30,
COLUMNS: Object.freeze({
code: 1,
name: 2,
current: 3,
safety: 4,
orderQty: 5,
supplier: 6,
status: 7,
priority: 8,
aiDraft: 9,
lastAlert: 10
})
});
function setupInventoryAutomation() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const properties = PropertiesService.getScriptProperties();
properties.setProperty(
CONFIG.SPREADSHEET_ID_KEY,
spreadsheet.getId()
);
let sheet = spreadsheet.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
sheet = spreadsheet.insertSheet(CONFIG.SHEET_NAME);
}
const headers = [
'상품코드',
'상품명',
'현재재고',
'안전재고',
'권장발주수량',
'공급처',
'알림상태',
'우선순위',
'AI발주초안',
'마지막알림'
];
sheet
.getRange(1, 1, 1, headers.length)
.setValues([headers])
.setFontWeight('bold')
.setBackground('#dbeafe');
sheet.setFrozenRows(1);
sheet.getRange('C:E').setNumberFormat('0');
sheet.getRange('J:J').setNumberFormat('yyyy-mm-dd hh:mm');
const widths = [
110,
220,
100,
100,
120,
180,
110,
100,
420,
150
];
widths.forEach(function(width, index) {
sheet.setColumnWidth(index + 1, width);
});
const statusValidation = SpreadsheetApp
.newDataValidation()
.requireValueInList(
['정상', '알림완료', '확인필요'],
true
)
.setAllowInvalid(false)
.build();
sheet
.getRange(
2,
CONFIG.COLUMNS.status,
Math.max(sheet.getMaxRows() - 1, 1),
1
)
.setDataValidation(statusValidation);
if (sheet.getLastRow() === 1) {
sheet
.getRange(2, 1, 3, headers.length)
.setValues([
[
'A-001',
'A4 복사용지',
2,
5,
10,
'사무용품 공급처',
'정상',
'',
'',
''
],
[
'B-002',
'검정 볼펜',
18,
10,
20,
'문구 공급처',
'정상',
'',
'',
''
],
[
'C-003',
'포장 테이프',
0,
4,
12,
'포장재 공급처',
'정상',
'',
'',
''
]
]);
}
sheet
.getRange(1, 1, sheet.getMaxRows(), headers.length)
.setVerticalAlignment('top')
.setWrap(true);
deleteExistingTrigger_();
ScriptApp
.newTrigger('checkLowStock')
.timeBased()
.everyHours(1)
.create();
spreadsheet.toast(
'재고관리 시트와 1시간 자동 실행 설정을 만들었습니다.',
'설정 완료',
6
);
}
function checkLowStock() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(10000)) {
return;
}
try {
const sheet = getInventorySheet_();
const lastRow = sheet.getLastRow();
```
if (lastRow < 2) {
return;
}
const rowCount = lastRow - 1;
const data = sheet
.getRange(2, 1, rowCount, 10)
.getValues();
const lowStockItems = [];
data.forEach(function(row, index) {
const rowNumber = index + 2;
const code = String(row[0] || '').trim();
const name = String(row[1] || '').trim();
if (!code && !name) {
return;
}
const currentRaw = row[2];
const safetyRaw = row[3];
const orderQtyRaw = row[4];
const current = Number(currentRaw);
const safety = Number(safetyRaw);
const orderQty = Number(orderQtyRaw);
const status = String(row[6] || '').trim();
if (
currentRaw === '' ||
safetyRaw === '' ||
!Number.isFinite(current) ||
!Number.isFinite(safety)
) {
row[6] = '확인필요';
row[7] = '';
row[8] = '현재재고와 안전재고를 숫자로 입력하세요.';
row[9] = '';
return;
}
if (current > safety) {
row[6] = '정상';
row[7] = '';
row[8] = '';
row[9] = '';
return;
}
if (status === '알림완료') {
return;
}
lowStockItems.push({
rowNumber: rowNumber,
code: code || '코드 없음',
name: name || '상품명 없음',
current: current,
safety: safety,
orderQty:
Number.isFinite(orderQty) && orderQty > 0
? orderQty
: '확인 필요',
supplier: String(row[5] || '확인 필요').trim(),
priority: getPriority_(current, safety)
});
});
sheet
.getRange(2, 1, rowCount, 10)
.setValues(data);
if (lowStockItems.length === 0) {
return;
}
const itemsToSend = lowStockItems.slice(
0,
CONFIG.MAX_ITEMS_PER_EMAIL
);
const draft = createOrderDraft_(itemsToSend);
sendAlertEmail_(
itemsToSend,
draft,
sheet.getParent().getUrl()
);
const now = new Date();
itemsToSend.forEach(function(item) {
sheet
.getRange(item.rowNumber, 7, 1, 4)
.setValues([
[
'알림완료',
item.priority,
draft,
now
]
]);
});
```
} finally {
lock.releaseLock();
}
}
function createOrderDraft_(items) {
const fallback = buildFallbackDraft_(items);
const apiKey = PropertiesService
.getScriptProperties()
.getProperty(CONFIG.API_KEY);
if (!apiKey) {
return '[기본 초안]\n' + fallback;
}
const safeItems = items.map(function(item) {
return {
code: item.code,
name: item.name,
current: item.current,
safety: item.safety,
orderQty: item.orderQty,
supplier: item.supplier,
priority: item.priority
};
});
const prompt = [
'너는 재고 부족 상품의 발주 문의 이메일 초안을 작성한다.',
'',
'아래 데이터에 있는 사실만 사용한다.',
'가격, 납기일, 할인, 최소주문수량을 만들지 않는다.',
'공급처에 바로 전송되는 문장이 아니라 담당자가 검토할 초안이다.',
'정중한 한국어 이메일 본문만 작성한다.',
'',
'[반드시 포함할 내용]',
'- 부족한 품목의 상품명과 상품코드',
'- 권장발주수량',
'- 공급 가능 여부 문의',
'- 예상 납기와 가격 조건 확인 요청',
'- 내부 확인 후 최종 수량을 확정한다는 안내',
'',
'[재고 부족 상품]',
JSON.stringify(safeItems)
].join('\n');
const url =
'https://generativelanguage.googleapis.com/' +
'v1beta/models/' +
CONFIG.MODEL +
':generateContent';
const payload = {
contents: [
{
role: 'user',
parts: [
{
text: prompt
}
]
}
],
generationConfig: {
maxOutputTokens: 700
}
};
try {
const response = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
headers: {
'x-goog-api-key': apiKey
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
```
if (response.getResponseCode() !== 200) {
return '[기본 초안]\n' + fallback;
}
const result = JSON.parse(response.getContentText());
const parts =
result.candidates &&
result.candidates[0] &&
result.candidates[0].content &&
result.candidates[0].content.parts;
if (!parts || parts.length === 0) {
return '[기본 초안]\n' + fallback;
}
const text = parts
.map(function(part) {
return part.text || '';
})
.join('')
.trim();
return text
? '[AI 생성]\n' + text
: '[기본 초안]\n' + fallback;
```
} catch (error) {
return '[기본 초안]\n' + fallback;
}
}
function sendAlertEmail_(items, draft, spreadsheetUrl) {
const email = PropertiesService
.getScriptProperties()
.getProperty(CONFIG.NOTIFY_EMAIL);
if (!email) {
throw new Error(
'NOTIFY_EMAIL이 없습니다. 스크립트 속성을 확인하세요.'
);
}
if (MailApp.getRemainingDailyQuota() < 1) {
throw new Error(
'오늘 사용할 수 있는 이메일 발송 한도가 없습니다.'
);
}
const itemList = items
.map(function(item, index) {
return [
index + 1 + '. ' + item.name + ' (' + item.code + ')',
'현재재고: ' + item.current,
'안전재고: ' + item.safety,
'권장발주수량: ' + item.orderQty,
'우선순위: ' + item.priority,
'공급처: ' + item.supplier
].join(' / ');
})
.join('\n');
const body = [
'재고 부족 상품이 확인되었습니다.',
'',
itemList,
'',
'[발주 문의 초안]',
draft,
'',
'재고관리 시트:',
spreadsheetUrl,
'',
'주의: 수량, 가격과 납기를 확인한 뒤 사용하세요.'
].join('\n');
MailApp.sendEmail({
to: email,
subject:
'[재고 부족] ' +
items.length +
'개 상품 확인 필요',
body: body,
name: 'AI 재고 알림'
});
}
function getPriority_(current, safety) {
if (current <= 0) {
return '긴급';
}
if (safety > 0 && current <= safety / 2) {
return '높음';
}
return '보통';
}
function buildFallbackDraft_(items) {
const lines = items.map(function(item) {
return (
'- ' +
item.name +
' (' +
item.code +
'), 권장발주수량: ' +
item.orderQty
);
});
return [
'안녕하세요.',
'',
'아래 품목의 재고가 안전재고 이하로 확인되어',
'공급 가능 여부를 문의드립니다.',
'',
lines.join('\n'),
'',
'예상 납기와 가격 조건을 회신 부탁드립니다.',
'수량과 조건은 내부 검토 후 최종 확정하겠습니다.',
'',
'감사합니다.'
].join('\n');
}
function getInventorySheet_() {
const spreadsheetId = PropertiesService
.getScriptProperties()
.getProperty(CONFIG.SPREADSHEET_ID_KEY);
if (!spreadsheetId) {
throw new Error(
'setupInventoryAutomation을 먼저 실행하세요.'
);
}
const spreadsheet = SpreadsheetApp.openById(spreadsheetId);
const sheet = spreadsheet.getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
throw new Error('재고관리 시트를 찾지 못했습니다.');
}
return sheet;
}
function deleteExistingTrigger_() {
ScriptApp
.getProjectTriggers()
.forEach(function(trigger) {
if (
trigger.getHandlerFunction() ===
'checkLowStock'
) {
ScriptApp.deleteTrigger(trigger);
}
});
}
function testInventoryAlert() {
checkLowStock();
}
코드를 붙여 넣은 뒤 위쪽의 디스크 모양 저장 버튼을 클릭합니다.
코드가 저장되고 편집기 왼쪽에 빨간 오류 표시가 나타나지 않습니다.
API 키를 블로그 본문, 공개 문서 또는 캡처 이미지에 노출하면 안 됩니다.
| 속성 이름 | 입력할 값 |
|---|---|
| GEMINI_API_KEY | Google AI Studio에서 복사한 API 키 |
| NOTIFY_EMAIL | 재고 부족 알림을 받을 담당자 이메일 |
스크립트 속성 목록에 GEMINI_API_KEY와 NOTIFY_EMAIL이 표시됩니다.
스프레드시트 아래쪽에 ‘재고관리’ 시트가 생성되고 예시 상품 3개가 표시됩니다.
Apps Script 트리거에는 checkLowStock 함수가 한 시간 간격으로 등록됩니다.
| 상품명 | 현재재고 | 안전재고 | 예상 결과 |
|---|---|---|---|
| A4 복사용지 | 2개 | 5개 | 높음 |
| 검정 볼펜 | 18개 | 10개 | 정상 |
| 포장 테이프 | 0개 | 4개 | 긴급 |
복사용지와 포장 테이프가 한 통의 이메일에 표시됩니다.
검정 볼펜은 현재재고가 안전재고보다 많기 때문에 이메일에 포함되지 않습니다.
Gemini API가 정상적으로 작동하면 시트의 AI발주초안 앞에 AI 생성이 표시됩니다.
API 키가 없거나 요청에 실패하면 기본 초안이 표시됩니다. 기본 초안이 표시돼도 재고 부족 이메일은 정상적으로 전송됩니다.
이미 알림완료 상태로 변경된 상품에는 같은 이메일이 다시 전송되지 않습니다.
현재재고가 안전재고보다 많아졌기 때문에 알림상태가 정상으로 돌아갑니다.
이후 현재재고가 다시 안전재고 이하로 내려가면 새로운 알림을 받을 수 있습니다.
재고 수량이 자주 바뀌지 않는 소규모 사업장이라면 1분이나 5분마다 확인할 필요가 없습니다. 한 시간 간격으로 시작한 뒤 실제 업무 속도에 맞춰 조정하는 것이 좋습니다.
원인: setupInventoryAutomation 함수를 실행하지 않았거나 스프레드시트 권한을 허용하지 않았습니다.
해결 방법:
원인: 알림 이메일 주소를 스크립트 속성에 저장하지 않았습니다.
해결 방법:
원인: Gemini API 키가 없거나 API 요청이 실패했습니다.
해결 방법:
현재재고나 안전재고에 ‘5개’처럼 문자가 함께 입력됐을 수 있습니다.
수량 셀에는 5, 10, 20처럼 숫자만 입력해야 합니다.
해당 상품의 알림상태를 정상으로 변경한 뒤 testInventoryAlert를 다시 실행하세요.
현재재고를 안전재고보다 큰 숫자로 변경한 뒤 testInventoryAlert를 실행하거나 다음 자동 실행까지 기다리세요.
코드가 일부 빠졌거나 API 요청 형식이 변경됐을 수 있습니다.
본문의 전체 코드를 다시 복사하고 Apps Script 실행 기록에서 실제 오류 메시지를 확인하세요.
API 키가 잘못됐거나 사용할 수 없는 상태일 수 있습니다.
Google AI Studio에서 새 API 키를 생성하고 GEMINI_API_KEY 값을 교체하세요.
Apps Script 이메일 발송량은 구글 계정 종류에 따라 제한됩니다.
이번 자동화는 부족한 상품을 한 통으로 묶어 전송하지만, 다른 Apps Script에서도 이메일을 많이 보냈다면 남은 할당량이 부족할 수 있습니다.
| 비교 항목 | 수동 확인 | Apps Script 자동화 | Make 자동화 |
|---|---|---|---|
| 기본 비용 | 무료 | 무료 범위에서 시작 가능 | 무료 플랜 제공, 사용량에 따라 유료 |
| 코드 | 필요 없음 | 코드 붙여 넣기 필요 | 대부분 화면에서 설정 |
| 자동 확인 | 불가능 | 가능 | 가능 |
| 이메일 알림 | 직접 작성 | 자동 전송 | 자동 전송 |
| AI 초안 | 직접 작성 | Gemini 연결 | OpenAI 등 외부 AI 연결 가능 |
| 오류 확인 | 별도 기록 없음 | Apps Script 실행 기록 | 모듈별 실행 기록 |
| 유지관리 | 매일 직접 확인 | 코드와 트리거 확인 | 시나리오와 크레딧 확인 |
| 추천 대상 | 상품 수가 매우 적은 개인 | 무료 자동화를 시작할 사용자 | 여러 서비스를 연결할 팀 |
복사용지, 프린터 토너, 볼펜, 물티슈처럼 자주 사용하는 사무용품을 관리할 수 있습니다.
특정 수량 이하가 되면 총무 담당자에게 자동으로 이메일을 보내도록 설정하면 됩니다.
택배 상자, 포장 테이프, 완충재, 사은품처럼 출고에 필요한 소모품을 관리하기 좋습니다.
판매 상품의 재고가 남아 있어도 포장재가 떨어지면 배송이 지연될 수 있습니다. 포장재를 별도 품목으로 등록하면 이런 상황을 줄일 수 있습니다.
컵, 빨대, 포장 용기, 냅킨 같은 비식품 소모품 관리에 활용할 수 있습니다.
식재료는 유통기한, 입고일과 보관 온도까지 확인해야 하므로 별도의 관리 항목이 필요합니다.
처음에는 구글 시트로 시작한 뒤 판매 시스템이나 사내 데이터베이스와 연결할 수 있습니다.
재고가 부족할 때 Gmail뿐 아니라 Slack이나 Microsoft Teams로 알림을 보내는 방식으로도 확장할 수 있습니다.
현재재고가 안전재고와 같거나 더 적어지면 알림 대상이 됩니다.
예를 들어 안전재고가 5개라면 현재재고가 5개 이하일 때 이메일이 발송됩니다.
가능합니다. API 키가 없거나 Gemini 요청이 실패하면 코드에 미리 작성된 기본 발주 문의 초안이 사용됩니다.
아닙니다. 이번 자동화는 재고 담당자에게만 검토용 이메일을 보냅니다.
담당자가 수량, 가격과 납기를 확인한 뒤 공급처에 보내야 합니다.
아닙니다. Apps Script는 구글 서버에서 실행되므로 컴퓨터가 꺼져 있어도 설정한 간격에 따라 작동합니다.
이메일을 보낸 상품은 알림완료 상태로 변경됩니다.
재고가 보충되어 정상 상태로 돌아가기 전까지 같은 알림은 반복되지 않습니다.
아닙니다. 권장발주수량은 담당자가 직접 입력해야 합니다.
판매량, 납기, 최소주문수량과 보관 공간을 모르는 상태에서 AI가 발주량을 결정하면 과잉 재고가 생길 수 있습니다.
시트에 많은 상품을 입력할 수 있지만 Apps Script의 실행 시간과 이메일 할당량이 적용됩니다.
이번 코드는 이메일 한 통에 부족 상품을 최대 30개까지 넣도록 설정했습니다.
구글 시트 재고 부족 알림 자동화는 복잡한 재고관리 프로그램을 도입하기 전에 가볍게 시작하기 좋은 방법입니다.
상품별 현재재고와 안전재고만 정확하게 관리하면 부족한 상품을 자동으로 찾고 담당자에게 이메일을 보낼 수 있습니다.
Gemini는 반복적인 이메일 작성을 줄여주는 초안을 만들지만 실제 발주를 결정하는 담당자는 아닙니다.
AI가 작성한 문장은 검토용으로만 사용하고 실제 수량, 가격, 납기와 공급 가능 여부는 사람이 반드시 확인해야 합니다.
처음에는 실제 발주가 필요 없는 시험 상품으로 이메일 전송, 중복 알림 차단과 상태 초기화가 정상적으로 작동하는지 확인한 뒤 업무에 적용해 보세요.
구글 시트와 AI를 연결하면 안전재고 이하 상품을 자동으로 찾고, 담당자가 검토할 발주 문의 초안까지 준비할 수 있습니다.
| 구글 시트 상품 설명 AI 자동 생성, 상세페이지 문구 무료로 만들기 (2026 최신판) (0) | 2026.07.30 |
|---|---|
| 구글 시트 고객 리뷰 AI 감정 분석 자동화, 불만 유형과 답변 초안까지 (2026 최신판) (1) | 2026.07.30 |
| Make란? 코딩 없이 무료로 시작하는 초보자 업무 자동화 도구 (2026 최신판) (0) | 2026.07.29 |
| 영수증 AI 자동 정리|사진을 구글 시트 지출 내역으로 저장하기 (0) | 2026.07.28 |
| 🚀 "[AI 개발 7단계] 생성형 AI(Generative AI) 기반 가이드: 영화, 이미지, 음악을 즐기는 인공지능" (6) | 2025.02.24 |