조달청 평가시스템 — DB 설계 최적안
hwpx ProductRecord 구조 기반 분석 · eval-system-premium / pps-mono-repo
작성: 2026-05-21
대상 PR: jodal-eval-ai/pps-mono-repo#162 (reverted)
데모일: 2026-05-22 (D-1)
1 TL;DR
핵심 결론: hwpx 의 ProductRecord 는 11 영역의 깊은 계층 구조 — JSONB 1컬럼 통째 저장은 검색·매칭 비효율 , 정규화 100% 는 hwpx 스키마 진화 비용 폭발. 하이브리드 (top-level 정규화 + 가변 영역 JSONB) 가 최적 .
최적 권장 5 코어 테이블 + ProductRecord 의 patents/detailed_items/quality_tests/image_assets 만 정규화. 나머지는 JSONB.
현재 갭 specs/proposals 가 InMemoryStore (휘발). matcher cross-DB write 이슈로 compare 가 fixture 반환.
D-1 결정 5/22 데모는 최소 4 테이블(bids/proposals/requirement_items/matches) + MinIO 파일 저장. chat 제거.
2 hwpx ProductRecord 전체 구조
hwpx-intelligence 가 1개 HWPX 파일 추출 후 반환하는 데이터클래스. 11 영역, 4 단 중첩 까지 들어감. 모든 필드에 SourceRef (문단 위치 + extractor + 신뢰도) 부착 — 평가위원이 출처 추적 가능.
ProductRecord
├─ product_id : str
├─ identity : Identity
│ ├─ title_ko , designation_no , author_or_org , product_category , scope
│ └─ source_refs : list[SourceRef]
├─ feature_summary : FeatureSummary
│ ├─ summary : str
│ └─ bullets : list[FeatureBullet { text, source_ref }]
├─ patents : list[Patent]
│ └─ { type, name, number, date, issuer, description, source_ref }
├─ detailed_items : list[DetailedItemGroup]
│ ├─ { item_name_ko, item_code, row_count }
│ └─ rows : list[ModelRow]
│ └─ { identifier, model_name, size_wxhxd_mm, specification_text, note }
├─ quality_tests : list[QualityTestSet]
│ ├─ { standard_title, test_type, confidence, missing_reason }
│ └─ test_items : list[{ name, value }]
├─ image_assets : list[ImageAsset]
│ └─ { asset_type, filename, caption_ko, related_model_names[], para_index, data_uri }
├─ manufacturing : list[ManufacturingStep]
│ └─ { step, work, equipment, inspection }
├─ warranty : Warranty { period, conditions }
├─ confidence : float
├─ parser_version : str
├─ source_file : str
└─ parse_stats : dict
영역별 데이터 특성 분석
영역 row 수 (1 제안서당) 스키마 진화 빈도 검색·매칭 빈도 저장 방식
identity 1 (fixed) 낮음 높음 (필터/정렬)정규화 컬럼
feature_summary summary 1 + bullets N 중 중 (LLM 매칭 입력) JSONB
patents 0~수십 낮음 중 (특허번호 검색) 정규화 테이블
detailed_items.rows 0~수백 (큰 표)낮음 높음 (모델·사양 비교)정규화 테이블
quality_tests 0~수십 중 중 (시험기준 매칭) 정규화 테이블
image_assets 0~수십 낮음 낮음 (UI 표시만) 정규화 + MinIO
manufacturing 0~수십 중 낮음 JSONB
warranty 1 낮음 낮음 (UI 표시만) JSONB
parse_stats / source_refs meta 높음 (hwpx 버전별) 낮음 JSONB
3 설계 옵션 비교
전략 설명 장점 단점 적합성
A. 100% JSONB
proposals.extraction 1 컬럼에 ProductRecord 통째 저장
구현 1시간. hwpx 스키마 변경 무관
검색·집계 SQL 복잡. JSONB GIN 인덱스 필수. 모델 비교 시 array unnest 반복
데모 직전 임시
B. 100% 정규화
patents·rows·quality_tests·images 전부 별도 테이블
표준 SQL 로 조인·필터·집계. 매칭/검색 쾌적
hwpx 필드 추가 시 마이그레이션 폭발. 11 영역 × 평균 2단 = ~20 테이블
과설계
C. 하이브리드 (권장)
identity + 4개 list 영역(patents, model_rows, quality_tests, images) 만 정규화. 나머지(feature_summary / manufacturing / warranty / parse_stats / source_refs) JSONB
비교 표·필터·매칭은 SQL 로 빠름. hwpx 진화 시 JSONB 영역만 영향
마이그레이션 5개 + ORM 5개 — 1일 작업
최적
4 권장 ERD (옵션 C)
PK
FK
UNIQUE
bids
id UUID
notice_no TEXT
title TEXT
rfp_doc JSONB
status TEXT
created_at TIMESTAMPTZ
requirement_items
id TEXT
bid_id UUID → bids
category ENUM(tech/req/eval)
title TEXT
description TEXT
weight REAL
order_index INT
UNIQUE(bid_id, id)
proposals
id UUID
bid_id UUID → bids
company_name TEXT
designation_no TEXT
product_category TEXT
title_ko TEXT
scope TEXT
filename TEXT
file_storage_key TEXT
file_size_bytes BIGINT
hwpx_job_id TEXT
status TEXT
error_message TEXT
extraction_extra JSONB
parser_version TEXT
parse_confidence REAL
created_at, extracted_at
UNIQUE(bid_id, designation_no)
proposal_patents
id UUID
proposal_id UUID → proposals
type TEXT
name TEXT
number TEXT
date TEXT
issuer TEXT
description TEXT
source_ref JSONB
proposal_model_rows
id UUID
proposal_id UUID → proposals
group_name_ko TEXT
group_code TEXT
identifier TEXT
model_name TEXT
size_wxhxd_mm TEXT
specification_text TEXT
note TEXT
order_index INT
source_ref JSONB
proposal_quality_tests
id UUID
proposal_id UUID → proposals
standard_title TEXT
test_type TEXT
test_items JSONB
confidence REAL
missing_reason TEXT
source_ref JSONB
proposal_images
id UUID
proposal_id UUID → proposals
asset_type TEXT
filename TEXT
caption_ko TEXT
related_model_names TEXT[]
para_index INT
storage_key TEXT
extraction_confidence REAL
matches
id UUID
proposal_id UUID → proposals
bid_id UUID → bids
item_id TEXT → requirement_items
category ENUM(tech/req/eval)
score REAL
rationale TEXT
evidence JSONB
model_name TEXT
confidence REAL
created_at TIMESTAMPTZ
UNIQUE(proposal_id, bid_id, item_id)
evaluations (옵션)
id UUID
user_id UUID → users
proposal_id UUID → proposals
item_id TEXT
override_score REAL
comment TEXT
created_at TIMESTAMPTZ
users (옵션)
id UUID
email TEXT (uniq)
role ENUM(admin/evaluator)
display_name TEXT
hashed_password TEXT
5 현재 PR #162 (revert 됨) vs 권장
테이블 현재 PR #162 권장 (옵션 C) Δ
chat_message / chat_feedback 있음 (T4) 제거 scope 축소
tech_match / requirement_match / eval_item_match 3 테이블 분리 matches 1 테이블 + category스키마 동일 → 통합 권장
specs (HWPX 파일/추출) InMemoryStore (휘발) proposals 신설 + MinIO영속화 필수
patents / model_rows / quality_tests / images JSONB 안 묻힘 4 정규화 테이블 모델 코드/특허번호 검색 가능
bids 없음 (UUID 하드코딩) 정식 FK 마스터 다중 입찰 / 무결성
requirement_items seed 시 string ID 만 FK 마스터 + weight + category 매칭 무결성
users / evaluations 없음 옵션 (데모 후) 다중 평가위원 시 필요
6 마이그레이션 시퀀스 (옵션 C)
0001_bids_and_proposals.py
+ bids
+ proposals (extraction_extra JSONB 포함)
+ requirement_items
0002_proposal_subtables.py
+ proposal_patents
+ proposal_model_rows
+ proposal_quality_tests
+ proposal_images
0003_matches.py
+ matches (UNIQUE proposal_id, bid_id, item_id)
0004_evaluators.py (옵션, 데모 후)
+ users
+ evaluations
주의 — chat 0001 충돌
PR #162 의 0001_chat_tables 는 새 PR 에서 처음부터 제거. 0002_match_tables 의 down_revision 의존성 끊고 새 0001 로 시작. 이미 머지된 마이그레이션 수정 금지 규칙은 PR #163 revert 로 무력화됐으므로 클린 슬레이트 가능.
7 5/22 데모일 최소 시퀀스 (Minimum Viable)
단계 작업 예상 시간 리스크
1 chat 관련 파일 / 마이그레이션 / 모델 / 테스트 삭제 30분 낮음
2 bids + proposals 마이그레이션 (extraction JSONB 통째) — patents 등 별도 테이블은 데모 후1시간 낮음
3 specs.py InMemoryStore → SQLAlchemy ORM 교체1시간 중 (기존 webhook 흐름 영향)
4 MinIO 파일 저장 — uvicorn 에서 boto3 client + bucket 자동 생성 1시간 중 (.env.onprem 자격 검증)
5 matches 3 테이블 그대로 유지 (Suman 코드 보존), 데모 후 통합 0 (작업 없음) 없음
6 compare endpoint fixture fallback 유지 — LLM 의존 회피 0 없음
7 로컬 Playwright e2e 통과 후 새 PR + 머지 1시간 중 (AdminGuard SSR 회귀 같이 fix)
총 예상: 4~5시간 — D-1 안에 가능. 안전한 fallback (in-memory 유지) 도 가능하나 데모 후 어차피 손봐야 하므로 한 번에 처리 권장.
8 주요 결정 트레이드오프
Q1. proposals.extraction JSONB ↔ 별도 정규화 테이블
D-1 JSONB 통째 (옵션 A)
데모 후 patents/model_rows/quality_tests/images 정규화 (옵션 C)
이유: 데모일에 정규화 분리하면 ORM 변경 폭이 큼. JSONB 로 시작 → 점진 이관.
Q2. matches 3 테이블 ↔ 통합
D-1 3 테이블 유지 (Suman 코드 보존)
데모 후 matches 통합 + category 컬럼
이유: 통합 시 ORM/API/seed 동시 변경. 데모 영향 없는 리팩토링은 후순위.
Q3. MinIO 파일 저장 ↔ DB JSONB image data_uri
D-1 MinIO 권장
이유: ProductRecord.image_assets[].data_uri 가 base64 라 row 가 수 MB 됨. DB 비효율. .env.onprem:71-75 MinIO 이미 설정.
Q4. matcher cross-DB write
현재 갭 analysis_pipeline 이 knowledge_hub eval_db 로 씀 → BFF compare 못 읽음
D-1 fixture fallback 유지 (LLM 호출 회피)
데모 후 matcher 가 eval_system_premium DB 로도 쓰게 보강
9 참고 — 코드 위치
파일 내용
hwpx-intelligence / src / pipeline / models.py ProductRecord + Identity + FeatureSummary + Patent + ModelRow + DetailedItemGroup + QualityTestSet + ImageAsset + ManufacturingStep + Warranty 데이터클래스
apps / eval-system-premium / backend / app / schemas / __init__.py BFF 측 ProductRecord subset (Pydantic) + ProposalExtracted 매핑
apps / eval-system-premium / backend / app / models / __init__.py InMemoryStore (specs) — 영속화 대체 대상
apps / eval-system-premium / backend / app / models / match.py TechMatch / RequirementMatch / EvalItemMatch ORM (T6)
apps / eval-system-premium / backend / alembic / versions / 0001_chat_tables.py 제거 대상
apps / eval-system-premium / backend / alembic / versions / 0002_match_tables.py 유지 (또는 새 0001 로 흡수)
apps / eval-system-premium / backend / scripts / seed_demo_bid.py 주석에 "specs / proposals 테이블은 InMemoryStore 라 시드 안 함" 명시
.env.onprem (lines 71-75) MinIO 자격 정의 — 파일 저장에 활용 가능
생성: Claude Code · 조달청 PR #162 분석 · 2026-05-21