apt-chat
연봉 기반 아파트 시뮬레이터 + Text-to-SQL AI 챗봇
서울 아파트 실거래 데이터를 가지고, 내 연봉으로 살 수 있는 집을 한 화면에서 찾아볼 수 있는 웹 서비스입니다. 좌측 시뮬레이터는 DSR 기반 대출 한도와 총예산을 계산해 매물 목록을 보여주고, 우측 AI 챗봇은 같은 데이터셋에 대해 자연어 질문을 SQL로 변환해 결과를 시각화합니다. 두 기능이 한 대시보드에서 컨텍스트를 공유합니다. 그래서 슬라이더를 조정한 뒤 바로 "방금 조건에서 평당가가 가장 높은 동은?" 같은 질문으로 이어갈 수 있습니다. 기획부터 개발까지 엔드투엔드로 혼자 완성한 프로젝트입니다.

사용 기술 Next.js 16 (App Router), React 19, TypeScript, TanStack Query FastAPI, Anthropic SDK, sqlglot Supabase (PostgreSQL)
전체 구현 흐름
프론트(Next.js) · API 서버(FastAPI) · 데이터 수집기(ETL)를 pnpm workspace 모노레포로 묶고, Anthropic 호출과 DB 연결은 서버에서만 일어나도록 했습니다.
AI 오케스트레이션 설계
AI native 방식으로 제작했습니다. 설계·파이프라인·성능 최적화는 직접 개입하고, 코드 작성은 LLM에 위임하되 의도대로 결과가 나오도록 가드레일을 설계했습니다.
Plan 문서를 진실 소스로 두고 15개 phase로 구현 단계를 쪼갠 뒤, 커스텀 스킬이 각 phase마다 탐색(Explore) → 구현(Implement) → 검증(Verify) 3단계 루프를 반복 실행하도록 만들었습니다. 이 루프는 단일 워커가 아니라 역할별 서브에이전트에 분배되어, 탐색·구현·검증 워커가 백그라운드에서 병렬로 돌아가도록 설계했습니다. 단계별로 스킬·모델을 다르게 주입해 LLM이 의도한 방향으로 코드를 작성하게 했고, 구현 워커와 검증 워커를 분리해 "성공했습니다" 류의 거짓 양성을 차단했습니다.
검증 이후에는 테스트 코드를 모두 통과하고 에러가 없어야 커밋할 수 있도록 lefthook pre-commit 훅으로 통제했습니다. LLM이 임의로 커밋/푸시하지 못하도록 막아, plan과 코드 상태가 어긋나지 않게 잠금장치를 두었습니다.
Text-to-SQL 파이프라인 / 비용 상한과 SQL 안전성 제어
핵심적으로 고려한 부분
자연어 → SQL 변환은 챗봇의 중심 기능이지만, LLM 기반 파이프라인에는 3가지 위험이 있었습니다. ① API 호출 비용이 끝없이 불어날 수 있다는 점, ② SQL injection이나 DELETE 같은 파괴적 구문을 막기 어렵다는 점, ③ 실패 시 자동 재시도 로직이 비용을 증가시킬 수 있다는 점이었습니다.
LLM 호출 파이프라인
- Step 1. Haiku로 intent 추출 + 키워드 정규식 기반 schema retrieval
- Step 2. intent가 SUPPORTED_INTENTS(최근 거래, 지역별 평균가, 월별 추이, 가격 필터, Top N)에 매칭되면 intent_mapper의 코드 상수 템플릿 SQL로 종결, 미매칭일 때만 Sonnet으로 SQL 생성
SQL 검증
sqlglot AST로 파싱해 규칙을 강제: ALLOWED_TABLES 3개로 제한, SELECT 단일 statement만 허용, MAX_JOINS=3 / MAX_SUBQUERY_DEPTH=2 / MAX_LIMIT=100 강제 주입. 검증에 실패하면 재시도 없이 곧장 HTTP 400으로 전파합니다.
이중 안전장치
- query_cache: TTLCache(1000, 24h) + threading.Lock. 캐시 히트 시 LLM 호출 0회.
- rate_limit: 세션당 3회 제한(서버 + 클라이언트 이중 카운터).
- Anthropic API 키는 서버 .env에만 두고, Next.js 프록시는 관련 헤더를 포워딩하지 않습니다.
결과
요청당 LLM 호출 상한이 명확해졌습니다. 캐시 히트 시 0회, intent 매칭 시 1~2회, fallback 시 2~3회로 일정합니다. injection·허가되지 않은 테이블·파괴적 구문은 검증 단계에서 예외 없이 차단됩니다.