devlog.md

[주식 종목분석_자동화] 바이브코딩으로 텔레그램 종목분석봇 만들기 (GAS 개발일지)_01

오늘은 종목명을 텔레그램에 입력하면 시세·재무·증권사 리포트·뉴스를 모아 Gemini로 정성 분석해 회신하는 개인용 봇을 서버 없이 Google Apps Script(GAS)만으로 만들어봤다. 설계 확정부터 실제 배포·버그 수정까지 한 세션에 끝냈는데, 예상대로 배포와 데이터 파싱에서 삽질이 많았다. 그 과정을 순서대로 남긴다.

1. 설계 단계 — 시트 스키마와 Gemini 프롬프트 확정

코드보다 먼저 결정할 게 두 가지였다. 첫째, 구글 시트 스키마. 요청마다 새 행을 쌓는 히스토리 누적 방식과 종목당 1행만 유지하는 upsert 중, 목표주가·정성평가의 시간 변화를 추적할 수 있는 누적 방식을 택했다. 최신 상태만 보고 싶으면 나중에 QUERY로 뷰를 만들면 되지만, 반대로 upsert로 가면 히스토리를 영영 못 쓴다는 게 결정 이유였다.

둘째, Gemini 마스터 프롬프트의 입출력 스펙. 재무 수치를 JSON으로 넣고, 정성평가 5개 항목(수익성·위험성·유동성·현금흐름·시장환경민감도)을 배열로 받되 배열 순서 자체를 중요도 순서로 쓰기로 했다. structured output(responseSchema)으로 형식을 강제해 파싱 실패 위험을 없앴다. 여기에 최근 1개월 뉴스도 입력에 포함시켰다.

2. KIS API 엔드포인트와 KRX 종목 데이터 확보

한국투자증권(KIS) Open API의 실제 엔드포인트를 조사해 시세·대차대조표·손익계산서·수익성비율의 TR_ID를 확정했다. 토큰은 재발급 빈도 제한이 있어 CacheService에 캐싱하도록 설계했다.

종목명 오타 보정용 마스터 데이터는 KRX 상장법인목록에서 KOSPI 834개 + KOSDAQ 1,818개, 총 2,652개를 받아 ticker·name·정규화명·시장구분 4열 CSV로 정리했다. 이걸 Master 시트에 넣고 레벤슈타인 거리로 유사도 매칭, 애매하면 Gemini 폴백으로 확정하는 구조다.

텔레그램 종목분석봇 GAS 파이프라인 구조도 — 텔레그램 입력, GAS 웹훅, 종목명 정규화, 데이터 수집(KIS·네이버), Gemini 분석, 결과 회신의 6단계 가로 흐름도

3. GAS 배포와 텔레그램 웹훅 연결

봇은 기존 자산알림봇과 웹훅이 충돌하지 않게 BotFather에서 신규 발급했다. 11개 .gs 파일을 clasp으로 일괄 업로드하려 했는데 여기서부터 막혔다. clasp create가 "Apps Script API가 비활성화됨" 에러를 냈고, 활성화 후에도 --type webapp이 잘못된 값이라 거부됐다(정답은 standalone). 웹 앱은 생성 타입이 아니라 배포 시 선택하는 옵션이었다.

웹훅 등록은 clasp run이 별도 API 실행 배포를 요구해서 실패, 결국 텔레그램 setWebhook을 curl로 직접 호출해 해결했다.

4. 첫 조회 테스트, 그리고 버그와의 사투

"삼성전자"를 보냈는데 아무 응답이 없었다. 웹앱 URL을 직접 POST로 찔러보니 302 리다이렉트가 돌아왔고, 텔레그램이 이걸 못 받아 "Wrong response from the webhook: 302 Found"가 찍혀 있었다. GAS의 ContentService 응답이 원인이라 HtmlService로 바꾸니 바로 200이 떨어졌다.

