PROJECT DOCUMENTATION

4. 상세 설계서

사전상담·후속 과제 구현 기록기준일 2026-09-19담당 김충식 · 류준(기능별 구현)

서비스 저장소의 실제 코드와 실행 결과를 근거로 동작·인터페이스·예외 처리를 기록합니다.

기록 기준
9월 19일 밤 작업 로그까지 첫 배포·운영 페르소나 검증, 지도·사전상담 UI, 데이터 배선·11업종 지표를 반영했습니다. 최신 프론트 변경 재배포는 남아 있습니다.

개발 개요·목적·기능

서비스명은 localhostdaegu, 주 동선은 계약 전 창업자금 사전상담입니다. 채팅 첫 화면(/) → 지도에서 자리 선택(/map) → 계산·최초안/현재안 비교와 선택(/simulate) → 상담자료(/analysis, purpose=handoff)로 이어집니다. 지역·업종·연도 등은 URL로 전달하고 상담 초안은 sessionStorage에 보관합니다. 지역 분석(review)은 보조 경로입니다.

구분 구현한 기능 이번 범위에서 제외
REQ-002 자금 구성 분리, 계산안 비교·선택, 상담 후보 매칭, 세션·계획·노트·문서 기록, 최신 금리 기본값 매출 추정, 개인 신용 평가, 은행 API 전송
REQ-003 한 문장 의도 해석, 행정동 지표·위험도, 목적별 리포트, 상담자료 Markdown·브라우저 인쇄/PDF 저장 카드 소비·생활인구 기반 진단, PDF 전용 생성 라이브러리, 블록체인 앵커링

