- 볼트 길이 추출 로직 개선: '70.0000 LG' 형태 인식 추가 - 재질 중복 표시 수정: 'ASTM A193 ASTM A193 B7' → 'B7' - A193/A194 등급 추출 로직 개선: 'GR B7/2H' 형태 지원 - bolt_details 테이블에 pressure_rating 컬럼 추가 - 볼트 분류기 오분류 방지: 플랜지/피팅이 볼트로 분류되지 않도록 수정 - 업로드 성능 개선: 키워드 기반 빠른 분류기 선택 로직 추가 - 분류 키워드 대폭 확장: 피팅/파이프/플랜지 키워드 추가
1181 lines
55 KiB
Python
1181 lines
55 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text
|
|
from typing import List, Optional
|
|
import os
|
|
import shutil
|
|
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()
|
|
|
|
UPLOAD_DIR = Path("uploads")
|
|
UPLOAD_DIR.mkdir(exist_ok=True)
|
|
ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"}
|
|
|
|
@router.get("/")
|
|
async def get_files_info():
|
|
return {
|
|
"message": "파일 관리 API",
|
|
"allowed_extensions": list(ALLOWED_EXTENSIONS),
|
|
"upload_directory": str(UPLOAD_DIR)
|
|
}
|
|
|
|
@router.get("/test")
|
|
async def test_endpoint():
|
|
return {"status": "파일 API가 정상 작동합니다!"}
|
|
|
|
@router.post("/add-missing-columns")
|
|
async def add_missing_columns(db: Session = Depends(get_db)):
|
|
"""누락된 컬럼들 추가"""
|
|
try:
|
|
db.execute(text("ALTER TABLE files ADD COLUMN IF NOT EXISTS parsed_count INTEGER DEFAULT 0"))
|
|
db.execute(text("ALTER TABLE materials ADD COLUMN IF NOT EXISTS row_number INTEGER"))
|
|
db.commit()
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "누락된 컬럼들이 추가되었습니다",
|
|
"added_columns": ["files.parsed_count", "materials.row_number"]
|
|
}
|
|
except Exception as e:
|
|
db.rollback()
|
|
return {"success": False, "error": f"컬럼 추가 실패: {str(e)}"}
|
|
|
|
def validate_file_extension(filename: str) -> bool:
|
|
return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
|
|
|
|
def generate_unique_filename(original_filename: str) -> str:
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
unique_id = str(uuid.uuid4())[:8]
|
|
stem = Path(original_filename).stem
|
|
suffix = Path(original_filename).suffix
|
|
return f"{stem}_{timestamp}_{unique_id}{suffix}"
|
|
|
|
def parse_dataframe(df):
|
|
df = df.dropna(how='all')
|
|
# 원본 컬럼명 유지 (소문자 변환하지 않음)
|
|
df.columns = df.columns.str.strip()
|
|
|
|
column_mapping = {
|
|
'description': ['description', 'item', 'material', '품명', '자재명'],
|
|
'quantity': ['qty', 'quantity', 'ea', '수량'],
|
|
'main_size': ['main_nom', 'nominal_diameter', 'nd', '주배관'],
|
|
'red_size': ['red_nom', 'reduced_diameter', '축소배관'],
|
|
'length': ['length', 'len', '길이'],
|
|
'weight': ['weight', 'wt', '중량'],
|
|
'dwg_name': ['dwg_name', 'drawing', '도면명'],
|
|
'line_num': ['line_num', 'line_number', '라인번호']
|
|
}
|
|
|
|
mapped_columns = {}
|
|
for standard_col, possible_names in column_mapping.items():
|
|
for possible_name in possible_names:
|
|
# 대소문자 구분 없이 매핑
|
|
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', ''), ''))
|
|
quantity_raw = row.get(mapped_columns.get('quantity', ''), 0)
|
|
|
|
try:
|
|
quantity = float(quantity_raw) if pd.notna(quantity_raw) else 0
|
|
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())
|
|
if astm_match:
|
|
material_grade = astm_match.group(0).strip()
|
|
|
|
main_size = str(row.get(mapped_columns.get('main_size', ''), ''))
|
|
red_size = str(row.get(mapped_columns.get('red_size', ''), ''))
|
|
|
|
if main_size != 'nan' and red_size != 'nan' and red_size != '':
|
|
size_spec = f"{main_size} x {red_size}"
|
|
elif main_size != 'nan' and main_size != '':
|
|
size_spec = main_size
|
|
else:
|
|
size_spec = ""
|
|
|
|
if description and description not in ['nan', 'None', '']:
|
|
materials.append({
|
|
'original_description': description,
|
|
'quantity': quantity,
|
|
'unit': "EA",
|
|
'size_spec': size_spec,
|
|
'material_grade': material_grade,
|
|
'length': length_value,
|
|
'line_number': index + 1,
|
|
'row_number': index + 1
|
|
})
|
|
|
|
return materials
|
|
|
|
def parse_file_data(file_path):
|
|
file_extension = Path(file_path).suffix.lower()
|
|
|
|
try:
|
|
if file_extension == ".csv":
|
|
df = pd.read_csv(file_path, encoding='utf-8')
|
|
elif file_extension in [".xlsx", ".xls"]:
|
|
df = pd.read_excel(file_path, sheet_name=0)
|
|
else:
|
|
raise HTTPException(status_code=400, detail="지원하지 않는 파일 형식")
|
|
|
|
return parse_dataframe(df)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"파일 파싱 실패: {str(e)}")
|
|
|
|
@router.post("/upload")
|
|
async def upload_file(
|
|
file: UploadFile = File(...),
|
|
project_id: int = Form(...),
|
|
revision: str = Form("Rev.0"),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
if not validate_file_extension(str(file.filename)):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"지원하지 않는 파일 형식입니다. 허용된 확장자: {', '.join(ALLOWED_EXTENSIONS)}"
|
|
)
|
|
|
|
if file.size and file.size > 10 * 1024 * 1024:
|
|
raise HTTPException(status_code=400, detail="파일 크기는 10MB를 초과할 수 없습니다")
|
|
|
|
unique_filename = generate_unique_filename(str(file.filename))
|
|
file_path = UPLOAD_DIR / unique_filename
|
|
|
|
try:
|
|
with open(file_path, "wb") as buffer:
|
|
shutil.copyfileobj(file.file, buffer)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}")
|
|
|
|
try:
|
|
materials_data = parse_file_data(str(file_path))
|
|
parsed_count = len(materials_data)
|
|
|
|
# 파일 정보 저장
|
|
file_insert_query = text("""
|
|
INSERT INTO files (filename, original_filename, file_path, project_id, revision, description, file_size, parsed_count, is_active)
|
|
VALUES (:filename, :original_filename, :file_path, :project_id, :revision, :description, :file_size, :parsed_count, :is_active)
|
|
RETURNING id
|
|
""")
|
|
|
|
file_result = db.execute(file_insert_query, {
|
|
"filename": unique_filename,
|
|
"original_filename": file.filename,
|
|
"file_path": str(file_path),
|
|
"project_id": project_id,
|
|
"revision": revision,
|
|
"description": f"BOM 파일 - {parsed_count}개 자재",
|
|
"file_size": file.size,
|
|
"parsed_count": parsed_count,
|
|
"is_active": True
|
|
})
|
|
|
|
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, 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, :classification_details, :is_verified, :created_at
|
|
)
|
|
""")
|
|
|
|
db.execute(material_insert_query, {
|
|
"file_id": file_id,
|
|
"original_description": material_data["original_description"],
|
|
"quantity": material_data["quantity"],
|
|
"unit": material_data["unit"],
|
|
"size_spec": material_data["size_spec"],
|
|
"material_grade": material_data["material_grade"],
|
|
"line_number": material_data["line_number"],
|
|
"row_number": material_data["row_number"],
|
|
"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, outer_diameter, schedule,
|
|
material_spec, manufacturing_method, length_mm
|
|
)
|
|
VALUES (
|
|
(SELECT id FROM materials WHERE file_id = :file_id AND original_description = :description AND row_number = :row_number),
|
|
:file_id, :outer_diameter, :schedule,
|
|
:material_spec, :manufacturing_method, :length_mm
|
|
)
|
|
""")
|
|
|
|
db.execute(pipe_insert_query, {
|
|
"file_id": file_id,
|
|
"description": material_data["original_description"],
|
|
"row_number": material_data["row_number"],
|
|
"outer_diameter": pipe_info.get('nominal_diameter', ''),
|
|
"schedule": pipe_info.get('schedule', ''),
|
|
"material_spec": pipe_info.get('material_spec', ''),
|
|
"manufacturing_method": pipe_info.get('manufacturing_method', ''),
|
|
"length_mm": length_mm,
|
|
|
|
})
|
|
|
|
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}")
|
|
|
|
elif category == 'FLANGE' and confidence >= 0.5:
|
|
try:
|
|
flange_info = classification_result
|
|
|
|
flange_insert_query = text("""
|
|
INSERT INTO flange_details (
|
|
material_id, file_id, flange_type, facing_type,
|
|
pressure_rating, material_standard, material_grade,
|
|
size_inches, 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, :flange_type, :facing_type,
|
|
:pressure_rating, :material_standard, :material_grade,
|
|
:size_inches, :classification_confidence, :additional_info
|
|
)
|
|
""")
|
|
|
|
db.execute(flange_insert_query, {
|
|
"file_id": file_id,
|
|
"description": material_data["original_description"],
|
|
"row_number": material_data["row_number"],
|
|
"flange_type": flange_info.get('flange_type', {}).get('type', ''),
|
|
"facing_type": flange_info.get('face_finish', {}).get('finish', ''),
|
|
"pressure_rating": flange_info.get('pressure_rating', {}).get('rating', ''),
|
|
"material_standard": flange_info.get('material', {}).get('standard', ''),
|
|
"material_grade": flange_info.get('material', {}).get('grade', ''),
|
|
"size_inches": material_data.get('size_spec', ''),
|
|
"classification_confidence": confidence,
|
|
"additional_info": json.dumps(flange_info, ensure_ascii=False)
|
|
})
|
|
|
|
print(f"FLANGE 상세정보 저장 완료: {material_data['original_description']}")
|
|
|
|
except Exception as e:
|
|
print(f"FLANGE 상세정보 저장 실패: {e}")
|
|
|
|
elif category == 'BOLT' and confidence >= 0.5:
|
|
try:
|
|
bolt_info = classification_result
|
|
|
|
bolt_insert_query = text("""
|
|
INSERT INTO bolt_details (
|
|
material_id, file_id, bolt_type, thread_type,
|
|
diameter, length, material_standard, material_grade,
|
|
coating_type, includes_nut, includes_washer,
|
|
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, :bolt_type, :thread_type,
|
|
:diameter, :length, :material_standard, :material_grade,
|
|
:coating_type, :includes_nut, :includes_washer,
|
|
:classification_confidence, :additional_info
|
|
)
|
|
""")
|
|
|
|
# BOLT 분류기 결과 구조에 맞게 데이터 추출
|
|
bolt_details = bolt_info.get('bolt_details', {})
|
|
material_info = bolt_info.get('material', {})
|
|
|
|
db.execute(bolt_insert_query, {
|
|
"file_id": file_id,
|
|
"description": material_data["original_description"],
|
|
"row_number": material_data["row_number"],
|
|
"bolt_type": bolt_details.get('type', ''),
|
|
"thread_type": bolt_details.get('thread_type', ''),
|
|
"diameter": bolt_details.get('diameter', ''),
|
|
"length": bolt_details.get('length', ''),
|
|
"material_standard": material_info.get('standard', ''),
|
|
"material_grade": material_info.get('grade', ''),
|
|
"coating_type": material_info.get('coating', ''),
|
|
"includes_nut": bolt_details.get('includes_nut', False),
|
|
"includes_washer": bolt_details.get('includes_washer', False),
|
|
"classification_confidence": confidence,
|
|
"additional_info": json.dumps(bolt_info, ensure_ascii=False)
|
|
})
|
|
|
|
print(f"BOLT 상세정보 저장 완료: {material_data['original_description']}")
|
|
|
|
except Exception as e:
|
|
print(f"BOLT 상세정보 저장 실패: {e}")
|
|
|
|
elif category == 'GASKET' and confidence >= 0.5:
|
|
try:
|
|
gasket_info = classification_result
|
|
|
|
gasket_insert_query = text("""
|
|
INSERT INTO gasket_details (
|
|
material_id, file_id, gasket_type, gasket_subtype,
|
|
material_type, size_inches, pressure_rating,
|
|
thickness, temperature_range, fire_safe,
|
|
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, :gasket_type, :gasket_subtype,
|
|
:material_type, :size_inches, :pressure_rating,
|
|
:thickness, :temperature_range, :fire_safe,
|
|
:classification_confidence, :additional_info
|
|
)
|
|
""")
|
|
|
|
# GASKET 분류기 결과 구조에 맞게 데이터 추출
|
|
gasket_type_info = gasket_info.get('gasket_type', {})
|
|
gasket_material_info = gasket_info.get('gasket_material', {})
|
|
pressure_info = gasket_info.get('pressure_rating', {})
|
|
size_info = gasket_info.get('size_info', {})
|
|
temp_info = gasket_info.get('temperature_info', {})
|
|
|
|
# SWG 상세 정보 추출
|
|
swg_details = gasket_material_info.get('swg_details', {})
|
|
additional_info = {
|
|
"swg_details": swg_details,
|
|
"face_type": swg_details.get('face_type', ''),
|
|
"construction": swg_details.get('detailed_construction', ''),
|
|
"filler": swg_details.get('filler', ''),
|
|
"outer_ring": swg_details.get('outer_ring', ''),
|
|
"inner_ring": swg_details.get('inner_ring', '')
|
|
}
|
|
|
|
db.execute(gasket_insert_query, {
|
|
"file_id": file_id,
|
|
"description": material_data["original_description"],
|
|
"row_number": material_data["row_number"],
|
|
"gasket_type": gasket_type_info.get('type', ''),
|
|
"gasket_subtype": gasket_type_info.get('subtype', ''),
|
|
"material_type": gasket_material_info.get('material', ''),
|
|
"size_inches": material_data.get('main_nom', '') or material_data.get('size_spec', ''),
|
|
"pressure_rating": pressure_info.get('rating', ''),
|
|
"thickness": swg_details.get('thickness', None),
|
|
"temperature_range": temp_info.get('range', ''),
|
|
"fire_safe": gasket_info.get('fire_safe', False),
|
|
"classification_confidence": confidence,
|
|
"additional_info": json.dumps(additional_info, ensure_ascii=False)
|
|
})
|
|
|
|
print(f"GASKET 상세정보 저장 완료: {material_data['original_description']}")
|
|
|
|
except Exception as e:
|
|
print(f"GASKET 상세정보 저장 실패: {e}")
|
|
|
|
elif category == 'INSTRUMENT' and confidence >= 0.5:
|
|
try:
|
|
inst_info = classification_result
|
|
|
|
inst_insert_query = text("""
|
|
INSERT INTO instrument_details (
|
|
material_id, file_id, instrument_type, instrument_subtype,
|
|
measurement_type, measurement_range, accuracy,
|
|
connection_type, connection_size, body_material,
|
|
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, :instrument_type, :instrument_subtype,
|
|
:measurement_type, :measurement_range, :accuracy,
|
|
:connection_type, :connection_size, :body_material,
|
|
:classification_confidence, :additional_info
|
|
)
|
|
""")
|
|
|
|
# INSTRUMENT 분류기 결과 구조에 맞게 데이터 추출
|
|
inst_type_info = inst_info.get('instrument_type', {})
|
|
measurement_info = inst_info.get('measurement', {})
|
|
connection_info = inst_info.get('connection', {})
|
|
|
|
db.execute(inst_insert_query, {
|
|
"file_id": file_id,
|
|
"description": material_data["original_description"],
|
|
"row_number": material_data["row_number"],
|
|
"instrument_type": inst_type_info.get('type', ''),
|
|
"instrument_subtype": inst_type_info.get('subtype', ''),
|
|
"measurement_type": measurement_info.get('type', ''),
|
|
"measurement_range": measurement_info.get('range', ''),
|
|
"accuracy": measurement_info.get('accuracy', ''),
|
|
"connection_type": connection_info.get('type', ''),
|
|
"connection_size": connection_info.get('size', ''),
|
|
"body_material": inst_info.get('material', ''),
|
|
"classification_confidence": confidence,
|
|
"additional_info": json.dumps(inst_info, ensure_ascii=False)
|
|
})
|
|
|
|
print(f"INSTRUMENT 상세정보 저장 완료: {material_data['original_description']}")
|
|
|
|
except Exception as e:
|
|
print(f"INSTRUMENT 상세정보 저장 실패: {e}")
|
|
|
|
materials_inserted += 1
|
|
|
|
db.commit()
|
|
|
|
return {
|
|
"success": True,
|
|
"message": f"완전한 DB 저장 성공! {materials_inserted}개 자재 저장됨",
|
|
"original_filename": file.filename,
|
|
"file_id": file_id,
|
|
"parsed_materials_count": parsed_count,
|
|
"saved_materials_count": materials_inserted,
|
|
"sample_materials": materials_data[:3] if materials_data else []
|
|
}
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
if os.path.exists(file_path):
|
|
os.remove(file_path)
|
|
raise HTTPException(status_code=500, detail=f"파일 처리 실패: {str(e)}")
|
|
@router.get("/materials")
|
|
async def get_materials(
|
|
project_id: Optional[int] = None,
|
|
file_id: Optional[int] = None,
|
|
job_no: Optional[str] = None,
|
|
filename: Optional[str] = None,
|
|
revision: Optional[str] = None,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
search: Optional[str] = None,
|
|
item_type: Optional[str] = None,
|
|
material_grade: Optional[str] = None,
|
|
size_spec: Optional[str] = None,
|
|
file_filter: Optional[str] = None,
|
|
sort_by: Optional[str] = None,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""
|
|
저장된 자재 목록 조회 (job_no, filename, revision 3가지로 필터링 가능)
|
|
"""
|
|
try:
|
|
query = """
|
|
SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit,
|
|
m.size_spec, m.main_nom, m.red_nom, 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
|
|
FROM materials m
|
|
LEFT JOIN files f ON m.file_id = f.id
|
|
LEFT JOIN projects p ON f.project_id = p.id
|
|
WHERE 1=1
|
|
"""
|
|
params = {}
|
|
if project_id:
|
|
query += " AND f.project_id = :project_id"
|
|
params["project_id"] = project_id
|
|
if file_id:
|
|
query += " AND m.file_id = :file_id"
|
|
params["file_id"] = file_id
|
|
if job_no:
|
|
query += " AND f.job_no = :job_no"
|
|
params["job_no"] = job_no
|
|
if filename:
|
|
query += " AND f.original_filename = :filename"
|
|
params["filename"] = filename
|
|
if revision:
|
|
query += " AND f.revision = :revision"
|
|
params["revision"] = revision
|
|
if search:
|
|
query += " AND (m.original_description ILIKE :search OR m.material_grade ILIKE :search)"
|
|
params["search"] = f"%{search}%"
|
|
if item_type:
|
|
query += " AND m.classified_category = :item_type"
|
|
params["item_type"] = item_type
|
|
if material_grade:
|
|
query += " AND m.material_grade ILIKE :material_grade"
|
|
params["material_grade"] = f"%{material_grade}%"
|
|
if size_spec:
|
|
query += " AND m.size_spec ILIKE :size_spec"
|
|
params["size_spec"] = f"%{size_spec}%"
|
|
if file_filter:
|
|
query += " AND f.original_filename ILIKE :file_filter"
|
|
params["file_filter"] = f"%{file_filter}%"
|
|
|
|
# 정렬 처리
|
|
if sort_by:
|
|
if sort_by == "quantity_desc":
|
|
query += " ORDER BY m.quantity DESC"
|
|
elif sort_by == "quantity_asc":
|
|
query += " ORDER BY m.quantity ASC"
|
|
elif sort_by == "name_asc":
|
|
query += " ORDER BY m.original_description ASC"
|
|
elif sort_by == "name_desc":
|
|
query += " ORDER BY m.original_description DESC"
|
|
elif sort_by == "created_desc":
|
|
query += " ORDER BY m.created_at DESC"
|
|
elif sort_by == "created_asc":
|
|
query += " ORDER BY m.created_at ASC"
|
|
else:
|
|
query += " ORDER BY m.line_number ASC"
|
|
else:
|
|
query += " ORDER BY m.line_number ASC"
|
|
|
|
query += " LIMIT :limit OFFSET :skip"
|
|
params["limit"] = limit
|
|
params["skip"] = skip
|
|
|
|
result = db.execute(text(query), params)
|
|
materials = result.fetchall()
|
|
|
|
# 전체 개수 조회
|
|
count_query = """
|
|
SELECT COUNT(*) as total
|
|
FROM materials m
|
|
LEFT JOIN files f ON m.file_id = f.id
|
|
WHERE 1=1
|
|
"""
|
|
count_params = {}
|
|
|
|
if project_id:
|
|
count_query += " AND f.project_id = :project_id"
|
|
count_params["project_id"] = project_id
|
|
|
|
if file_id:
|
|
count_query += " AND m.file_id = :file_id"
|
|
count_params["file_id"] = file_id
|
|
|
|
if search:
|
|
count_query += " AND (m.original_description ILIKE :search OR m.material_grade ILIKE :search)"
|
|
count_params["search"] = f"%{search}%"
|
|
|
|
if item_type:
|
|
count_query += " AND m.classified_category = :item_type"
|
|
count_params["item_type"] = item_type
|
|
|
|
if material_grade:
|
|
count_query += " AND m.material_grade ILIKE :material_grade"
|
|
count_params["material_grade"] = f"%{material_grade}%"
|
|
|
|
if size_spec:
|
|
count_query += " AND m.size_spec ILIKE :size_spec"
|
|
count_params["size_spec"] = f"%{size_spec}%"
|
|
|
|
if file_filter:
|
|
count_query += " AND f.original_filename ILIKE :file_filter"
|
|
count_params["file_filter"] = f"%{file_filter}%"
|
|
|
|
count_result = db.execute(text(count_query), count_params)
|
|
total_count = count_result.fetchone()[0]
|
|
|
|
return {
|
|
"success": True,
|
|
"total_count": total_count,
|
|
"returned_count": len(materials),
|
|
"skip": skip,
|
|
"limit": limit,
|
|
"materials": [
|
|
{
|
|
"id": m.id,
|
|
"file_id": m.file_id,
|
|
"filename": m.original_filename,
|
|
"project_id": m.project_id,
|
|
"project_code": m.official_project_code,
|
|
"project_name": m.project_name,
|
|
"original_description": m.original_description,
|
|
"quantity": float(m.quantity) if m.quantity else 0,
|
|
"unit": m.unit,
|
|
"size_spec": m.size_spec,
|
|
"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
|
|
]
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"자재 조회 실패: {str(e)}")
|
|
|
|
@router.get("/materials/summary")
|
|
async def get_materials_summary(
|
|
project_id: Optional[int] = None,
|
|
file_id: Optional[int] = None,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""자재 요약 통계"""
|
|
try:
|
|
query = """
|
|
SELECT
|
|
COUNT(*) as total_items,
|
|
COUNT(DISTINCT m.original_description) as unique_descriptions,
|
|
COUNT(DISTINCT m.size_spec) as unique_sizes,
|
|
COUNT(DISTINCT m.material_grade) as unique_materials,
|
|
SUM(m.quantity) as total_quantity,
|
|
AVG(m.quantity) as avg_quantity,
|
|
MIN(m.created_at) as earliest_upload,
|
|
MAX(m.created_at) as latest_upload
|
|
FROM materials m
|
|
LEFT JOIN files f ON m.file_id = f.id
|
|
WHERE 1=1
|
|
"""
|
|
|
|
params = {}
|
|
|
|
if project_id:
|
|
query += " AND f.project_id = :project_id"
|
|
params["project_id"] = project_id
|
|
|
|
if file_id:
|
|
query += " AND m.file_id = :file_id"
|
|
params["file_id"] = file_id
|
|
|
|
result = db.execute(text(query), params)
|
|
summary = result.fetchone()
|
|
|
|
return {
|
|
"success": True,
|
|
"summary": {
|
|
"total_items": summary.total_items,
|
|
"unique_descriptions": summary.unique_descriptions,
|
|
"unique_sizes": summary.unique_sizes,
|
|
"unique_materials": summary.unique_materials,
|
|
"total_quantity": float(summary.total_quantity) if summary.total_quantity else 0,
|
|
"avg_quantity": round(float(summary.avg_quantity), 2) if summary.avg_quantity else 0,
|
|
"earliest_upload": summary.earliest_upload,
|
|
"latest_upload": summary.latest_upload
|
|
}
|
|
}
|
|
|
|
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)}")
|