🎯 주요 기능: - 재질 분류 모듈 (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
232 lines
7.4 KiB
Python
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
|
|
]
|
|
}
|