내 집을 구해줄 복덕방 김사장 Agent 활용기

청약 공고는 놓치면 그걸로 끝이다. 접수 기간이 사나흘짜리인 것도 흔하고, 마감 다음 날 알게 되면 할 수 있는 게 없다. 그래서 한동안은 청약홈이랑 LH 사이트를 번갈아 들어가 봤는데, 이게 생각보다 귀찮은 일이었다. 사이트마다 목록이 따로 놀고, 하나씩 열어서 내 조건에 걸리는지 확인해야 하고, 대부분은 열어보면 해당 없음이었다.

매일 같은 판단을 반복하고 있다는 걸 깨닫고 나서, 그 판단을 대신할 에이전트를 하나 세웠다. 이름은 김사장. 동네 복덕방 사장님 컨셉이다.


메인 비서랑 섞지 않은 이유

홈 서버에 올려둔 Hermes Agent에는 이미 평소 쓰는 비서가 붙어 있다. 거기에 청약 업무를 얹을 수도 있었는데 그렇게 안 했다. 프로필을 따로 팠다.

~/.hermes/profiles/kimsajang/
  SOUL.md          ← 정체성, 말투, 절대 규칙
  profile.yaml
  skills/
  cron/

성격이 다른 일이라 그랬다. 청약은 마감일과 자격요건을 틀리면 안 되는 일인데, 잡다한 대화 맥락이 섞이면 그 기준이 흐려진다. 프로필을 나누면 SOUL.md에 "너는 청약만 본다"고 못을 박아둘 수 있고, 작업 결과도 /home/duncan/kimsajang/ 한 곳에만 쌓인다. 웨딩 플래너 김실장을 만들 때와 같은 방식이다.


제일 먼저 정한 건 "안 할 일"이었다

SOUL.md를 쓰면서 가장 먼저 박아 넣은 게 금지 항목이었다.

## 절대 규칙

1. **신청 대행 금지.** 청약 신청, 서류 제출, 본인인증, 결제는 석규님이 직접 한다.
2. **출처 없는 정보 금지.** 모든 공고는 공식 출처 링크를 함께 제시한다.
3. **마감일은 KST 기준**으로 계산하고 D-day를 항상 표기한다.
4. **불확실하면 불확실하다고 말한다.**
5. **개인 금융정보·주민번호는 저장하지 않는다.**
6. **추측으로 공고를 만들어내지 않는다.** 조회에 실패하면 실패했다고 말한다.

1번이 핵심이다. 청약 신청은 본인인증이 걸린 일이고, 한 번 잘못 넣으면 되돌릴 수 없다. 에이전트가 거기까지 손대게 두면 편해지는 게 아니라 무서워진다. 김사장의 일은 판단 재료를 깔아주는 데서 끝나고, 넣는 건 내가 한다.

6번도 겪어보면 중요하다. API가 죽었을 때 "오늘은 공고가 없습니다"라고 조용히 넘어가면, 그날 공고를 통째로 놓친다. 조회 실패는 실패라고 리포트에 남기게 했다.


어디서 긁어올 것인가

청약 정보는 한 군데 모여 있지 않다. 크게 두 곳을 봐야 한다.

소스 담당 키
LH 임대·분양·주거복지(신혼희망타운) 불필요
청약홈 민영 아파트 분양, 무순위 data.go.kr 키 필요

LH는 NomaDamas의 k-skill이 공공데이터포털 API를 대신 호출해주는 공개 프록시를 열어둬서, 키 없이 바로 붙었다.

curl -fsS --get 'https://k-skill-proxy.nomadamas.org/v1/lh-notice/search' \
  --data-urlencode 'status=공고중' \
  --data-urlencode 'region=서울특별시' \
  --data-urlencode 'pageSize=50'

청약홈은 한국부동산원 API를 써야 한다. data.go.kr에서 활용신청하면 개발계정은 자동승인이고, 무료에 하루 4만 건이다. 신혼부부 특별공급이 여기 있어서 이쪽이 사실상 정본이다.

