조달청 평가시스템 — 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)

1TL;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 제거.

2hwpx 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 제안서당)스키마 진화 빈도검색·매칭 빈도저장 방식
identity1 (fixed)낮음높음 (필터/정렬)정규화 컬럼
feature_summarysummary 1 + bullets N중 (LLM 매칭 입력)JSONB
patents0~수십낮음중 (특허번호 검색)정규화 테이블
detailed_items.rows0~수백 (큰 표)낮음높음 (모델·사양 비교)정규화 테이블
quality_tests0~수십중 (시험기준 매칭)정규화 테이블
image_assets0~수십낮음낮음 (UI 표시만)정규화 + MinIO
manufacturing0~수십낮음JSONB
warranty1낮음낮음 (UI 표시만)JSONB
parse_stats / source_refsmeta높음 (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 (MinIO)
file_size_bytes BIGINT
hwpx_job_id TEXT
status TEXT
error_message TEXT
extraction_extra JSONB (featureSummary / manufacturing / warranty / stats / source_refs)
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 (가변 name/value)
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 (MinIO)
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_match3 테이블 분리matches 1 테이블 + category스키마 동일 → 통합 권장
specs (HWPX 파일/추출)InMemoryStore (휘발)proposals 신설 + MinIO영속화 필수
patents / model_rows / quality_tests / imagesJSONB 안 묻힘4 정규화 테이블모델 코드/특허번호 검색 가능
bids없음 (UUID 하드코딩)정식 FK 마스터다중 입찰 / 무결성
requirement_itemsseed 시 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_tablesdown_revision 의존성 끊고 새 0001 로 시작. 이미 머지된 마이그레이션 수정 금지 규칙은 PR #163 revert 로 무력화됐으므로 클린 슬레이트 가능.

75/22 데모일 최소 시퀀스 (Minimum Viable)

단계작업예상 시간리스크
1chat 관련 파일 / 마이그레이션 / 모델 / 테스트 삭제30분낮음
2bids + proposals 마이그레이션 (extraction JSONB 통째) — patents 등 별도 테이블은 데모 후1시간낮음
3specs.py InMemoryStore → SQLAlchemy ORM 교체1시간중 (기존 webhook 흐름 영향)
4MinIO 파일 저장 — uvicorn 에서 boto3 client + bucket 자동 생성1시간중 (.env.onprem 자격 검증)
5matches 3 테이블 그대로 유지 (Suman 코드 보존), 데모 후 통합0 (작업 없음)없음
6compare 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.pyProductRecord + Identity + FeatureSummary + Patent + ModelRow + DetailedItemGroup + QualityTestSet + ImageAsset + ManufacturingStep + Warranty 데이터클래스
apps / eval-system-premium / backend / app / schemas / __init__.pyBFF 측 ProductRecord subset (Pydantic) + ProposalExtracted 매핑
apps / eval-system-premium / backend / app / models / __init__.pyInMemoryStore (specs) — 영속화 대체 대상
apps / eval-system-premium / backend / app / models / match.pyTechMatch / 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 자격 정의 — 파일 저장에 활용 가능