feat: 자재 분류 시스템 개선 및 상세 테이블 추가

- 모든 자재 카테고리별 상세 테이블 생성 (fitting, valve, flange, bolt, gasket, instrument)
- PIPE, FITTING, VALVE 분류 결과를 각 상세 테이블에 저장하는 로직 구현
- 프론트엔드 라우팅 정리 및 BOM 현황 페이지 기능 개선
- 자재확인 페이지 에러 처리 개선

TODO: FLANGE, BOLT, GASKET, INSTRUMENT 저장 로직 추가 필요
This commit is contained in:
Hyungi Ahn
2025-07-17 10:44:19 +09:00
parent ea111433e4
commit 5f7a6f0b3a
30 changed files with 3963 additions and 923 deletions

View File

@@ -8,9 +8,18 @@ from datetime import datetime
import uuid
import pandas as pd
import re
import json
from pathlib import Path
from ..database import get_db
from app.services.material_classifier import classify_material
from app.services.bolt_classifier import classify_bolt
from app.services.flange_classifier import classify_flange
from app.services.fitting_classifier import classify_fitting
from app.services.gasket_classifier import classify_gasket
from app.services.instrument_classifier import classify_instrument
from app.services.pipe_classifier import classify_pipe
from app.services.valve_classifier import classify_valve
router = APIRouter()
@@ -59,7 +68,8 @@ def generate_unique_filename(original_filename: str) -> str:
def parse_dataframe(df):
df = df.dropna(how='all')
df.columns = df.columns.str.strip().str.lower()
# 원본 컬럼명 유지 (소문자 변환하지 않음)
df.columns = df.columns.str.strip()
column_mapping = {
'description': ['description', 'item', 'material', '품명', '자재명'],
@@ -75,10 +85,16 @@ def parse_dataframe(df):
mapped_columns = {}
for standard_col, possible_names in column_mapping.items():
for possible_name in possible_names:
if possible_name in df.columns:
mapped_columns[standard_col] = possible_name
# 대소문자 구분 없이 매핑
for col in df.columns:
if possible_name.lower() == col.lower():
mapped_columns[standard_col] = col
break
if standard_col in mapped_columns:
break
print(f"찾은 컬럼 매핑: {mapped_columns}")
materials = []
for index, row in df.iterrows():
description = str(row.get(mapped_columns.get('description', ''), ''))
@@ -89,6 +105,15 @@ def parse_dataframe(df):
except:
quantity = 0
# 길이 정보 파싱
length_raw = row.get(mapped_columns.get('length', ''), None)
length_value = None
if pd.notna(length_raw) and length_raw != '':
try:
length_value = float(length_raw)
except:
length_value = None
material_grade = ""
if "ASTM" in description.upper():
astm_match = re.search(r'ASTM\s+([A-Z0-9\s]+)', description.upper())
@@ -112,6 +137,7 @@ def parse_dataframe(df):
'unit': "EA",
'size_spec': size_spec,
'material_grade': material_grade,
'length': length_value,
'line_number': index + 1,
'row_number': index + 1
})
@@ -140,7 +166,7 @@ async def upload_file(
revision: str = Form("Rev.0"),
db: Session = Depends(get_db)
):
if not validate_file_extension(file.filename):
if not validate_file_extension(str(file.filename)):
raise HTTPException(
status_code=400,
detail=f"지원하지 않는 파일 형식입니다. 허용된 확장자: {', '.join(ALLOWED_EXTENSIONS)}"
@@ -149,7 +175,7 @@ async def upload_file(
if file.size and file.size > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="파일 크기는 10MB를 초과할 수 없습니다")
unique_filename = generate_unique_filename(file.filename)
unique_filename = generate_unique_filename(str(file.filename))
file_path = UPLOAD_DIR / unique_filename
try:
@@ -183,19 +209,133 @@ async def upload_file(
file_id = file_result.fetchone()[0]
# 자재 데이터 저장
# 자재 데이터 저장 (분류 포함)
materials_inserted = 0
for material_data in materials_data:
# 자재 타입 분류기 적용 (PIPE, FITTING, VALVE 등)
description = material_data["original_description"]
size_spec = material_data["size_spec"]
# 각 분류기로 시도 (올바른 매개변수 사용)
print(f"분류 시도: {description}")
# 분류기 호출 시 타임아웃 및 예외 처리
classification_result = None
try:
# 파이프 분류기 호출 시 length 매개변수 전달
length_value = None
if 'length' in material_data:
try:
length_value = float(material_data['length'])
except:
length_value = None
# None이면 0.0으로 대체
if length_value is None:
length_value = 0.0
# 타임아웃 설정 (10초)
import signal
def timeout_handler(signum, frame):
raise TimeoutError("분류기 실행 시간 초과")
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(10) # 10초 타임아웃
try:
classification_result = classify_pipe("", description, size_spec, length_value)
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
finally:
signal.alarm(0) # 타임아웃 해제
if classification_result.get("overall_confidence", 0) < 0.5:
signal.alarm(10)
try:
classification_result = classify_fitting("", description, size_spec)
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
finally:
signal.alarm(0)
if classification_result.get("overall_confidence", 0) < 0.5:
signal.alarm(10)
try:
classification_result = classify_valve("", description, size_spec)
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
finally:
signal.alarm(0)
if classification_result.get("overall_confidence", 0) < 0.5:
signal.alarm(10)
try:
classification_result = classify_flange("", description, size_spec)
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
finally:
signal.alarm(0)
if classification_result.get("overall_confidence", 0) < 0.5:
signal.alarm(10)
try:
classification_result = classify_bolt("", description, size_spec)
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
finally:
signal.alarm(0)
if classification_result.get("overall_confidence", 0) < 0.5:
signal.alarm(10)
try:
classification_result = classify_gasket("", description, size_spec)
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
finally:
signal.alarm(0)
if classification_result.get("overall_confidence", 0) < 0.5:
signal.alarm(10)
try:
classification_result = classify_instrument("", description, size_spec)
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
finally:
signal.alarm(0)
except (TimeoutError, Exception) as e:
print(f"분류기 실행 중 오류 발생: {e}")
# 기본 분류 결과 생성
classification_result = {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": f"분류기 오류: {str(e)}"
}
print(f"최종 분류 결과: {classification_result.get('category', 'UNKNOWN')}")
# 분류 결과에서 상세 정보 추출
if classification_result.get('category') == 'PIPE':
classification_details = classification_result
elif classification_result.get('category') == 'FITTING':
classification_details = classification_result
elif classification_result.get('category') == 'VALVE':
classification_details = classification_result
else:
classification_details = {}
# DB에 저장 시 JSON 직렬화
classification_details = json.dumps(classification_details, ensure_ascii=False)
# 디버깅: 저장 직전 데이터 확인
print(f"=== 자재[{materials_inserted + 1}] 저장 직전 ===")
print(f"자재명: {material_data['original_description']}")
print(f"분류결과: {classification_result.get('category')}")
print(f"신뢰도: {classification_result.get('overall_confidence', 0)}")
print(f"classification_details 길이: {len(classification_details)}")
print(f"classification_details 샘플: {classification_details[:200]}...")
print("=" * 50)
material_insert_query = text("""
INSERT INTO materials (
file_id, original_description, quantity, unit, size_spec,
material_grade, line_number, row_number, classified_category,
classification_confidence, is_verified, created_at
classification_confidence, classification_details, is_verified, created_at
)
VALUES (
:file_id, :original_description, :quantity, :unit, :size_spec,
:material_grade, :line_number, :row_number, :classified_category,
:classification_confidence, :is_verified, :created_at
:classification_confidence, :classification_details, :is_verified, :created_at
)
""")
@@ -208,11 +348,155 @@ async def upload_file(
"material_grade": material_data["material_grade"],
"line_number": material_data["line_number"],
"row_number": material_data["row_number"],
"classified_category": None,
"classification_confidence": None,
"classified_category": classification_result.get("category", "UNKNOWN"),
"classification_confidence": classification_result.get("overall_confidence", 0.0),
"classification_details": classification_details,
"is_verified": False,
"created_at": datetime.now()
})
# 각 카테고리별로 상세 테이블에 저장
category = classification_result.get('category')
confidence = classification_result.get('overall_confidence', 0)
if category == 'PIPE' and confidence >= 0.5:
try:
# 분류 결과에서 파이프 상세 정보 추출
pipe_info = classification_result
# cutting_dimensions에서 length 정보 가져오기
cutting_dims = pipe_info.get('cutting_dimensions', {})
length_mm = cutting_dims.get('length_mm')
# length_mm가 없으면 원본 데이터의 length 사용
if not length_mm and material_data.get('length'):
length_mm = material_data['length']
pipe_insert_query = text("""
INSERT INTO pipe_details (
material_id, file_id, size_inches, schedule_type, material_spec,
manufacturing_method, length_mm, outer_diameter_mm, wall_thickness_mm,
weight_per_meter_kg, classification_confidence, additional_info
)
VALUES (
(SELECT id FROM materials WHERE file_id = :file_id AND original_description = :description AND row_number = :row_number),
:file_id, :size_inches, :schedule_type, :material_spec,
:manufacturing_method, :length_mm, :outer_diameter_mm, :wall_thickness_mm,
:weight_per_meter_kg, :classification_confidence, :additional_info
)
""")
db.execute(pipe_insert_query, {
"file_id": file_id,
"description": material_data["original_description"],
"row_number": material_data["row_number"],
"size_inches": pipe_info.get('nominal_diameter', ''),
"schedule_type": pipe_info.get('schedule', ''),
"material_spec": pipe_info.get('material_spec', ''),
"manufacturing_method": pipe_info.get('manufacturing_method', ''),
"length_mm": length_mm,
"outer_diameter_mm": pipe_info.get('outer_diameter_mm'),
"wall_thickness_mm": pipe_info.get('wall_thickness_mm'),
"weight_per_meter_kg": pipe_info.get('weight_per_meter_kg'),
"classification_confidence": classification_result.get('overall_confidence', 0.0),
"additional_info": json.dumps(pipe_info, ensure_ascii=False)
})
print(f"PIPE 상세정보 저장 완료: {material_data['original_description']}")
except Exception as e:
print(f"PIPE 상세정보 저장 실패: {e}")
# 에러가 발생해도 전체 프로세스는 계속 진행
elif category == 'FITTING' and confidence >= 0.5:
try:
fitting_info = classification_result
fitting_insert_query = text("""
INSERT INTO fitting_details (
material_id, file_id, fitting_type, fitting_subtype,
connection_method, connection_code, pressure_rating, max_pressure,
manufacturing_method, material_standard, material_grade, material_type,
main_size, reduced_size, classification_confidence, additional_info
)
VALUES (
(SELECT id FROM materials WHERE file_id = :file_id AND original_description = :description AND row_number = :row_number),
:file_id, :fitting_type, :fitting_subtype,
:connection_method, :connection_code, :pressure_rating, :max_pressure,
:manufacturing_method, :material_standard, :material_grade, :material_type,
:main_size, :reduced_size, :classification_confidence, :additional_info
)
""")
db.execute(fitting_insert_query, {
"file_id": file_id,
"description": material_data["original_description"],
"row_number": material_data["row_number"],
"fitting_type": fitting_info.get('fitting_type', {}).get('type', ''),
"fitting_subtype": fitting_info.get('fitting_type', {}).get('subtype', ''),
"connection_method": fitting_info.get('connection_method', {}).get('method', ''),
"connection_code": fitting_info.get('connection_method', {}).get('matched_code', ''),
"pressure_rating": fitting_info.get('pressure_rating', {}).get('rating', ''),
"max_pressure": fitting_info.get('pressure_rating', {}).get('max_pressure', ''),
"manufacturing_method": fitting_info.get('manufacturing', {}).get('method', ''),
"material_standard": fitting_info.get('material', {}).get('standard', ''),
"material_grade": fitting_info.get('material', {}).get('grade', ''),
"material_type": fitting_info.get('material', {}).get('material_type', ''),
"main_size": fitting_info.get('size_info', {}).get('main_size', ''),
"reduced_size": fitting_info.get('size_info', {}).get('reduced_size', ''),
"classification_confidence": confidence,
"additional_info": json.dumps(fitting_info, ensure_ascii=False)
})
print(f"FITTING 상세정보 저장 완료: {material_data['original_description']}")
except Exception as e:
print(f"FITTING 상세정보 저장 실패: {e}")
elif category == 'VALVE' and confidence >= 0.5:
try:
valve_info = classification_result
valve_insert_query = text("""
INSERT INTO valve_details (
material_id, file_id, valve_type, valve_subtype, actuator_type,
connection_method, pressure_rating, pressure_class,
body_material, trim_material, size_inches,
fire_safe, low_temp_service, classification_confidence, additional_info
)
VALUES (
(SELECT id FROM materials WHERE file_id = :file_id AND original_description = :description AND row_number = :row_number),
:file_id, :valve_type, :valve_subtype, :actuator_type,
:connection_method, :pressure_rating, :pressure_class,
:body_material, :trim_material, :size_inches,
:fire_safe, :low_temp_service, :classification_confidence, :additional_info
)
""")
db.execute(valve_insert_query, {
"file_id": file_id,
"description": material_data["original_description"],
"row_number": material_data["row_number"],
"valve_type": valve_info.get('valve_type', ''),
"valve_subtype": valve_info.get('valve_subtype', ''),
"actuator_type": valve_info.get('actuator_type', ''),
"connection_method": valve_info.get('connection_method', ''),
"pressure_rating": valve_info.get('pressure_rating', ''),
"pressure_class": valve_info.get('pressure_class', ''),
"body_material": valve_info.get('body_material', ''),
"trim_material": valve_info.get('trim_material', ''),
"size_inches": valve_info.get('size', ''),
"fire_safe": valve_info.get('fire_safe', False),
"low_temp_service": valve_info.get('low_temp_service', False),
"classification_confidence": confidence,
"additional_info": json.dumps(valve_info, ensure_ascii=False)
})
print(f"VALVE 상세정보 저장 완료: {material_data['original_description']}")
except Exception as e:
print(f"VALVE 상세정보 저장 실패: {e}")
materials_inserted += 1
db.commit()
@@ -256,6 +540,7 @@ async def get_materials(
query = """
SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit,
m.size_spec, m.material_grade, m.line_number, m.row_number,
m.classified_category, m.classification_confidence, m.classification_details,
m.created_at,
f.original_filename, f.project_id, f.job_no, f.revision,
p.official_project_code, p.project_name
@@ -383,6 +668,9 @@ async def get_materials(
"material_grade": m.material_grade,
"line_number": m.line_number,
"row_number": m.row_number,
"classified_category": m.classified_category,
"classification_confidence": float(m.classification_confidence) if m.classification_confidence else 0,
"classification_details": json.loads(m.classification_details) if m.classification_details else None,
"created_at": m.created_at
}
for m in materials
@@ -444,3 +732,267 @@ async def get_materials_summary(
except Exception as e:
raise HTTPException(status_code=500, detail=f"요약 조회 실패: {str(e)}")
@router.get("/materials/compare-revisions")
async def compare_revisions(
job_no: str,
filename: str,
old_revision: str,
new_revision: str,
db: Session = Depends(get_db)
):
"""
리비전 간 자재 비교
"""
try:
# 기존 리비전 자재 조회
old_materials_query = text("""
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
m.material_grade, m.classified_category, m.classification_confidence
FROM materials m
JOIN files f ON m.file_id = f.id
WHERE f.job_no = :job_no
AND f.original_filename = :filename
AND f.revision = :old_revision
""")
old_result = db.execute(old_materials_query, {
"job_no": job_no,
"filename": filename,
"old_revision": old_revision
})
old_materials = old_result.fetchall()
# 새 리비전 자재 조회
new_materials_query = text("""
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
m.material_grade, m.classified_category, m.classification_confidence
FROM materials m
JOIN files f ON m.file_id = f.id
WHERE f.job_no = :job_no
AND f.original_filename = :filename
AND f.revision = :new_revision
""")
new_result = db.execute(new_materials_query, {
"job_no": job_no,
"filename": filename,
"new_revision": new_revision
})
new_materials = new_result.fetchall()
# 자재 키 생성 함수
def create_material_key(material):
return f"{material.original_description}_{material.size_spec}_{material.material_grade}"
# 기존 자재를 딕셔너리로 변환
old_materials_dict = {}
for material in old_materials:
key = create_material_key(material)
old_materials_dict[key] = {
"original_description": material.original_description,
"quantity": float(material.quantity) if material.quantity else 0,
"unit": material.unit,
"size_spec": material.size_spec,
"material_grade": material.material_grade,
"classified_category": material.classified_category,
"classification_confidence": material.classification_confidence
}
# 새 자재를 딕셔너리로 변환
new_materials_dict = {}
for material in new_materials:
key = create_material_key(material)
new_materials_dict[key] = {
"original_description": material.original_description,
"quantity": float(material.quantity) if material.quantity else 0,
"unit": material.unit,
"size_spec": material.size_spec,
"material_grade": material.material_grade,
"classified_category": material.classified_category,
"classification_confidence": material.classification_confidence
}
# 변경 사항 분석
all_keys = set(old_materials_dict.keys()) | set(new_materials_dict.keys())
added_items = []
removed_items = []
changed_items = []
for key in all_keys:
old_item = old_materials_dict.get(key)
new_item = new_materials_dict.get(key)
if old_item and not new_item:
# 삭제된 항목
removed_items.append({
"key": key,
"item": old_item,
"change_type": "removed"
})
elif not old_item and new_item:
# 추가된 항목
added_items.append({
"key": key,
"item": new_item,
"change_type": "added"
})
elif old_item and new_item:
# 수량 변경 확인
if old_item["quantity"] != new_item["quantity"]:
changed_items.append({
"key": key,
"old_item": old_item,
"new_item": new_item,
"quantity_change": new_item["quantity"] - old_item["quantity"],
"change_type": "quantity_changed"
})
# 분류별 통계
def calculate_category_stats(items):
stats = {}
for item in items:
category = item.get("item", {}).get("classified_category", "OTHER")
if category not in stats:
stats[category] = {"count": 0, "total_quantity": 0}
stats[category]["count"] += 1
stats[category]["total_quantity"] += item.get("item", {}).get("quantity", 0)
return stats
added_stats = calculate_category_stats(added_items)
removed_stats = calculate_category_stats(removed_items)
changed_stats = calculate_category_stats(changed_items)
return {
"success": True,
"comparison": {
"old_revision": old_revision,
"new_revision": new_revision,
"filename": filename,
"job_no": job_no,
"summary": {
"added_count": len(added_items),
"removed_count": len(removed_items),
"changed_count": len(changed_items),
"total_changes": len(added_items) + len(removed_items) + len(changed_items)
},
"changes": {
"added": added_items,
"removed": removed_items,
"changed": changed_items
},
"category_stats": {
"added": added_stats,
"removed": removed_stats,
"changed": changed_stats
}
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"리비전 비교 실패: {str(e)}")
@router.post("/materials/update-classification-details")
async def update_classification_details(
file_id: Optional[int] = None,
db: Session = Depends(get_db)
):
"""기존 자재들의 classification_details 업데이트"""
try:
# 업데이트할 자재들 조회
query = """
SELECT id, original_description, size_spec, classified_category
FROM materials
WHERE classification_details IS NULL OR classification_details = '{}'
"""
params = {}
if file_id:
query += " AND file_id = :file_id"
params["file_id"] = file_id
query += " ORDER BY id"
result = db.execute(text(query), params)
materials = result.fetchall()
if not materials:
return {
"success": True,
"message": "업데이트할 자재가 없습니다.",
"updated_count": 0
}
updated_count = 0
for material in materials:
material_id = material.id
description = material.original_description
size_spec = material.size_spec
category = material.classified_category
print(f"자재 {material_id} 재분류 중: {description}")
# 카테고리별로 적절한 분류기 호출
classification_result = None
if category == 'PIPE':
classification_result = classify_pipe("", description, size_spec, 0.0)
elif category == 'FITTING':
classification_result = classify_fitting("", description, size_spec)
elif category == 'VALVE':
classification_result = classify_valve("", description, size_spec)
elif category == 'FLANGE':
classification_result = classify_flange("", description, size_spec)
elif category == 'BOLT':
classification_result = classify_bolt("", description, size_spec)
elif category == 'GASKET':
classification_result = classify_gasket("", description, size_spec)
elif category == 'INSTRUMENT':
classification_result = classify_instrument("", description, size_spec)
else:
# 카테고리가 없으면 모든 분류기 시도
classification_result = classify_pipe("", description, size_spec, 0.0)
if classification_result.get("overall_confidence", 0) < 0.5:
classification_result = classify_fitting("", description, size_spec)
if classification_result.get("overall_confidence", 0) < 0.5:
classification_result = classify_valve("", description, size_spec)
if classification_result.get("overall_confidence", 0) < 0.5:
classification_result = classify_flange("", description, size_spec)
if classification_result.get("overall_confidence", 0) < 0.5:
classification_result = classify_bolt("", description, size_spec)
if classification_result.get("overall_confidence", 0) < 0.5:
classification_result = classify_gasket("", description, size_spec)
if classification_result.get("overall_confidence", 0) < 0.5:
classification_result = classify_instrument("", description, size_spec)
if classification_result:
# classification_details를 JSON으로 직렬화
classification_details = json.dumps(classification_result, ensure_ascii=False)
# DB 업데이트
update_query = text("""
UPDATE materials
SET classification_details = :classification_details,
updated_at = NOW()
WHERE id = :material_id
""")
db.execute(update_query, {
"material_id": material_id,
"classification_details": classification_details
})
updated_count += 1
print(f"자재 {material_id} 업데이트 완료")
db.commit()
return {
"success": True,
"message": f"{updated_count}개 자재의 분류 상세정보가 업데이트되었습니다.",
"updated_count": updated_count,
"total_materials": len(materials)
}
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=f"분류 상세정보 업데이트 실패: {str(e)}")

View File

@@ -440,6 +440,7 @@ def parse_file(file_path: str) -> List[Dict]:
column_mapping = {
'description': ['DESCRIPTION', 'Description', 'description', 'DESC', 'Desc', 'desc', 'ITEM', 'Item', 'item'],
'quantity': ['QTY', 'Quantity', 'quantity', 'QTY.', 'Qty', 'qty', 'AMOUNT', 'Amount', 'amount'],
'length': ['LENGTH', 'Length', 'length', 'LG', 'Lg', 'lg', 'LENGTH_MM', 'Length_mm', 'length_mm'],
'unit': ['UNIT', 'Unit', 'unit', 'UOM', 'Uom', 'uom'],
'drawing': ['DRAWING', 'Drawing', 'drawing', 'DWG', 'Dwg', 'dwg'],
'area': ['AREA', 'Area', 'area', 'AREA_CODE', 'Area_Code', 'area_code'],
@@ -466,6 +467,8 @@ def parse_file(file_path: str) -> List[Dict]:
description = str(row.get(found_columns.get('description', ''), '') or '')
quantity_raw = row.get(found_columns.get('quantity', 1), 1)
quantity = float(quantity_raw) if quantity_raw is not None else 1.0
length_raw = row.get(found_columns.get('length', 0), 0)
length = float(length_raw) if length_raw is not None else 0.0
unit = str(row.get(found_columns.get('unit', 'EA'), 'EA') or 'EA')
drawing = str(row.get(found_columns.get('drawing', ''), '') or '')
area = str(row.get(found_columns.get('area', ''), '') or '')
@@ -475,6 +478,7 @@ def parse_file(file_path: str) -> List[Dict]:
"line_number": index + 1,
"original_description": description,
"quantity": quantity,
"length": length,
"unit": unit,
"drawing_name": drawing,
"area_code": area,
@@ -505,42 +509,85 @@ def classify_material_item(material: Dict) -> Dict:
)
description = material.get("original_description", "")
size_spec = material.get("size_spec", "")
length = material.get("length", 0.0) # 길이 정보 추가
# 각 분류기로 분류 시도
classifiers = [
("PIPE", pipe_classifier.classify_pipe),
("FITTING", fitting_classifier.classify_fitting),
("BOLT", bolt_classifier.classify_bolt),
("VALVE", valve_classifier.classify_valve),
("INSTRUMENT", instrument_classifier.classify_instrument),
("FLANGE", flange_classifier.classify_flange),
("GASKET", gasket_classifier.classify_gasket)
]
print(f"분류 시도: {description}")
best_result = None
best_confidence = 0.0
# 각 분류기로 분류 시도 (개선된 순서와 기준)
desc_upper = description.upper()
for category, classifier_func in classifiers:
try:
result = classifier_func(description)
if result and result.get("confidence", 0) > best_confidence:
best_result = result
best_confidence = result.get("confidence", 0)
except Exception:
continue
# 1. 명확한 키워드 우선 확인 (높은 신뢰도)
if any(keyword in desc_upper for keyword in ['FLG', 'FLANGE', '플랜지', 'RF', 'WN', 'SO', 'BLIND']):
classification_result = flange_classifier.classify_flange("", description, size_spec, length)
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
elif any(keyword in desc_upper for keyword in ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', '밸브', '게이트', '']):
classification_result = valve_classifier.classify_valve("", description, size_spec, length)
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
elif any(keyword in desc_upper for keyword in ['ELBOW', 'ELL', 'TEE', 'REDUCER', 'CAP', 'COUPLING', '엘보', '', '리듀서', '']):
classification_result = fitting_classifier.classify_fitting("", description, size_spec, length)
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
elif any(keyword in desc_upper for keyword in ['BOLT', 'STUD', 'NUT', 'SCREW', '볼트', '너트', '스터드']):
classification_result = bolt_classifier.classify_bolt("", description, size_spec, length)
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
elif any(keyword in desc_upper for keyword in ['GASKET', 'GASK', '가스켓']):
classification_result = gasket_classifier.classify_gasket("", description, size_spec, length)
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
elif any(keyword in desc_upper for keyword in ['GAUGE', 'SENSOR', 'TRANSMITTER', 'INSTRUMENT', '계기', '게이지']):
classification_result = instrument_classifier.classify_instrument("", description, size_spec, length)
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
elif any(keyword in desc_upper for keyword in ['PIPE', 'TUBE', '파이프', '배관']):
classification_result = pipe_classifier.classify_pipe("", description, size_spec, length)
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
else:
# 2. 일반적인 분류 시도 (낮은 신뢰도 임계값)
classification_result = flange_classifier.classify_flange("", description, size_spec, length)
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
if classification_result.get("overall_confidence", 0) < 0.3:
classification_result = valve_classifier.classify_valve("", description, size_spec, length)
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
if classification_result.get("overall_confidence", 0) < 0.3:
classification_result = fitting_classifier.classify_fitting("", description, size_spec, length)
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
if classification_result.get("overall_confidence", 0) < 0.3:
classification_result = pipe_classifier.classify_pipe("", description, size_spec, length)
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
if classification_result.get("overall_confidence", 0) < 0.3:
classification_result = bolt_classifier.classify_bolt("", description, size_spec, length)
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
if classification_result.get("overall_confidence", 0) < 0.3:
classification_result = gasket_classifier.classify_gasket("", description, size_spec, length)
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
if classification_result.get("overall_confidence", 0) < 0.3:
classification_result = instrument_classifier.classify_instrument("", description, size_spec, length)
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
print(f"최종 분류 결과: {classification_result.get('category', 'UNKNOWN')}")
# 재질 분류
material_result = material_classifier.classify_material(description)
# 최종 결과 조합
# schedule이 딕셔너리인 경우 문자열로 변환
schedule_value = classification_result.get("schedule", "")
if isinstance(schedule_value, dict):
schedule_value = schedule_value.get("schedule", "")
final_result = {
**material,
"classified_category": best_result.get("category", "UNKNOWN") if best_result else "UNKNOWN",
"classified_subcategory": best_result.get("subcategory", "") if best_result else "",
"classified_category": classification_result.get("category", "UNKNOWN"),
"classified_subcategory": classification_result.get("subcategory", ""),
"material_grade": material_result.get("grade", "") if material_result else "",
"schedule": best_result.get("schedule", "") if best_result else "",
"size_spec": best_result.get("size_spec", "") if best_result else "",
"classification_confidence": best_confidence
"schedule": schedule_value,
"size_spec": classification_result.get("size_spec", ""),
"classification_confidence": classification_result.get("overall_confidence", 0.0),
"length": length # 길이 정보 추가
}
return final_result

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, Numeric, ForeignKey
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, Numeric, ForeignKey, JSON
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from datetime import datetime
@@ -59,12 +59,13 @@ class Material(Base):
size_spec = Column(String(50))
quantity = Column(Numeric(10, 3), nullable=False)
unit = Column(String(10), nullable=False)
# length = Column(Numeric(10, 3)) # 임시로 주석 처리
drawing_name = Column(String(100))
area_code = Column(String(20))
line_no = Column(String(50))
classification_confidence = Column(Numeric(3, 2))
is_verified = Column(Boolean, default=False)
verified_by = Column(String(100))
verified_by = Column(String(50))
verified_at = Column(DateTime)
drawing_reference = Column(String(100))
notes = Column(Text)
@@ -72,3 +73,239 @@ class Material(Base):
# 관계 설정
file = relationship("File", back_populates="materials")
# ========== 자재 규격/재질 기준표 테이블들 ==========
class MaterialStandard(Base):
"""자재 규격 표준 (ASTM, KS, JIS 등)"""
__tablename__ = "material_standards"
id = Column(Integer, primary_key=True, index=True)
standard_code = Column(String(20), unique=True, nullable=False, index=True) # ASTM_ASME, KS, JIS
standard_name = Column(String(100), nullable=False) # 미국재질학회, 한국산업표준, 일본공업규격
description = Column(Text)
country = Column(String(50)) # USA, KOREA, JAPAN
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
categories = relationship("MaterialCategory", back_populates="standard")
class MaterialCategory(Base):
"""제조방식별 카테고리 (FORGED, WELDED, CAST 등)"""
__tablename__ = "material_categories"
id = Column(Integer, primary_key=True, index=True)
standard_id = Column(Integer, ForeignKey("material_standards.id"))
category_code = Column(String(50), nullable=False) # FORGED_GRADES, WELDED_GRADES, CAST_GRADES
category_name = Column(String(100), nullable=False) # 단조품, 용접품, 주조품
description = Column(Text)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
standard = relationship("MaterialStandard", back_populates="categories")
specifications = relationship("MaterialSpecification", back_populates="category")
class MaterialSpecification(Base):
"""구체적인 규격 (A182, A105, D3507 등)"""
__tablename__ = "material_specifications"
id = Column(Integer, primary_key=True, index=True)
category_id = Column(Integer, ForeignKey("material_categories.id"))
spec_code = Column(String(20), nullable=False) # A182, A105, D3507
spec_name = Column(String(100), nullable=False) # 탄소강 단조품, 배관용 탄소강관
description = Column(Text)
material_type = Column(String(50)) # carbon_alloy, stainless, carbon
manufacturing = Column(String(50)) # FORGED, WELDED_FABRICATED, CAST, SEAMLESS
pressure_rating = Column(String(100)) # 150LB ~ 9000LB
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
category = relationship("MaterialCategory", back_populates="specifications")
grades = relationship("MaterialGrade", back_populates="specification")
patterns = relationship("MaterialPattern", back_populates="specification")
class MaterialGrade(Base):
"""등급별 상세 정보 (F1, F5, WPA, WPB 등)"""
__tablename__ = "material_grades"
id = Column(Integer, primary_key=True, index=True)
specification_id = Column(Integer, ForeignKey("material_specifications.id"))
grade_code = Column(String(20), nullable=False) # F1, F5, WPA, WPB
grade_name = Column(String(100))
composition = Column(String(200)) # 0.5Mo, 5Cr-0.5Mo, 18Cr-8Ni
applications = Column(String(200)) # 중온용, 고온용, 저압용
temp_max = Column(String(50)) # 482°C, 649°C
temp_range = Column(String(100)) # -29°C ~ 400°C
yield_strength = Column(String(50)) # 30 ksi, 35 ksi
tensile_strength = Column(String(50))
corrosion_resistance = Column(String(50)) # 보통, 우수
stabilizer = Column(String(50)) # Titanium, Niobium
base_grade = Column(String(20)) # 304, 316
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
specification = relationship("MaterialSpecification", back_populates="grades")
class MaterialPattern(Base):
"""정규식 패턴들"""
__tablename__ = "material_patterns"
id = Column(Integer, primary_key=True, index=True)
specification_id = Column(Integer, ForeignKey("material_specifications.id"))
pattern = Column(Text, nullable=False) # 정규식 패턴
description = Column(String(200))
priority = Column(Integer, default=1) # 패턴 우선순위
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
specification = relationship("MaterialSpecification", back_populates="patterns")
class SpecialMaterial(Base):
"""특수 재질 (INCONEL, HASTELLOY, TITANIUM 등)"""
__tablename__ = "special_materials"
id = Column(Integer, primary_key=True, index=True)
material_type = Column(String(50), nullable=False) # SUPER_ALLOYS, TITANIUM, COPPER_ALLOYS
material_name = Column(String(100), nullable=False) # INCONEL, HASTELLOY, TITANIUM
description = Column(Text)
composition = Column(String(200))
applications = Column(Text)
temp_max = Column(String(50))
manufacturing = Column(String(50))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
grades = relationship("SpecialMaterialGrade", back_populates="material")
patterns = relationship("SpecialMaterialPattern", back_populates="material")
class SpecialMaterialGrade(Base):
"""특수 재질 등급"""
__tablename__ = "special_material_grades"
id = Column(Integer, primary_key=True, index=True)
material_id = Column(Integer, ForeignKey("special_materials.id"))
grade_code = Column(String(20), nullable=False) # 600, 625, C276
composition = Column(String(200))
applications = Column(String(200))
temp_max = Column(String(50))
strength = Column(String(50))
purity = Column(String(100))
corrosion = Column(String(50))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
material = relationship("SpecialMaterial", back_populates="grades")
class SpecialMaterialPattern(Base):
"""특수 재질 정규식 패턴"""
__tablename__ = "special_material_patterns"
id = Column(Integer, primary_key=True, index=True)
material_id = Column(Integer, ForeignKey("special_materials.id"))
pattern = Column(Text, nullable=False)
description = Column(String(200))
priority = Column(Integer, default=1)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
material = relationship("SpecialMaterial", back_populates="patterns")
# ========== 파이프 상세 정보 및 사용자 요구사항 테이블 ==========
class PipeDetail(Base):
"""파이프 상세 정보"""
__tablename__ = "pipe_details"
id = Column(Integer, primary_key=True, index=True)
file_id = Column(Integer, ForeignKey("files.id"), nullable=False)
# 재질 정보
material_standard = Column(String(50)) # ASTM, KS, JIS 등
material_grade = Column(String(50)) # A106, A53, STPG370 등
material_type = Column(String(50)) # CARBON, STAINLESS 등
# 파이프 특화 정보
manufacturing_method = Column(String(50)) # SEAMLESS, WELDED, CAST
end_preparation = Column(String(50)) # BOTH_ENDS_BEVELED, ONE_END_BEVELED, NO_BEVEL
schedule = Column(String(50)) # SCH 10, 20, 40, 80 등
wall_thickness = Column(String(50)) # 벽두께 정보
# 치수 정보
nominal_size = Column(String(50)) # MAIN_NOM (인치, 직경)
length_mm = Column(Numeric(10, 3)) # LENGTH (길이)
# 신뢰도
material_confidence = Column(Numeric(3, 2))
manufacturing_confidence = Column(Numeric(3, 2))
end_prep_confidence = Column(Numeric(3, 2))
schedule_confidence = Column(Numeric(3, 2))
# 메타데이터
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
file = relationship("File", backref="pipe_details")
class RequirementType(Base):
"""요구사항 타입 마스터"""
__tablename__ = "requirement_types"
id = Column(Integer, primary_key=True, index=True)
type_code = Column(String(50), unique=True, nullable=False) # 'IMPACT_TEST', 'HEAT_TREATMENT' 등
type_name = Column(String(100), nullable=False) # '임팩테스트', '열처리' 등
category = Column(String(50), nullable=False) # 'TEST', 'TREATMENT', 'CERTIFICATION', 'CUSTOM' 등
description = Column(Text) # 타입 설명
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
requirements = relationship("UserRequirement", back_populates="requirement_type")
class UserRequirement(Base):
"""사용자 추가 요구사항"""
__tablename__ = "user_requirements"
id = Column(Integer, primary_key=True, index=True)
file_id = Column(Integer, ForeignKey("files.id"), nullable=False)
# 요구사항 타입
requirement_type = Column(String(50), nullable=False) # 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
# 요구사항 내용
requirement_title = Column(String(200), nullable=False) # '임팩테스트', '열처리', '인증서' 등
requirement_description = Column(Text) # 상세 설명
requirement_spec = Column(Text) # 구체적 스펙 (예: "Charpy V-notch -20°C")
# 상태 관리
status = Column(String(20), default='PENDING') # 'PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'
priority = Column(String(20), default='NORMAL') # 'LOW', 'NORMAL', 'HIGH', 'URGENT'
# 담당자 정보
assigned_to = Column(String(100)) # 담당자명
due_date = Column(DateTime) # 완료 예정일
# 메타데이터
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow)
# 관계 설정
file = relationship("File", backref="user_requirements")
requirement_type_rel = relationship("RequirementType", back_populates="requirements")

File diff suppressed because it is too large Load Diff

View File

@@ -183,7 +183,7 @@ BOLT_GRADES = {
}
}
def classify_bolt(dat_file: str, description: str, main_nom: str) -> Dict:
def classify_bolt(dat_file: str, description: str, main_nom: str, length: float = None) -> Dict:
"""
완전한 BOLT 분류

View File

@@ -86,11 +86,11 @@ FITTING_TYPES = {
},
"OLET": {
"dat_file_patterns": ["SOL_", "WOL_", "TOL_", "OLET_"],
"description_keywords": ["OLET", "올렛", "O-LET"],
"dat_file_patterns": ["SOL_", "WOL_", "TOL_", "OLET_", "SOCK-O-LET", "WELD-O-LET"],
"description_keywords": ["OLET", "올렛", "O-LET", "SOCK-O-LET", "WELD-O-LET", "SOCKOLET", "WELDOLET"],
"subtypes": {
"SOCKOLET": ["SOCK-O-LET", "SOCKOLET", "SOL"],
"WELDOLET": ["WELD-O-LET", "WELDOLET", "WOL"],
"SOCKOLET": ["SOCK-O-LET", "SOCKOLET", "SOL", "SOCK O-LET"],
"WELDOLET": ["WELD-O-LET", "WELDOLET", "WOL", "WELD O-LET"],
"THREADOLET": ["THREAD-O-LET", "THREADOLET", "TOL"],
"ELBOLET": ["ELB-O-LET", "ELBOLET", "EOL"]
},
@@ -171,7 +171,7 @@ PRESSURE_RATINGS = {
}
def classify_fitting(dat_file: str, description: str, main_nom: str,
red_nom: str = None) -> Dict:
red_nom: str = None, length: float = None) -> Dict:
"""
완전한 FITTING 분류
@@ -185,7 +185,21 @@ def classify_fitting(dat_file: str, description: str, main_nom: str,
완전한 피팅 분류 결과
"""
# 1. 재질 분류 (공통 모듈 사용)
desc_upper = description.upper()
dat_upper = dat_file.upper()
# 1. 명칭 우선 확인 (피팅 키워드가 있으면 피팅)
fitting_keywords = ['ELBOW', 'TEE', 'REDUCER', 'CAP', 'NIPPLE', 'SWAGE', 'OLET', 'COUPLING', '엘보', '', '리듀서', '', '니플', '스웨지', '올렛', '커플링', 'SOCK-O-LET', 'WELD-O-LET', 'SOCKOLET', 'WELDOLET']
is_fitting = any(keyword in desc_upper or keyword in dat_upper for keyword in fitting_keywords)
if not is_fitting:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "피팅 키워드 없음"
}
# 2. 재질 분류 (공통 모듈 사용)
material_result = classify_material(description)
# 2. 피팅 타입 분류
@@ -328,7 +342,7 @@ def classify_fitting_subtype(fitting_type: str, description: str,
# 2. 사이즈 분석이 필요한 경우 (TEE, REDUCER 등)
if type_data.get("size_analysis"):
if red_nom and red_nom.strip() and red_nom != main_nom:
if red_nom and str(red_nom).strip() and red_nom != main_nom:
return {
"subtype": "REDUCING",
"confidence": 0.85,
@@ -343,7 +357,7 @@ def classify_fitting_subtype(fitting_type: str, description: str,
# 3. 두 사이즈가 필요한 경우 확인
if type_data.get("requires_two_sizes"):
if red_nom and red_nom.strip():
if red_nom and str(red_nom).strip():
confidence = 0.8
evidence = [f"TWO_SIZES_PROVIDED: {main_nom} x {red_nom}"]
else:
@@ -511,11 +525,12 @@ def determine_fitting_manufacturing(material_result: Dict, connection_result: Di
def format_fitting_size(main_nom: str, red_nom: str = None) -> str:
"""피팅 사이즈 표기 포맷팅"""
if red_nom and red_nom.strip() and red_nom != main_nom:
return f"{main_nom} x {red_nom}"
main_nom_str = str(main_nom) if main_nom is not None else ""
red_nom_str = str(red_nom) if red_nom is not None else ""
if red_nom_str.strip() and red_nom_str != main_nom_str:
return f"{main_nom_str} x {red_nom_str}"
else:
return main_nom
return main_nom_str
def calculate_fitting_confidence(confidence_scores: Dict) -> float:
"""피팅 분류 전체 신뢰도 계산"""

View File

@@ -10,8 +10,8 @@ from .material_classifier import classify_material, get_manufacturing_method_fro
# ========== SPECIAL FLANGE 타입 ==========
SPECIAL_FLANGE_TYPES = {
"ORIFICE": {
"dat_file_patterns": ["FLG_ORI_", "ORI_"],
"description_keywords": ["ORIFICE", "오리피스", "유량측정"],
"dat_file_patterns": ["FLG_ORI_", "ORI_", "ORIFICE_"],
"description_keywords": ["ORIFICE", "오리피스", "유량측정", "구멍"],
"characteristics": "유량 측정용 구멍",
"special_features": ["BORE_SIZE", "FLOW_MEASUREMENT"]
},
@@ -164,7 +164,7 @@ FLANGE_PRESSURE_RATINGS = {
}
def classify_flange(dat_file: str, description: str, main_nom: str,
red_nom: str = None) -> Dict:
red_nom: str = None, length: float = None) -> Dict:
"""
완전한 FLANGE 분류
@@ -178,7 +178,21 @@ def classify_flange(dat_file: str, description: str, main_nom: str,
완전한 플랜지 분류 결과
"""
# 1. 재질 분류 (공통 모듈 사용)
desc_upper = description.upper()
dat_upper = dat_file.upper()
# 1. 명칭 우선 확인 (플랜지 키워드가 있으면 플랜지)
flange_keywords = ['FLG', 'FLANGE', '플랜지']
is_flange = any(keyword in desc_upper or keyword in dat_upper for keyword in flange_keywords)
if not is_flange:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "플랜지 키워드 없음"
}
# 2. 재질 분류 (공통 모듈 사용)
material_result = classify_material(description)
# 2. SPECIAL vs STANDARD 분류
@@ -490,11 +504,12 @@ def determine_flange_manufacturing(material_result: Dict, flange_type_result: Di
def format_flange_size(main_nom: str, red_nom: str = None) -> str:
"""플랜지 사이즈 표기 포맷팅"""
if red_nom and red_nom.strip() and red_nom != main_nom:
return f"{main_nom} x {red_nom}"
main_nom_str = str(main_nom) if main_nom is not None else ""
red_nom_str = str(red_nom) if red_nom is not None else ""
if red_nom_str.strip() and red_nom_str != main_nom_str:
return f"{main_nom_str} x {red_nom_str}"
else:
return main_nom
return main_nom_str
def calculate_flange_confidence(confidence_scores: Dict) -> float:
"""플랜지 분류 전체 신뢰도 계산"""

View File

@@ -160,7 +160,7 @@ GASKET_SIZE_PATTERNS = {
"thickness": r"THK?\s*(\d+(?:\.\d+)?)\s*MM"
}
def classify_gasket(dat_file: str, description: str, main_nom: str) -> Dict:
def classify_gasket(dat_file: str, description: str, main_nom: str, length: float = None) -> Dict:
"""
완전한 GASKET 분류

View File

@@ -46,7 +46,7 @@ INSTRUMENT_TYPES = {
}
}
def classify_instrument(dat_file: str, description: str, main_nom: str) -> Dict:
def classify_instrument(dat_file: str, description: str, main_nom: str, length: float = None) -> Dict:
"""
간단한 INSTRUMENT 분류

View File

@@ -23,7 +23,7 @@ def classify_material(description: str) -> Dict:
재질 분류 결과 딕셔너리
"""
desc_upper = description.upper().strip()
desc_upper = str(description).upper().strip() if description is not None else ""
# 1단계: 특수 재질 우선 확인 (가장 구체적)
special_result = check_special_materials(desc_upper)

View File

@@ -63,7 +63,7 @@ PIPE_SCHEDULE = {
}
def classify_pipe(dat_file: str, description: str, main_nom: str,
length: float = None) -> Dict:
length: Optional[float] = None) -> Dict:
"""
완전한 PIPE 분류
@@ -77,7 +77,38 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
완전한 파이프 분류 결과
"""
# 1. 재질 분류 (공통 모듈 사용)
desc_upper = description.upper()
# 1. 명칭 우선 확인 (다른 자재 타입 키워드가 있으면 파이프가 아님)
other_material_keywords = [
'FLG', 'FLANGE', '플랜지', # 플랜지
'ELBOW', 'ELL', 'TEE', 'REDUCER', 'CAP', 'COUPLING', '엘보', '', '리듀서', # 피팅
'VALVE', 'BALL', 'GATE', 'GLOBE', 'CHECK', '밸브', # 밸브
'BOLT', 'STUD', 'NUT', 'SCREW', '볼트', '너트', # 볼트
'GASKET', 'GASK', '가스켓', # 가스켓
'GAUGE', 'SENSOR', 'TRANSMITTER', 'INSTRUMENT', '계기' # 계기
]
for keyword in other_material_keywords:
if keyword in desc_upper:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": f"다른 자재 키워드 발견: {keyword}"
}
# 2. 파이프 키워드 확인
pipe_keywords = ['PIPE', 'TUBE', '파이프', '배관']
is_pipe = any(keyword in desc_upper for keyword in pipe_keywords)
if not is_pipe:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "파이프 키워드 없음"
}
# 3. 재질 분류 (공통 모듈 사용)
material_result = classify_material(description)
# 2. 제조 방법 분류
@@ -89,8 +120,8 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
# 4. 스케줄 분류
schedule_result = classify_pipe_schedule(description)
# 5. 절단 치수 처리
cutting_dimensions = extract_pipe_cutting_dimensions(length, description)
# 5. 길이(절단 치수) 처리
length_info = extract_pipe_length_info(length, description)
# 6. 최종 결과 조합
return {
@@ -124,11 +155,11 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
"confidence": schedule_result.get('confidence', 0.0)
},
"cutting_dimensions": cutting_dimensions,
"length_info": length_info,
"size_info": {
"nominal_size": main_nom,
"length_mm": cutting_dimensions.get('length_mm')
"length_mm": length_info.get('length_mm')
},
# 전체 신뢰도
@@ -234,10 +265,10 @@ def classify_pipe_schedule(description: str) -> Dict:
"confidence": 0.0
}
def extract_pipe_cutting_dimensions(length: float, description: str) -> Dict:
"""파이프 절단 치수 정보 추출"""
def extract_pipe_length_info(length: Optional[float], description: str) -> Dict:
"""파이프 길이(절단 치수) 정보 추출"""
cutting_info = {
length_info = {
"length_mm": None,
"source": None,
"confidence": 0.0,
@@ -246,31 +277,31 @@ def extract_pipe_cutting_dimensions(length: float, description: str) -> Dict:
# 1. LENGTH 필드에서 추출 (우선)
if length and length > 0:
cutting_info.update({
length_info.update({
"length_mm": round(length, 1),
"source": "LENGTH_FIELD",
"confidence": 0.95,
"note": f"도면 명기 치수: {length}mm"
"note": f"도면 명기 길이: {length}mm"
})
# 2. DESCRIPTION에서 백업 추출
else:
desc_length = extract_length_from_description(description)
if desc_length:
cutting_info.update({
length_info.update({
"length_mm": desc_length,
"source": "DESCRIPTION_PARSED",
"confidence": 0.8,
"note": f"설명란에서 추출: {desc_length}mm"
})
else:
cutting_info.update({
length_info.update({
"source": "NO_LENGTH_INFO",
"confidence": 0.0,
"note": "절단 치수 정보 없음 - 도면 확인 필요"
"note": "길이 정보 없음 - 도면 확인 필요"
})
return cutting_info
return length_info
def extract_length_from_description(description: str) -> Optional[float]:
"""DESCRIPTION에서 길이 정보 추출"""
@@ -318,7 +349,7 @@ def generate_pipe_cutting_plan(pipe_data: Dict) -> Dict:
cutting_plan = {
"material_spec": f"{pipe_data['material']['grade']} {pipe_data['schedule']['schedule']}",
"length_mm": pipe_data['cutting_dimensions']['length_mm'],
"length_mm": pipe_data['length_info']['length_mm'],
"end_preparation": pipe_data['end_preparation']['cutting_note'],
"machining_required": pipe_data['end_preparation']['machining_required']
}
@@ -332,6 +363,6 @@ def generate_pipe_cutting_plan(pipe_data: Dict) -> Dict:
가공여부: {'베벨가공 필요' if cutting_plan['machining_required'] else '직각절단만'}
""".strip()
else:
cutting_plan["cutting_instruction"] = "도면 확인 후 절단 치수 입력 필요"
cutting_plan["cutting_instruction"] = "도면 확인 후 길이 정보 입력 필요"
return cutting_plan

View File

@@ -66,7 +66,7 @@ VALVE_TYPES = {
"RELIEF_VALVE": {
"dat_file_patterns": ["RELIEF_", "RV_", "PSV_"],
"description_keywords": ["RELIEF VALVE", "PRESSURE RELIEF", "SAFETY VALVE", "릴리프"],
"description_keywords": ["RELIEF VALVE", "PRESSURE RELIEF", "SAFETY VALVE", "PSV", "압력 안전", "릴리프"],
"characteristics": "안전 압력 방출용",
"typical_connections": ["FLANGED", "THREADED"],
"pressure_range": "150LB ~ 2500LB",
@@ -196,20 +196,34 @@ VALVE_PRESSURE_RATINGS = {
}
}
def classify_valve(dat_file: str, description: str, main_nom: str) -> Dict:
def classify_valve(dat_file: str, description: str, main_nom: str, length: float = None) -> Dict:
"""
완전한 VALVE 분류
Args:
dat_file: DAT_FILE 필드
description: DESCRIPTION 필드
main_nom: MAIN_NOM 필드 (밸브 사이즈)
main_nom: MAIN_NOM 필드 (사이즈)
Returns:
완전한 밸브 분류 결과
"""
# 1. 재질 분류 (공통 모듈 사용)
desc_upper = description.upper()
dat_upper = dat_file.upper()
# 1. 명칭 우선 확인 (밸브 키워드가 있으면 밸브)
valve_keywords = ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', 'BUTTERFLY', 'NEEDLE', 'RELIEF', 'SOLENOID', 'PLUG', '밸브', '게이트', '', '글로브', '체크', '버터플라이', '니들', '릴리프', '솔레노이드', '플러그']
is_valve = any(keyword in desc_upper or keyword in dat_upper for keyword in valve_keywords)
if not is_valve:
return {
"category": "UNKNOWN",
"overall_confidence": 0.0,
"reason": "밸브 키워드 없음"
}
# 2. 재질 분류 (공통 모듈 사용)
material_result = classify_material(description)
# 2. 밸브 타입 분류