DailyByte
일상과 연결되는 AI
DailyByte
일상과 연결되는 AI
← 전체 코드 목록
📖 사용 방법
구글 시트에 이미지 설명을 입력하면, 스테이블 디퓨전 AI가 이미지를 만들고 구글 문서에 자동으로 정리해주는 똑똑한 비서 스크립트입니다.
이런 분께 추천해요
- AI 이미지 생성에 관심 있지만 복잡한 코딩은 어려운 분
- 반복적인 이미지 생성 및 문서화 작업을 자동화하고 싶은 분
- 구글 시트와 구글 문서를 자주 활용하는 분
- 나만의 AI 이미지 아카이브를 만들고 싶은 분
완성하면 이렇게 됩니다
- 구글 시트의 B열(이미지 설명)에 텍스트를 입력하면, C열에 구글 문서 링크가 자동으로 생성됩니다.
- 생성된 구글 문서에는 AI가 만든 이미지가 삽입되어 있습니다.
- A열(제목)이 같으면 같은 문서에 이미지가 추가되고, 다르면 새로운 문서가 만들어집니다.
- 한글로 프롬프트(이미지 설명)를 입력해도 자동으로 영어로 번역되어 AI에 전달됩니다.
준비물
- 구글 계정 (구글 시트, 구글 문서, 구글 앱스 스크립트 사용)
- Stability AI (스테이블리티 AI) 계정 및 API 키
따라하기
1단계: Stability AI API 키 발급받기
Stability AI 웹사이트에 접속하여 회원가입/로그인 후, 'API Keys' 메뉴에서 새로운 API 키를 발급받아 복사해 둡니다. 이 키는 AI가 이미지를 생성할 수 있도록 허락하는 일종의 비밀번호입니다.
2단계: 구글 시트 준비 및 스크립트 붙여넣기
- 새 구글 시트를 만드세요. 시트 이름을 'AI 이미지 생성기' 등으로 알아보기 쉽게 변경해 두시면 좋습니다.
- 시트의 A1셀에 '제목', B1셀에 '이미지 설명', C1셀에 '문서 링크'를 입력합니다.
- 구글 시트 상단 메뉴에서 '확장 프로그램'을 클릭한 후, 'Apps Script'를 선택합니다. 새로운 탭에 스크립트 편집기(Apps Script 에디터)가 열립니다.
- 스크립트 편집기에 기본으로 작성되어 있는 코드(
function myFunction() { ... })를 모두 지웁니다. - 이 콘텐츠에 제공된 [코드]를 전체 복사하여 스크립트 편집기에 붙여넣습니다.
- 붙여넣은 코드 중 상단 '설정 영역' 부분을 찾아 수정합니다.
const API_KEY = 'Stability AI API 키 넣는 곳';부분에 1단계에서 복사해 둔 Stability AI API 키를 붙여넣으세요.' '(작은따옴표) 안에 키를 넣어야 합니다.const SHEET_NAME = '시트1';부분에 현재 사용 중인 구글 시트의 정확한 이름을 입력하세요. (예:'AI 이미지 생성기')const IMAGE_WIDTH = 800;은 문서에 삽입될 이미지의 너비(픽셀 단위)를 설정하는 부분입니다. 원하시는 대로 숫자를 변경할 수 있습니다.
- 스크립트 편집기 상단의 저장 버튼(디스크 모양 아이콘)을 클릭하여 스크립트를 저장합니다.
3단계: 트리거 설정하기
이 스크립트는 구글 시트의 B열이 수정될 때 자동으로 실행되도록 설정해야 합니다.
- Apps Script 에디터 좌측 메뉴에서 '트리거' 아이콘(시계 모양)을 클릭합니다.
- 화면 오른쪽 하단의 '트리거 추가' 버튼을 클릭합니다.
- 트리거 설정 창에서 다음 항목들을 선택합니다.
- 실행할 함수 선택:
onEdit - 이벤트 소스 선택:
스프레드시트에서 - 이벤트 유형 선택:
수정 시
- 실행할 함수 선택:
- '저장' 버튼을 클릭합니다.
- 처음 스크립트를 실행할 때 구글 계정 권한 요청 팝업이 나타날 수 있습니다. '내 계정 선택' → '고급' → '안전하지 않은 앱으로 이동' → '허용'을 클릭하여 스크립트가 구글 시트와 문서에 접근할 수 있도록 권한을 부여해 주세요.
4단계: AI 이미지 생성 테스트
- 구글 시트로 돌아갑니다.
- B열(이미지 설명)에 원하는 이미지 프롬프트(AI에게 만들라고 지시하는 문장)를 입력합니다. 예를 들어 "숲속을 걷는 귀여운 강아지", "미래 도시의 풍경" 등을 입력해 보세요.
- A열(제목)에는 해당 이미지들이 들어갈 구글 문서의 제목을 입력합니다. 예를 들어 "강아지 그림 모음" 또는 "미래 도시"라고 입력할 수 있습니다.
- B열에 내용을 입력하고 엔터를 누르면, C열에 '처리 중...' 메시지가 나타납니다. 잠시 후 AI가 이미지를 생성하고 구글 문서에 삽입한 뒤, C열에 해당 구글 문서의 링크가 생성됩니다.
- 생성된 링크를 클릭하여 AI가 만든 이미지를 확인해 보세요. A열의 제목이 같으면 같은 문서에 이미지가 계속 추가되고, 제목이 다르면 새로운 문서가 만들어집니다.
자주 막히는 부분
- API 키 오류: 스크립트의
API_KEY변수에 Stability AI API 키가 정확히 입력되었는지 다시 확인해 주세요. 작은따옴표(' ') 안에 키가 들어가야 합니다. - 트리거 설정 오류: Apps Script 에디터의 '트리거' 메뉴에서
onEdit함수가 '수정 시' 이벤트로 올바르게 설정되었는지 확인해 주세요. - 권한 허용 문제: 스크립트 실행 시 구글 계정 권한을 '허용'했는지 다시 확인해 보세요. 권한이 없으면 구글 문서에 이미지를 삽입할 수 없습니다.
- 시트 이름 불일치: 스크립트의
SHEET_NAME변수에 입력한 시트 이름이 실제 구글 시트의 이름과 띄어쓰기, 대소문자까지 정확히 일치하는지 확인하세요. - B열 외 다른 열 수정: 이 스크립트는 B열(이미지 설명)이 수정될 때만 작동합니다. 다른 열을 수정해도 AI 이미지가 생성되지 않습니다.
이렇게도 써보세요 (응용)
- 다양한 이미지 생성: B열에 여러 프롬프트를 입력하여 다양한 이미지를 한 번에 생성하고, A열 제목을 같게 하여 하나의 문서에 모아보세요.
- 아이디어 스케치: 제품 디자인, 캐릭터 구상 등 초기 아이디어 단계에서 빠르게 시각 자료를 만들 때 활용해 보세요.
- 콘텐츠 제작 보조: 블로그 글, 발표 자료 등에 필요한 이미지를 효율적으로 생성하고 관리할 수 있습니다.
- 이미지 너비 조절: 스크립트 상단의
IMAGE_WIDTH값을 변경하여 문서에 삽입될 이미지의 너비를 조절할 수 있습니다. (높이는 비율에 맞게 자동으로 조절됩니다.)
한 줄 정리
구글 시트에 프롬프트만 입력하면 AI 이미지가 자동으로 생성되어 구글 문서에 착착 정리되는 나만의 AI 이미지 비서를 만들어 보세요!
💻 코드
/**
* 구글 시트와 스테이블 디퓨전 API를 연동하여 문서를 생성하는 스크립트
* * 기능:
* 1. 시트의 B열(프롬프트)에 내용이 입력되면 자동으로 실행됩니다 (onEdit 트리거).
* 2. 한글 프롬프트는 자동으로 영어로 번역되어 API에 전달됩니다.
* 3. A열의 제목이 연속되면 같은 문서에, 다르면 새 문서에 이미지를 추가합니다.
* 4. 생성된 문서 링크를 C열에 기록합니다.
*/
// ==========================================
// [설정 영역] 사용자의 API 정보를 입력하세요
// ==========================================
// 예시: Stability AI (DreamStudio) API 키
const API_KEY = 'Stability AI API 키 넣는 곳';
// 최신 SDXL 1.0 정식 버전 엔드포인트 (1024x1024 지원)
const API_URL = 'https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image';
// 작업할 시트 이름 (없으면 첫 번째 시트를 자동으로 사용합니다)
const SHEET_NAME = '시트1';
// [요청 반영] 문서에 삽입할 이미지 너비 (높이는 비율에 맞게 자동 조절)
const IMAGE_WIDTH = 800;
// ==========================================
// [트리거 함수] B열 수정 시 자동 실행되도록 설정
// ==========================================
/**
* 시트가 수정될 때 자동으로 실행되는 함수입니다.
* B열에 내용이 입력되면 generateImageForSingleRow 함수를 호출합니다.
* 이 함수를 사용하려면 Apps Script 에디터에서 트리거를 설정해야 합니다.
* 설정 방법은 아래 설명을 참고해주세요.
*/
function onEdit(e) {
// 이벤트 객체에서 수정된 셀 정보 추출
const range = e.range;
const sheet = range.getSheet();
const sheetName = sheet.getName();
// 1. 설정된 시트 이름과 일치하는지 확인
// 2. 수정된 열이 B열(2번째 열)인지 확인
// 3. 행이 데이터 영역(2행 이상)인지 확인
if (sheetName === SHEET_NAME && range.getColumn() === 2 && range.getRow() > 1) {
// B열이 수정되면 해당 행만 처리하는 함수를 호출
generateImageForSingleRow(sheet, range.getRow());
}
}
// ==========================================
// [메인 함수] 단일 행을 처리하는 함수
// ==========================================
function generateImageForSingleRow(sheet, rowIndex) {
const rowData = sheet.getRange(rowIndex, 1, 1, 3).getValues()[0];
const bookTitle = rowData[0]; // A열: 책 제목
const prompt = rowData[1]; // B열: 이미지 설명
const existingLink = rowData[2]; // C열: 기존 링크
// 제목이나 프롬프트가 없으면 중단
if (!bookTitle || !prompt) return;
// 이미 링크가 생성되어 있고(성공 케이스), 프롬프트가 변경되지 않은 경우 중복 방지
if (existingLink && !existingLink.toString().startsWith("실패") && !existingLink.toString().startsWith("에러")) {
// 이미 처리가 완료된 경우 스킵할 수 있으나, onEdit은 프롬프트 변경 시 항상 실행됨.
// 여기서는 프롬프트 변경 시 재작업을 허용함.
}
Logger.log(`처리 중(행 ${rowIndex}): ${bookTitle}`);
sheet.getRange(rowIndex, 3).setValue("처리 중...");
SpreadsheetApp.flush();
// 1. 번역 로직
let translatedPrompt = prompt;
try {
if (/[가-힣]/.test(prompt)) {
translatedPrompt = LanguageApp.translate(prompt, 'ko', 'en');
Logger.log(`번역됨: "${prompt}" -> "${translatedPrompt}"`);
}
} catch (e) {
Logger.log(`번역 실패, 원본 사용: ${e.toString()}`);
translatedPrompt = prompt;
}
try {
// 2. 이미지 생성 API 호출 (번역된 프롬프트 사용)
const apiResult = callStableDiffusionAPI(translatedPrompt);
if (!apiResult.success) {
const errorMsg = `실패: ${apiResult.error}`;
Logger.log(errorMsg);
sheet.getRange(rowIndex, 3).setValue(errorMsg);
return;
}
const imageBlob = apiResult.blob;
// 3. 문서 ID 및 URL 결정 (이전 행의 데이터 가져오기)
let doc;
let body;
let currentDocUrl;
let currentDocId;
// 이전 행의 제목과 비교 (i는 1부터 시작하므로 rowIndex - 1)
const prevRowIndex = rowIndex - 1;
const prevRowData = prevRowIndex > 1 ? sheet.getRange(prevRowIndex, 1, 1, 3).getValues()[0] : null;
const previousTitle = prevRowData ? prevRowData[0] : "";
const prevDocLink = prevRowData ? prevRowData[2] : "";
// 이전 행의 제목과 같고, 이전 행에 유효한 문서 링크가 있는 경우
if (bookTitle === previousTitle && prevDocLink && !prevDocLink.toString().startsWith("실패")) {
try {
currentDocId = DocumentApp.openByUrl(prevDocLink).getId();
currentDocUrl = prevDocLink;
doc = DocumentApp.openById(currentDocId);
body = doc.getBody();
} catch(e) {
// 문서 열기 실패 시 새 문서 생성
doc = DocumentApp.create(bookTitle + " - 이미지 모음");
Utilities.sleep(500); // 안전장치
currentDocId = doc.getId();
currentDocUrl = doc.getUrl();
body = doc.getBody();
try {
const titlePara = body.insertParagraph(0, bookTitle);
titlePara.setHeading(DocumentApp.ParagraphHeading.HEADING_1);
} catch (styleError) {
Logger.log(`제목 스타일 적용 실패(내용은 유지): ${styleError}`);
}
}
} else {
// 제목이 다르거나 첫 시작이면 새 문서 생성
doc = DocumentApp.create(bookTitle + " - 이미지 모음");
Utilities.sleep(500); // 안전장치
currentDocId = doc.getId();
currentDocUrl = doc.getUrl();
body = doc.getBody();
try {
const titlePara = body.insertParagraph(0, bookTitle);
titlePara.setHeading(DocumentApp.ParagraphHeading.HEADING_1);
} catch (styleError) {
Logger.log(`제목 스타일 적용 실패(내용은 유지): ${styleError}`);
}
}
// 4. 문서에 이미지 및 설명 추가
// [요청 반영] 프롬프트 텍스트 제거하고 이미지와 구분선만 추가
try {
// 이미지 삽입
body.appendImage(imageBlob)
.setWidth(IMAGE_WIDTH);
} catch (imgError) {
sheet.getRange(rowIndex, 3).setValue("실패: 이미지 포맷 오류");
return;
}
// 문서 구분을 위한 내용 추가 (선택 사항)
body.appendParagraph("--------------------------------------------------");
doc.saveAndClose();
// 5. 시트 C열에 링크 기록
sheet.getRange(rowIndex, 3).setValue(currentDocUrl);
} catch (e) {
Logger.log(`에러 발생 (행 ${rowIndex}): ${e.toString()}`);
sheet.getRange(rowIndex, 3).setValue(`에러: ${e.toString()}`);
}
}
// ==========================================
// [API 호출 함수] 스테이블 디퓨전 API 통신
// ==========================================
function callStableDiffusionAPI(prompt) {
const payload = {
"text_prompts": [
{
"text": prompt,
"weight": 1
}
],
"cfg_scale": 7,
"height": 1024,
"width": 1024,
"samples": 1,
"steps": 30
};
const options = {
"method": "post",
"headers": {
"Content-Type": "application/json",
"Accept": "image/png",
"Authorization": `Bearer ${API_KEY}`
},
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
};
try {
const response = UrlFetchApp.fetch(API_URL, options);
const responseCode = response.getResponseCode();
if (responseCode === 200) {
return { success: true, blob: response.getBlob() };
} else {
const errorText = response.getContentText();
Logger.log(`API 오류(${responseCode}): ${errorText}`);
try {
const errorJson = JSON.parse(errorText);
return { success: false, error: errorJson.message || `API 오류(${responseCode})` };
} catch (e) {
return { success: false, error: `API 오류(${responseCode})` };
}
}
} catch (e) {
Logger.log(`API 통신 실패: ${e}`);
return { success: false, error: "통신 실패(인터넷/URL 확인)" };
}
}