오토포스트 + AI: 완전 자동화를 위해 AI를 어디에 붙이고 어디서 멈췄나

TL;DR
- 목표는 "상품만 등록하면 세 플랫폼 글이 저절로"였다. 그런데 글 전체를 AI에게 맡기지 않았다. 고정 문구는 템플릿, 상품마다 바뀌는 몇 줄만 AI가 쓴다.
- AI 호출은 상품 하나에 한 번이다. 네이버 소개·인스타 첫 줄·인스타 소개·태그를 JSON 스키마로 한꺼번에 받고, URL을 지우고 길이를 자르는 가드레일을 거친다.
- 이미지는 AI가 아니라 코드가 그린다. 매번 같은 품질이 나와야 하는 곳에는 확률적인 생성 모델을 쓰지 않았다.
- 프롬피드 글은 정한 시각에 가져오기 → AI 문구 → 인스타 게시까지 사람 없이 돈다. 네이버만은 에디터를 채워 두고 발행 버튼은 사람이 누른다.
들어가며
첫 글에서 postflow를 만든 이야기를, 두 번째 글에서 열흘 동안 쓰며 고친 이야기를 했다. 이번 글은 그 사이에 계속 붙여 온 AI 부분만 따로 정리한다.
처음 목표는 단순했다. 카카오톡 테마 하나를 완성하면 네이버 블로그, 포스타입, 인스타그램에 같은 소식을 올려야 한다. 손으로 하면 한 시간이 넘는다. 이걸 "상품 등록 버튼 하나"로 줄이고 싶었다.
"AI한테 글 다 써 달라고 하면 되지 않나?" 처음엔 그렇게 생각했다. 결론부터 말하면 그렇게 하지 않았다. 이 글은 어디에 AI를 붙였고, 어디서 일부러 멈췄는지에 대한 기록이다.
전체 흐름: 누가 무엇을 하나
상품 하나가 세 플랫폼에 올라가기까지를 단계별로 나누면 이렇다.
| 단계 | 하는 일 | 담당 |
|---|---|---|
| 1. 테마 만들기 | 캐릭터 그림 → 에셋 시트 → .ktheme | 이미지 AI + 사람 (ktheme-maker) |
| 2. 상품 등록 | 실기기 캡처, 캐릭터, 시리즈·버전 이름 | 사람 |
| 3. 글 틀 | 인사말, 구매 링크, 이용 안내, 태그 기본값 | 템플릿 (Mustache) |
| 4. 바뀌는 문구 | 소개 문단, 인스타 첫 줄, 롱테일 태그 | AI (Gemini) |
| 5. 이미지 | 인스타 첫 장·안쪽 장·마지막 장, 블로그 첫 사진 | 코드 (Java2D) |
| 6. 인스타 게시 | 캐러셀 업로드 | 서버 (Graph API) |
| 7. 네이버·포스타입 | 에디터 열고 제목·본문·사진 채우기 | Mac 에이전트 (Playwright) |
| 8. 발행 | 마지막 확인 후 [발행] | 사람 |
AI가 맡는 건 4번 한 칸이다. 나머지는 템플릿, 코드, 에이전트가 정해진 대로 한다. 이렇게 나눈 이유를 하나씩 적어 본다.
1. 고정 문구는 템플릿, 바뀌는 문구만 AI
처음 시도: 글 전체를 AI에게
처음에는 상품 정보를 주고 블로그 글 전체를 써 달라고 했다. 결과물은 그럴듯했지만 쓸 수가 없었다.
- 구매 링크와 이용 안내 문구가 매번 조금씩 달랐다. 이건 틀리면 안 되는 부분이다.
- 가끔 사이트에 없는 기능이나 가격을 지어냈다.
- 원래 내가 쓰던 글의 순서(인사 → 소개 → 미리보기 → 구매 링크 → 이용 안내)가 흐트러졌다.
바꾼 방식: AI 슬롯
글을 다시 뜯어보니 상품마다 실제로 바뀌는 건 몇 줄뿐이었다. 그래서 글 틀은 Mustache 템플릿으로 고정하고, 바뀌는 자리만 {{ai_intro}}, {{ai_caption_hook}} 같은 AI 슬롯으로 비워 뒀다.
| 슬롯 | 들어가는 곳 | 길이 상한 |
|---|---|---|
naver.intro | 네이버 블로그 소개 문단 | 300자 |
instagram.caption_hook | 인스타 캡션 첫 줄 (시리즈 소개) | 50자 |
instagram.intro | 인스타 버전 소개 문단 | 250자 |
common.tags | 추가 태그 (쉼표 구분) | 200자, 최대 10개 |
템플릿은 설정 화면에서 고칠 수 있고, 고친 기록이 남아 되돌릴 수도 있다. 링크, 가격, 이용 안내처럼 틀리면 안 되는 문장은 전부 템플릿 쪽에 있다. AI 문구는 그 사이에 끼워질 뿐이다.
![오토포스트의 AI 문구 칸: 슬롯마다 [다시 쓰기]와 직접 고치기](/generated/img/images-blogPost-postflow-3-ai-panel-1600.webp)
이 구조 덕분에 AI가 이상한 문장을 써도 글 전체가 망가지지 않는다. 한 칸만 [다시 쓰기]하거나 직접 고치면 된다.
2. 상품 하나에 호출 한 번, JSON 스키마로
왜 한 번인가
슬롯이 네 개라고 네 번 부르면 비용과 지연이 네 배가 되고, 슬롯끼리 말투가 따로 놀았다. 그래서 상품 하나에 한 번만 부르고, 네 슬롯을 한 JSON으로 받는다. Gemini의 responseSchema를 쓰면 모양이 틀린 답이 오지 않는다.
private static final Map<String, Object> RESPONSE_SCHEMA = Map.of(
"type", "OBJECT",
"properties", Map.of(
"naver", object(Map.of("intro", string()), "intro"),
"instagram", object(Map.of("caption_hook", string(), "intro", string()), "caption_hook", "intro"),
"tags", Map.of("type", "ARRAY", "items", string())),
"required", List.of("naver", "instagram", "tags"));무엇을 넣어 보내나
| 넣는 것 | 이유 |
|---|---|
| 시스템 프롬프트 | 말투(친근한 존댓말, 이모지는 문단마다 1~2개)와 금지 규칙 |
| 실제 게시글 예시 | 내가 예전에 쓴 글. 말투를 흉내 내게 |
| 시리즈·버전·OS·컨셉, 버전 수 | "이번 시리즈는 총 N가지 버전" 같은 문장에 필요 |
| 첫 번째 사진 (512px로 줄여서) | 배경색·분위기를 글에 반영. 캐릭터 이름을 지어내지 않게 "색감과 분위기만 참고" 규칙 |
| 슬롯별 형식 지시 | 예: "첫 줄은 '이번에는 ~ 테마로 준비해봤답니다.' 형식" |
시스템 프롬프트의 규칙 중 가장 중요한 줄은 이것이다.
- URL, 가격, 이용 제한·재배포 관련 문구는 절대 쓰지 않는다(템플릿이 따로 넣는다).AI가 쓰면 안 되는 것을 미리 템플릿으로 빼 두고, 프롬프트에서 "그건 네 일이 아니다"라고 못 박는 방식이다.
태그는 롱테일로
태그도 그냥 "관련 태그 써 줘"라고 하면 #카톡테마 같은 큰 태그만 나온다. 이런 태그는 이미 템플릿이 붙인다. 그래서 실제로 검색되는 형식을 예시로 줬다.
tags: 이 테마가 검색될 롱테일 태그 6~8개. 사람들이 실제로 검색하는 형식으로:
'캐릭터+카톡테마'(시나모롤카톡테마), 'OS+캐릭터+카톡테마'(아이폰키티카톡테마),
'스타일+카톡테마'(도트키티카톡테마) ...
'카톡테마'처럼 캐릭터·스타일 없는 일반 태그는 쓰지 않는다(따로 붙는다)3. AI 답을 그대로 믿지 않는다: 가드레일
프롬프트로 부탁해도 AI는 가끔 규칙을 어긴다. 그래서 받은 답을 저장하기 전에 코드로 한 번 더 거른다.
static String sanitize(AiSlot slot, String text) {
String t = text.replaceAll("(https?://|www\\.)\\S+", "").strip(); // 링크는 템플릿만 넣는다
if (slot == AiSlot.TAGS) {
return Tags.split(t).stream().distinct().limit(AiSlot.MAX_TAGS)...; // 중복 제거, 최대 10개
}
// 소개 문단은 줄바꿈만 살리고 빈 줄은 없앤다 (빈 줄은 템플릿의 문단 간격과 겹친다)
...
return t.codePointCount(0, t.length()) <= slot.maxLength() ? t : 잘라냄;
}| 가드레일 | 막는 것 |
|---|---|
| URL 지우기 | 지어낸 링크, 이상한 사이트 링크 |
| 길이 자르기 (글자 수는 코드 포인트 기준) | 인스타 첫 줄이 길어져 "더 보기"에 묻히는 것. 이모지가 반으로 잘리지 않게 |
| 태그 정리 | 중복, # 붙은 태그, 10개 초과 |
| 빈 줄 정리 | 템플릿이 넣는 문단 간격과 겹쳐 빈 줄이 두 번 생기는 것 |
그리고 AI가 쓴 글이 화면에 HTML로 들어갈 때는 이스케이프한다. AI 문구나 남이 올린 프롬프트 제목에 <img onerror> 같은 게 섞여도 실행되지 않게 하려는 것이다. 이건 보안 점검 때 따로 고친 부분이다.
4. 사람이 고친 문장은 AI가 덮어쓰지 않는다
AI 문구가 마음에 안 들면 직접 고친다. 문제는 그다음이다. 사진을 바꾸거나 [고치지 않은 슬롯 모두 다시 쓰기]를 누르면 AI가 다시 돈다. 이때 내가 고친 문장이 날아가면 안 된다.
그래서 슬롯마다 edited 표시를 둔다.
for (AiSlot slot : targets) {
AiDraft d = ...;
boolean explicitlyAsked = forced != null && forced.contains(slot);
if (d.isEdited() && !explicitlyAsked) {
continue; // 직접 고친 슬롯은 건너뛴다
}
d.generated(sanitize(slot, values.get(slot)), copy.model());
}| 누른 버튼 | 직접 고친 슬롯 | 안 고친 슬롯 |
|---|---|---|
| [고치지 않은 슬롯 모두 다시 쓰기] | 그대로 | 새로 씀 |
| 그 슬롯의 [다시 쓰기] | 새로 씀 (콕 집어 요청했으니) | 새로 씀 |
AI가 쓰면 안 되는 슬롯도 있다
예전에 손으로 올린 글을 가져와 다시 올리면, 캡션이 예전 게시물과 달라지는 문제가 있었다. 그래서 "인스타 캡션 전체" 슬롯을 따로 만들었다. 이 칸에 글이 있으면 템플릿과 AI를 모두 건너뛰고 그대로 올린다. 예전 글을 가져올 때는 원래 캡션이 여기에 들어간다.
이 슬롯은 AI 생성 대상에서 빠져 있다(generated() == false). AI에게 맡기지 않는 칸을 코드로 명시해 둔 셈이다.
5. 기다리게 하지 않기: 비동기와 화면 갱신
AI 응답은 몇 초에서 길면 수십 초가 걸린다. 상품을 등록하는 순간 그걸 기다리게 하고 싶지 않았다.
- 상품을 저장하면 AI 상태를
PENDING으로 두고 바로 상품 화면으로 넘어간다. - AI 호출은
@Async로 뒤에서 돈다. 상태는RUNNING → DONE / FAILED / SKIPPED. - 화면의 AI 칸은 상태가
RUNNING인 동안 htmx로 3초마다 상태를 묻는다. - 끝나면 서버가
HX-Trigger: ai-done헤더를 보내고, 화면은 AI 칸과 초안 미리보기만 다시 받아 바꾼다. 새로고침이 없다.
실패해도 글은 나간다
AI가 실패하면 슬롯이 빈 채로 템플릿만으로 초안이 만들어진다. 글이 조금 심심할 뿐 깨지지는 않는다. 실패 종류에 따라 처리가 다르다.
| 상황 | 처리 |
|---|---|
| API 키 없음 | SKIPPED, "템플릿만으로 초안을 만들었습니다" 안내 |
| 429(한도 초과)·503(과부하) | 설정한 다음 모델로 바로 넘어간다 (gemini-flash-latest → gemini-flash-lite-latest) |
| 모든 모델이 429·503 | 60초 쉬고 한 번 더 |
| 그 밖의 오류 | FAILED, 오류 메시지를 화면에 표시 |
모델을 쉼표로 여러 개 적을 수 있게 한 건 무료 티어 때문이다. 무료 한도는 모델마다 따로 잡혀서, 하나가 막혀도 다른 하나는 열려 있는 경우가 많았다. 생성기는 인터페이스(TextGenerator)로 감싸 두어서, 조건이 바뀌면 다른 모델로 갈아 끼울 수 있다.
6. 이미지는 AI가 아니라 코드가 그린다
글은 AI에게 맡겼지만, 이미지는 일부러 AI를 쓰지 않았다. 인스타 첫 장, 안쪽 테마 카드, 마지막 안내 장, 블로그 첫 사진은 모두 Java2D 코드(ThemeCover)가 그린다.

