인서면 제작기
부산 서면 상권 분석 및 실시간 부동산 상담 관리 대시보드

카카오 맵 API 기반의 상권 매물 시각화 및 필터링 대시보드 화면
인서면 (In-Seomyeon) 제작기
부산 서면 상권의 매물 정보를 실시간 모니터링하고 PDF 문서를 AI로 자동 분석/정형화하여 지도와 연동하는 부동산 상담 관리 대시보드
1. 문제 정의 및 기획 배경
친한 친구가 부산 서면의 상가 전문 부동산 중개 사무소에 취직하여 업무를 시작하게 되었습니다. 그러나 실무 현장에서는 여러 팀원이 저마다의 스타일로 각기 다른 양식의 PDF 매물 명세서를 작성해 오고 있었고, 그렇게 규격화되지 않은 비정형 매물이 이미 수천 건 이상 누적되어 쌓여 있는 상태였습니다. 이로 인해 초기 업무 파악을 위해 수천 건에 달하는 기존 매물 자료를 일일이 대조하며 수동으로 엑셀화하는 데 막대한 시간과 노력이 요구되었습니다. 또한 고객이 원하는 조건의 상가를 실시간으로 매칭해 주기 위해서는 파편화된 PDF 문서들을 일일이 열어 확인해야 하는 비효율이 있었습니다. 친구가 겪고 있는 이러한 데이터 통합 및 조회상의 어려움을 해소하고자, 비정형 PDF에서 핵심 임대 정보를 AI로 자동 정형화하고 지도를 기반으로 신속히 검색할 수 있는 특화형 B2B 매물 관리 대시보드를 직접 개발하게 되었습니다.
2. 핵심 사양 및 구현 목표
- AI 기반 PDF 매물 마이그레이션: 임대차 조건, 권리금, 면적 등이 비정형 텍스트로 적힌 PDF 파일을 드래그 앤 드롭하면 AI가 이를 정형 데이터로 자동 변환 및 Firestore에 저장.
- 실시간 지도 상권 분석 및 줌 연동: 카카오/네이버 맵을 결합해 상권 내 매물 분포를 마커로 파악하고, 상세 필터(보증금, 권리금, 평수 등) 조작 시 지도 화면에 동적 반영.
- 클라우드 운영 비용 최소화: 소형 부동산 사무소 규모에서도 비용 걱정 없이 상용 수준으로 운영 가능하도록 Gemini API 및 인프라 최적화.
- 핵심 기술 스택:
- Frontend: React, Tailwind CSS, Kakao/Naver Maps API,
xlsx(Excel Export/Import) - Backend: Node.js, Express, Google Generative AI SDK (Gemini 2.5 Flash)
- Database & Auth: Google Firebase (Cloud Firestore, Firebase Authentication)
- Libraries:
pdf-parse(PDF 텍스트 추출),@napi-rs/canvas
- Frontend: React, Tailwind CSS, Kakao/Naver Maps API,
3. 기술 검증 및 트러블슈팅
[1단계] PDF 파싱 및 MD5 중복 차단 설계
실무자가 실수로 다량의 매물 폴더를 중복 업로드하더라도, 내용 변화가 없는 기존 파일은 필터링하도록 파일 해시(MD5) 비교 파이프라인을 구현했습니다.
[2단계] Gemini API JSON 구조화 및 프롬프트 경량화
비정형 추출 텍스트를 파라미터로 주입하고, responseMimeType: 'application/json' 방식을 사용하여 응답 텍스트에 부수적인 마크다운 백틱이나 서술형 꼬리말이 섞이지 않도록 엄격히 제어했습니다.
[3단계] 대규모 매물 매핑 및 클라이언트 이중 바인딩
지도 캔버스와 사이드바 카드 목록을 연동하고, 지도를 줌인/아웃하거나 특정 중심 반경으로 스크롤할 때 필요한 구역의 매물 데이터만 점진적으로 페이징 수신하도록 스케일링을 잡았습니다.
R&D 및 트러블슈팅
1. Firestore 복잡 조건 결합 시 다중 인덱스(Composite Index) 요구 오류
- 문제: 보증금 범위, 평수 범위, 권리금 유무 등 수십 개의 필터 조건과 최종 매물 등록일 순 정렬(
orderBy)을 결합하여 Firestore 쿼리를 실행하자 인덱스 누락 에러가 빈번했습니다. 모든 조합의 복합 인덱스를 수동 빌드하는 것은 데이터베이스 한계에 도달했습니다. - 해결: Firestore 쿼리 수준에서는 정렬과 범위 조건을 제거하여 데이터를 비교적 평탄하게 읽어온 뒤, 정렬 및 세부 다중 조건 필터링은 클라이언트 메모리(In-Memory) 상에서 처리하도록 로직을 이식하여 인덱스 복잡도 제한을 완벽히 우회했습니다.
2. pdf-parse 라이브러리 엔트리 로드 의존성 순서 충돌
- 문제: 서버 실행 시 PDF 파서 모듈이 정상 인스턴스화되기 이전에, 백엔드 기동 단계에서 특정 native 의존성이 누출되어 서버 자체가 부팅되지 않고 크래시를 일으켰습니다.
- 해결: Lazy 로딩 방식을 폐기하고
server.js최상단 엔트리 단계에서pdfPolyfills및 native canvas 래핑 모듈이pdf-parse모듈 호출보다 명시적으로 앞서서 import 및 바인딩되도록 의존성 로딩 시퀀스를 강제 정돈해 해결했습니다.
3. Cloud Run 32MB 페이로드 제한 및 대량 업로드 순차 배치 처리
- 문제: 사용자가 한 번에 100개 이상의 PDF 파일(수십 MB)을 업로드할 때, Google Cloud Run의 단일 HTTP 요청 크기 제한(32MB)을 초과하여
413 Payload Too Large에러가 발생하거나 업로드가 비정상 중단되는 이슈가 있었습니다. - 해결: 프론트엔드 API 계층(
analyzeMultiplePdfs)에서 업로드할 파일의 크기를 실시간으로 연산하여 최대 25MB 임계점 이하 단위로 분할(Batch Chunking)하고, 백엔드에 순차적으로 업로드를 요청하는 파이프라인을 구축하여 서버 과부하 및 페이로드 에러를 원천 차단했습니다.
4. 네트워크 불안정에 대비한 비동기 작업 트래킹 (Job Status Polling)
- 문제: 대용량 PDF 데이터에 대한 Gemini AI 호출 및 지오코딩 작업은 수 분 이상 소요될 수 있으며, 브라우저 탭 닫힘이나 일시적 네트워크 순단으로 진행 상태 확인이 불가능해지는 트랜잭션 고립 현상이 우려되었습니다.
- 해결: 업로드 시작 즉시
jobId를 발급하고 백엔드 처리 진행률을 FirestorepdfJobs컬렉션에 갱신하도록 설계했습니다. 프론트엔드는 2초 주기로 이를 추적(Polling)하며, 모달 최소화(Minimize) 및 복구 모드를 통해 브라우저가 새로고침되거나 지연되더라도 유실 없이 작업 상태를 유지하도록 설계했습니다.
4. 핵심 기능 및 구현 코드
-
Gemini API 데이터 추출 및 예외 복구(Resilience) 설계: Gemini 2.5 Flash API의
responseMimeType: 'application/json'응답 강제 규격을 적용하여 포맷의 일관성을 제어하며 호출당 비용을 **85% 절감(18원 ➔ 2~3원)**했습니다. 또한, AI 모델의 불안정한 응답(마크다운 백틱 포함, 콤마 누락 등)으로 인한 JSON 파싱 크래시를 방지하기 위해 정규식 기반 클렌징(Sanitization) 전처리와 Fallback 복구 메커니즘을 도입했습니다. -
하이브리드 필터 패널 조작: 보증금, 월세, 권리금, 평수 범위를 직관적으로 제어할 수 있는 하이브리드 필터 패널을 구현했습니다.
-
다차원 인메모리(In-Memory) 조건 검색: 평수, 보증금, 월세, 권리금, 층수 등 다양한 범위 조건과 화장실/엘리베이터 등 다중 논리 필터링이 결합될 때 발생하는 Firestore 복합 인덱스 누락 오류(Composite Index Limit)를 해결했습니다. 원본 매물 데이터를 1차로 로드한 뒤, 모든 상세 다중 검색과 복잡 정렬 연산을 클라이언트 브라우저의 인메모리 메모리 힙상에서 동적 가공 처리하도록 아키텍처를 설계하여 쿼리 한계를 완벽히 우회했습니다.
-
B2B 고객 관계 관리(CRM) 및 매칭 엔진: 상가 매칭 상담을 위해 손님의 세부 임대 요구사항(원하는 상권, 용도, 엘리베이터 여부, 화장실 형태, 예산/평수 범위 등)을 카드 형태의 태그 데이터로 구조화해 기록합니다. 상담 카드 내부의 관심 매물을 클릭하면 실시간으로 지도 핀 포인트가 포커싱되고 매물 상세 프로필 사이드바(
PropertySidebar)가 연동 호출되는 유기적인 매칭 인터랙션을 구현했습니다. -
비동기 배치 청킹 업로드 및 실시간 Job Polling: 사용자가 100개 이상의 PDF 파일을 동시에 업로드할 때 Google Cloud Run의 HTTP 페이로드 크기 제한(32MB)을 초과해
413 Payload Too Large오류가 발생하는 문제를 해결했습니다. 프론트엔드 API 계층에서 파일 단위를 최대 25MB의 청크로 쪼개어 순차 업로드(Batch Chunking)하는 파이프라인을 설계하고, 백엔드 진행 상태를 FirestorepdfJobs를 통해 실시간 상태 추적(Job Status Polling)하도록 구성하여 네트워크 순단에도 복구 가능한 트랜잭션을 확보했습니다. -
2단계 해시(Hash) 스킵 검증: 대량 업로드 시 1차 파일명 대조 및 2차 파일 MD5 해시 검사를 거쳐 내용 변화가 없는 기존 데이터(
skipped)는 Gemini API를 아예 호출하지 않고 스킵함으로써 중복 호출 비용을 방지합니다. -
소프트 딜리트(Soft Delete) 및 휴지통 복구: 중개인의 실수로 매물이 파기되는 사고를 원천 차단하기 위해 삭제된 데이터는 1차적으로 임시 유예 보존 테이블(
deletionQueue)로 격리 보존하고, 전용 롤백 인터페이스(RollbackModal)를 통해 클릭 한 번으로 완벽 복원할 수 있는 세션 관리 체계를 적용했습니다. -
역할 기반 데이터 액세스 격리 (Security Rules & Multi-Role): 상업용 부동산 매물의 극비 정보를 일반 관람객에게 격리하기 위해 로그인 세션 역할군(
demo,visitor,guest,admin)과 데이터 모델의dataType필드를 대조합니다. Firestore Security Rules 설정과 비즈니스 로직을 유기적으로 바인딩하여 백엔드/클라이언트 이중 데이터 격리 통제를 구현했습니다.
핵심 코드 (Gemini PDF Data Normalization & Schema Mapping)
// geminiService.js - Gemini 2.5 Flash 연동 및 데이터 정형화 const { GoogleGenerativeAI } = require('@google/generative-ai'); class GeminiService { constructor() { this.genAI = process.env.GEMINI_API_KEY ? new GoogleGenerativeAI(process.env.GEMINI_API_KEY) : null; } async formalizePropertyData(pdfText, 상권, fileName = null) { if (!this.genAI) throw new Error('Gemini API 키가 설정되지 않았습니다.'); // 최신 gemini-2.5-flash 모델 적용 const model = this.genAI.getGenerativeModel({ model: 'gemini-2.5-flash' }); // responseMimeType을 application/json으로 설정하여 순수 JSON 출력 강제 (비용 85% 절감의 핵심) const prompt = ` ## 임무: 부동산 매물 PDF 파싱 데이터 정형화 및 JSON 변환 아래 [RAW_PARSED_DATA] 텍스트에서 부동산 정보를 추출하여 JSON 스키마 규격에 맞게 반환하세요. - 모든 금액(보증금, 월세, 권리금, 관리비)은 만원 단위 숫자만 입력하세요. (예: "3000만원" -> 3000, "1억" -> 10000) - 면적(area)은 "X평 (Y㎡)" 형식으로 병기하세요. (1평 = 3.30578㎡) [RAW_PARSED_DATA] ${pdfText} `; try { const result = await model.generateContent({ contents: prompt, generationConfig: { responseMimeType: 'application/json', // JSON 강제 옵션 }, }); const responseText = result.response.text(); const propertyData = JSON.parse(responseText); // 데이터 가공 및 Firestore 규격에 맞춘 변환 모델 반환 return this.convertToPropertyModel(propertyData, 상권, fileName, pdfText); } catch (error) { console.error('Gemini API 호출 오류:', error); throw error; } } }
5. 미래 확장성 및 고도화 계획
- Multer Memory Storage를 Disk Storage/Stream 파싱 방식으로 대체 (대량 업로드 시 서버 RAM OOM 크래시 방지)
- TypeScript 점진적 마이그레이션을 통한 매물/상담 데이터 타입 안정성 강화
- 주소 검색 및 좌표 수동 매핑 보정 UI 도구 도입 (텍스트 오타 등으로 인한 지오코딩 실패 대응)