실제 Data Flow

  1. 의도 해석 — POST /intent {text} → intent_type, 구·동 코드와 이름, 업종 id, 예산(원), 빠진 항목. 사전은 8개 구·군, 144개 행정동, 랜드마크 15곳, 업종 동의어입니다. LLM을 쓰지 않는 규칙 기반입니다.
  2. 지도·진단 — GET /regions/geojson(경계), GET /metrics?industry&metric&year(동별 지표), GET /regions/{code}/summary(요약 카드), GET /metrics/risk(위험도), GET /stores(점포 마커).
  3. 자금 계산 — 프론트가 GET /shocks/rates/latest?rate_type=loan_sme로 대출금리 기본값을 채우고, POST /finance/simulate로 계산합니다.
  4. 계산안·상담 후보 — 첫 성공 계산을 최초안으로 고정하고 재계산으로 현재안을 갱신합니다. 선택안의 자기자본 외 조달 필요액과 자격·지역 조건으로 GET /matching/consultation을 조회합니다. include_unverified=true이면 차선 후보도 받습니다.
  5. 상담자료·지역 분석 — POST /analysis {region, industry, year?, question?, finance?, purpose?, consultation?} → {analysis_id} → GET /analysis/{id}/events(SSE, 한 번만 소비). handoff에는 finance·consultation이 필수입니다.
  6. 서버 기록·내보내기 — 상담자료 생성 시 상담 세션·계획·노트를 기록하고 재생성 시 같은 세션을 갱신합니다. 완성된 자료를 내려받으면 선택안과 sha256 해시를 서버에 기록합니다. 공식 링크 이동은 은행으로 자료를 전송하지 않습니다.
  7. 수집 — crontab이 scripts/*.sh를 실행: 뉴스 매시 10분, 인허가 04:20(수집 → 행정동 배정 → 지표 빌드), 공고 05:10, 검색 색인 05:30, 금리·임대료 월요일 05:20.

요구사항별 상세 설계 기록

REQ-001 · 세무·매출 AI 도우미

이번 범위에서 제외했습니다. 관련 코드·API·테스트가 없습니다(요구사항).

REQ-002 · 자금 계산과 금융상품 매칭

기록 항목 내용
구현 범위 재무 엔진(결정론), 계산안 보관·비교·선택, 상담 후보, 최신 금리 조회. 결과 주 지표는 손익분기 매출·총 준비자금·조달 필요액입니다.
근거 백엔드 apps/finance, apps/matching, apps/product, apps/consultation, 프론트 features/simulator
입력 금액 10개(원, 0 이상), 원가율·수수료율·대출금리(0 이상 1 미만), 원가율 + 수수료율 < 1
처리 초기 투자비 = 보증금 + 권리금 + 인테리어 + 설비 / 월 고정비 = 월세 + 대출이자(희망 대출 × 금리 ÷ 12) + 보험 + 인건비 / 손익분기 매출 = 월 고정비 ÷ (1 − 원가율 − 수수료율). 자금 구성은 아래 표처럼 분리하며 기존 산식을 노출한 작업입니다.
시나리오 예상 월매출 × 0.6(비관)·1.0(기준)·1.6(낙관)마다 영업이익, 영업이익이 양수면 투자 회수 개월, 음수면 버틸 수 있는 개월. 금리 +1%p·+2%p일 때 고정비와 기준 영업이익
출력 capex, monthly_fixed, bep_revenue, operating_reserve, total_required_funds, external_funding_need, funding_gap, scenarios[], stress[]
상담 매칭 규칙 조달 필요액(external_funding_need) 기준. 한도가 작아도 후보를 유지하고 부족분을 설명합니다. 은행 연결 등급 direct → linked → unverified를 우선하고 같은 등급 안에서 보증 → 은행 → 정책자금 순입니다. none은 차선 후보에도 넣지 않습니다. 자격 조건은 양쪽 그룹에 동일 적용하며 모르는 나이·자치구는 확인 사항으로 남깁니다.
지역 매칭 finance_product.district_code는 자치구 5자리 nullable FK. 행정동 코드 앞 5자리로 비교합니다. 달성군 dgsinbo-5·북구 youth-1은 해당 지역의 linked 후보입니다. 구·군 한정 상품은 기존 GET /matching에서는 계속 제외합니다.
저장 구조 조사 JSON 12건을 product BC의 DB 테이블로 시드합니다. 레거시 GET /matching의 15필드 계약과 JSON 폴백을 유지하고, 상담 경로는 별도 로더로 메타데이터·절차를 읽습니다. 업종 null(무관)·[](해당 업종 없음)·목록(제한)은 DB 왕복에서도 구분합니다.
실패 처리 스키마 위반·원가율 + 수수료율 ≥ 1 → 422. 금리 조회 중이거나 실패하면 4.5% 사용, 사용자가 입력한 금리는 덮어쓰지 않음
알려진 제한 예상 매출·시나리오 배수·6개월 운영준비금은 가정입니다. 금리 미공개는 “은행별 상이”, 한도 미공개는 “한도 미정”으로 표시합니다. 재무 입력이 없으면 계산과 상품 매칭을 건너뜁니다. 미확인 금액은 빈 칸으로 보이고 PlanSnapshot.unconfirmed에 기록합니다. 비율은 업종 벤치마크·ECOS 출처가 있어 미입력 판정에서 제외합니다.
검증 연결 TC-002, TC-005
자금 항목 필드 산식·의미
운영준비금 operating_reserve 월 고정비 × 6
총 준비자금 total_required_funds 초기 투자비 + 운영준비금
자기자본 외 조달 필요액 external_funding_need max(0, 총 준비자금 − 자기자본)
희망대출 반영 후 남는 부족액 funding_gap max(0, 총 준비자금 − 자기자본 − 희망대출)

자기자본 4,000만 원·희망대출 2,500만 원인 검산 사례에서는 조달 필요액 2,260만 원, 남는 부족액 0원입니다. 부족액이 0원이어도 자기자본만으로 충분하다는 뜻은 아닙니다.

계산안과 서버 기록. 화면 초안의 정본은 sessionStorage의 localhostdaegu.consultation.v1입니다. 첫 성공 계산을 최초안으로 고정하고 이후 현재안을 갱신합니다. 지역·업종 변경 시 이전 결과와 선택을 무효화하고, 미제출 수정 중에는 이전 결과임을 알리며 선택을 잠급니다. 비교표에는 바뀐 입력만 표시합니다.

상담자료 생성 시 서버에 감사·재현용 스냅샷을 기록하지만 리포트는 그 저장값을 읽지 않고 입력 13필드로 다시 계산합니다. 초안의 session_id로 세션을 재사용하며 PUT /consultation/{id}는 전체 초안 교체입니다. assumptions·open_questions는 toConsultationContext와 같은 규칙으로 만들고, replace_notes로 노트를 교체해 재전송에도 쌓이지 않게 합니다. 계획안은 같은 plan_kind로 재전송해도 행이 늘지 않습니다.

완성된 handoff 자료만 Markdown으로 내려받거나 브라우저 인쇄로 PDF 저장합니다. 문서 기록에서는 서버가 (session_id, plan_kind)로 계획안을 찾아 sha256 해시를 남깁니다. 세션 저장 실패는 상담자료 생성을 막지 않고, 문서 기록 실패도 이미 끝난 다운로드를 되돌리지 않습니다. sha256은 변경 확인용이며 블록체인 앵커링이 아닙니다.

REQ-003 · 상권 진단과 AI 리포트

기록 항목 내용
구현 범위 의도 해석, 행정동 × 업종 × 연도 지표, 위험도(단건·업종별 순위·지역별 순위), 섹션별 AI 리포트
근거 백엔드 apps/intent, apps/metric, apps/master, apps/analysis, apps/rag / 프론트 features/intent-gate, features/map-explorer, features/agent-report
의도 해석 긴 지명을 먼저 비교하고, 금액은 단위가 붙은 첫 표현을 찾아 “1억 5천만원”처럼 이어진 단위를 합산합니다. 랜드마크는 실제 행정동에 연결합니다(동대구역은 좌표·지번 기준 신암4동).
지표 인허가의 개업·폐업 일자로 연도별 점포 수를 재구성합니다. 폐업률 = 해당 연도 폐업 수 ÷ 전년 말 점포 수, 점포 증감률 = (개업 수 − 폐업 수) ÷ 전년 말 점포 수. 행정동은 좌표 공간조인으로 배정합니다.
기준 연도 선택 연도를 AnalysisRequest.year → MarketDataPort.fetch → 요약·지표·위험도까지 전달합니다. 기본값 None은 마지막 완결 연도(현재 2025)이며 2026은 “(집계 중)” 표시입니다. 캐시 프록시의 year 누락도 모델 평가 중 발견해 수정했습니다.
위험도 폐업률·점포 수·성장률 각각의 지역 간 백분위를 0.4 · 0.4 · 0.2로 섞은 0~100점. 70점 이상 red, 40점 이상 yellow, 나머지 green. 점포가 순증해도 점수가 오르며, 창업 실패 확률이 아닙니다.
리포트 섹션 review: 종합 진단·상권·충격·정책자금, 재무 입력 시 계산표. handoff: plan·comparison·calculator·funding·questions·market이며 verdict·shock은 없습니다. _SECTION_PLANS 팩토리가 목적에 맞는 구성을 주입합니다. 비교표는 서버 엔진으로 다시 계산하고 LLM을 쓰지 않으며, 확인 질문은 코드가 미확인 항목을 정한 뒤 LLM이 문장만 씁니다.
이벤트 계약 agent_status(orchestrator·market·shock·funding × running/done/error), tool_call, report_delta(section, markdown), report_done(report_id, citations)
검색 뉴스·공고를 pgvector에서 검색합니다. 색인과 같은 임베더(gemini-embedding-001)로 만든 벡터만 조회합니다. 인용은 번호 대신 문서 제목을 「」로 붙이고, 매칭 상품 목록에는 붙이지 않습니다.
저장 구조 분석 요청은 메모리 저장소(워커 1개 전제). 이벤트를 소비하면 요청이 사라집니다.
실패 처리 에이전트 실패 시 error 상태를 알리고 계속, 섹션 생성 실패 시 대체 문구. 없거나 이미 소비한 ID → 404 ANALYSIS_NOT_FOUND. region·industry 32자, question 500자 초과 → 422. handoff에 재무·상담 입력이 빠져도 422
알려진 제한 “충격 분석”은 관련 뉴스 검색이며 금리·매출 영향 계산이 아닙니다. 9/19 다른 구·군 전용 문서 제외, 개업 30일 이내 종료 제외, 지표 분모 설명·상대 순위·AI 진행 문구를 반영했습니다. 기간 필터는 없으며 학원·부동산·어린이집은 폐업률(추정), 편의점은 담배소매인 기준임을 표시합니다.
검증 연결 TC-001, TC-003

API와 데이터 모델

구현된 API

경로 용도
POST /intent 한 문장 → 지역·업종·예산
GET /regions/geojson · GET /regions/{code}/summary 행정동 경계 · 요약 카드
GET /metrics · GET /metrics/risk 동별 지표(closure_rate·growth_rate·store_count) · 위험도
GET /stores 영업 중 점포 마커
POST /finance/simulate 재무 시뮬레이션
GET /matching 금융상품 매칭
GET /matching/consultation iM뱅크 상담 후보·선택적 차선 후보, 자치구 조건 적용
POST /consultation · GET /consultation/{id} 상담 세션 생성·조회
PUT /consultation/{id} 선택안·변경 이유 등 전체 초안 교체
PUT /consultation/{id}/plans/{kind} 최초안·현재안 기록, 같은 종류 재전송 시 갱신
POST /consultation/{id}/documents 선택 계획안과 상담자료 해시 기록
GET /shocks · GET /shocks/rates/latest 충격 이벤트 · 최신 금리
GET /funding 접수 중 지원사업
POST /analysis · GET /analysis/{id}/events AI 리포트 시작 · SSE 스트림
GET /health 서버 상태

지표·위험도·AI 리포트의 404·400 오류는 {error: {code, message}} 형식을 씁니다. 입력 검증 실패(422)는 FastAPI 기본 형식입니다. /metrics/risk는 응답 형태가 세 가지라 OpenAPI 스키마가 없습니다.

주요 테이블

테이블 내용 9/19 밤 작업 로그 기준(행별 시점 구분)
district · region · industry 구·군 8, 행정동 144(경계 142), 업종 11 마스터 시드
store 인허가 이력(과거 폐업 포함) 173,816건(인허가 161,185건 포함). 인허가 좌표 158,870건·행정동 158,859건
region_industry_metric 행정동 × 업종 × 연도 지표 12,360건·등록 11업종(2019~2026)
population_stat 주민등록 인구 8개 시점 48,174건. 지도에 주민등록 인구·연령대 변화 연결
rent_price · interest_rate 임대료·공실률 · 금리 786건 · 365건
funding_program · news_article · rag_chunk 지원사업 · 뉴스 · 검색 색인 1,735 · 1,926 · 3,643건(9/19 22:30 스냅샷, 정기 수집으로 변동)
shock_event 대구 코로나 타임라인·거리두기 구간 29건(9/18)
finance_product · finance_product_category · product_consultation_metadata · product_procedure_step 상품·업종 조건·상담 메타데이터·절차 상품 12건, 9/18 원문 대조 12/12. 은행 연결 등급 9/18 direct 2·linked 3·unverified 7 → 9/19 direct 2·linked 7·unverified 3(재단 보증 4건 승격, 제도 구조에 대한 팀 확인 근거)
external_dataset · regional_indicator 외부 데이터셋·지역 지표 공공데이터 5종 적재·지도 연결. 센터 민간 데이터는 미확보
consultation_* 4테이블 세션·계획·가정/확인 질문·문서 해시 서버 쓰기 경로 연결. 화면 초안은 sessionStorage가 정본

9/19 ERD에서 비어 있는 근거를 정리한 테이블은 shock_event_region·finance_product_category·consultation_document입니다. 담배소매업 33,804건·학원 수강료 21,646건·편의점 2,019건은 적재됐고, 학원 수강료의 화면 활용은 남아 있습니다. 스키마 변경은 Alembic 마이그레이션으로 관리합니다.

스키마·경계 후속 작업. 19 → 29테이블 확장 마이그레이션(b93358fab70e)은 기존 컬럼 변경 없이 테이블 10개·인덱스 8개를 추가했고 테스트 DB 왕복·alembic check를 통과했습니다. 이후 지역 한정 상품용 district_code를 별도 마이그레이션(079cb96619ca)으로 추가했습니다. 개발 DB 상품 재시드와 상담 세션·계획 생성도 후속 로그에서 확인했습니다. 이후 어린이집 등을 포함한 현재 31테이블 ERD 원문은 서비스 저장소 docs/erd.md에 있습니다.

regional_indicator는 업종·슬라이스가 NULL인 행도 중복 적재되지 않도록 NULLS NOT DISTINCT 고유 인덱스를 사용합니다. 고립 테이블 interest_rate는 전국 시계열, funding_program은 안전한 지역 관계를 만들 근거가 부족해 예외로 유지하며 test_schema_isolation.py로 감시합니다. matching은 product의 어댑터를 직접 불러오던 경로를 FinanceProductUseCase 입력 포트 주입으로 바꿨고 test_matching_bc_boundary.py가 다른 BC 어댑터 import를 금지합니다.

애플리케이션 설정과 예외 처리

구분 현재 설정 확인 근거
모델 설정 기본 리포트 gemini-3.8-flash, 임베딩 gemini-embedding-001. 리포트는 REPORT_WRITER_PROVIDER=ollama·OLLAMA_REPORT_MODEL=gemma4:12b로 로컬 선택 가능. 9/19 halfvec(2560) 변경·provider 설정화 완료. qwen 전환은 해당 임베더 재색인·환경변수 변경·백엔드 재시작 9/19 모델 평가
연결 설정 루트 .env → backend/.env 순으로 읽고 빈 값은 무시. DB·Redis·Neo4j 주소와 외부 API 키 이름은 서비스 저장소의 API 목록 문서에 정리(값은 커밋하지 않음) 설정 테스트
CORS·mock CORS_ALLOW_ORIGINS로 운영 출처를 추가하고 로컬 출처는 유지. NEXT_PUBLIC_API_BASE 미설정 시 mock 사용. mock도 handoff 목적과 대구 기준 검산값을 반영 9/18 preflight·mock 후속 수정
재시도 온통청년 4xx 포함 지수 백오프 4회, Gemini 임베딩 429·5xx 재시도. 리포트 생성은 재시도 없이 섹션 대체 문구 게이트웨이 테스트
데이터 부족 위험도 데이터가 없으면 단건 404, 순위는 빈 배열. 리포트 상권 섹션은 “데이터가 없습니다” 표기 코드 확인
사용자 설정 지도 연도(기본 2025), 지표·업종 선택, 재무 입력 13항목, 추가 질문 프론트 테스트·E2E
관측 수집 스크립트별 로그 파일, 가장 나쁜 종료 코드 기록, 중복 실행 방지. 외부 API 오류 메시지에서 키를 제거 가짜 키 오류 테스트

상위 설계와 달라진 점

9월 15일 기획서와 실제 구현의 차이입니다.

기획 구현 이유·영향
위험도 = 경쟁밀도 0.3 + 폐업률 0.3 + 매출감소 0.2 + 프랜차이즈 포화 0.2 폐업률 0.4 + 점포 수 0.4 + 성장률 0.2의 상대 순위 매출·프랜차이즈 데이터를 쓰지 않고, 점포 수는 면적·인구·반경으로 나눈 밀도가 아님. 점수 해석 수위는 OPEN-009에서 결정
상가정보·실거래가·먹거리골목·대규모점포·전통시장 반경 등 대구 특화 변수 일부 반영 편의점 상가정보 적재, 공공데이터 5종 적재·지도 배선 완료(9/19). 실거래가·골목·대규모점포 및 반경 분석은 미구현
네이버 뉴스 API 구글 뉴스 RSS 네이버 키 문제(DEC-008)
9/28 멘토링 때 센터 집계 반출 미확보·일정 미확인 확인된 정책·예약이 아니었음(OPEN-004)
Redis·Neo4j 활용 컨테이너만 구성 분석 요청 저장소는 메모리로 구현(워커 1개 전제)
서울 거리두기 충격 데이터 대구 코로나 타임라인 + 공공데이터 구간 리뷰에서 서울 원본 잔재 발견(DEC-012)
(선택) 리포트 해시 블록체인 기록 범위 제외. 상담자료 sha256 해시만 서버 기록 9/18 결정(DEC-018)

맨 위로

053 · DAEGU · 2026