계약을 앞둔 사람이 숫자를 확인하는 곳. 서울 아파트 실거래와 한국은행의 대출·예금 평균금리를 매일 모아, 전세와 월세 중 어느 쪽이 싼지·지금 갱신하는 것이 나가서 새로 구하는 것보다 싼지·중개사가 부르는 매매가와 견줄 같은 단지 같은 평형의 최근 거래 범위까지 계산해 두는 정적 사이트. 표본이 모자라면 값을 내지 않고 모자란다고 적는다. GitHub Actions가 데이터를 매일 모아 커밋하고, 사이트 HTML은 배포 때 그 데이터로 만들어 GitHub Pages로 올린다.
사이트: https://kyhsa93.github.io/jipgye/
이름은 '집계'다 — 살 집과 세는 일을 한 낱말로 읽는다. 세는 데까지가 이 사이트가 하는 일이라는 뜻이기도 하다. 주소는 그대로 둔다. 색인이 이 경로에 붙어 있고, 도메인을 옮기면 나이 0에서 다시 시작한다.
계약을 앞둔 사람이 숫자를 확인하는 곳이다. 실거래 데이터에는 우위가 없고(경쟁사 전부 같은 국토부 API를 쓴다) 뉴스도 금리 목록도 이길 수 없다. 이길 수 있는 자리는 셋뿐이다 — 실거래에 금리와 전환율을 자동으로 물리는 교차점, 표본이 모자라면 모자란다고 적는 것, 그리고 낸 값을 얼려 두고 채점·정정한 기록. 매매에서는 지난 거래를 오늘 값으로 고치는 공식 지수와 가격에 걸린 세금·대출선이 금리·전환율의 자리에 온다 — 부르는 값이 정상인지를 같은 단지 같은 평형의 거래로 재되, 그 칸에 거래가 모자라면 범위를 내지 않는다.
누구를 위한 것인지, 무엇을 안 하기로 했는지, 새 아이디어가 통과해야 하는 체크리스트,
그리고 다음에 무엇을 할지는 DIRECTION.md에 있다. 화면이나 지표를
새로 만들기 전에 그 문서를 먼저 연다.
하루 한 번 저장소를 직접 돌려 보고, 고칠 값이 있는 것만 GitHub 이슈로 올린다.
사양은 audit/HOWTO.md에 있다. 전용 실행 스크립트는 없다(2026-10-07 삭제 — 과금 호출 금지 규칙).
점검은 저장소에 커밋하지 않는다. 남기는 것은 이슈뿐이고 고치는 것은 사람이 한다. 0건으로 끝나는 날이 정상이다 — 매일 뭔가를 올리는 이슈 봇은 스무 개쯤에서 아무도 목록을 안 읽게 되고, 그때부터는 진짜 하나가 섞여 있어도 같이 묻힌다.
일주일에 한 번 경쟁 서비스와 사람들이 실제로 묻는 것을 살펴, 집계에 없는 답 가운데 값이
되는 것만 GitHub 이슈로 올린다. 사양은 research/HOWTO.md에 있다. 전용 실행 스크립트는
없다. 라벨은 research다.
자동 점검과 같은 구조다 — 커밋하지 않고 이슈만 남긴다.
주 단위인 것은 경쟁 서비스가 하루 만에 바뀌지 않기 때문이다. 매일 돌리면 같은 것을 다시 보고 없는 차이를 만들어 낸다. 그리고 0건으로 끝나는 주가 정상이다 — 한 주 최대 2건으로, 버그를 찾는 자동 점검(3건)보다 문턱이 높다. 그쪽은 틀린 것을 고치자는 말이고 이쪽은 없는 것을 만들자는 말이라, 제안은 참/거짓이 없어 무한히 만들어 낼 수 있다.
개발과 운영을 역할별로 나눠 맡기는 Claude Code 서브에이전트가 .claude/agents/에 있다. 원본은 비공개 저장소 kyhsa93/agents의 teams/jipgye/이고 여기는 배포 사본이다 — 정의 변경은 group-chro(협의: ceo).
최종 권한은 저장소 소유자에게 있고, 임원은 그룹 임원 아래에서 일한다(CEO·CSO는 group-ceo, CTO·CIO는 group-cto, COO는 group-coo, CMO는 group-cmo, CLO는 group-clo에 보고 — 기능 기준은 그룹 임원이 정하고 덮어쓸 수 있되, ceo가 이의를 남기면 group-ceo 판정까지 팀 결정이 유지된다). 또 임원은 각자 소유하는 파일이 겹치지 않게 나뉜다.
남의 파일을 고쳐야 한다고 보면 그 주인 앞으로 넘긴다.
| 에이전트 | 맡는 것 | 소유하는 곳 |
|---|---|---|
ceo |
방향, 최종 순서, 임원 간 충돌 판정, 라운드 끝의 대조 | DIRECTION.md 1부(변경은 소유자 결정), README "방향", 위키 Home·사이드바 |
cpo |
다섯 장면 → 화면, 체크리스트 집행, 화면 간 동선·문장 (옛 PO 검토의 후임) | DIRECTION.md 2·3부(3부 순서는 CEO 확정), 손으로 고치는 틀 열네 장의 내용 |
cto |
정적 사이트 설계, 수집·빌드·테스트·CI, 드리프트 검사, 보안 | scripts/, test/, docs/style.css·nav.js, 워크플로 코드 |
coo |
매일 도는 것이 도는가 — 데일리 수집, 신선도, 백필, 로컬 봇, 기준일 달력, 이슈 위생 | audit/HOWTO.md |
cio |
바깥 정보원의 뜻·규격·키·활용신청·신뢰도, 못 붙인 소스(사람 필요) |
정보원 대장, raw/·cache/의 형식 |
cso |
경쟁 지형과 해자 검증(확인일 필수), 주간 조사 사양. 보안이 아니라 전략 | research/HOWTO.md, 위키 경쟁 지형 |
cdo |
무엇을 어떻게 세나 — 표본, 부트스트랩 문턱, 시점 보정, 전망 채점 | docs/method.html, research/<주제>/ |
cmo |
색인 → 노출 → 클릭, 애드센스 재신청, 생성 페이지 중복 진단 | 위키 성공 지표 |
clo |
세율·대출 상한·LTV를 법령·금융위 원문과 대조, 단정·추천 문장, 출처·인용 | 규칙 모듈의 BASIS_DATE·SOURCES 대조 기록 |
CFO는 두지 않았다. 드는 돈이 없고(GitHub 무료, 요약은 자체 Ollama) 수입은 애드센스 승인 전이라, 맡을 일이 CMO(애드센스)와 CIO(유료 소스는 소유자 결정)에 다 들어간다.
임원은 커밋·푸시·이슈 생성을 하지 않는다. 작업 트리에 파일을 쓰고 이슈는 초안으로 돌려주며, 저장소 규칙(작업 전 이슈, 작업마다 워크트리, 끝나면 이슈 닫기)은 부른 쪽이 집행한다.
여러 임원을 한 질문에 부를 때의 순서:
- CEO 브리프 — 질문 하나, 주인 임원, 끝났다고 볼 조건.
- 초안 — 해당 임원들이 각자 소유 범위에서. 여기까지는 병렬로 돌려도 된다.
- 최종 라운드는 한 명씩 차례로. 병렬로 돌리면 양쪽이 동시에 "상대 안을 받아들인다"고 써서 합의가 거꾸로 엇갈린다 — 다른 저장소의 디렉터 에이전트 라운드에서 네 건이 그렇게 나왔고, 차례로 돌린 다음 라운드에선 0건이었다.
- CEO 대조 — 문서끼리 견줘 한쪽에만 반영된 합의를 찾는다. 차례로 돌려도 이건 여전히 나온다.
- 소유자 결정 요청 — 1부 변경, 돈, 사람의 신청이 필요한 것만 모아 선택지와 권장안으로.
이 봇은 DIRECTION.md의 집행자다. 무엇이 값이 되는지는 사양서가 아니라 그 문서가
정한다 — 대상(계약을 앞둔 사람), 이길 수 있는 자리 셋, 하지 않는 것, 그리고 새 아이디어가
통과해야 하는 체크리스트. 이슈마다 그 항목 하나하나의 통과 여부를 적게 되어 있다.
| 페이지 | 볼 수 있는 것 |
|---|---|
| 오늘 | 실거래 검색 입구, 목적별 비교 도구, 자치구 가격 지도·시세·시장 요약 |
| 뉴스 | 경제·부동산 기사, AI 요약(한/영), 카테고리 필터·검색, ?date= 지난 기록 |
| 아파트 시세 | 서울 25개 자치구 매매·전세·월세 평당가와 84㎡ 환산가, 전주 대비 증감 |
| 실거래 검색 | 신고된 거래를 지역·금액·면적·연식·단지명으로 찾기 (매매·전세·월세) |
| 전세 vs 월세 | 자치구·면적대별 전월세전환율과, 가진 돈을 넣어 재는 양쪽 월 실부담 |
| 재계약 | 갱신 보증금이 같은 단지·평형의 새 계약보다 얼마나 싼지, 5% 상한에 닿았는지 |
| 층 | 같은 단지·평형에서 1층이 얼마나 싼지, 최상층에 웃돈이 붙는지 |
| 신고가 | 신고가 다음 거래가 그 값을 따라갔는지 |
| 갈아타기 | 내 동네와 갈 동네의 아파트값이 1년 동안 얼마나 움직였는지 |
| 3개월 뒤 | 서울 아파트값 3개월 뒤 예측과 그 예측이 틀린 만큼 |
| 예산대별 목록 | 3억대~20억대 실거래와, 그 예산이 매달 얼마가 되는지(1억당 월 상환액) |
| 해제·등기 | 신고 뒤 해제된 거래와 등기가 끝나지 않은 거래를 자치구별로 |
| 숫자를 만드는 방법 | 집계 기준과 출처. 신고/계약 기준의 차이, 네 주 겹쳐 세기, 전수 파일을 쓰는 이유 |
| 소개와 연락처 | 누가 무엇으로 만들었고 무엇을 하지 않는지 |
| 무엇 | 출처 | 갱신 |
|---|---|---|
| 뉴스 | 경제지 RSS | 하루 1회 |
| 아파트 실거래 | 국토교통부 매매·전월세 실거래가 (서울 25개 자치구) | 하루 1회 |
| 시장 지표 | 코스피·원/달러 환율·기준금리(한국은행 ECOS) | 하루 1회 |
| 대출·예금 평균금리 | 주택담보대출·전세자금대출·정기예금 평균금리(한국은행 ECOS) | 하루 1회(월별 값) |
| 가격 지수·입주 물량 | 아파트 가격지수(한국은행 ECOS·한국부동산원 R-ONE), 공동주택 입주예정물량(공공데이터포털) | 하루 1회 |
| AI 요약 | 위 뉴스를 Ollama(qwen3:14b)로 요약 | 뉴스 갱신 뒤 새 기사가 쌓였을 때(하루 1회) |
뉴스 네 장과 실거래 검색, 금리 비교 종료 안내 다섯 장(rates·deposit-rates·saving-rates·mortgage-rates·rent-loan-rates)은 noindex다. 남의 기사 제목과 그 요약, 그리고 조건을
넣기 전에는 아무것도 없는 검색 화면은 우리가 만든 것이 아니라서 검색에 내보내지 않는다.
근거표 장(buy-by-budget, #871)도 색인 해제 조건(N1~N6)을 채울 때까지 noindex다.
링크는 따라가게 두었으니(follow) 자치구·예산대 페이지로 가는 길은 그대로다.
자치구 페이지는 시세 문장 아래에 그 구에서만 눈에 띄는 것을 덧붙인다 — 한 동에 몰렸는지
흩어졌는지, 소형이 팔리는지 대형이 팔리는지, 준공 30년을 넘긴 단지가 얼마나 되는지,
직거래 비중이 큰지. 문턱을 넘은 것만 말한다. 스물다섯 구에 같은 다섯 문장을 더 붙이면
여섯 문장짜리 틀이 열한 문장짜리 틀이 될 뿐이라, 튀는 데가 없는 구는 튀는 데가 없다고
적고 끝낸다. 어느 관찰이 어느 구에 붙는지는 scripts/district-facts.ts에 있다.
화면이 받는 파일은 docs/data/*.json에 있다. 국토부 응답은 raw/에 원본 그대로 보관하고,
실거래 관련 파일은 전부 거기서 다시 만든다.
보관 창과 화면이 세는 창은 다르다. 화면은 여섯 달(WINDOW_MONTHS)만 세고, 원본은
스물네 달(RETENTION_MONTHS)을 들고 있다. 같은 물건이 두 번 팔린 것을 견주려면 두 거래
사이가 벌어져 있어야 하는데 여섯 달 안에서는 그런 쌍이 거의 없어서다 — 층까지 맞춘
반복거래 쌍 5,610개 중 90일 이상 떨어진 것이 567개뿐이고, 가격변화 표준편차(10.3%p)가
중앙값(2.1%)의 다섯 배다. 두 창을 한 상수로 묶으면 저장 기간을 늘린 날 추이 그래프가
조용히 몇 년짜리로 바뀌므로 갈라 두었다.
과거 슬롯은 planFetch가 하루 MOLIT_BACKFILL_LIMIT개(기본 50)씩 메운다. 스물다섯 구
두 종류 스물네 달이면 1,200슬롯이라, 빈자리가 다 차기까지 몇 주가 걸린다. 반복거래
지수는 그것이 쌓인 뒤의 일이다.
여기 나오는 거래는 매물이 아니라 이미 신고된 실거래다. 계약 후 30일 이내 신고라 최근 거래일수록 나중에 반영된다. 그래서 화면 시세는 두 가지 기준 중 하나로 낸다.
- 신고 기준 — 그 주에 새로 신고된 거래. 지연이 없고 사후에 바뀌지 않는다. 자치구는 한 주 표본이 얇아 네 주를 겹쳐 센다.
- 계약 기준 — 신고 기한이 지나 마감된 최근 네 주 계약분. 신고 기록이 모자랄 때 쓴다.
어느 쪽으로 냈는지는 화면과 realestate.json의 window.basis에 적힌다.
추이 그래프는 계약일 기준 주간이다 — 여섯 달을 소급해 그릴 수 있는 쪽이라서다.
월세 화면은 보증금과 월세를 따로 그린다. 둘은 서로 바꿔 넣을 수 있는 값이라 —
보증금을 올리면 월세가 내려간다 — 보증금 추이만 보면 시장이 어디로 가는지 절반만 보인다.
그래프를 짚으면 같은 격자에 있는 그래프가 모두 그 주에 함께 찍히고, 말풍선이 그 주의 값을 한꺼번에 보여준다. 옮길 때 인덱스가 아니라 날짜로 찾는다 — 값이 없는 주는 계열에서 빠지므로 같은 화면의 그래프끼리도 점 개수가 다를 수 있다(광진구는 매매가 스물세 주, 전세가 스물네 주다). 격자 단위로 묶는 것은 축이 다른 그래프가 한 화면에 있어서다: 첫 화면의 시장 지표는 날 단위, 부동산은 주 단위라 "같은 좌표"가 성립하지 않는다. 신고 기한이 지난 주까지는 실선, 아직 기한이 남아 신고가 덜 들어온 주는 점선으로 잇는다. 점선 구간은 그림에만 쓰고 카드에 적는 값·최고·최저에는 넣지 않는다.
전월세 시세는 신규 계약만 센다. 갱신계약은 기존 조건을 잇는 것이라 지금 시세와 다르고, 서울 전월세 신고의 절반가량을 차지한다.
그 절반을 통째로 버리지는 않는다. 갱신 신고에는 같은 세입자가 전에 내던 조건이 나란히 있고, 그건 시세표에 없는 것이다. 다만 인상률을 평균 내지는 않는다 — 계약갱신요구권을 쓰면 5%가 상한이라 요구권 행사분의 절반 이상이 5% 언저리에 몰리고, 상한이 안 걸리는 합의 갱신조차 중앙값이 5.0%에서 안 움직인다(5%가 법을 떠나 관행의 기준점이 됐다). 그래서 재는 것은 인상률이 아니라 천장에 닿았는지다. 상한까지 못 올린 계약이 많다는 것은 집주인이 부르는 값을 그 동네가 다 안 받아 줬다는 뜻이고, 이건 상한이 있어서 오히려 읽히는 신호다. 자치구별로 33~59%로 갈린다.
갱신 신고에는 하나가 더 있다 — 지금 갱신하는 것과 나가서 새로 구하는 것의 차이다. 세입자가 실제로 묻는 것이 이건데 시세표는 신규만 세므로 어디에도 답이 없었다. 같은 단지, 같은 전용면적, 같은 달의 신규 전세 중앙값을 "지금 새로 구하면 얼마"로 보고 갱신 보증금을 견준다. 서울은 중앙값 9.7% 낮고 열 건 중 여덟이 시세보다 싸다. 면적을 구간으로 묶지 않는 것은 같은 단지라도 평형이 다르면 값이 다르기 때문이고(묶으면 4,176→6,118건으로 늘지만 59㎡ 갱신을 63㎡ 신규와 견주게 된다), 순수 전세끼리만 견주는 것은 반전세를 한 값으로 묶는 배수가 곧 전월세전환율이라 답해야 할 것을 먼저 가정하기 때문이다. 거래가 도는 큰 단지 쪽으로 기운다 — 맞물리려면 같은 칸에 신규가 세 건은 있어야 한다.
말하는 문턱은 부트스트랩으로 정했다. 서울 표본을 표본 크기별로 400번 다시 뽑아 보면 60건에서 서울과 똑같은 구도 −12.5% ~ −5.0%에 떨어진다. 그보다 적으면 폭이 9%p를 넘어 아무 차이 없는 구가 양쪽 문장 다 받을 수 있다. 그래서 60건을 하한으로 두고 두 문턱 (1.5배·0.45배)을 그 범위 바깥에 놨다. 지금 말하는 구는 둘이다 — 강동 −16.7%, 중랑 0.0%(시세보다 싸게 맺어진 재계약이 22%뿐).
전세→월세 전환은 "비"가 아니라 비율로 센다. 월세→전세가 구당 한 자릿수라 비로 내면 분모 1건이 75:1을 만든다. 분모를 "이전이 전세인 갱신" 전체로 두면 구마다 200건이 넘고, 그러면 은평 3.2%에서 종로 14.3%까지가 실제 차이가 된다(서울 7.5%).
전세가율은 평균 둘을 나눈 값이라 어느 단지의 것도 아닐 수 있다. 시세표의 전세가율은
구 전체의 전세 평당보증금 ÷ 매매 평당가인데, 두 평균이 서로 다른 단지들에서 나온다.
매매와 전세가 함께 신고된 단지의 같은 평형에서 하나씩 낸 값과 견주면 구 값이 중앙
6.1%p 어긋나고, 종로구는 83.7% 대 55.6%다. 어긋남은 거래가 적을수록 커진다 —
맞물린 칸 40개 미만인 구는 평균 14.7%p, 80개 이상은 3.6%p(상관 −0.62). 그래서 구 값
뒤에 칸별 분포를 붙이고, 시세표 옆에 '단지별 중앙값' 카드를 나란히 둔다(칸 20개 미만이면
사분위 없이 중앙값만, 10개 미만이면 침묵). 표의 값을 갈아 끼우지는 않는다 — 한 행에
매매 평당가와 전세 평당가가 나란히 있어서, 전세가율만 다른 방식으로 내면 같은 행의 두
숫자로 반박되는 숫자가 된다. 추이 그래프도 평당가 기준으로 남는다(주 단위라 한 주에 같은
단지의 매매·전세가 각각 세 건씩 잡히는 일이 거의 없어 칸 기반 시계열이 안 나온다).
극단은 실재한다 — 소형 96100%, 재건축 대상 노후 단지 1112%. 다만 연식·면적으로
갈린다고 말할 수는 없다(30년↑ 48% vs 10년↓ 50%). 갈리는 건 극단 몇 칸이지 전체가 아니다.
전세와 월세 중 어느 쪽이 싼지는 시세표로는 나오지 않는다. 둘을 견주려면 같은 물건의 두 조건이 있어야 하는데, 그것이 같은 자치구·같은 면적대에서 전세와 월세가 둘 다 신고된 단지다. 거기서 전월세전환율(월세 × 12 ÷ 보증금 차액)을 내고, 같은 저장소가 매일 받는 한국은행 ECOS 전세자금대출·예금 평균금리를 붙이면 양쪽의 월 실부담이 같은 자로 재진다. 전환율이 대출금리보다 높으면 빌려서 전세로 가는 쪽이 싸다 — 가진 돈과 무관하게 성립하는 부분이라 첫 문단에 그것부터 적는다.
이 화면만 여섯 달을 본다. 자치구를 세 면적대로 쪼개면 석 달로는 못 채우는 칸이 생기고, 전환율은 주 단위로 움직이는 값이 아니라 길게 봐도 무디어지지 않는다. 대출 한도 80%는 가정이며 화면과 방법론에 그렇게 적는다.
시세 화면은 해제된 거래를 빼고 센다. 빼는 것이 맞지만 뺀 것이 무엇이었는지는 어디에도 남지 않아, 해제·등기 화면에서 그 뺀 쪽을 본다. 여기서 나온 것 하나는 흔한 이야기와 어긋난다 — 해제 거래는 그 단지 최고가가 아니다. 같은 단지·같은 면적에 남은 거래의 중앙값보다 높았던 것이 21%인 반면 낮았던 것이 30%로 오히려 많다. 세어 본 결과가 그러므로 그렇게 적는다.
미등기율은 "익은 달"의 계약만 센다. 그 달 계약의 열에 여덟이 이미 등기를 마쳤다면 익은 달이고, 안 익은 달은 통째로 뺀다. 이번 달 계약이 등기 전인 것은 시간이 모자라서지 늦어서가 아니라, 같이 세면 최근 거래가 많은 구가 무조건 나빠 보인다. 기준을 등기까지 걸린 날의 분포에서 뽑으면 안 되는 것도 같은 이유다 — 그 분포에는 이미 등기된 건만 들어 있어 빠른 쪽으로 기운다.
사용자가 가진 돈을 바꿀 때마다 다시 재야 해서 이 계산만은 브라우저에도 한 벌 있다.
두 벌이 갈라지면 어느 쪽도 틀렸다고 말해 주지 않으므로, 같은 입력에 같은 답이 나오는지를
test/conversion-page.test.mjs가 지킨다. 문장은 그렇지 않다 — 빌드에서
두 언어를 미리 만들어 넘기고 화면은 고르기만 한다.
npm install
npm test
npm run update # 수집 → 데이터 빌더 → docs/data·raw 만 커밋·푸시 (사이트 HTML은 만들지 않는다)
npm run update는 인증키가 필요한 실거래(fetch-realestate) 수집을 부르지
않는다 — 키 없이 돌리는 로컬 갱신이라서다. 그것은 하루 한 번 daily-update.yml의 full 실행이
시크릿을 넣어 부른다. 로컬에서 받으려면 키를 넘겨 그 스크립트를 따로 돌린다.
Node 24 이상이 필요하다. npm test는 scripts/run-tests.mjs를 거쳐 러너를 띄우고 시험 개수 하한(test/COUNT)을
같이 지키는데, 시작할 때 타입 스트리핑(process.features.typescript가 "strip")을 단언해서 Node 22에서는 시험이 있어도
종료 1이다(메시지에 찾은 Node 버전이 찍힌다). 리포터도 래퍼가 정하므로 NODE_OPTIONS에 --test-reporter를 두었다면 빼야 한다.
테스트는 인증키 없이 전부 돈다(외부 API는 스텁 서버로 대신한다).
수집을 직접 돌리려면 인증키를 환경변수로 넘겨야 한다. 스크립트가 .env를 읽지 않으므로
파일에 두려면 node --env-file=.env scripts/…로 돌린다.
MOLIT_API_KEY/MOLIT_API_ENDPOINT— 아파트 매매 실거래MOLIT_RENT_API_KEY/MOLIT_RENT_API_ENDPOINT— 아파트 전월세 실거래ECOS_API_KEY— 한국은행 기준금리·코스피 종가·원/달러 환율 (없어도 된다)
코스피는 ECOS의 일별 종가(802Y001)를 받는다. 전에는 네이버 비공식 폴링에서 받았는데, 그쪽은 장중 스냅숏을 주므로 언제 부르느냐에 따라 뜻이 달라진다 — 개장 전에 부르면 전일 종가에 등락 0.00이 붙어 오고, 장중에 부르면 그 순간 값이 온다. 하루 한 번 받아 "그날 값"으로 저장하니 한 계열에 두 뜻이 섞였고, 저장된 값끼리 빼면 45포인트가 움직인 날에 등락이 0.00으로 적혀 있었다. ECOS는 마감된 종가를 거래일에 맞춰 준다. 대신 하루 늦으므로 화면에 어느 장의 종가인지 적는다.
등락은 받아 오지 않고 그 계열의 종가 둘에서 직접 뺀다. 값과 등락을 서로 다른 곳에서 받으면 둘이 서로를 설명하지 못한다. 앞 종가가 없으면 등락을 비운다 — 0.00은 "안 움직였다"는 뜻이라 "모른다"의 자리에 놓으면 안 된다.
환율도 ECOS의 731Y001(원/미국달러 매매기준율, 일별)에서 받는다. 영업일 값만 주고 하루
늦으므로 usdKrw의 asOf가 마지막 영업일이고, 증감은 앞 영업일과 직접 뺀다(최근 1년은
usdKrwDaily). 브라우저가 따로 부르지 않으며 LIVE 배지도 없다.
기준금리는 한국은행 ECOS Open API에서 받는다. 인증키가 없으면 공개된 sample 키로
도는데 한 번에 열 건까지만 주므로, 오늘 값은 읽히고 시계열은 못 읽는다. 키는
ecos.bok.or.kr/api에서 가입 즉시 무료로 나온다.
평균금리 계열(fetch-indicators.ts → raw/indicators/series.json, 월별): 주택담보대출 mortgage_rate(121Y006
BECBLA0302, 신규취급액 기준), 전세자금대출 jeonse_loan_rate(121Y015 BECBLB020210, 잔액 기준 — 신규취급액
표에는 전세자금대출 항목이 없다), 정기예금 deposit_rate(121Y002 BEABAA211, 신규취급액 기준). 뒤의 둘은 전세 vs
월세 비교가 읽으려고 받는다(#577).
AI 요약은 로컬에 Ollama가 있어야 한다(OLLAMA_THINK=false 필요).
main은 생성 HTML을 들고 있지 않다. docs/에는 손으로 쓰는 틀 14장·금리 비교 종료 안내 5장·정적 자산·docs/data뿐이고, 생성 49장을 포함한 68장은 배포 때
node scripts/render-pipeline.ts --out=build/site가 main HEAD(코드+데이터)로 새로 만든다(build/는 .gitignore다). 로컬에서 사이트를
보거나 build/site를 읽는 시험을 돌리려면 그 명령을 먼저 돌린다 — npm test는 pretest로 자동으로 한다.
deploy.yml(Deploy site)이 사이트를 만들고 Pages에 올리는 유일한 워크플로다. main 푸시와daily-update.yml·summarize.yml완료 때 돈다. 게이트는 렌더 폴백 없음 +npm run test:site(build/site를 읽는 시험만) 둘이고, 실패하면 사이트는 직전 배포에 머문다.daily-update.yml·summarize.yml은docs/data·raw만 커밋하고 렌더·시험·배포를 하지 않는다(뉴스·요약은 하루 1회). 그래서 수집은 렌더·시험 실패에 막히지 않고, 기능 병합은 수집이 커밋하는 파일과 겹치지 않는다. 그래서 병합에 시계 기준 조건(병합 창·수집 중 병합 금지)은 없다(#511).scripts/dev/merge.sh는docs/data·raw·워크플로·수집기 경로를 건드리는 PR만 진행 중 실행이 끝나길 기다린다.npm run health(tools/health)는 데이터와 build/site가 맞는지 보는 비차단 점검이다. 기능 시험(틀·스크립트·워크플로 글자)은 PR의test.yml이 맡는다.- 배포본이 같은 HEAD로 만든 build/site와 같은지는
scripts/deploy-check.ts, 두 시점의 배포본끼리는scripts/site-snapshot.ts로 읽기 전용 대조한다. - 집계 전용 사이트맵
build/site/sitemap.xml(#769)은 렌더 마지막 단계(build-updated-stamp,scripts/sitemap.ts)가 noindex 가 아닌 장의 canonical 로 만든다 — 커밋 파일이 아니다.lastmod는 장에 찍힌 데이터 기준일뿐이고(소개·방법은 없음) 배포일을 쓰지 않는다. 호스트 루트/sitemap.xml·robots.txt는 블로그 저장소 소유라 건드리지 않는다.
의존은 fast-xml-parser·rss-parser 둘이었는데 react·react-dom·scheduler·esbuild를 정확한 버전으로 더했다(#237,
esbuild의 플랫폼 바이너리 @esbuild/*는 esbuild의 optionalDependencies라 OS에 맞는 것이 npm ci로 자동으로 깔린다). 브라우저로 나가는
것은 없다 - 빌드 때 HTML 문자열을 만드는 데만 쓰고, 틀에 구워 넣은 결과는 전과 같은 바이트다.
scripts/prerender.ts의 *Html() 조각 함수는 전부 React로 그린다(#246 컷오버, 2026-10-12 소유자 지시. 문자열 legacy 본문과 렌더러 스위치는
#248 P3 가 지웠다). 그래서 prerender.ts·build-*-pages.ts는 npm ci로 node_modules가 깔린 곳에서만 돈다. 조각의 마크업은
scripts/render/entry.jsx의 fragments(지금은 조각 41개 전부)에 있고, prerender.ts의 같은 이름 함수는 그 조각을 부르는 래퍼다.
react 쪽 오류는 삼키지 않고 예외로 올라오며, 렌더 파이프라인(scripts/render-pipeline.ts)은 첫 실패에서 멈춰 종료 1이다(폴백은 없다, #248 P1).
진입점은 scripts/render/react.ts 한 곳이고, JSX를 읽는 로더는 esbuild로 진입 파일을 묶어 import하는 scripts/render/load-jsx.ts다
(Node의 --loader 플래그는 쓰지 않는다). 렌더 스크립트는 전 장을 메모리에서 그린 뒤에 한꺼번에 쓴다(scripts/render-writes.ts) -
도중에 던져도 일부 장만 새 출력이 되지 않는다. 렌더 출력이 바뀌지 않았는지는 tools/golden/README.md의 같은-실행 비교로 본다.
68장 HTML이 전부 docs/style.css 한 장을 본다. 손으로 고치는 틀은 열네 장이고
(index news realestate deal-search jeonse-vs-wolse cancelled-deals
renewal-vs-new floor-gap record-high switch-house price-outlook buy-by-budget about method)
금리 비교 종료 안내 다섯 장(rates·deposit-rates·saving-rates·mortgage-rates·rent-loan-rates, noindex)도 docs/에 있다.
색인 대상은 57장, noindex는 11장(뉴스 4·실거래 검색 1·근거표 1·종료 안내 5)이다.
나머지 생성 49장은 이 중 몇 장을 틀로 삼아 scripts/build-*-pages.ts가 배포 때 찍어낸다(main에는 없다). 그러니 생성된 페이지를 직접 고치지 말고 틀과 스타일시트를 고친다.
전에는 62장이 각자 <style>을 품고 있었고 그 사이에 일곱 갈래로 갈라져 있었다.
.data-table이 한 장에서는 왼쪽 정렬, 나머지 61장에서는 오른쪽 정렬인 식이었다.
페이지 안에 <style>을 다시 넣으면 그 길이 다시 열리므로 테스트가 이를 막는다.
이름의 마크는 그림 파일이 아니라 도형이다. 화면 왼쪽 위의 워드마크는 style.css의
.wordmark::before(첫 화면 밖에서는 .brand::before)가 그러데이션 넷과 가로지르는 한 획으로 그리고, 앱 아이콘
(docs/icons/*.png)은 같은 모양을 scripts/build-icons.ts가 그린다 — 손으로 갈아
끼우면 둘이 조용히 갈라지므로 test/icons.test.mjs가 스크립트 출력과 대조한다.
자치구·예산대 페이지에는 사이트 이름이 한 번도 안 나왔었다. 검색으로 그 페이지에 떨어진
사람은 자기가 어디에 있는지 모른 채 표부터 보게 되므로, 워드마크를 예순여덟 장 머리에 둔다.
넓은 화면에서는 껍데기만 넓어지고 글은 안 넓어진다. 오래도록 미디어쿼리가 560·520·480 셋뿐이었다 — 전부 아래쪽이라 모바일에서 좁아지는 규칙만 있고 넓어지는 규칙이 없었고, 1920px 모니터에서 화면의 40%만 썼다. 지금은 1024px에서 1120px, 1440px에서 1280px로 연다(네이버 메인 1280, 다음 1168, 통계표를 다루는 KOSIS 1600px대).
폭을 쓰는 것은 표와 격자다. .history-grid·.overall-cards·단지 카드는 원래
auto-fit이라 폭만 생기면 알아서 열이 늘고, 예산 거래 목록은 넓은 화면에서 여러 줄로
쪼갠다(한 줄에 이름과 값이 양 끝으로 벌어지는 모양이라 넓어지면 가운데가 뚫린다).
산문은 --prose(46rem)에 묶는다. 728px에서 이미 한 줄이 한글 46~54자로 편한 범위의
위쪽 끝이라, 껍데기를 따라 늘리면 여든 자가 되어 넓히기 전보다 나빠진다. method.html이
쓰던 규칙을 데이터 화면의 문단·안내·AI 요약·뉴스 목록까지 넓힌 것이다. 어느 블록이
묶여 있어야 하는지는 test/style.test.mjs가 지킨다.
색과 크기는 :root의 토큰에서만 고른다. 글자 크기 --fs-* 아홉 단(바닥 0.78rem,
입력칸은 iOS가 화면을 확대하지 않는 --fs-field: 1rem), 손가락이 닿는 --tap: 44px,
그리고 브랜드색 --accent.
액센트는 데이터 안에 쓰지 않는다. 표 안의 파랑은 이미 '하락'이라는 뜻(--down)이라
같은 자리에 다른 뜻의 파랑을 놓으면 둘 다 못 읽는다. 액센트는 지금 어디에 있고 무엇을
골랐는지를 말하는 자리(1층·2층 탭, 칩, 초점 테두리, 섹션 제목 밑줄), 그리고 사이트가
자기 자신에 대해 말하는 자리(첫 화면의 집계 기준 안내)에만 쓴다.
이 규칙들은 test/style.test.mjs가 지킨다 — 대비비까지 계산해서 본다.
질문 입구는 격자다. 전에는 한 줄로 접어 가로로 밀게 했는데, 그러면 여섯 중 뒤의 셋이 안 보인다. 사이트에서 제일 눌려야 하는 자리를 스크롤 뒤에 감춰 두고 있었다. 격자로 펴면 지금의 열 개가 접히지 않고 다 보인다.
선택지가 적은 조건은 칩으로 고른다. 거래유형·보증금·월세·면적·연식은 서넛뿐인데 셀렉트로 두면 고르는 데 세 번을 눌러야 하고, 무엇이 켜져 있는지 보려면 일곱 칸을 하나씩 읽어야 했다. 지역(25개)과 예산(29개)은 셀렉트로 남긴다 — 칩 스물다섯 개를 가로로 훑어 은평구를 찾는 것은 목록에서 고르는 것보다 나쁘다. 칩이 언제나 나은 게 아니라 선택지가 적을 때 낫다. 조건이 하나라도 걸리면 '조건 지우기'가 뜬다.
실거래 검색은 조건을 넣기 전에도 결과를 보여준다. 매매는 원래 그랬다(서울 전체
건수 + 최근 30건). 전세·월세만 자치구를 고르기 전엔 빈 화면이었는데, 전월세 전수 파일이
자치구별로 쪼개져 있고 스물다섯 개가 6.8MB(그렇게 정한 2026-08-29 값. 아래 크기·개수도 결정 당시 값이고 매일 바뀐다)라 서울 전체를 그 자리에서 못 만들기 때문이다.
그래서 서울 전체 건수와 최근 30건만 미리 만들어 둔다(build-rent-preview.ts, 그때 9KB).
다만 그 맛보기로 조건을 거르지는 않는다. 최근 30건 위에 "60㎡ 미만"을 얹으면 나오는 건수는 서울에서 그 조건에 맞는 거래 수가 아니라 이 목록 안의 수인데, 화면에서는 구별되지 않는다. 조건이 하나라도 걸리면 자치구를 고르게 해서 전수 파일을 읽는다 — 시세 화면이 자치구별 전수 파일을 따로 두는 것과 같은 이유다.
검색에서 단지를 고르면 평형별 카드가 뜬다. 목록은 "얼마에 팔렸나"에는 답하지만
"이 단지 전세가율이 얼마냐"에는 답하지 않는다 — 그건 평형마다 매매와 전세를 따로 모아야
나온다. 규칙은 scripts/complex-ratio.ts와 같고(한쪽이라도 3건 미만이면 값을 내지 않음),
표본이 모자란 평형은 비워 두지 않고 "신고 1건"이라고 적는다. 이 카드의 값어치는 숫자가
아니라 그 숫자를 낼 수 있었는지까지 보이는 데 있다. 같은 규칙이 빌드와 화면 양쪽에 있으므로
test/complex-card.test.mjs가 실제 자치구 데이터로 두 결과를 맞대어 드리프트를 잡는다.
페이지로 만들지 않는다. 단지가 3,360개(2026-08-29 값)인데 페이지로 찍으면 애드센스가 자치구 25장으로 반려했던 그 자동 생성이 된다. 검색 결과를 한 화면에서 JSON으로 그리므로 색인 문제가 없다.
검색은 정해진 이름이 아닌 말도 받는다. 색인의 이름만 맞추면 "8억"이나 "84㎡"나
"강남구 84㎡"가 전부 0건이 되는데, 사람은 그렇게 친다. 이름으로 못 맞춘 글자를 숫자로
한 번 더 읽어 예산·면적 조건으로 바꾼다(34평 → 112㎡ → 85~135㎡ 구간). 법정동 285개(2026-08-29 값)도
색인에 넣었다(+4KB) — 사람은 "강남구"보다 "역삼동"으로 동네를 부른다. deal-search에
dong 조건을 새로 뒀다.
이름으로 딱 맞으면 조건 추측을 얹지 않는다. "상계동"을 친 사람에게 "노원구 상계동 (조건으로 찾기)"를 먼저 주면 같은 말이 두 번이다.
못 찾으면 사라지지 않고 말한다. 전에는 드롭다운이 그냥 없어져서, 검색이 고장 났는지
잘못 쳤는지 그런 게 없는 건지 알 수 없었다 — 셋 중 무엇이든 막다른 길이다. 무엇을 찾을
수 있는지 적고, 없는 것은 없다고 말한다(학군·교통·재건축). 못 찾은 말은
search_miss로 모아 두므로, 동의어는 추측이 아니라 실제로 들어온 글자로 채우면 된다.
검색은 하나로 모은다. 전에는 첫 화면의 지역 검색(표 거르기)과 금리 화면의 상품
검색이 따로 있었고, '실거래 검색'이라는 이름의 화면에는 정작 검색창이 없었다(셀렉트
일곱 개). 단지명 검색은 자치구를 먼저 골라야 동작해서 "래미안"을 서울 전체에서 못 찾았다.
docs/search.js가 자치구·예산대·화면·단지를 한 입구에서 받는다.
색인(scripts/build-search-index.ts → docs/data/search-index.json)은 자치구별로 묶어
이름만 담는다. 항목마다 자치구와 주소를 되풀이했더니 579KB였는데, 주소를 브라우저가
만들게 하고 자치구를 묶으니 37KB(gzip 22KB, 2026-08-29 값)가 됐다. 고른 뒤 그 자치구 전수 파일 하나만
더 받으면 되므로 스물다섯 개를 다 받을 이유가 없다. 색인은 처음 쓸 때 받는다.
1층 탭 넷은 우리가 데이터를 어디서 받는지의 목록이지 사용자 질문의 목록이 아니다.
이미 계산해 둔 답이 여럿인데(전환율, 갱신 격차, 해제율, 세후 이자) 그게 어느 페이지에
있는지 알 방법이 없었다. 그래서 첫 화면 위쪽에 질문으로 들어가는 입구(.question-nav)를
한 줄 둔다. 새 데이터도 새 계산도 아니고 라우팅이다. 질문은 조건을 넣지 않아도 답이
보이는 화면으로만 보낸다 — 빈 화면으로 보내면 누른 사람이 거기서 또 막힌다.
test/question-nav.test.mjs가 목적지가 미리 그려 둔 답을 갖고 있는지 본다.
집계 기준 안내(.method-callout)는 첫 화면에서 첫 데이터 섹션보다 앞에 있어야 하고
두 문장을 넘지 않는다. 이 사이트가 대형 부동산 앱과 다른 점이 사실상 그것 하나뿐이라
푸터에 두면 아무도 안 보고, 길어지면 시세표를 다섯 행으로 접어 번 자리를 도로 까먹는다.
test/method-callout.test.mjs가 위치와 길이를 둘 다 본다.