curl -fsS --get 'https://api.odcloud.kr/api/ApplyhomeInfoDetailSvc/v1/getAPTLttotPblancDetail' \
  --data-urlencode "serviceKey=$DATA_GO_KR_API_KEY" \
  --data-urlencode 'cond[RCRIT_PBLANC_DE::GTE]=2026-09-01' \
  --data-urlencode 'cond[SUBSCRPT_AREA_CODE_NM::EQ]=서울'

odcloud는 cond[필드::연산자]=값 형식으로 거른다. 연산자는 EQ, GTE, LTE, LIKE.


삽질 1: LH 지역 필터가 필터가 아니었다

region=서울특별시를 넣었는데 충청도 공고가 딸려 왔다. 파라미터를 잘못 쓴 줄 알고 cnpCdNm으로도 바꿔 보고 인코딩도 확인했는데 결과가 같았다.

원인은 조합이었다. LH API에서 지역 파라미터는 category(uppAisTpCd)와 같이 보내면 사실상 무시된다. 정렬 힌트에 가깝게 동작한다. 서버가 안 걸러주니 클라이언트에서 다시 거르는 수밖에 없었다.

def in_target_region(region_name):
    ...

collect.py가 응답을 받은 뒤 지역을 한 번 더 확인한다. API 문서만 보고 짠 코드가 왜 이상하게 도는지 한참 봤는데, 결국 응답을 직접 찍어보고 나서야 알았다.


삽질 2: 첫 텔레그램이 400으로 튕겼다

수집도 되고 리포트도 만들어졌는데 발송에서 막혔다.

  → chat_id=… HTTP 400
=== 완료: 0명 발송 ===

메시지 포맷 문제인 줄 알았다. parse_mode도 안 쓰는데 뭐가 문제인가 싶었는데, 봇 쪽 설정을 손보고 3분 뒤 다시 돌리니 200이 떨어졌다. 봇과 대화가 시작되지 않은 상태라 텔레그램이 거부한 것으로 보인다(추정).

그래서 발송 스크립트는 HTTP 코드를 로그에 그대로 남기게 해뒀다.

CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 25 \
    "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
    --data-urlencode "chat_id=${uid}" \
    --data-urlencode "text=${MSG}")
echo "  → chat_id=${uid} HTTP ${CODE}" >> "$LOG"

조용히 실패하는 자동화가 제일 나쁘다. 안 왔으면 왜 안 왔는지는 남아 있어야 한다.


진짜 일은 모으는 쪽이 아니라 거르는 쪽이었다

만들기 전에는 "공고를 다 모아서 보여주면 되겠지" 했는데, 막상 돌려보니 하루에 70건 넘게 잡혔다. 그대로 보내면 사이트 들어가서 보던 거랑 다를 게 없다.

그래서 collect.py에 자동 제외 규칙을 넣었다. LH 임대주택 상당수는 전국 공통 소득기준표를 쓰는데, 그 표의 최대 완화구간조차 우리 합산소득보다 낮다. 공고문을 열어볼 필요도 없이 탈락이 확정이라는 뜻이다.

def verdict_for(it):
    """조건 대비 자동 판정. (exclude, reason) 또는 (None, None)."""

이런 건 아예 목록에서 빼고, 왜 뺐는지 이유를 리포트 하단에 적게 했다. 소득·자산 기준이 없는 공공전세(든든전세)는 예외로 남겨뒀다.

판정은 세 갈래다. 조건에 맞으면 상단, 애매하면 "확인 필요", 확정 탈락이면 제외. 애매한 걸 억지로 맞다/아니다로 밀어붙이지 않게 한 게 오히려 쓸모 있었다. 소득 기준이나 부양가족 수처럼 공고마다 다른 항목은 "이걸 확인해야 한다"고만 알려준다.

김사장이 매일 아침 텔레그램으로 보내는 청약 리포트 화면 _텔레그램으로 온 아침 리포트. 마감 임박과 조건 맞는 공고를 나눠서 보낸다. 파일명: image.png_

