- 공공데이터 + LLM으로<br>아파트 입찰 낙찰 분석 시스템 만들기2026년 03월 09일 16시 33분 02초에 업로드 된 글입니다.작성자: zinokSide Project공공데이터 + LLM으로
아파트 입찰 낙찰 분석 시스템 만들기K-APT 공공 API에서 아파트 입찰/낙찰 데이터를 수집하고, Claude AI로 자동 분석하는 풀스택 웹 애플리케이션 구축기
2026.03·React + Express + Docker + HAProxyReact 19 Vite Express Node.js Claude API Docker HAProxy Let's Encrypt 공공데이터01프로젝트 배경 — 왜 만들었는가
입주자대표로 활동하다 보면 주기적으로 크고 작은 공사 입찰이 생깁니다. 바닥 에폭시, 방수, 외벽 도장, 창호 교체… 처음 몇 번은 비교 견적을 꼼꼼히 받아봤는데, 솔직히 어느 금액이 적정한지 기준이 없으니 막막했습니다. 업체가 제시하는 숫자를 곧이곧대로 믿을 수도 없고, 그렇다고 매번 전문가를 부를 수도 없는 노릇이고요.
그러다 문득 생각했습니다. K-APT(공동주택관리정보시스템)에 전국 아파트 입찰/낙찰 데이터가 공공 API로 열려 있다는 걸. 우리 단지만 모르고 있을 뿐, 비슷한 규모의 아파트들이 같은 공사를 얼마에 낙찰받았는지 이미 다 공개된 정보였던 겁니다.
문제는 데이터가 방대하다는 것. 건별로 들여다보는 건 사람이 하기엔 너무 지루하고 시간이 많이 걸립니다. 그래서 LLM에 맡겨보자는 생각으로 이 프로젝트를 시작했습니다.
핵심 아이디어 공공데이터로 대량의 입찰/낙찰 데이터를 자동 수집하고, LLM(Claude AI)에게 분석을 맡겨 낙찰업체 순위, 세대수별 금액 분포, 입찰 조건 패턴 등의 인사이트를 도출한다.02전체 아키텍처
구조 자체는 복잡하지 않습니다. 프론트엔드(React)에서 조건을 설정하고, 백엔드(Express)가 Claude API 프록시 역할을 하며, 앞단에 HAProxy가 SSL을 처리하는 단순한 3-tier 구조입니다.
System ArchitectureBrowser (Client)↓HAProxy :80/:443SSL Termination
+ Map-based Routing↓Express :3000↙↘Static Files (React Build)/api/claude → Anthropic APIACME Container
Let's Encrypt 자동 갱신프로젝트 구조
treekapt/ ├── src/ │ ├── App.jsx # 메인 UI + 데이터 수집 로직 │ └── main.jsx # React 엔트리포인트 ├── server.js # Express (Claude API 프록시 + 정적 파일) ├── Dockerfile # 멀티스테이지 빌드 ├── docker-compose.yml # HAProxy + Node + ACME 오케스트레이션 ├── haproxy.cfg # SSL termination, map 기반 라우팅 ├── acme/ │ ├── dns_ncloud.sh # NCloud DNS API 훅 │ └── issue.sh # 와일드카드 인증서 발급 └── .env # 환경변수 (API 키)03공공데이터 API 연동
이 프로젝트에서는 공공데이터포털에서 제공하는 두 가지 API를 활용합니다. 처음엔 API 명세가 워낙 길어서 읽기 지쳤는데, 실제로 쓰는 필드는 몇 개 안 됩니다.
API 용도 주요 데이터 ApHusBidResultNoticeInfoOfferServiceV2 입찰 공고 / 낙찰 상태 / 업체 조회 공고명, 낙찰금액, 낙찰방식, 입찰조건, 참여업체 AptBasisInfoServiceV4 아파트 기본정보 조회 단지명, 세대수, 단지코드 데이터 수집 흐름
1연도별 병렬 조회선택한 연도들에 대해 Promise.all로 병렬 API 호출. 낙찰상태 직접조회 또는 공고명 키워드 검색 두 가지 모드를 지원합니다.2키워드 AND 필터링"바닥공사 에폭시"처럼 공백 구분 키워드를 AND 조건으로 필터링합니다. API는 단일 키워드만 지원하므로 클라이언트에서 후처리합니다.3세대수 조회 & 필터링각 단지의 aptCode로 V4 API를 호출해 세대수를 조회합니다. useRef 캐시로 중복 호출을 방지했습니다.4낙찰업체 정보 조회필터링된 각 건에 대해 입찰 참여/낙찰 업체 정보를 추가 조회합니다. bidSuccessfulYn === "Y"로 낙찰 업체를 식별합니다.jsx// 연도별 병렬 조회 핵심 코드 const fetches = years.map(async year => { const url = `${BASE}/getBidSttusSearchV2?serviceKey=${apiKey} &bidState=5&searchYear=${year}&numOfRows=${numRows}&type=json`; const r = await fetch(url); const d = await r.json(); return d.response.body.items || []; }); let allItems = (await Promise.all(fetches)).flat();주의할 점 공공데이터 API에는 일일 트래픽 제한이 있습니다. 세대수 조회 시 캐시를 적극 활용하고 불필요한 중복 호출을 막아야 합니다. 실제로 테스트하다 하루 한도를 다 써버린 경험이 있어서, useRef 기반 캐시는 꽤 중요한 부분입니다.04프론트엔드 — React 19 + Vite
프론트엔드는 React 19와 Vite 6를 씁니다. UI 라이브러리는 따로 쓰지 않고 인라인 스타일로 다크 테마를 직접 구성했습니다. 어차피 이 도구는 저 혼자 쓸 용도라, 디자인에 너무 공들이기보다 기능 중심으로 빠르게 만드는 게 목적이었습니다.
UI 구성 요소
- API 키 입력 — 공공데이터포털 인증키를 사용자가 직접 입력 (서버에 저장하지 않음)
- 수집 조건 설정 — 키워드 태그 선택, 연도 복수 선택, 조회 방식/건수/최소 세대수 필터
- 실시간 수집 로그 — 단계별 진행 상황을 모노스페이스 로그로 표시
- 결과 요약 카드 — 총 건수, 평균 금액, 낙찰업체 수를 대시보드 형태로 표시
- LLM 분석 프롬프트 — 사용자가 분석 방향을 직접 편집 가능
디자인 시스템
컬러 팔레트를 객체로 정의하고, 스타일 팩토리 함수로 일관된 UI를 유지했습니다. 단일 컴포넌트라 CSS 파일을 따로 두기 애매해서 이 방식을 택했는데, 나중에 보면 읽기 편합니다.
jsx// 컬러 팔레트 + 버튼 팩토리 const C = { bg: "#0d1117", ac: "#00d2ff", ac2: "#3a7bd5", ok: "#3fb950", mu: "#7d8590", }; const btn = (color) => ({ background: color === "ac" ? `linear-gradient(90deg, ${C.ac2}, ${C.ac})` : `linear-gradient(90deg, #2d6a4f, ${C.ok})`, color: "#000", fontWeight: 700, });05백엔드 — Express API 프록시
백엔드는 Express 5로 최소한만 처리합니다. 사실 백엔드가 하는 일은 딱 두 가지입니다. 프로덕션에서 React 빌드 파일을 서빙하는 것, 그리고 Claude API 키를 클라이언트에 노출하지 않도록 프록시하는 것. API 키 관리 때문에 서버를 따로 띄우는 셈입니다.
javascript// server.js — Claude API 프록시 app.post("/api/claude", async (req, res) => { const apiKey = process.env.CLAUDE_API_KEY; if (!apiKey) return res.status(500).json({ error: "Not configured" }); const resp = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01", }, body: JSON.stringify(req.body), }); res.status(resp.status).json(await resp.json()); });개발/프로덕션 모드 분리
javascriptif (isProd) { app.use(express.static(join(__dirname, "dist"))); app.get("{*path}", (req, res) => res.sendFile(join(__dirname, "dist/index.html"))); } else { const vite = await (await import("vite")).createServer({ server: { middlewareMode: true } }); app.use(vite.middlewares); }설계 포인트 공공데이터 API 키는 프론트엔드에서 직접 입력받고, Claude API 키는 서버 환경변수로 관리합니다. Claude API 키를 프론트에 두면 브라우저 개발자도구에서 그대로 노출되니, 이 구분은 반드시 지켜야 합니다.06LLM 분석 파이프라인
처음엔 원본 데이터를 그대로 던져봤는데 결과가 별로였습니다. 수백 건의 raw JSON을 넘기면 LLM도 중요한 걸 놓치고 엉뚱한 얘기를 합니다. 구조화된 요약 데이터를 먼저 만들어 전달하는 방식으로 바꾸고 나서야 쓸만한 분석이 나오기 시작했습니다.
데이터 전처리
- 낙찰업체 순위 TOP 15 — 낙찰 횟수 기준 업체 랭킹
- 세대수별 낙찰금액 통계 — ~500, 500~1000, 1000~1500, 1500~2000, 2000+ 구간별 평균/최소/최대
- 입찰 조건 통계 — 보증보험, 신용평가, 실적증명, 제한경쟁, 적격심사 비율
- 샘플 데이터 상위 20건 — 단지명, 세대수, 공고명, 금액, 업체, 조건
json{ "검색조건": { "키워드": "바닥공사", "연도": [2026, 2025], "총건수": 42 }, "낙찰업체순위TOP15": [{ "업체명": "○○건설", "낙찰건수": 5 }, ...], "세대수별낙찰금액통계": [ { "range": "1000~1500", "avg": 8500, "min": 3200, "max": 15000 } ], "입찰조건통계": [...], "샘플데이터상위20건": [...] }프롬프트 설계
기본 분석 프롬프트 1. 낙찰업체 순위 및 특징
2. 세대수 규모별 평균 낙찰금액 분포
3. 아파트들이 공통으로 내건 입찰 조건 (보증보험, 신용평가, 실적증명 등)
4. 주목할 만한 패턴이나 이상치
5. 1500세대 기준 예상 공사금액 범위07인프라 — Docker + HAProxy + ACME
Docker 멀티스테이지 빌드
dockerfileFROM node:22-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build FROM node:22-alpine WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY server.js . COPY --from=build /app/dist ./dist ENV NODE_ENV=production PORT=3000 CMD ["node", "server.js"]HAProxy — SSL 종료 + Map 기반 라우팅
host2backend.map 파일을 통해 호스트명 기반으로 백엔드를 동적 라우팅합니다. 서버를 재시작하지 않고 런타임에 매핑을 추가/삭제할 수 있어서, 같은 서버에 새 프로젝트를 올릴 때마다 HAProxy 설정을 건드릴 필요가 없습니다. 이게 생각보다 편합니다.
haproxy# haproxy.cfg 핵심 설정 frontend https bind *:443 ssl crt /etc/haproxy/certs/zinok.me-combined.pem alpn h2,http/1.1 use_backend %[req.hdr(host),lower,map(/usr/local/etc/haproxy/host2backend.map)] # 런타임 매핑 관리 $ docker compose exec haproxy haproxy-map.sh list $ docker compose exec haproxy haproxy-map.sh set app.zinok.me node $ docker compose exec haproxy haproxy-map.sh del app.zinok.meACME — Let's Encrypt 와일드카드 자동화
별도의 ACME 컨테이너에서 acme.sh를 사용해 NCloud Global DNS API 기반 DNS-01 챌린지로 와일드카드 인증서(*.zinok.me)를 자동 발급합니다. 매일 크론으로 갱신을 체크하고, HAProxy 런타임에 즉시 반영합니다. 서브도메인을 새로 열어도 인증서 걱정이 없어지니 꽤 마음 편합니다.
Docker Compose Serviceshaproxy:3.1-alpineHAProxy:80 / :443node:22-alpineExpress App:3000acme.sh + python3ACMEcron 02:00Shared Volumes: certs, haproxy-sock, acme-data08배운 점과 회고
잘 된 점
- 공공데이터 + LLM 조합의 가능성 — 정형 데이터를 구조화해서 LLM에 넘기면, 사람이 직접 분석하기 어려운 패턴과 인사이트가 빠르게 나옵니다. 처음 분석 결과를 봤을 때 "이거 진짜 쓸 수 있겠다"는 생각이 들었습니다.
- Vite 미들웨어 모드 — Express에 Vite를 미들웨어로 통합하면 API 프록시와 HMR을 동시에 쓸 수 있습니다. 개발 서버 따로, API 서버 따로 띄우는 번거로움이 없어집니다.
- HAProxy map 런타임 관리 — 재시작 없이 라우팅을 변경할 수 있어, 같은 서버에 프로젝트를 추가할 때마다 설정 파일을 건드리지 않아도 됩니다.
- ACME 자동화 — DNS-01 챌린지로 와일드카드 인증서를 자동 발급/갱신하니 서브도메인 추가 시 인증서 걱정이 없습니다.
개선할 점
- 세대수 조회 병목 — 현재 순차 호출이라 건수가 많으면 느립니다. 배치 처리나 병렬화가 필요합니다. 100건 넘어가면 체감이 납니다.
- 데이터 캐싱 — 같은 조건으로 다시 조회해도 API를 재호출합니다. IndexedDB 등을 활용한 클라이언트 캐시를 고려할 수 있습니다.
- LLM 토큰 관리 — 데이터가 많으면 토큰 한도를 초과할 수 있습니다. 요약 단계를 추가하거나 청킹 전략이 필요합니다.
기술 스택 요약
레이어 기술 선택 이유 프론트엔드 React 19 + Vite 6 빠른 개발 + HMR, 최신 React 기능 백엔드 Express 5 최소한의 API 프록시만 필요 AI Claude API (Anthropic) 한국어 분석 품질, 구조화된 데이터 해석 리버스 프록시 HAProxy 3.1 SSL 종료 + map 기반 동적 라우팅 인증서 acme.sh + NCloud DNS 와일드카드 인증서 자동 발급/갱신 컨테이너 Docker Compose 멀티서비스 오케스트레이션, 볼륨 공유 결론 공공데이터 API와 LLM을 붙이면 특정 도메인 분석 도구를 생각보다 빠르게 만들 수 있습니다. 핵심은 LLM에 넘기기 전 데이터를 잘 구조화하는 것입니다. raw 데이터를 그대로 던지지 말고, 통계·집계·샘플 데이터로 요약해서 전달하면 분석 품질이 확실히 달라집니다.'IT' 카테고리의 다른 글
Edge service 란 ? (1) 2026.03.10 Real User Monitoring ( RUM ) 개발 (0) 2026.03.10 영상은 어떻게 인터넷을 타고 흐르는가 — HTTP 스트리밍 기술 해부 (0) 2026.03.09 다음글이 없습니다.이전글이 없습니다.댓글