Files
TK-BOM-Project/backend/app/services/spool_manager_v2.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

230 lines
7.9 KiB
Python

"""
수정된 스풀 관리 시스템
도면별 스풀 넘버링 + 에리어는 별도 관리
"""
import re
from typing import Dict, List, Optional, Tuple
from datetime import datetime
# ========== 스풀 넘버링 규칙 ==========
SPOOL_NUMBERING_RULES = {
"SPOOL_NUMBER": {
"sequence": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z"],
"description": "도면별 스풀 넘버"
},
"AREA_NUMBER": {
"pattern": r"#(\d{2})", # #01, #02, #03...
"format": "#{:02d}", # 2자리 숫자
"range": (1, 99), # 01~99
"description": "물리적 구역 넘버 (별도 관리)"
}
}
class SpoolManagerV2:
"""수정된 스풀 관리 클래스"""
def __init__(self, project_id: int = None):
self.project_id = project_id
def generate_spool_identifier(self, dwg_name: str, spool_number: str) -> str:
"""
스풀 식별자 생성 (도면명 + 스풀넘버)
Args:
dwg_name: 도면명 (예: "A-1", "B-3")
spool_number: 스풀넘버 (예: "A", "B")
Returns:
스풀 식별자 (예: "A-1-A", "B-3-B")
"""
# 스풀 넘버 포맷 검증
spool_formatted = self.format_spool_number(spool_number)
# 조합: {도면명}-{스풀넘버}
return f"{dwg_name}-{spool_formatted}"
def parse_spool_identifier(self, spool_id: str) -> Dict:
"""스풀 식별자 파싱"""
# 패턴: DWG_NAME-SPOOL_NUMBER
# 예: A-1-A, B-3-B, 1-IAR-3B1D0-0129-N-A
# 마지막 '-' 기준으로 분리
parts = spool_id.rsplit('-', 1)
if len(parts) == 2:
dwg_base = parts[0]
spool_number = parts[1]
return {
"original_id": spool_id,
"dwg_name": dwg_base,
"spool_number": spool_number,
"is_valid": self.validate_spool_number(spool_number),
"format": "CORRECT"
}
else:
return {
"original_id": spool_id,
"dwg_name": None,
"spool_number": None,
"is_valid": False,
"format": "INVALID"
}
def format_spool_number(self, spool_input: str) -> str:
"""스풀 넘버 포맷팅 및 검증"""
spool_clean = spool_input.upper().strip()
# 유효한 스풀 넘버인지 확인 (A-Z 단일 문자)
if re.match(r'^[A-Z]$', spool_clean):
return spool_clean
raise ValueError(f"유효하지 않은 스풀 넘버: {spool_input} (A-Z 단일 문자만 가능)")
def validate_spool_number(self, spool_number: str) -> bool:
"""스풀 넘버 유효성 검증"""
return bool(re.match(r'^[A-Z]$', spool_number))
def get_next_spool_number(self, dwg_name: str, existing_spools: List[str] = None) -> str:
"""해당 도면의 다음 사용 가능한 스풀 넘버 추천"""
if not existing_spools:
return "A" # 첫 번째 스풀
# 해당 도면의 기존 스풀들 파싱
used_spools = set()
for spool_id in existing_spools:
parsed = self.parse_spool_identifier(spool_id)
if parsed["dwg_name"] == dwg_name and parsed["is_valid"]:
used_spools.add(parsed["spool_number"])
# 다음 사용 가능한 넘버 찾기
sequence = SPOOL_NUMBERING_RULES["SPOOL_NUMBER"]["sequence"]
for spool_num in sequence:
if spool_num not in used_spools:
return spool_num
raise ValueError(f"도면 {dwg_name}에서 사용 가능한 스풀 넘버가 없습니다")
def validate_spool_identifier(self, spool_id: str) -> Dict:
"""스풀 식별자 전체 유효성 검증"""
parsed = self.parse_spool_identifier(spool_id)
validation_result = {
"is_valid": True,
"errors": [],
"warnings": [],
"parsed": parsed
}
# 도면명 확인
if not parsed["dwg_name"]:
validation_result["is_valid"] = False
validation_result["errors"].append("도면명이 없습니다")
# 스풀 넘버 확인
if not parsed["spool_number"]:
validation_result["is_valid"] = False
validation_result["errors"].append("스풀 넘버가 없습니다")
elif not self.validate_spool_number(parsed["spool_number"]):
validation_result["is_valid"] = False
validation_result["errors"].append("스풀 넘버 형식이 잘못되었습니다 (A-Z 단일 문자)")
return validation_result
# ========== 에리어 관리 (별도 시스템) ==========
class AreaManager:
"""에리어 관리 클래스 (물리적 구역)"""
def __init__(self, project_id: int = None):
self.project_id = project_id
def format_area_number(self, area_input: str) -> str:
"""에리어 넘버 포맷팅"""
# 숫자만 추출
numbers = re.findall(r'\d+', area_input)
if numbers:
area_num = int(numbers[0])
if 1 <= area_num <= 99:
return f"#{area_num:02d}"
raise ValueError(f"유효하지 않은 에리어 넘버: {area_input} (#01-#99)")
def assign_drawings_to_area(self, area_number: str, drawing_names: List[str]) -> Dict:
"""도면들을 에리어에 할당"""
area_formatted = self.format_area_number(area_number)
return {
"area_number": area_formatted,
"assigned_drawings": drawing_names,
"assignment_count": len(drawing_names),
"assignment_date": datetime.now().isoformat()
}
def classify_pipe_with_corrected_spool(dat_file: str, description: str, main_nom: str,
length: float = None, dwg_name: str = None,
spool_number: str = None, area_number: str = None) -> Dict:
"""파이프 분류 + 수정된 스풀 정보"""
# 기본 파이프 분류
from .pipe_classifier import classify_pipe
pipe_result = classify_pipe(dat_file, description, main_nom, length)
# 스풀 관리자 생성
spool_manager = SpoolManagerV2()
area_manager = AreaManager()
# 스풀 정보 처리
spool_info = {
"dwg_name": dwg_name,
"spool_number": None,
"spool_identifier": None,
"area_number": None, # 별도 관리
"manual_input_required": True,
"validation": None
}
# 스풀 넘버가 제공된 경우
if dwg_name and spool_number:
try:
spool_identifier = spool_manager.generate_spool_identifier(dwg_name, spool_number)
spool_info.update({
"spool_number": spool_manager.format_spool_number(spool_number),
"spool_identifier": spool_identifier,
"manual_input_required": False,
"validation": {"is_valid": True, "errors": []}
})
except ValueError as e:
spool_info["validation"] = {
"is_valid": False,
"errors": [str(e)]
}
# 에리어 정보 처리 (별도)
if area_number:
try:
area_formatted = area_manager.format_area_number(area_number)
spool_info["area_number"] = area_formatted
except ValueError as e:
spool_info["area_validation"] = {
"is_valid": False,
"errors": [str(e)]
}
# 기존 결과에 스풀 정보 추가
pipe_result["spool_info"] = spool_info
return pipe_result