← 현진스

실봄

아파트 시세·청약·공공주택·행복주택 공고를 지도에 모아 보여 주는 앱입니다. 공공데이터 수집 파이프라인부터 관리 화면, 뉴스 그래프 검색, 지도 웹과 모바일 앱, 배포까지 직접 만들었습니다.

만든 방법

상태
웹 운영 중 · 실봄 앱 App Store · Google Play 출시
운영
OCI 서버, realestate.hyunjins.co.kr
규모
시군구 255 · 거래 행 약 1,720만 · Python 21,000줄 · API 라우트 91 · pytest 275건

공공데이터 수집 파이프라인에는 국토부 실거래가, K-apt, 건축물대장, VWorld, 청약홈, KOSIS, 뉴스 RSS를 받는 커넥터 13개가 있고 받은 응답은 파싱하기 전 원문 그대로 보관합니다. 정규화 규칙은 코드가 아니라 데이터셋 YAML 28개에 적습니다. 잘못 들어온 값은 임의로 고치지 않고 사유를 붙여 따로 모읍니다. 서비스 데이터는 데이터셋 × 시군구 × 계약월로 나눈 파티션을 트랜잭션 안에서 한 번에 바꿉니다. 그래서 검증에 걸린 실행이 있어도 기존 데이터는 바뀌지 않습니다.

운영은 관리 화면에서 합니다. 수집 순서는 React Flow로 그린 DAG에서 고치고 데이터셋 YAML은 검증과 미리보기를 거쳐 저장합니다. 수집 범위나 순서를 바꾸는 일도 이 화면에서 끝나며 바꾼 내용은 모두 감사 로그에 남습니다.

같은 파이프라인으로 받은 뉴스 기사에서는 단지·지역·정책을 뽑아 Postgres와 Neo4j 그래프에 관계로 쌓고 본문은 pgvector 임베딩으로 찾습니다. 단지를 누르거나 질문하면 벡터 검색 결과와 그래프 이웃을 같이 보고 출처를 붙여 답하고 근거가 없으면 모른다고 합니다. LLM은 어댑터 하나로 부릅니다. 대량 추출과 임베딩은 로컬 Ollama가, 답변은 Gemini가 맡습니다.

지도 앱은 React 19와 네이버 지도로 만들었습니다. 실봄 앱은 이 웹 앱을 Capacitor 8로 감쌌고 기기에서는 지도만 네이티브 SDK로 그리도록 Swift·Kotlin 지도 플러그인을 직접 만들었습니다.

관리 화면

관리 화면은 관리 키로 로그인해야 열리며 수집 현황, 플로우, 데이터셋, 실행, 작업, 오류, 검수, 감사 로그를 다룹니다.

문제와 해결

코드는 대부분 Claude Code로 짰고 검토는 Codex에 맡겼습니다. 아래는 도구의 답이나 자동 테스트 결과를 그대로 믿지 않고 운영 서버와 로그, 실기기에서 다시 확인해 바로잡은 일입니다.

출시 검토에서 나온 보류 판정

2026-09-24 출시를 앞두고 Codex에 검토를 맡겼습니다. QA 중에 502 서버 오류가 이어졌는데 Codex는 운영 서버에 요청해 보지 않은 채 이 오류 기록만 보고 서버 재시작이나 메모리 부족(OOM) 탓이라며 출시 보류를 권했습니다. 운영 서버를 확인하니 API 컨테이너는 약 19시간 동안 재시작이 0회였고 메모리는 4.25%인 348MB, OOM 로그는 0건이었습니다. 그래서 그 추정은 걷어 냈습니다.

QA가 짐작한 원인과 실제 원인

같은 날 QA에서 생활권 정보를 받는 TAGO·소상공인·에어코리아 외부 API 3종이 모두 INVALID_REQUEST_PARAMETER_ERROR로 실패했습니다. QA 문서는 서비스키 인코딩이나 활용기간 만료를 원인으로 짐작했습니다. 실제로는 오류 수집 도구 Sentry의 httpx 계측이 나가는 요청마다 sentry-trace·baggage 헤더를 붙였고 공공데이터 포털이 그 요청을 거부하고 있었습니다. 화면에는 9/20부터 옛 캐시만 보였습니다. 이 헤더를 붙이지 않게 막고 회귀 테스트를 더했습니다.

ok로 끝났지만 멈춰 있던 뉴스 임베딩

뉴스 잡이 48시간 동안 20번 모두 status: ok로 끝나는 사이 DB에서는 기사 9,519건 중 98%인 9,351건이 임베딩되지 않은 채 2026-09-13부터 멈춰 있었습니다. ollama 로그에는 임베딩 요청이 정확히 1분 뒤 400으로 끝난 기록이 남아 있었습니다. 요청 타임아웃이 60초로 고정돼 있었고 묶음 하나가 실패하면 전부 버리는 구조였습니다. 저장은 10건마다 나눠 하도록 바꾸고 타임아웃은 설정으로 뺐습니다. 임베딩이 멈추면 점검 화면에 뜨게도 했습니다. 운영에서 다시 돌리자 기사 20건과 청크 100개가 실패 없이 들어갔습니다.

모델 교체 뒤 모든 질문이 「근거 없음」