| 생성형 이미지 AI | 코드로 그리기 | |
|---|---|---|
| 결과 | 매번 다르다 | 매번 같다 |
| 글자 | 한글이 자주 깨진다 | 글꼴 그대로 정확하다 |
| 실제 캡처 | 다시 그리면서 조금씩 바뀐다 | 원본 그대로 들어간다 |
| 비용·시간 | 장마다 호출 | 서버에서 바로 |
테마 판매 글에서 가장 중요한 건 실제 테마 화면이 정확히 보이는 것이다. 생성 모델이 캡처를 "예쁘게" 다시 그리면 그건 더 이상 실제 화면이 아니다. 그래서 디자인 규칙(캐릭터를 빈 곳에 놓기, 대표 색 뽑기, 프로필 그리드에서 안 잘리게 여백 두기)은 코드로 정하고, 사진은 원본을 그대로 쓴다.
대신 "자동으로 판단하는" 부분은 규칙으로 만들었다. 예를 들어 글자 색은 사진에서 가장 많이 쓰인 색 있는 색을 골라 정한다. 이 알고리즘은 따로 글을 쓸 만큼 시행착오가 많았다.
7. 프롬피드 글: 사람 없이 도는 파이프라인
카톡 테마 글은 사진을 사람이 올려야 시작된다. 반면 내가 운영하는 프롬프트 사이트 프롬피드의 소개 글은 재료가 이미 사이트에 있다. 그래서 여기는 끝까지 자동으로 돌게 만들었다.
| 순서 | 하는 일 |
|---|---|
| 1 | 매분 시계를 보고, 설정한 시각(예: 매일 오전 9시)이 지났는데 아직 안 했으면 시작한다 |
| 2 | 프롬피드 최신 글 중 인스타에 아직 안 올린 첫 글을 고른다 |
| 3 | 제목·요약·프롬프트·결과 이미지를 가져와 상품으로 만든다 |
| 4 | 인스타 카드 3장(표지, 프롬프트 미리보기, 팔로우 안내)을 코드로 그린다 |
| 5 | AI 문구를 만든다. 최대 5분 기다리고, 실패하면 거기서 멈추고 알린다 |
| 6 | 인스타에 캐러셀로 올린다 |
| 7 | 네이버는 Mac 에이전트가 에디터를 채워 두고 "발행 대기" 알림을 띄운다 |

