본문 바로가기

개발 기록/미국 증시 분석 자동화 시스템 구축기

Claude API로 블로그 자동 생성 + Notion 동기화 구현 — Markdown 변환과 DALL·E 이미지 파일 관리 [구축기 6편]

이전 글: 5편 — Gemini + GPT + Claude 분업으로 투자 분석하기
다음 글: 7편 — Docker Compose로 4개 컨테이너 운영하기


블로그 한 편을 만들기 위해 필요한 것

 

매일 09:10에 BlogService.createBlogDraft()가 실행된다.
이 메서드가 가장 먼저 하는 일은 데이터를 모으는 것이다.

  • ChatGPT 분석 결과 (종목별 action, confidence, analysisContent)
  • 포트폴리오 잔고 (총 평가액, 오늘 손익, 종목별 비중)
  • 시장 지수 (S&P500, 나스닥, 다우, WTI, 국채 수익률)
  • USD/KRW 환율
  • 오늘 매매 내역 (있는 경우)
  • 오늘 매매 코멘트 (있는 경우)

 
이 여섯 가지를 하나의 프롬프트로 조립해서 Claude API에 넘긴다.
Claude는 마크다운 형식의 블로그 본문을 반환한다.
 
구조는
시황 요약 → 보유 종목 분석 → 오늘의 투자 판단 → 포트폴리오 현황 → 마무리
순으로 고정되어 있다.
 

 
 


Markdown → Notion Block 변환

 

Claude가 반환한 마크다운을 Notion에 그대로 올릴 수는 없다.

Notion API는 자체 Block 형식을 사용하기 때문에
직접 변환 로직을 구현해야 한다.
 
규칙은 단순하다.

제목      →  heading_1 block
소제목   →  heading_2 block
소소제목 →  heading_3 block
일반 텍스트 →  paragraph block
빈 줄      →  skip

 
코드블록이나 테이블은 현재 지원하지 않는다.
Claude 프롬프트에 "코드블록과 테이블을 사용하지 말 것"을 명시해서
애초에 출력되지 않도록 했다.
 

 
 

// Markdown → Notion Block 변환 핵심 로직
for (String line : markdown.split("\n")) {
    if (line.startsWith("# ")) {
        blocks.add(createHeading1(line.substring(2)));
    } else if (line.startsWith("## ")) {
        blocks.add(createHeading2(line.substring(3)));
    } else if (line.startsWith("### ")) {
        blocks.add(createHeading3(line.substring(4)));
    } else if (!line.isBlank()) {
        blocks.add(createParagraph(line));
    }
}

 
Notion API는 한 번에 업로드할 수 있는 Block 수에 제한이 있다(100개).

블로그 한 편이 100개를 넘으면 분할해서 업로드해야 한다.
현재 Claude 출력 기준으로 평균 60~80개 블록이 생성되어 제한에 걸린 적은 없다.
 
 


DALL·E 이미지 생성과 파일 관리

블로그 대표 이미지는 DALL·E API로 생성한다.

프롬프트는 당일 시장 분위기와 주요 테마를 반영해서 동적으로 구성한다.
 
여기서 반드시 처리해야 하는 문제가 있다.
DALL·E가 반환하는 이미지 URL은 24시간 후 만료된다.
 
URL을 DB에 저장하면 다음 날 이미지가 깨진다.
그래서 반환 즉시 이미지를 다운로드해서 로컬에 저장하는 구조로 만들었다.
 
 

// 이미지 다운로드 + 리사이징 (imgscalr-lib)
byte[] imageBytes = downloadImage(dalleUrl);
BufferedImage original = ImageIO.read(new ByteArrayInputStream(imageBytes));
BufferedImage resized = Scalr.resize(original, 1200, 630); // OG 이미지 규격
String filename = "blog-" + today + ".jpg";
ImageIO.write(resized, "jpg", new File("/app/images/" + filename));

1200×630은 Open Graph 이미지 표준 규격이다. 소셜 미디어 공유 시 미리보기 이미지가 제대로 표시되려면 이 크기를 맞춰야 한다.

 
 


 

Telegram 알림으로 파이프라인 상태 모니터링

 

파이프라인이 성공하면 Telegram으로 블로그 제목이 포함된 메시지가 온다.
실패하면 어느 단계에서 무슨 에러가 났는지 담은 메시지가 온다.
 
Telegram 알림이 없던 시절에는 파이프라인이 조용히 실패해도 몰랐다.
아침에 Notion을 열었다가 초안이 없어서 아는 경우가 많았다.
 
지금은 알림이 안 오면 바로 로그를 확인한다.
실제로 두 번 Gemini Rate Limit 장애를 조기에 발견해서 수동으로 재실행했다.
 
알림 메시지 구조는 두 가지다.
 

 
 
Telegram Bot API 호출 자체는 간단하다.
문제는 어느 단계에서 실패했는지를 정확하게 잡아서 메시지에 담는 것이다.
파이프라인 각 단계를 try-catch로 감싸고, 예외 발생 시 단계명과 에러 메시지를 함께 Telegram으로 전송한다.
 

try {
    geminiService.refineAllNews();
} catch (Exception e) {
    telegramService.sendFailureAlert("GeminiService", e.getMessage());
    return; // 이후 단계 중단
}

 
 
실패 알림을 받으면 무엇을 해야 하는지 바로 알 수 있다.
 
어느 단계인지, 어떤 에러인지가 메시지에 담겨 있어서 로그를 열기 전에
이미 원인을 절반은 파악한 상태로 대응할 수 있다.


정리

데이터 조립6개 소스 → 단일 프롬프트데이터 누락 시 빈 섹션으로 표시
Claude API 호출HTTP POST, Markdown 출력코드블록·테이블 프롬프트에서 금지
Markdown → Notion줄 단위 직접 파싱Block 100개 제한 주의
DALL·E 이미지반환 즉시 다운로드URL 24시간 만료 → 저장 필수
Notion 업로드Block API 순차 전송타임아웃 시 Nginx 설정 확인
Telegram 알림성공/실패 양방향단계명 + 에러 메시지 포함

 
 
다음 편에서는 이 파이프라인 전체가 돌아가는 인프라를 다룰 예정이다
.
Docker Compose 4개 컨테이너 구성, 민감 정보 분리 전략, 운영 중 발견한 실제 문제들을 기록한다.