Files
TK-BOM-Project/backend/app/api/spools.py
Hyungi Ahn 12ecb93741 feat: 완전한 자재 분류 시스템 구현 (v1.0)
🎯 주요 기능:
- 재질 분류 모듈 (ASTM/ASME 규격 자동 인식)
- PIPE 분류 시스템 (제조방법, 끝가공, 스케줄, 절단계획)
- FITTING 분류 시스템 (10가지 타입, 연결방식, 압력등급)
- FLANGE 분류 시스템 (SPECIAL/STANDARD 구분, 면가공)
- 스풀 관리 시스템 (도면별 A,B,C 넘버링, 에리어 관리)

📁 새로 추가된 파일들:
- app/services/materials_schema.py (재질 규격 데이터베이스)
- app/services/material_classifier.py (공통 재질 분류 엔진)
- app/services/pipe_classifier.py (파이프 전용 분류기)
- app/services/fitting_classifier.py (피팅 전용 분류기)
- app/services/flange_classifier.py (플랜지 전용 분류기)
- app/services/spool_manager_v2.py (수정된 스풀 관리)
- app/services/test_*.py (각 시스템별 테스트 파일)

🔧 기술적 특징:
- 정규표현식 기반 패턴 매칭
- 신뢰도 점수 시스템 (0.0-1.0)
- 증거 기반 분류 (evidence tracking)
- 모듈화된 구조 (재사용 가능)

🎯 분류 정확도:
- 재질 분류: 90-95% 신뢰도
- PIPE 분류: 85-95% 신뢰도
- FITTING 분류: 85-95% 신뢰도
- FLANGE 분류: 85-95% 신뢰도

💾 데이터베이스 연동:
- 모든 분석 결과 자동 저장
- 프로젝트/도면 정보 자동 연결
- 스풀 정보 사용자 입력 대기

🧪 테스트 커버리지:
- 실제 BOM 데이터 기반 테스트
- 예외 케이스 처리
- 10+ 개 테스트 시나리오

Version: v1.0
Date: 2024-07-15
Author: hyungiahn
2025-07-15 09:43:39 +09:00

232 lines
7.4 KiB
Python