아침 8시인 이유

크론은 하나뿐이다.

0 8 * * * TZ=Asia/Seoul /home/duncan/kimsajang/scripts/daily_report.sh

시각을 8시로 잡은 건 취향이 아니라 데이터 사정 때문이다. 청약홈 API는 익일 새벽 0~2시에 갱신된다. 밤에 돌리면 하루 지난 목록을 보게 된다. 먼저 만들어진 비슷한 오픈소스들을 뒤져보다가 알게 된 사실인데, 이거 하나 알고 시작한 덕에 시행착오를 한 번 줄였다.

스크립트가 하는 일은 셋이다. 수집하고, daily/YYYY-MM-DD.md로 저장하고, 요약을 텔레그램으로 보낸다. 이미 보고한 공고는 notices/seen.json에 기록해서 다음 날 또 알리지 않는다.


닷새 돌려보니

9월 17일부터 아침마다 온다. 22일 아침에는 71건을 수집해서, 확정 탈락 10건을 걷어내고, 최종으로 세 건이 남았다.

D-day 유형 공고
D-1 공공분양 힐스테이트 고덕엘리스트 A65BL
D-1 공공분양 힐스테이트 고덕엘리스트 A12BL
D-7 신혼희망타운 시흥하중지구 A-4블록

71건에서 3건. 이게 원래 내가 사이트 두 군데 들어가서 손으로 하던 일이다.

달라진 건 정보량이 아니라 마음가짐 쪽이 크다. 예전에는 "이번 주에 청약 뭐 떴나" 하고 찾아봐야 한다는 부담이 늘 깔려 있었는데, 지금은 안 뒤진다. 안 오면 없는 거고, 오면 그때 본다. D-1짜리가 뜬 날은 바로 눈에 들어오니까 놓칠 일도 줄었다.

그리고 리포트가 daily/ 폴더에 md로 쌓이니까, 지난주에 뭐가 떴었는지 되짚어보기도 편하다. 텔레그램은 흘러가지만 파일은 남는다.


아직 안 붙인 것

  • SH·GH — 서울·경기 도시공사 물량은 아직 안 봤다. 필요해지면 붙일 생각이다.
  • 분양가 적정성 — 국토부 실거래가 API로 주변 시세와 비교해서 비싼지 싼지 판정해주면 좋겠는데, 아직이다. house-ping이 이걸 구현해뒀길래 참고해두고 있다.
  • 가점 계산 — 지금은 자격 여부까지만 본다. 당첨 가능성까지 계산하려면 데이터가 더 필요하다.

정리

  • 시작: 청약홈과 LH를 매일 들어가 확인하는 게 반복 노동이라서
  • 구조: Hermes 프로필 하나를 따로 파서 청약 전담으로 세움
  • 설계 1순위: 신청 대행 금지 — 판단 재료까지만, 신청은 직접
  • 데이터: LH는 공개 프록시로 키 없이, 청약홈은 data.go.kr 무료 키
  • 삽질 1: LH 지역 파라미터가 category와 같이 쓰면 무시됨 → 클라이언트에서 재필터
  • 삽질 2: 첫 텔레그램 발송 400 → HTTP 코드를 로그에 남기도록 수정
  • 핵심: 모으는 것보다 거르는 게 일이었다. 71건 → 3건
  • 주기: 아침 8시 크론, 청약홈 API가 새벽에 갱신되기 때문

에이전트를 하나 더 만들면서 배운 건, 일 잘하는 에이전트는 많이 가져오는 쪽이 아니라 안 되는 걸 안 된다고 잘라내는 쪽이라는 점이다. 김사장이 하루에 하는 일의 대부분은 버리는 일이다.


환경: 우분투 서버, Hermes Agent 프로필, Python 3, cron, 텔레그램 봇 API, LH 공고 API(k-skill 프록시), 청약홈 API(data.go.kr)

Posts