임베딩을 로컬 embeddinggemma로 바꾸자 모든 질문이 오류 한 줄 없이 「근거 없음」으로 떨어졌습니다. 답할 근거가 있는지 가르는 RAG 근거 임계 0.62는 Gemini 임베딩에 맞춘 값이었습니다. 다시 재 보니 관련 질문도 가장 높은 유사도가 0.44~0.65였고 무관한 질문은 0.28~0.35였습니다. 임계를 0.40으로 내린 뒤 평가셋 5문항에서 무관한 질문만 「근거 없음」이 되는지 확인했습니다. 모델을 바꾸면 이 평가셋으로 임계를 다시 잰다는 규칙도 남겼습니다.

사라진 거래 8,971건 중 90%가 거짓 양성

원본에서 사라졌다고 표시된 거래 기록이 줄지 않고 늘기만 했습니다. 아직 확인하지 않은 8,971건을 현재 데이터와 하나씩 대조하자 90.3%인 8,105건은 거래가 남아 있는 거짓 양성이었고 실제로 사라진 것은 2.9%인 262건뿐이었습니다. 주된 원인은 거래를 맞춰 볼 때 쓰는 값이었습니다. 이 값에 등기가 끝나야 채워지는 항목이 들어 있어서 등기만 끝나도 다른 거래로 보였습니다. 기존 테스트는 거래가 빠지는 경우만 검사해서 이를 잡지 못했습니다. 비교에는 바뀌지 않는 항목만 쓰도록 고치고 남은 기록을 다시 대조했습니다. 원본 응답을 비교하다가 거래가 해제되면 새 행 대신 원래 행이 바뀌어 들어온다는 것도 확인해 설계 문서도 고쳤습니다.

iOS 실기기에서만 눌리지 않던 메뉴

실봄 앱은 네이티브 지도를 WebView 뒤에 두고 터치가 웹 요소 위인지 지도 위인지 직접 판정합니다. iOS 앱에서 로고 메뉴의 「정보 · 약관」과 테마 항목이 눌리지 않았습니다. 데스크톱과 Android에서는 멀쩡해서 버전 1.0(5)까지 모른 채 올렸습니다. 원인은 실기기 녹화 시험을 하고서야 찾았습니다. 터치 판정이 최상위 요소의 테두리만 세느라 헤더 밖으로 펼쳐지는 메뉴를 빼먹고 있었습니다. 펼쳐지는 메뉴까지 터치 영역에 넣자 검색 결과 목록과 필터 팝업도 같이 고쳐졌습니다.

터치 방식을 처음 정할 때도 비슷했습니다. 공식 Google Maps 플러그인이 쓰는 방식은 iOS 26 시뮬레이터에서 터치가 지도에 닿지 않아서 직접 재 본 뒤 터치를 웹과 지도에 나눠 주는 방식으로 바꿨습니다.

지도 앱과 실봄 앱 화면

구조

[외부 소스] 국토부 실거래가(XML) · K-apt(JSON) · 건축물대장 · VWorld · 청약홈 · KOSIS/행안부 · NEIS · 뉴스 RSS/Google News
     │ collectors/ (커넥터 13개: fetch(partition, cursor) → page, tenacity 재시도, 키 마스킹)
     ▼
raw_pages (원문 그대로 보관, 본문 해시)                  ← ingestion_runs (파티션·설정 버전·코드 버전·건수)
     │ core/   (datasets/*.yaml 템플릿 상속 → 변환 함수 레지스트리 → 실패는 rejected_records 격리)
     │ enrich/ (지오코딩 + 캐시, K-apt 매칭, 공급면적, 임베딩, LLM 엔티티 추출)
     ▼
serving_generations (파티션 단위로 세대를 트랜잭션 안에서 교체. 실패한 실행은 기존 세대를 건드리지 않음)
     ├─ deals(deals_current 뷰) · property_master · property_summary(단지×거래종류×면적대)
     ├─ news_chunks(pgvector HNSW) · entities · relations · mentions ──▶ Neo4j 미러
     └─ region_stats · public_prices · building_registers
     ▼
FastAPI ── 공개 조회 API · /admin/* (X-Admin-Key, audit_log) · /rag/* · 내장 스케줄러 + 플로우 DAG · memwatch
     ▼
nginx (SPA + /api 프록시) ◀── 리버스 프록시(TLS) ◀── 브라우저 / 실봄 앱

기술 스택

영역선택
언어·패키징Python 3.12 (uv workspace 6패키지), TypeScript (pnpm workspace 3앱)
DBPostgres 16 + PostGIS 3 + pgvector + pg_trgm, SQLAlchemy + alembic 27개, Neo4j 5.26(미러)
API·오케스트레이션FastAPI(91 라우트), APScheduler 내장 스케줄러 + 플로우 DAG(잡 19종), Dagster(선택), advisory lock
LLM·임베딩어댑터 complete() 하나로 Gemini flash-lite(답변), 로컬 Ollama gemma4(추출), embeddinggemma(임베딩)
프론트·모바일React 19, Vite, 네이버 지도 SDK, React Flow, Capacitor 8 + 자체 플러그인(Swift·Kotlin)
배포docker compose(OCI), nginx, mem_limit + 메모리 감시, pg_dump 14세대