"""
스풀 관리 API 엔드포인트
"""
from fastapi import APIRouter, Depends, HTTPException, Form
from sqlalchemy.orm import Session
from sqlalchemy import text
from typing import List, Optional
from datetime import datetime
from ..database import get_db
from ..services.spool_manager import SpoolManager, classify_pipe_with_spool
router = APIRouter()
@router.get("/")
async def get_spools_info():
return {
"message": "스풀 관리 API",
"features": [
"스풀 식별자 생성",
"에리어/스풀 넘버 관리",
"스풀별 파이프 그룹핑",
"스풀 유효성 검증"
]
}
@router.post("/validate-identifier")
async def validate_spool_identifier(
spool_identifier: str = Form(...),
db: Session = Depends(get_db)
):
"""스풀 식별자 유효성 검증"""
spool_manager = SpoolManager()
validation_result = spool_manager.validate_spool_identifier(spool_identifier)
return {
"spool_identifier": spool_identifier,
"validation": validation_result,
"timestamp": datetime.now().isoformat()
}
@router.post("/generate-identifier")
async def generate_spool_identifier(
dwg_name: str = Form(...),
area_number: str = Form(...),
spool_number: str = Form(...),
db: Session = Depends(get_db)
):
"""새로운 스풀 식별자 생성"""
try:
spool_manager = SpoolManager()
spool_identifier = spool_manager.generate_spool_identifier(
dwg_name, area_number, spool_number
)
# 중복 확인
duplicate_check = db.execute(
text("SELECT id FROM spools WHERE spool_identifier = :spool_id"),
{"spool_id": spool_identifier}
).fetchone()
return {
"success": True,
"spool_identifier": spool_identifier,
"is_duplicate": bool(duplicate_check),
"components": {
"dwg_name": dwg_name,
"area_number": spool_manager.format_area_number(area_number),
"spool_number": spool_manager.format_spool_number(spool_number)
}
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("/next-spool-number")
async def get_next_spool_number(
dwg_name: str,
area_number: str,
db: Session = Depends(get_db)
):
"""다음 사용 가능한 스풀 넘버 추천"""
try:
# 기존 스풀들 조회
existing_spools_query = text("""
SELECT spool_identifier
FROM spools
WHERE dwg_name = :dwg_name
""")
result = db.execute(existing_spools_query, {"dwg_name": dwg_name})
existing_spools = [row[0] for row in result.fetchall()]
spool_manager = SpoolManager()
next_spool = spool_manager.get_next_spool_number(
dwg_name, area_number, existing_spools
)
return {
"dwg_name": dwg_name,
"area_number": spool_manager.format_area_number(area_number),
"next_spool_number": next_spool,
"existing_spools_count": len(existing_spools)
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/materials/{material_id}/assign-spool")
async def assign_spool_to_material(
material_id: int,
area_number: str = Form(...),
spool_number: str = Form(...),
db: Session = Depends(get_db)
):
"""자재에 스풀 정보 할당"""
try:
# 자재 정보 조회
material_query = text("""
SELECT m.id, m.file_id, f.project_id, f.dwg_name
FROM materials m
JOIN files f ON m.file_id = f.id
WHERE m.id = :material_id
""")
material = db.execute(material_query, {"material_id": material_id}).fetchone()
if not material:
raise HTTPException(status_code=404, detail="자재를 찾을 수 없습니다")
# 스풀 식별자 생성
spool_manager = SpoolManager()
area_formatted = spool_manager.format_area_number(area_number)
spool_formatted = spool_manager.format_spool_number(spool_number)
spool_identifier = spool_manager.generate_spool_identifier(
material.dwg_name, area_formatted, spool_formatted
)
# 자재 업데이트
update_query = text("""
UPDATE materials
SET area_number = :area_number,
spool_number = :spool_number,
spool_identifier = :spool_identifier,
spool_input_required = FALSE,
spool_validated = TRUE,
updated_at = NOW()
WHERE id = :material_id
""")
db.execute(update_query, {
"material_id": material_id,
"area_number": area_formatted,
"spool_number": spool_formatted,
"spool_identifier": spool_identifier
})
# 스풀 테이블에 등록 (없다면)
spool_insert_query = text("""
INSERT INTO spools (project_id, dwg_name, area_number, spool_number, spool_identifier, created_at)
VALUES (:project_id, :dwg_name, :area_number, :spool_number, :spool_identifier, NOW())
ON CONFLICT (project_id, dwg_name, area_number, spool_number) DO NOTHING
""")
db.execute(spool_insert_query, {
"project_id": material.project_id,
"dwg_name": material.dwg_name,
"area_number": area_formatted,
"spool_number": spool_formatted,
"spool_identifier": spool_identifier
})
db.commit()
return {
"success": True,
"material_id": material_id,
"spool_identifier": spool_identifier,
"message": "스풀 정보가 할당되었습니다"
}
except ValueError as e:
db.rollback()
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"스풀 할당 실패: {str(e)}")
@router.get("/project/{project_id}/spools")
async def get_project_spools(
project_id: int,
db: Session = Depends(get_db)
):
"""프로젝트의 모든 스풀 조회"""
query = text("""
SELECT s.*,
COUNT(m.id) as material_count,
SUM(m.quantity) as total_quantity
FROM spools s
LEFT JOIN materials m ON s.spool_identifier = m.spool_identifier
WHERE s.project_id = :project_id
GROUP BY s.id
ORDER BY s.dwg_name, s.area_number, s.spool_number
""")
result = db.execute(query, {"project_id": project_id})
spools = result.fetchall()
return {
"project_id": project_id,
"spools_count": len(spools),
"spools": [
{
"id": spool.id,
"spool_identifier": spool.spool_identifier,
"dwg_name": spool.dwg_name,
"area_number": spool.area_number,
"spool_number": spool.spool_number,
"material_count": spool.material_count or 0,
"total_quantity": float(spool.total_quantity or 0),
"status": spool.status,
"created_at": spool.created_at
}
for spool in spools
]
}