프롬피드 글은 테마 글과 쓰는 내용이 달라서 요청문(brief)을 따로 둔다. 슬롯 이름은 같지만 "이 프롬프트가 무엇을 해 주는지, 왜 요즘 SNS에서 많이 하는지"를 쓰라고 한다. 프롬프트 원문은 800자까지만 참고로 넣고 "그대로 옮기지 않는다"고 적었다. 사이트에 가야 볼 수 있는 내용을 SNS 글에 다 풀면 안 되기 때문이다.
시스템 프롬프트에는 이런 규칙도 넣었다.
- 프롬프트가 실제로 하는 일만 쓴다. 결과를 직접 해 본 것처럼 꾸며 쓰지 않는다.
- 과장 광고 문구('역대급', '무조건')는 쓰지 않는다.같은 시각에 두 번 돌지 않게
자동 발행은 "매분 확인"이라 서버가 재시작되거나 느려지면 같은 시각에 두 번 돌 수 있다. 그래서 시작하자마자 "이 시각은 했다"를 먼저 저장한다. 실패하더라도 같은 글을 두 번 올리는 것보다 한 번 빠지는 게 낫다고 판단했다. 그리고 예정 시각에서 2시간이 지나면 따라잡지 않는다. 밤새 꺼져 있던 서버가 아침에 켜지면서 어제 몫을 몰아서 올리는 일을 막기 위해서다.
8. 일부러 사람에게 남긴 것
완전 자동화를 목표로 했지만, 끝까지 사람에게 남긴 것이 있다.
| 남긴 것 | 이유 |
|---|---|
| 네이버·포스타입 [발행] 버튼 | 공식 API가 없어 브라우저 자동화로 채운다. 발행까지 자동으로 누르면 약관·계정 정지 위험이 있다. 그리고 마지막 화면을 한 번 보는 것이 가장 확실한 검수다 |
| 실기기 캡처 | 테마가 실제 폰에서 어떻게 보이는지가 상품의 핵심이다 |
| 캐릭터(대표 오브젝트) 고르기 | 어떤 캐릭터를 내세울지는 판매 판단이다 |
인스타는 공식 Graph API가 있어서 서버가 끝까지 올린다. 네이버는 에이전트가 제목, 본문, 사진, 태그까지 다 채워 두고 Mac 알림으로 "발행 대기"를 알린다. 나는 알림을 눌러 화면을 한 번 보고 [발행]만 누른다.
"완전 자동"을 고집했다면 이 마지막 한 번의 클릭도 없앴을 것이다. 하지만 한 사람이 운영하는 채널에서 계정 하나가 막히면 모든 게 끝난다. 클릭 한 번으로 그 위험을 지우는 건 아주 싼 값이라고 봤다.
9. 앞단의 AI: 테마 만들기
오토포스트가 "글"을 자동으로 만든다면, 그 앞의 테마 자체도 AI로 만들기 시작했다. ktheme-maker의 테마 자동 생성이다.
| 단계 | 담당 |
|---|---|
| 캐릭터 그림을 참고해 에셋 시트(캐릭터, 꾸미기, 탭 아이콘) 그리기 | 이미지 AI (ChatGPT 구독의 Codex, 또는 Gemini·OpenAI 이미지 API) |
| 시트에서 요소를 잘라 번호를 붙인 모음 그림 만들기 | 코드 |
| 번호마다 역할(캐릭터·꾸미기·탭 5개·안 씀) 정하기 | Claude 또는 Gemini (그림을 보고 판단) |
| 색, 말풍선, 배경 적용 → .ktheme | 코드 |
여기서도 같은 원칙을 썼다. AI는 그림을 그리는 일과 그림을 보고 판단하는 일만 하고, 테마 파일을 조립하는 건 코드가 한다. 결과가 마음에 안 들면 역할만 고쳐서 다시 조립할 수 있다.
그래서 지금의 전체 흐름은 "캐릭터 그림 → (AI) 테마 → 실기기 캡처 → 오토포스트 (AI 문구 + 코드 이미지) → 세 플랫폼"이다. 사람이 하는 일은 캡처와 확인, 그리고 발행 클릭이다.
결과
10월 22일 기준으로 오토포스트 홈에 찍힌 숫자다.
| 항목 | 값 |
|---|---|
| 이번 달 게시 | 29개 (카톡 테마 12, 프롬피드 17) |
| 이번 달 아낀 시간 | 7시간 33분 |
"아낀 시간"은 플랫폼마다 손으로 할 때 걸리는 시간(설정값)에서 실제로 내가 손댄 시간을 뺀 값이다. 추정이 섞인 숫자라 정확한 측정값은 아니다. 다만 체감은 분명하다. 프롬피드 소개 글은 이제 아침에 알림을 받고, 네이버 화면을 한 번 보고, [발행]을 누르는 것으로 끝난다.
회고
잘한 것
- AI 범위를 좁게 잡았다. 글 전체가 아니라 슬롯 네 개. AI가 틀려도 피해 범위가 한 칸이다.
- 틀리면 안 되는 것을 AI 밖으로 뺐다. 링크, 가격, 이용 안내는 템플릿. 프롬프트로 "쓰지 마"라고 하고, 코드로 한 번 더 지운다.
- 사람의 수정을 존중했다. 직접 고친 문장은 AI가 다시 돌아도 남는다. 이게 없으면 결국 AI 기능을 끄게 된다.
- 이미지는 결정적으로 만들었다. 같은 입력이면 같은 결과가 나와서 버그를 재현하고 고칠 수 있었다.
아쉬운 것
- AI 문구의 품질을 숫자로 재지 못했다. 몇 번 [다시 쓰기]를 눌렀는지, 몇 번 직접 고쳤는지를 기록하면 프롬프트를 바꿀 때 비교할 수 있었을 것이다.
- 예전 글을 가져올 때 AI 슬롯이 비는 걸 늦게 알았다. 그 결과 캡션이 예전 게시물과 다르게 올라간 글이 두 개 생겼다. "AI가 쓰지 않는 칸"을 처음부터 생각했어야 했다.
다음
- [다시 쓰기]·직접 고침 횟수를 슬롯별로 기록해서 프롬프트 개선의 근거로 쓰기
- 테마 자동 생성과 오토포스트를 이어서, 테마 완성과 동시에 상품 초안까지 만들기
- 포스타입도 네이버처럼 "발행 대기" 알림까지 한 흐름으로 묶기
마치며
AI를 붙이면서 가장 많이 한 고민은 "AI가 무엇을 할 수 있나"가 아니라 "AI가 틀렸을 때 무엇이 망가지나"였다. 그 답에 따라 AI 자리, 템플릿 자리, 코드 자리, 사람 자리가 정해졌다. 완전 자동화는 모든 걸 AI에게 맡기는 게 아니라, 틀려도 괜찮은 곳에만 AI를 두고 나머지를 단단하게 만드는 일이었다.