그 다음은 연쇄 삽질이었다. Gemini 모델명이 deprecated(404)돼 있었고, 시세가 전부 0으로 나왔다. 종목코드 "005930"이 시트에서 숫자 5930으로 변환돼 앞자리 0이 날아간 게 원인이라, 코드에서 6자리로 복원하도록 방어했다. 재무 수치는 매출이 급감한 것처럼 보였는데, 한국 손익계산서가 "당해 누적"값이라 분기 리셋 착시가 생긴 것이었다. 손익은 전년 동월(YoY), 대차대조표는 직전 분기(QoQ)로 비교 기준을 분리해 해결했다. 네이버금융은 페이지마다 인코딩이 달랐고(UTF-8/EUC-KR 혼재), 뉴스는 Referer 헤더가 없으면 봇으로 간주돼 빈 목록만 줬다. 차트는 캔들스틱을 직접 그리려다 GAS Charts 서비스가 지원 안 해서, 네이버가 서빙하는 일봉 이미지를 그대로 가져다 쓰는 쪽으로 틀었다.

5. 웹앱 확장 설계 (A안)

텔레그램 메시지로는 표·시계열을 보여주기 어려워, 같은 GAS 백엔드를 재사용하는 웹앱을 설계했다. 파일이나 프로젝트를 나눠도 부하가 분산되지 않는다는 점(GAS는 .gs를 전부 번들링, 쿼터는 계정 단위)을 확인하고, 단일 프로젝트에 doGet을 추가해 google.script.run으로 호출하는 A안으로 확정했다. 확장 항목으로 배당수익률·외국인 보유율·동종업계 PER·투자자별 매매동향(수급 추세)·미국 벤치마크 지표를 잡았다. 수급 추세는 LLM이 표 산수에 약하므로 추세 판정을 서버에서 미리 계산해 넘기기로 했고, 미국 벤치마크는 업종별 대표 종목(예: 반도체 메모리→마이크론)을 장 개장 전 선행지표로 보여주는 방식이다.

Trouble Shooting

증상원인해결 방법
봇이 응답 없음, 로그엔 302 FoundGAS ContentService 응답이 리다이렉트 유발, 텔레그램이 302를 못 받음doPost 응답을 HtmlService.createHtmlOutput으로 교체
clasp create 실패Apps Script API 비활성화 + --type webapp 잘못된 값usersettings에서 API 켜고 --type standalone 사용
Gemini 호출 실패(404)gemini-2.5-flash 모델 deprecatedgemini-3.1-flash-lite로 교체
시세가 전부 0시트가 "005930"을 숫자 5930으로 변환(앞자리 0 손실)조회 전 6자리로 zero-pad 복원
매출이 급감한 것처럼 표시손익계산서가 당해 누적값이라 분기 리셋 착시손익은 YoY, 대차대조표는 QoQ로 비교 기준 분리
뉴스가 항상 빈 목록Referer 헤더 없으면 네이버가 봇으로 간주finance.naver.com Referer 헤더 추가
캔들차트 생성 불가GAS Charts 서비스에 CANDLESTICK 빌더 없음네이버 일봉 캔들 이미지를 직접 fetch해 전달

느낀 점 / 다음에 할 일

외부 데이터(KIS 응답 필드, 네이버 페이지 구조)는 문서보다 실제 응답 한 번이 훨씬 정확했다. 특히 "누적값 착시"나 "페이지별 인코딩 차이"처럼 감으로 넘겼으면 오래 헤맸을 함정이 많아, 테스트 함수로 원본을 찍어보는 게 결국 가장 빨랐다. 배포는 고정 deploymentId로 버전만 올려 웹훅 URL을 유지하는 전략이 잘 먹혔다. 다음 세션에서는 handleStockQuery_를 순수 데이터 반환 함수로 리팩터링해 봇과 웹앱이 코어를 공유하게 만들고, 확장 데이터 5종을 붙일 예정이다. 컨텍스트는 핸드오프 문서로 정리해 새 세션에서 이어가기로 했다.

댓글