zino 블로그
  • 공공데이터 + LLM으로<br>아파트 입찰 낙찰 분석 시스템 만들기
    2026년 03월 09일 16시 33분 02초에 업로드 된 글입니다.
    작성자: zinok
    Side Project
    공공데이터 + LLM으로
    아파트 입찰 낙찰 분석 시스템 만들기

    K-APT 공공 API에서 아파트 입찰/낙찰 데이터를 수집하고, Claude AI로 자동 분석하는 풀스택 웹 애플리케이션 구축기

    2026.03· React + Express + Docker + HAProxy
    React 19 Vite Express Node.js Claude API Docker HAProxy Let's Encrypt 공공데이터
    01

    프로젝트 배경 — 왜 만들었는가

    아파트 관리를 하다 보면 주기적으로 각종 공사 입찰이 발생합니다. 바닥공사, 방수, 도장, 창호 교체 등 — 적정한 낙찰 금액은 얼마인지, 어떤 업체가 실적이 좋은지 판단하기가 쉽지 않습니다.

    다행히 K-APT(공동주택관리정보시스템)에서 공공데이터 API를 통해 전국 아파트의 입찰/낙찰 정보를 제공하고 있었습니다. 하지만 데이터가 방대하고 개별 건을 하나하나 비교하기엔 한계가 있었습니다.

    핵심 아이디어 공공데이터로 대량의 입찰/낙찰 데이터를 자동 수집하고, LLM(Claude AI)에게 분석을 맡겨 낙찰업체 순위, 세대수별 금액 분포, 입찰 조건 패턴 등의 인사이트를 자동으로 도출하자.
    02

    전체 아키텍처

    시스템은 크게 프론트엔드(React), 백엔드(Express), 리버스 프록시(HAProxy), 인증서 자동화(ACME) 네 개의 레이어로 구성됩니다.

    System Architecture
    Browser (Client)
    HAProxy :80/:443
    SSL Termination
    + Map-based Routing
    Express :3000
    Static Files (React Build)
    /api/claude → Anthropic API
    ACME Container
    Let's Encrypt 자동 갱신

    프로젝트 구조

    tree kapt/ ├── 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용도주요 데이터
    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 19Vite 6를 사용합니다. 별도의 UI 라이브러리 없이 인라인 스타일로 다크 테마 UI를 구성했습니다.

    UI 구성 요소

    • API 키 입력 — 공공데이터포털 인증키를 사용자가 직접 입력 (서버에 저장하지 않음)
    • 수집 조건 설정 — 키워드 태그 선택, 연도 복수 선택, 조회 방식/건수/최소 세대수 필터
    • 실시간 수집 로그 — 단계별 진행 상황을 모노스페이스 로그로 표시
    • 결과 요약 카드 — 총 건수, 평균 금액, 낙찰업체 수를 대시보드 형태로 표시
    • LLM 분석 프롬프트 — 사용자가 분석 방향을 직접 편집 가능

    디자인 시스템

    컬러 팔레트를 객체로 정의하고, 스타일 팩토리 함수로 일관된 UI를 유지했습니다. 버튼은 linear-gradient로 시안-블루 그라데이션을 적용하고, 데이터 테이블은 세대수에 따라 초록/노란색으로 시각적 구분을 줬습니다.

    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로 최소한의 역할만 수행합니다. 핵심은 Claude API 프록시 — 클라이언트가 API 키를 노출하지 않도록 서버에서 대신 Anthropic 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()); });

    개발/프로덕션 모드 분리

    javascript if (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 키는 반드시 서버에서만 사용하도록 프록시를 통해 보호합니다.
    06

    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 멀티스테이지 빌드

    dockerfile FROM 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.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.me

    ACME — Let's Encrypt 와일드카드 자동화

    별도의 ACME 컨테이너에서 acme.sh를 사용해 NCloud Global DNS API 기반 DNS-01 챌린지로 와일드카드 인증서(*.zinok.me)를 자동 발급합니다. 매일 크론으로 갱신을 체크하고, HAProxy 런타임에 즉시 반영합니다.

    Docker Compose Services
    haproxy:3.1-alpine
    HAProxy
    :80 / :443
    node:22-alpine
    Express App
    :3000
    acme.sh + python3
    ACME
    cron 02:00
    Shared Volumes: certs, haproxy-sock, acme-data
    08

    배운 점과 회고

    잘 된 점

    • 공공데이터 + LLM 조합의 가능성 — 정형 데이터를 구조화해서 LLM에 넘기면 사람이 직접 분석하기 어려운 패턴과 인사이트를 빠르게 도출할 수 있었습니다.
    • Vite 미들웨어 모드 — Express에 Vite를 미들웨어로 통합하면 API 프록시와 HMR을 동시에 사용할 수 있어 개발 경험이 좋았습니다.
    • HAProxy map 런타임 관리 — 재시작 없이 라우팅을 변경할 수 있어 다른 프로젝트를 같은 서버에 추가하기가 매우 편리합니다.
    • ACME 자동화 — DNS-01 챌린지로 와일드카드 인증서를 자동 발급/갱신하니 서브도메인 추가 시 인증서 걱정이 없습니다.

    개선할 점

    • 세대수 조회 병목 — 현재 순차적으로 API를 호출하므로 건수가 많으면 느립니다. 배치 처리나 병렬화가 필요합니다.
    • 데이터 캐싱 — 같은 조건의 반복 조회 시 API를 다시 호출합니다. IndexedDB 등을 활용한 클라이언트 캐시를 고려할 수 있습니다.
    • LLM 토큰 관리 — 데이터가 많으면 토큰 한도를 초과할 수 있습니다. 요약 단계를 추가하거나 청킹 전략이 필요합니다.

    기술 스택 요약

    레이어기술선택 이유
    프론트엔드React 19 + Vite 6빠른 개발 + HMR, 최신 React 기능
    백엔드Express 5최소한의 API 프록시만 필요
    AIClaude API (Anthropic)한국어 분석 품질, 구조화된 데이터 해석
    리버스 프록시HAProxy 3.1SSL 종료 + map 기반 동적 라우팅
    인증서acme.sh + NCloud DNS와일드카드 인증서 자동 발급/갱신
    컨테이너Docker Compose멀티서비스 오케스트레이션, 볼륨 공유
    결론 공공데이터 API와 LLM을 결합하면 특정 도메인에 대한 데이터 분석 도구를 빠르게 만들 수 있습니다. 핵심은 LLM에 넘기기 전 데이터를 잘 구조화하는 것입니다. 통계·집계·샘플 데이터로 요약해서 전달하면 훨씬 정확하고 실용적인 분석 결과를 얻을 수 있습니다.
    📌 이 포스트 추천 태그 (에디터 태그 입력란에 복사)
    공공데이터KAPTLLM분석 React19ViteExpress ClaudeAPIDockerHAProxy 아파트입찰낙찰분석풀스택
    댓글