feat: 자재 분류 시스템 대폭 개선
🔧 주요 개선사항: - EXCLUDE 분류기 추가 (WELD GAP 등 제외 대상 처리) - FITTING 분류기 키워드 확장 (ELL, RED 추가) - PIPE 재질 중복 문제 해결 (material_grade 파싱 개선) - NIPPLE 특별 처리 추가 (스케줄 + 길이 정보 포함) - OLET 타입 중복 표시 제거 📊 분류 정확도: - UNKNOWN: 0개 (100% 분류 성공) - EXCLUDE: 1,014개 (제외 대상) - 실제 자재: 1,823개 정확 분류 🎯 해결된 문제: - PIPE 재질 'ASTM A106 ASTM A106' → 'ASTM A106 GR B' - WELD GAP 오분류 → EXCLUDE 카테고리 - FITTING 키워드 인식 실패 → ELL, RED 키워드 추가 - 프론트엔드 중복 표시 제거
This commit is contained in:
@@ -374,15 +374,13 @@ async def upload_file(
|
||||
|
||||
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
|
||||
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, :size_inches, :schedule_type, :material_spec,
|
||||
:manufacturing_method, :length_mm, :outer_diameter_mm, :wall_thickness_mm,
|
||||
:weight_per_meter_kg, :classification_confidence, :additional_info
|
||||
:file_id, :outer_diameter, :schedule,
|
||||
:material_spec, :manufacturing_method, :length_mm
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -390,16 +388,12 @@ async def upload_file(
|
||||
"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', ''),
|
||||
"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,
|
||||
"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']}")
|
||||
@@ -713,7 +707,7 @@ async def get_materials(
|
||||
try:
|
||||
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.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,
|
||||
|
||||
@@ -236,656 +236,9 @@ async def root():
|
||||
# print(f"Jobs 조회 에러: {str(e)}")
|
||||
# return {"error": f"Jobs 조회 실패: {str(e)}"}
|
||||
|
||||
# 파일 업로드 API
|
||||
@app.post("/upload")
|
||||
async def upload_file(
|
||||
file: UploadFile = File(...),
|
||||
job_no: str = Form(...), # project_id 대신 job_no 사용
|
||||
bom_name: str = Form(""), # BOM 이름 추가
|
||||
bom_type: str = Form(""),
|
||||
revision: str = Form("Rev.0"),
|
||||
parent_bom_id: Optional[int] = Form(None),
|
||||
description: str = Form(""),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""파일 업로드 및 자재 분류 (자동 리비전 관리)"""
|
||||
try:
|
||||
print("=== main.py 업로드 API 호출됨 ===")
|
||||
print(f"파일명: {file.filename}")
|
||||
print(f"job_no: {job_no}")
|
||||
print(f"bom_name: {bom_name}")
|
||||
print(f"bom_type: {bom_type}")
|
||||
|
||||
# job_no로 job 확인
|
||||
job_query = text("SELECT job_no FROM jobs WHERE job_no = :job_no AND is_active = true")
|
||||
job_result = db.execute(job_query, {"job_no": job_no})
|
||||
job = job_result.fetchone()
|
||||
|
||||
if not job:
|
||||
return {"error": f"Job No. '{job_no}'에 해당하는 작업을 찾을 수 없습니다."}
|
||||
|
||||
# 업로드 디렉토리 생성
|
||||
upload_dir = "uploads"
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
# 파일 저장
|
||||
if file.filename:
|
||||
file_path = os.path.join(upload_dir, file.filename)
|
||||
print(f"파일 저장 경로: {file_path}")
|
||||
print(f"원본 파일명: {file.filename}")
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
print(f"파일 저장 완료: {file_path}")
|
||||
else:
|
||||
return {"error": "파일명이 없습니다."}
|
||||
|
||||
# 파일 크기 계산
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
# 파일 타입 결정
|
||||
file_type = "excel" if file.filename.endswith(('.xls', '.xlsx')) else "csv" if file.filename.endswith('.csv') else "unknown"
|
||||
|
||||
# BOM 종류별 자동 리비전 관리
|
||||
if bom_name and not parent_bom_id:
|
||||
# 같은 job_no의 같은 BOM 이름에 대한 최신 리비전 조회
|
||||
latest_revision_query = text("""
|
||||
SELECT revision FROM files
|
||||
WHERE job_no = :job_no AND bom_name = :bom_name
|
||||
ORDER BY revision DESC LIMIT 1
|
||||
""")
|
||||
|
||||
result = db.execute(latest_revision_query, {
|
||||
"job_no": job_no,
|
||||
"bom_name": bom_name
|
||||
})
|
||||
|
||||
latest_file = result.fetchone()
|
||||
if latest_file:
|
||||
# 기존 리비전이 있으면 다음 리비전 번호 생성
|
||||
current_rev = latest_file.revision
|
||||
if current_rev.startswith("Rev."):
|
||||
try:
|
||||
rev_num = int(current_rev.replace("Rev.", ""))
|
||||
revision = f"Rev.{rev_num + 1}"
|
||||
except ValueError:
|
||||
revision = "Rev.1"
|
||||
else:
|
||||
revision = "Rev.1"
|
||||
else:
|
||||
# 첫 번째 업로드인 경우 Rev.0
|
||||
revision = "Rev.0"
|
||||
|
||||
# 데이터베이스에 파일 정보 저장
|
||||
insert_query = text("""
|
||||
INSERT INTO files (
|
||||
job_no, filename, original_filename, file_path,
|
||||
file_size, upload_date, revision, file_type, uploaded_by, bom_name
|
||||
) VALUES (
|
||||
:job_no, :filename, :original_filename, :file_path,
|
||||
:file_size, NOW(), :revision, :file_type, :uploaded_by, :bom_name
|
||||
) RETURNING id
|
||||
""")
|
||||
|
||||
result = db.execute(insert_query, {
|
||||
"job_no": job_no,
|
||||
"filename": file.filename,
|
||||
"original_filename": file.filename,
|
||||
"file_path": file_path,
|
||||
"file_size": file_size,
|
||||
"revision": revision,
|
||||
"file_type": file_type,
|
||||
"uploaded_by": "system",
|
||||
"bom_name": bom_name
|
||||
})
|
||||
|
||||
file_id = result.fetchone()[0]
|
||||
|
||||
# 1차: 파일 파싱 (CSV/Excel 파일 읽기)
|
||||
materials_data = parse_file(file_path)
|
||||
|
||||
# 2차: 각 자재를 분류기로 분류
|
||||
classified_materials = []
|
||||
|
||||
# 리비전 업로드인 경우 기존 분류 정보 가져오기
|
||||
existing_classifications = {}
|
||||
if parent_bom_id:
|
||||
parent_materials = db.execute(
|
||||
text("SELECT original_description, classified_category, classified_subcategory, material_grade, schedule, size_spec FROM materials WHERE file_id = :file_id"),
|
||||
{"file_id": parent_bom_id}
|
||||
).fetchall()
|
||||
|
||||
for material in parent_materials:
|
||||
existing_classifications[material.original_description] = {
|
||||
"classified_category": material.classified_category,
|
||||
"classified_subcategory": material.classified_subcategory,
|
||||
"material_grade": material.material_grade,
|
||||
"schedule": material.schedule,
|
||||
"size_spec": material.size_spec
|
||||
}
|
||||
|
||||
for material in materials_data:
|
||||
# 리비전 업로드인 경우 기존 분류 사용, 아니면 새로 분류
|
||||
if parent_bom_id and material.get("original_description") in existing_classifications:
|
||||
existing_class = existing_classifications[material.get("original_description")]
|
||||
classified_material = {
|
||||
**material,
|
||||
**existing_class,
|
||||
"classification_confidence": 1.0 # 기존 분류이므로 높은 신뢰도
|
||||
}
|
||||
else:
|
||||
classified_material = classify_material_item(material)
|
||||
classified_materials.append(classified_material)
|
||||
|
||||
# 3차: 분류된 자재를 데이터베이스에 저장
|
||||
for material in classified_materials:
|
||||
insert_material_query = text("""
|
||||
INSERT INTO materials (
|
||||
file_id, line_number, original_description,
|
||||
classified_category, classified_subcategory,
|
||||
material_grade, schedule, size_spec,
|
||||
quantity, unit, drawing_name, area_code, line_no,
|
||||
classification_confidence, is_verified, created_at
|
||||
) VALUES (
|
||||
:file_id, :line_number, :original_description,
|
||||
:classified_category, :classified_subcategory,
|
||||
:material_grade, :schedule, :size_spec,
|
||||
:quantity, :unit, :drawing_name, :area_code, :line_no,
|
||||
:classification_confidence, :is_verified, NOW()
|
||||
)
|
||||
RETURNING id
|
||||
""")
|
||||
|
||||
result = db.execute(insert_material_query, {
|
||||
"file_id": file_id,
|
||||
"line_number": material.get("line_number", 0),
|
||||
"original_description": material.get("original_description", ""),
|
||||
"classified_category": material.get("classified_category", ""),
|
||||
"classified_subcategory": material.get("classified_subcategory", ""),
|
||||
"material_grade": material.get("material_grade", ""),
|
||||
"schedule": material.get("schedule", ""),
|
||||
"size_spec": material.get("size_spec", ""),
|
||||
"quantity": material.get("quantity", 0),
|
||||
"unit": material.get("unit", ""),
|
||||
"drawing_name": material.get("drawing_name", ""),
|
||||
"area_code": material.get("area_code", ""),
|
||||
"line_no": material.get("line_no", ""),
|
||||
"classification_confidence": material.get("classification_confidence", 0.0),
|
||||
"is_verified": False
|
||||
})
|
||||
|
||||
# 저장된 material의 ID 가져오기
|
||||
material_id = result.fetchone()[0]
|
||||
|
||||
# 카테고리별 상세 정보 저장
|
||||
category = material.get("classified_category", "")
|
||||
|
||||
if category == "PIPE" and "pipe_details" in material:
|
||||
pipe_details = material["pipe_details"]
|
||||
pipe_insert_query = text("""
|
||||
INSERT INTO pipe_details (
|
||||
material_id, file_id, nominal_size, schedule,
|
||||
material_standard, material_grade, material_type,
|
||||
manufacturing_method, length_mm
|
||||
) VALUES (
|
||||
:material_id, :file_id, :nominal_size, :schedule,
|
||||
:material_standard, :material_grade, :material_type,
|
||||
:manufacturing_method, :length_mm
|
||||
)
|
||||
""")
|
||||
db.execute(pipe_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"nominal_size": material.get("size_spec", ""),
|
||||
"schedule": pipe_details.get("schedule", material.get("schedule", "")),
|
||||
"material_standard": pipe_details.get("material_spec", material.get("material_grade", "")),
|
||||
"material_grade": material.get("material_grade", ""),
|
||||
"material_type": material.get("material_grade", "").split("-")[0] if material.get("material_grade", "") else "",
|
||||
"manufacturing_method": pipe_details.get("manufacturing_method", ""),
|
||||
"length_mm": material.get("length", 0.0) if material.get("length", 0.0) else 0.0 # 이미 mm 단위임
|
||||
})
|
||||
|
||||
elif category == "FITTING" and "fitting_details" in material:
|
||||
fitting_details = material["fitting_details"]
|
||||
fitting_insert_query = text("""
|
||||
INSERT INTO fitting_details (
|
||||
material_id, file_id, fitting_type, fitting_subtype,
|
||||
connection_method, pressure_rating, material_standard,
|
||||
material_grade, main_size, reduced_size
|
||||
) VALUES (
|
||||
:material_id, :file_id, :fitting_type, :fitting_subtype,
|
||||
:connection_method, :pressure_rating, :material_standard,
|
||||
:material_grade, :main_size, :reduced_size
|
||||
)
|
||||
""")
|
||||
db.execute(fitting_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"fitting_type": fitting_details.get("fitting_type", ""),
|
||||
"fitting_subtype": fitting_details.get("fitting_subtype", ""),
|
||||
"connection_method": fitting_details.get("connection_method", ""),
|
||||
"pressure_rating": fitting_details.get("pressure_rating", ""),
|
||||
"material_standard": fitting_details.get("material_standard", material.get("material_grade", "")),
|
||||
"material_grade": fitting_details.get("material_grade", material.get("material_grade", "")),
|
||||
"main_size": material.get("size_spec", ""),
|
||||
"reduced_size": fitting_details.get("reduced_size", "")
|
||||
})
|
||||
|
||||
elif category == "VALVE" and "valve_details" in material:
|
||||
valve_details = material["valve_details"]
|
||||
valve_insert_query = text("""
|
||||
INSERT INTO valve_details (
|
||||
material_id, file_id, valve_type, valve_subtype,
|
||||
actuator_type, connection_method, pressure_rating,
|
||||
body_material, size_inches
|
||||
) VALUES (
|
||||
:material_id, :file_id, :valve_type, :valve_subtype,
|
||||
:actuator_type, :connection_method, :pressure_rating,
|
||||
:body_material, :size_inches
|
||||
)
|
||||
""")
|
||||
db.execute(valve_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"valve_type": valve_details.get("valve_type", ""),
|
||||
"valve_subtype": valve_details.get("valve_subtype", ""),
|
||||
"actuator_type": valve_details.get("actuator_type", "MANUAL"),
|
||||
"connection_method": valve_details.get("connection_method", ""),
|
||||
"pressure_rating": valve_details.get("pressure_rating", ""),
|
||||
"body_material": material.get("material_grade", ""),
|
||||
"size_inches": material.get("size_spec", "")
|
||||
})
|
||||
|
||||
elif category == "FLANGE" and "flange_details" in material:
|
||||
flange_details = material["flange_details"]
|
||||
flange_insert_query = text("""
|
||||
INSERT INTO flange_details (
|
||||
material_id, file_id, flange_type, flange_subtype,
|
||||
facing_type, pressure_rating, material_standard,
|
||||
material_grade, size_inches
|
||||
) VALUES (
|
||||
:material_id, :file_id, :flange_type, :flange_subtype,
|
||||
:facing_type, :pressure_rating, :material_standard,
|
||||
:material_grade, :size_inches
|
||||
)
|
||||
""")
|
||||
db.execute(flange_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"flange_type": flange_details.get("flange_type", ""),
|
||||
"flange_subtype": flange_details.get("flange_subtype", ""),
|
||||
"facing_type": flange_details.get("facing_type", ""),
|
||||
"pressure_rating": flange_details.get("pressure_rating", ""),
|
||||
"material_standard": material.get("material_grade", ""),
|
||||
"material_grade": material.get("material_grade", ""),
|
||||
"size_inches": material.get("size_spec", "")
|
||||
})
|
||||
|
||||
elif category == "BOLT" and "bolt_details" in material:
|
||||
bolt_details = material["bolt_details"]
|
||||
bolt_insert_query = text("""
|
||||
INSERT INTO bolt_details (
|
||||
material_id, file_id, bolt_type, bolt_subtype,
|
||||
thread_standard, diameter, length, thread_pitch,
|
||||
material_standard, material_grade, coating
|
||||
) VALUES (
|
||||
:material_id, :file_id, :bolt_type, :bolt_subtype,
|
||||
:thread_standard, :diameter, :length, :thread_pitch,
|
||||
:material_standard, :material_grade, :coating
|
||||
)
|
||||
""")
|
||||
db.execute(bolt_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"bolt_type": bolt_details.get("bolt_type", ""),
|
||||
"bolt_subtype": bolt_details.get("bolt_subtype", ""),
|
||||
"thread_standard": bolt_details.get("thread_standard", ""),
|
||||
"diameter": material.get("size_spec", ""),
|
||||
"length": bolt_details.get("length", ""),
|
||||
"thread_pitch": bolt_details.get("thread_pitch", ""),
|
||||
"material_standard": material.get("material_grade", ""),
|
||||
"material_grade": material.get("material_grade", ""),
|
||||
"coating": bolt_details.get("coating", "")
|
||||
})
|
||||
|
||||
elif category == "GASKET" and "gasket_details" in material:
|
||||
gasket_details = material["gasket_details"]
|
||||
gasket_insert_query = text("""
|
||||
INSERT INTO gasket_details (
|
||||
material_id, file_id, gasket_type, gasket_material,
|
||||
flange_size, pressure_rating, temperature_range,
|
||||
thickness, inner_diameter, outer_diameter
|
||||
) VALUES (
|
||||
:material_id, :file_id, :gasket_type, :gasket_material,
|
||||
:flange_size, :pressure_rating, :temperature_range,
|
||||
:thickness, :inner_diameter, :outer_diameter
|
||||
)
|
||||
""")
|
||||
db.execute(gasket_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"gasket_type": gasket_details.get("gasket_type", ""),
|
||||
"gasket_material": gasket_details.get("gasket_material", ""),
|
||||
"flange_size": material.get("size_spec", ""),
|
||||
"pressure_rating": gasket_details.get("pressure_rating", ""),
|
||||
"temperature_range": gasket_details.get("temperature_range", ""),
|
||||
"thickness": gasket_details.get("thickness", ""),
|
||||
"inner_diameter": gasket_details.get("inner_diameter", ""),
|
||||
"outer_diameter": gasket_details.get("outer_diameter", "")
|
||||
})
|
||||
|
||||
elif category == "INSTRUMENT" and "instrument_details" in material:
|
||||
instrument_details = material["instrument_details"]
|
||||
instrument_insert_query = text("""
|
||||
INSERT INTO instrument_details (
|
||||
material_id, file_id, instrument_type, measurement_type,
|
||||
measurement_range, output_signal, connection_size,
|
||||
process_connection, accuracy_class
|
||||
) VALUES (
|
||||
:material_id, :file_id, :instrument_type, :measurement_type,
|
||||
:measurement_range, :output_signal, :connection_size,
|
||||
:process_connection, :accuracy_class
|
||||
)
|
||||
""")
|
||||
db.execute(instrument_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"instrument_type": instrument_details.get("instrument_type", ""),
|
||||
"measurement_type": instrument_details.get("measurement_type", ""),
|
||||
"measurement_range": instrument_details.get("measurement_range", ""),
|
||||
"output_signal": instrument_details.get("output_signal", ""),
|
||||
"connection_size": material.get("size_spec", ""),
|
||||
"process_connection": instrument_details.get("process_connection", ""),
|
||||
"accuracy_class": instrument_details.get("accuracy_class", "")
|
||||
})
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"file_id": file_id,
|
||||
"filename": file.filename,
|
||||
"materials_count": len(classified_materials),
|
||||
"revision": revision,
|
||||
"message": f"파일이 성공적으로 업로드되고 {len(classified_materials)}개의 자재가 분류되었습니다. (리비전: {revision})"
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"업로드 실패: {str(e)}")
|
||||
# HTTP 400 에러로 변경
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=400, detail=f"파일 업로드 및 분류 실패: {str(e)}")
|
||||
# 파일 업로드는 /files/upload 엔드포인트를 사용하세요 (routers/files.py)
|
||||
|
||||
def parse_file(file_path: str) -> List[Dict]:
|
||||
"""파일 파싱 (CSV/Excel)"""
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
try:
|
||||
print(f"parse_file 호출됨: {file_path}")
|
||||
print(f"파일 존재 여부: {os.path.exists(file_path)}")
|
||||
print(f"파일 확장자: {os.path.splitext(file_path)[1]}")
|
||||
|
||||
# 파일 확장자를 소문자로 변환하여 검증
|
||||
file_extension = os.path.splitext(file_path)[1].lower()
|
||||
print(f"소문자 변환된 확장자: {file_extension}")
|
||||
|
||||
if file_extension == '.csv':
|
||||
df = pd.read_csv(file_path)
|
||||
elif file_extension in ['.xls', '.xlsx']:
|
||||
df = pd.read_excel(file_path)
|
||||
else:
|
||||
print(f"지원되지 않는 파일 형식: {file_path}")
|
||||
print(f"파일 확장자: {file_extension}")
|
||||
raise ValueError("지원하지 않는 파일 형식입니다.")
|
||||
|
||||
print(f"파일 파싱 시작: {file_path}")
|
||||
print(f"데이터프레임 형태: {df.shape}")
|
||||
print(f"컬럼명: {list(df.columns)}")
|
||||
|
||||
# 컬럼명 매핑 (대소문자 구분 없이)
|
||||
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'],
|
||||
'size': ['SIZE', 'Size', 'size', 'NOM_SIZE', 'Nom_Size', 'nom_size', 'MAIN_NOM', 'Main_Nom', 'main_nom'],
|
||||
'drawing': ['DRAWING', 'Drawing', 'drawing', 'DWG', 'Dwg', 'dwg'],
|
||||
'area': ['AREA', 'Area', 'area', 'AREA_CODE', 'Area_Code', 'area_code'],
|
||||
'line': ['LINE', 'Line', 'line', 'LINE_NO', 'Line_No', 'line_no', 'PIPELINE', 'Pipeline', 'pipeline']
|
||||
}
|
||||
|
||||
# 실제 컬럼명 찾기
|
||||
found_columns = {}
|
||||
for target_col, possible_names in column_mapping.items():
|
||||
for col_name in possible_names:
|
||||
if col_name in df.columns:
|
||||
found_columns[target_col] = col_name
|
||||
break
|
||||
|
||||
print(f"찾은 컬럼 매핑: {found_columns}")
|
||||
|
||||
materials = []
|
||||
for index, row in df.iterrows():
|
||||
# 빈 행 건너뛰기
|
||||
if row.isna().all():
|
||||
continue
|
||||
|
||||
# 안전한 값 추출
|
||||
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')
|
||||
size = str(row.get(found_columns.get('size', ''), '') or '')
|
||||
drawing = str(row.get(found_columns.get('drawing', ''), '') or '')
|
||||
area = str(row.get(found_columns.get('area', ''), '') or '')
|
||||
line = str(row.get(found_columns.get('line', ''), '') or '')
|
||||
|
||||
material = {
|
||||
"line_number": index + 1,
|
||||
"original_description": description,
|
||||
"quantity": quantity,
|
||||
"length": length,
|
||||
"unit": unit,
|
||||
"size_spec": size,
|
||||
"drawing_name": drawing,
|
||||
"area_code": area,
|
||||
"line_no": line
|
||||
}
|
||||
|
||||
# 빈 설명은 건너뛰기
|
||||
if not material["original_description"] or material["original_description"].strip() == '':
|
||||
continue
|
||||
|
||||
materials.append(material)
|
||||
|
||||
print(f"파싱된 자재 수: {len(materials)}")
|
||||
if materials:
|
||||
print(f"첫 번째 자재 예시: {materials[0]}")
|
||||
|
||||
return materials
|
||||
except Exception as e:
|
||||
print(f"파일 파싱 오류: {str(e)}")
|
||||
raise Exception(f"파일 파싱 실패: {str(e)}")
|
||||
|
||||
def classify_material_item(material: Dict) -> Dict:
|
||||
"""개별 자재 분류"""
|
||||
from .services import (
|
||||
pipe_classifier, fitting_classifier, bolt_classifier,
|
||||
valve_classifier, instrument_classifier, flange_classifier,
|
||||
gasket_classifier, material_classifier
|
||||
)
|
||||
|
||||
description = material.get("original_description", "")
|
||||
size_spec = material.get("size_spec", "")
|
||||
length = material.get("length", 0.0) # 길이 정보 추가
|
||||
|
||||
print(f"분류 시도: {description}")
|
||||
|
||||
# 각 분류기로 분류 시도 (개선된 순서와 기준)
|
||||
desc_upper = description.upper()
|
||||
|
||||
# 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": classification_result.get("category", "UNKNOWN"),
|
||||
"classified_subcategory": classification_result.get("subcategory", ""),
|
||||
"material_grade": material_result.get("grade", "") if material_result else "",
|
||||
"schedule": schedule_value,
|
||||
"size_spec": classification_result.get("size_spec", ""),
|
||||
"classification_confidence": classification_result.get("overall_confidence", 0.0),
|
||||
"length": length # 길이 정보 추가
|
||||
}
|
||||
|
||||
# 카테고리별 상세 정보 추가
|
||||
category = classification_result.get("category", "")
|
||||
|
||||
if category == "PIPE":
|
||||
# PIPE 상세 정보 추출
|
||||
final_result["pipe_details"] = {
|
||||
"size_inches": size_spec,
|
||||
"schedule": classification_result.get("schedule", {}).get("schedule", ""),
|
||||
"material_spec": classification_result.get("material", {}).get("standard", ""),
|
||||
"manufacturing_method": classification_result.get("manufacturing", {}).get("method", ""),
|
||||
"length_mm": length * 1000 if length else 0, # meter to mm
|
||||
"outer_diameter_mm": 0.0, # 추후 계산
|
||||
"wall_thickness_mm": 0.0, # 추후 계산
|
||||
"weight_per_meter_kg": 0.0 # 추후 계산
|
||||
}
|
||||
elif category == "FITTING":
|
||||
# FITTING 상세 정보 추출
|
||||
final_result["fitting_details"] = {
|
||||
"fitting_type": classification_result.get("fitting_type", {}).get("type", ""),
|
||||
"fitting_subtype": classification_result.get("fitting_type", {}).get("subtype", ""),
|
||||
"connection_method": classification_result.get("connection_method", {}).get("method", ""),
|
||||
"pressure_rating": classification_result.get("pressure_rating", {}).get("rating", ""),
|
||||
"material_standard": classification_result.get("material", {}).get("standard", ""),
|
||||
"material_grade": classification_result.get("material", {}).get("grade", ""),
|
||||
"main_size": size_spec,
|
||||
"reduced_size": ""
|
||||
}
|
||||
elif category == "VALVE":
|
||||
# VALVE 상세 정보 추출
|
||||
final_result["valve_details"] = {
|
||||
"valve_type": classification_result.get("valve_type", {}).get("type", ""),
|
||||
"valve_subtype": classification_result.get("valve_type", {}).get("subtype", ""),
|
||||
"actuator_type": classification_result.get("actuation", {}).get("method", "MANUAL"),
|
||||
"connection_method": classification_result.get("connection_method", {}).get("method", ""),
|
||||
"pressure_rating": classification_result.get("pressure_rating", {}).get("rating", ""),
|
||||
"body_material": classification_result.get("material", {}).get("grade", ""),
|
||||
"size_inches": size_spec
|
||||
}
|
||||
elif category == "FLANGE":
|
||||
# FLANGE 상세 정보 추출
|
||||
final_result["flange_details"] = {
|
||||
"flange_type": classification_result.get("flange_type", {}).get("type", ""),
|
||||
"flange_subtype": classification_result.get("flange_type", {}).get("subtype", ""),
|
||||
"facing_type": classification_result.get("face_finish", {}).get("finish", ""),
|
||||
"pressure_rating": classification_result.get("pressure_rating", {}).get("rating", ""),
|
||||
"material_standard": classification_result.get("material", {}).get("standard", ""),
|
||||
"material_grade": classification_result.get("material", {}).get("grade", ""),
|
||||
"size_inches": size_spec
|
||||
}
|
||||
elif category == "BOLT":
|
||||
# BOLT 상세 정보 추출
|
||||
final_result["bolt_details"] = {
|
||||
"bolt_type": classification_result.get("fastener_type", {}).get("type", ""),
|
||||
"bolt_subtype": classification_result.get("fastener_type", {}).get("subtype", ""),
|
||||
"thread_standard": classification_result.get("thread_specification", {}).get("standard", ""),
|
||||
"diameter": classification_result.get("dimensions", {}).get("diameter", size_spec),
|
||||
"length": classification_result.get("dimensions", {}).get("length", ""),
|
||||
"thread_pitch": classification_result.get("thread_specification", {}).get("pitch", ""),
|
||||
"material_standard": classification_result.get("material", {}).get("standard", ""),
|
||||
"material_grade": classification_result.get("material", {}).get("grade", ""),
|
||||
"coating": ""
|
||||
}
|
||||
elif category == "GASKET":
|
||||
# GASKET 상세 정보 추출
|
||||
final_result["gasket_details"] = {
|
||||
"gasket_type": classification_result.get("gasket_type", {}).get("type", ""),
|
||||
"gasket_material": classification_result.get("gasket_material", {}).get("material", ""),
|
||||
"flange_size": size_spec,
|
||||
"pressure_rating": classification_result.get("pressure_rating", {}).get("rating", ""),
|
||||
"temperature_range": classification_result.get("gasket_material", {}).get("temperature_range", ""),
|
||||
"thickness": classification_result.get("size_info", {}).get("thickness", ""),
|
||||
"inner_diameter": classification_result.get("size_info", {}).get("inner_diameter", ""),
|
||||
"outer_diameter": classification_result.get("size_info", {}).get("outer_diameter", "")
|
||||
}
|
||||
elif category == "INSTRUMENT":
|
||||
# INSTRUMENT 상세 정보 추출
|
||||
final_result["instrument_details"] = {
|
||||
"instrument_type": classification_result.get("instrument_type", {}).get("type", ""),
|
||||
"measurement_type": "",
|
||||
"measurement_range": classification_result.get("measurement_info", {}).get("range", ""),
|
||||
"output_signal": classification_result.get("measurement_info", {}).get("signal_type", ""),
|
||||
"connection_size": size_spec,
|
||||
"process_connection": "",
|
||||
"accuracy_class": ""
|
||||
}
|
||||
|
||||
return final_result
|
||||
# parse_file과 classify_material_item 함수는 routers/files.py로 이동되었습니다
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
|
||||
@@ -104,13 +104,19 @@ def parse_dataframe(df):
|
||||
|
||||
material_grade = ""
|
||||
if "ASTM" in description.upper():
|
||||
astm_match = re.search(r'ASTM\s+([A-Z0-9\s]+)', description.upper())
|
||||
# ASTM 표준과 등급만 추출, end_preparation(BOE, POE, BBE 등)은 제외
|
||||
astm_match = re.search(r'ASTM\s+([A-Z0-9]+(?:\s+GR\s+[A-Z0-9]+)?)', 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', ''), ''))
|
||||
|
||||
# main_nom과 red_nom 별도 저장 (원본 값 유지)
|
||||
main_nom = main_size if main_size != 'nan' and main_size != '' else None
|
||||
red_nom = red_size if red_size != 'nan' and red_size != '' else None
|
||||
|
||||
# 기존 size_spec도 유지 (호환성을 위해)
|
||||
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 != '':
|
||||
@@ -133,6 +139,8 @@ def parse_dataframe(df):
|
||||
'quantity': quantity,
|
||||
'unit': "EA",
|
||||
'size_spec': size_spec,
|
||||
'main_nom': main_nom, # 추가
|
||||
'red_nom': red_nom, # 추가
|
||||
'material_grade': material_grade,
|
||||
'length': length_value,
|
||||
'line_number': index + 1,
|
||||
@@ -230,32 +238,60 @@ async def upload_file(
|
||||
except (ValueError, TypeError):
|
||||
length_value = None
|
||||
|
||||
classification_result = classify_pipe("", description, size_spec, length_value)
|
||||
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
# main_nom과 red_nom 추출
|
||||
main_nom = material_data.get("main_nom")
|
||||
red_nom = material_data.get("red_nom")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_fitting("", description, size_spec)
|
||||
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_valve("", description, size_spec)
|
||||
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_flange("", description, size_spec)
|
||||
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_bolt("", description, size_spec)
|
||||
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_gasket("", description, size_spec)
|
||||
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_instrument("", description, size_spec)
|
||||
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
classification_result = None
|
||||
try:
|
||||
# EXCLUDE 분류기 우선 호출 (제외 대상 먼저 걸러냄)
|
||||
from app.services.exclude_classifier import classify_exclude
|
||||
classification_result = classify_exclude("", description, main_nom or "")
|
||||
print(f"EXCLUDE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
# 파이프 분류기 호출
|
||||
classification_result = classify_pipe("", description, main_nom or "", length_value)
|
||||
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
# 피팅 분류기 호출 (main_nom, red_nom 개별 전달)
|
||||
classification_result = classify_fitting("", description, main_nom or "", red_nom)
|
||||
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
# 플랜지 분류기 호출 (main_nom, red_nom 개별 전달)
|
||||
classification_result = classify_flange("", description, main_nom or "", red_nom)
|
||||
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
# 밸브 분류기 호출
|
||||
classification_result = classify_valve("", description, main_nom or "")
|
||||
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
# 볼트 분류기 호출
|
||||
classification_result = classify_bolt("", description, main_nom or "")
|
||||
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
# 가스켓 분류기 호출
|
||||
classification_result = classify_gasket("", description, main_nom or "")
|
||||
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
# 계기 분류기 호출
|
||||
classification_result = classify_instrument("", description, main_nom or "")
|
||||
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
except 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')}")
|
||||
|
||||
@@ -263,13 +299,13 @@ async def upload_file(
|
||||
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
|
||||
main_nom, red_nom, material_grade, line_number, row_number,
|
||||
classified_category, classification_confidence, 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
|
||||
:main_nom, :red_nom, :material_grade, :line_number, :row_number,
|
||||
:classified_category, :classification_confidence, :is_verified, :created_at
|
||||
)
|
||||
RETURNING id
|
||||
""")
|
||||
@@ -287,6 +323,8 @@ async def upload_file(
|
||||
"quantity": material_data["quantity"],
|
||||
"unit": material_data["unit"],
|
||||
"size_spec": material_data["size_spec"],
|
||||
"main_nom": material_data.get("main_nom"), # 추가
|
||||
"red_nom": material_data.get("red_nom"), # 추가
|
||||
"material_grade": material_data["material_grade"],
|
||||
"line_number": material_data["line_number"],
|
||||
"row_number": material_data["row_number"],
|
||||
@@ -309,16 +347,11 @@ async def upload_file(
|
||||
# material_id도 함께 저장하도록 수정
|
||||
pipe_detail_insert_query = text("""
|
||||
INSERT INTO pipe_details (
|
||||
material_id, file_id, material_standard, material_grade, material_type,
|
||||
manufacturing_method, end_preparation, schedule, wall_thickness,
|
||||
nominal_size, length_mm, material_confidence, manufacturing_confidence,
|
||||
end_prep_confidence, schedule_confidence
|
||||
)
|
||||
VALUES (
|
||||
:material_id, :file_id, :material_standard, :material_grade, :material_type,
|
||||
:manufacturing_method, :end_preparation, :schedule, :wall_thickness,
|
||||
:nominal_size, :length_mm, :material_confidence, :manufacturing_confidence,
|
||||
:end_prep_confidence, :schedule_confidence
|
||||
material_id, file_id, outer_diameter, schedule,
|
||||
material_spec, manufacturing_method, end_preparation, length_mm
|
||||
) VALUES (
|
||||
:material_id, :file_id, :outer_diameter, :schedule,
|
||||
:material_spec, :manufacturing_method, :end_preparation, :length_mm
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -329,25 +362,98 @@ async def upload_file(
|
||||
schedule_info = classification_result.get("schedule", {})
|
||||
size_info = classification_result.get("size_info", {})
|
||||
|
||||
# main_nom을 outer_diameter로 활용
|
||||
outer_diameter = material_data.get("main_nom") or material_data.get("size_spec", "")
|
||||
|
||||
# end_preparation 정보 추출 (분류 결과에서)
|
||||
end_prep = ""
|
||||
if isinstance(end_prep_info, dict):
|
||||
end_prep = end_prep_info.get("type", "")
|
||||
else:
|
||||
end_prep = str(end_prep_info) if end_prep_info else ""
|
||||
|
||||
# 재질 정보 - 이미 정제된 material_grade 사용
|
||||
material_spec = material_data.get("material_grade", "")
|
||||
|
||||
# 제조방법 추출
|
||||
manufacturing_method = ""
|
||||
if isinstance(manufacturing_info, dict):
|
||||
manufacturing_method = manufacturing_info.get("method", "UNKNOWN")
|
||||
else:
|
||||
manufacturing_method = str(manufacturing_info) if manufacturing_info else "UNKNOWN"
|
||||
|
||||
# 스케줄 정보 추출
|
||||
schedule = ""
|
||||
if isinstance(schedule_info, dict):
|
||||
schedule = schedule_info.get("schedule", "UNKNOWN")
|
||||
else:
|
||||
schedule = str(schedule_info) if schedule_info else "UNKNOWN"
|
||||
|
||||
db.execute(pipe_detail_insert_query, {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"material_standard": material_info.get("standard"),
|
||||
"material_grade": material_info.get("grade"),
|
||||
"material_type": material_info.get("material_type"),
|
||||
"manufacturing_method": manufacturing_info.get("method"),
|
||||
"end_preparation": end_prep_info.get("type"),
|
||||
"schedule": schedule_info.get("schedule"),
|
||||
"wall_thickness": schedule_info.get("wall_thickness"),
|
||||
"nominal_size": material_data.get("size_spec", ""), # material_data에서 직접 가져옴
|
||||
"length_mm": length_mm,
|
||||
"material_confidence": material_info.get("confidence", 0.0),
|
||||
"manufacturing_confidence": manufacturing_info.get("confidence", 0.0),
|
||||
"end_prep_confidence": end_prep_info.get("confidence", 0.0),
|
||||
"schedule_confidence": schedule_info.get("confidence", 0.0)
|
||||
"outer_diameter": outer_diameter,
|
||||
"schedule": schedule,
|
||||
"material_spec": material_spec,
|
||||
"manufacturing_method": manufacturing_method,
|
||||
"end_preparation": end_prep,
|
||||
"length_mm": material_data.get("length", 0.0) if material_data.get("length") else 0.0
|
||||
})
|
||||
|
||||
print("PIPE 상세 정보 저장 완료")
|
||||
|
||||
# FITTING 분류 결과인 경우 상세 정보 저장
|
||||
elif classification_result.get("category") == "FITTING":
|
||||
print("FITTING 상세 정보 저장 시작")
|
||||
|
||||
# 피팅 정보 추출
|
||||
fitting_type_info = classification_result.get("fitting_type", {})
|
||||
connection_info = classification_result.get("connection_method", {})
|
||||
pressure_info = classification_result.get("pressure_rating", {})
|
||||
material_info = classification_result.get("material", {})
|
||||
|
||||
# 피팅 타입 및 서브타입
|
||||
fitting_type = fitting_type_info.get("type", "UNKNOWN")
|
||||
fitting_subtype = fitting_type_info.get("subtype", "UNKNOWN")
|
||||
|
||||
# 연결 방식
|
||||
connection_method = connection_info.get("method", "UNKNOWN")
|
||||
|
||||
# 압력 등급
|
||||
pressure_rating = pressure_info.get("rating", "UNKNOWN")
|
||||
|
||||
# 재질 정보
|
||||
material_standard = material_info.get("standard", "")
|
||||
material_grade = material_info.get("grade", "")
|
||||
|
||||
# main_size와 reduced_size
|
||||
main_size = material_data.get("main_nom") or material_data.get("size_spec", "")
|
||||
reduced_size = material_data.get("red_nom", "")
|
||||
|
||||
db.execute(text("""
|
||||
INSERT INTO fitting_details (
|
||||
material_id, file_id, fitting_type, fitting_subtype,
|
||||
connection_method, pressure_rating, material_standard,
|
||||
material_grade, main_size, reduced_size
|
||||
) VALUES (
|
||||
:material_id, :file_id, :fitting_type, :fitting_subtype,
|
||||
:connection_method, :pressure_rating, :material_standard,
|
||||
:material_grade, :main_size, :reduced_size
|
||||
)
|
||||
"""), {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"fitting_type": fitting_type,
|
||||
"fitting_subtype": fitting_subtype,
|
||||
"connection_method": connection_method,
|
||||
"pressure_rating": pressure_rating,
|
||||
"material_standard": material_standard,
|
||||
"material_grade": material_grade,
|
||||
"main_size": main_size,
|
||||
"reduced_size": reduced_size
|
||||
})
|
||||
|
||||
print(f"FITTING 상세 정보 저장 완료: {fitting_type} - {fitting_subtype}")
|
||||
|
||||
db.commit()
|
||||
print(f"자재 저장 완료: {materials_inserted}개")
|
||||
@@ -457,14 +563,17 @@ async def get_materials(
|
||||
try:
|
||||
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.size_spec, m.main_nom, m.red_nom, m.material_grade, m.line_number, m.row_number,
|
||||
m.created_at, m.classified_category, m.classification_confidence,
|
||||
m.classification_details,
|
||||
f.original_filename, f.project_id, f.job_no, f.revision,
|
||||
p.official_project_code, p.project_name
|
||||
p.official_project_code, p.project_name,
|
||||
pd.outer_diameter, pd.schedule, pd.material_spec, pd.manufacturing_method,
|
||||
pd.end_preparation, pd.length_mm
|
||||
FROM materials m
|
||||
LEFT JOIN files f ON m.file_id = f.id
|
||||
LEFT JOIN projects p ON f.project_id = p.id
|
||||
LEFT JOIN pipe_details pd ON m.id = pd.material_id
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = {}
|
||||
@@ -579,6 +688,8 @@ async def get_materials(
|
||||
"quantity": float(m.quantity) if m.quantity else 0,
|
||||
"unit": m.unit,
|
||||
"size_spec": m.size_spec,
|
||||
"main_nom": m.main_nom, # 추가
|
||||
"red_nom": m.red_nom, # 추가
|
||||
"material_grade": m.material_grade,
|
||||
"line_number": m.line_number,
|
||||
"row_number": m.row_number,
|
||||
@@ -588,22 +699,17 @@ async def get_materials(
|
||||
"created_at": m.created_at
|
||||
}
|
||||
|
||||
# 카테고리별 상세 정보 추가
|
||||
# 카테고리별 상세 정보 추가 (JOIN 결과 사용)
|
||||
if m.classified_category == 'PIPE':
|
||||
pipe_query = text("SELECT * FROM pipe_details WHERE material_id = :material_id")
|
||||
pipe_result = db.execute(pipe_query, {"material_id": m.id})
|
||||
pipe_detail = pipe_result.fetchone()
|
||||
if pipe_detail:
|
||||
# JOIN된 결과에서 pipe_details 정보 가져오기
|
||||
if hasattr(m, 'outer_diameter') and m.outer_diameter is not None:
|
||||
material_dict['pipe_details'] = {
|
||||
"nominal_size": pipe_detail.nominal_size,
|
||||
"schedule": pipe_detail.schedule,
|
||||
"material_standard": pipe_detail.material_standard,
|
||||
"material_grade": pipe_detail.material_grade,
|
||||
"material_type": pipe_detail.material_type,
|
||||
"manufacturing_method": pipe_detail.manufacturing_method,
|
||||
"end_preparation": pipe_detail.end_preparation,
|
||||
"wall_thickness": pipe_detail.wall_thickness,
|
||||
"length_mm": float(pipe_detail.length_mm) if pipe_detail.length_mm else None
|
||||
"outer_diameter": m.outer_diameter,
|
||||
"schedule": m.schedule,
|
||||
"material_spec": m.material_spec,
|
||||
"manufacturing_method": m.manufacturing_method,
|
||||
"end_preparation": m.end_preparation,
|
||||
"length_mm": float(m.length_mm) if m.length_mm else None
|
||||
}
|
||||
elif m.classified_category == 'FITTING':
|
||||
fitting_query = text("SELECT * FROM fitting_details WHERE material_id = :material_id")
|
||||
@@ -906,19 +1012,12 @@ async def get_pipe_details(
|
||||
"original_description": pd.original_description,
|
||||
"quantity": pd.quantity,
|
||||
"unit": pd.unit,
|
||||
"material_standard": pd.material_standard,
|
||||
"material_grade": pd.material_grade,
|
||||
"material_type": pd.material_type,
|
||||
"material_spec": pd.material_spec,
|
||||
"manufacturing_method": pd.manufacturing_method,
|
||||
"end_preparation": pd.end_preparation,
|
||||
"schedule": pd.schedule,
|
||||
"wall_thickness": pd.wall_thickness,
|
||||
"nominal_size": pd.nominal_size,
|
||||
"outer_diameter": pd.outer_diameter,
|
||||
"length_mm": pd.length_mm,
|
||||
"material_confidence": pd.material_confidence,
|
||||
"manufacturing_confidence": pd.manufacturing_confidence,
|
||||
"end_prep_confidence": pd.end_prep_confidence,
|
||||
"schedule_confidence": pd.schedule_confidence,
|
||||
"created_at": pd.created_at,
|
||||
"updated_at": pd.updated_at
|
||||
}
|
||||
|
||||
85
backend/app/services/exclude_classifier.py
Normal file
85
backend/app/services/exclude_classifier.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
EXCLUDE 분류 시스템
|
||||
실제 자재가 아닌 계산용/제외 항목들 분류
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# ========== 제외 대상 타입 ==========
|
||||
EXCLUDE_TYPES = {
|
||||
"WELD_GAP": {
|
||||
"description_keywords": ["WELD GAP", "WELDING GAP", "GAP", "용접갭", "웰드갭"],
|
||||
"characteristics": "용접 시 수축 고려용 계산 항목",
|
||||
"reason": "실제 자재 아님 - 용접 갭 계산용"
|
||||
},
|
||||
"CUTTING_LOSS": {
|
||||
"description_keywords": ["CUTTING LOSS", "CUT LOSS", "절단로스", "컷팅로스"],
|
||||
"characteristics": "절단 시 손실 고려용 계산 항목",
|
||||
"reason": "실제 자재 아님 - 절단 로스 계산용"
|
||||
},
|
||||
"SPARE_ALLOWANCE": {
|
||||
"description_keywords": ["SPARE", "ALLOWANCE", "여유분", "스페어"],
|
||||
"characteristics": "예비품/여유분 계산 항목",
|
||||
"reason": "실제 자재 아님 - 여유분 계산용"
|
||||
},
|
||||
"THICKNESS_NOTE": {
|
||||
"description_keywords": ["THK", "THICK", "두께", "THICKNESS"],
|
||||
"characteristics": "두께 표기용 항목",
|
||||
"reason": "실제 자재 아님 - 두께 정보"
|
||||
},
|
||||
"CALCULATION_ITEM": {
|
||||
"description_keywords": ["CALC", "CALCULATION", "계산", "산정"],
|
||||
"characteristics": "기타 계산용 항목",
|
||||
"reason": "실제 자재 아님 - 계산 목적"
|
||||
}
|
||||
}
|
||||
|
||||
def classify_exclude(dat_file: str, description: str, main_nom: str = "") -> Dict:
|
||||
"""
|
||||
제외 대상 분류
|
||||
|
||||
Args:
|
||||
dat_file: DAT_FILE 필드
|
||||
description: DESCRIPTION 필드
|
||||
main_nom: MAIN_NOM 필드
|
||||
|
||||
Returns:
|
||||
제외 분류 결과
|
||||
"""
|
||||
|
||||
desc_upper = description.upper()
|
||||
|
||||
# 제외 대상 키워드 확인
|
||||
for exclude_type, type_data in EXCLUDE_TYPES.items():
|
||||
for keyword in type_data["description_keywords"]:
|
||||
if keyword in desc_upper:
|
||||
return {
|
||||
"category": "EXCLUDE",
|
||||
"exclude_type": exclude_type,
|
||||
"characteristics": type_data["characteristics"],
|
||||
"reason": type_data["reason"],
|
||||
"overall_confidence": 0.95,
|
||||
"evidence": [f"EXCLUDE_KEYWORD: {keyword}"],
|
||||
"recommendation": "BOM에서 제외 권장"
|
||||
}
|
||||
|
||||
# 제외 대상 아님
|
||||
return {
|
||||
"category": "UNKNOWN",
|
||||
"overall_confidence": 0.0,
|
||||
"reason": "제외 대상 키워드 없음"
|
||||
}
|
||||
|
||||
def is_exclude_item(description: str) -> bool:
|
||||
"""간단한 제외 대상 체크"""
|
||||
desc_upper = description.upper()
|
||||
|
||||
exclude_keywords = [
|
||||
"WELD GAP", "WELDING GAP", "GAP",
|
||||
"CUTTING LOSS", "CUT LOSS",
|
||||
"SPARE", "ALLOWANCE",
|
||||
"THK", "THICK"
|
||||
]
|
||||
|
||||
return any(keyword in desc_upper for keyword in exclude_keywords)
|
||||
@@ -39,7 +39,7 @@ FITTING_TYPES = {
|
||||
"dat_file_patterns": ["CNC_", "ECC_", "RED_", "REDUCER_"],
|
||||
"description_keywords": ["REDUCER", "RED", "리듀서"],
|
||||
"subtypes": {
|
||||
"CONCENTRIC": ["CONCENTRIC", "CNC", "동심", "CON"],
|
||||
"CONCENTRIC": ["CONCENTRIC", "CONC", "CNC", "동심", "CON"],
|
||||
"ECCENTRIC": ["ECCENTRIC", "ECC", "편심"]
|
||||
},
|
||||
"requires_two_sizes": True,
|
||||
@@ -59,6 +59,18 @@ FITTING_TYPES = {
|
||||
"size_range": "1/4\" ~ 24\""
|
||||
},
|
||||
|
||||
"PLUG": {
|
||||
"dat_file_patterns": ["PLUG_", "HEX_PLUG"],
|
||||
"description_keywords": ["PLUG", "플러그", "HEX.PLUG", "HEX PLUG", "HEXAGON PLUG"],
|
||||
"subtypes": {
|
||||
"HEX": ["HEX", "HEXAGON", "육각"],
|
||||
"SQUARE": ["SQUARE", "사각"],
|
||||
"THREADED": ["THD", "THREADED", "나사", "NPT"]
|
||||
},
|
||||
"common_connections": ["THREADED", "NPT"],
|
||||
"size_range": "1/8\" ~ 4\""
|
||||
},
|
||||
|
||||
"NIPPLE": {
|
||||
"dat_file_patterns": ["NIP_", "NIPPLE_"],
|
||||
"description_keywords": ["NIPPLE", "니플"],
|
||||
@@ -77,8 +89,8 @@ FITTING_TYPES = {
|
||||
"dat_file_patterns": ["SWG_"],
|
||||
"description_keywords": ["SWAGE", "스웨지"],
|
||||
"subtypes": {
|
||||
"CONCENTRIC": ["CONCENTRIC", "CN", "CON", "동심"],
|
||||
"ECCENTRIC": ["ECCENTRIC", "EC", "ECC", "편심"]
|
||||
"CONCENTRIC": ["CONCENTRIC", "CONC", "CN", "CON", "동심"],
|
||||
"ECCENTRIC": ["ECCENTRIC", "ECC", "EC", "편심"]
|
||||
},
|
||||
"requires_two_sizes": True,
|
||||
"common_connections": ["BUTT_WELD", "SOCKET_WELD"],
|
||||
@@ -87,12 +99,14 @@ FITTING_TYPES = {
|
||||
|
||||
"OLET": {
|
||||
"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"],
|
||||
"description_keywords": ["OLET", "올렛", "O-LET", "SOCK-O-LET", "WELD-O-LET", "SOCKOLET", "WELDOLET", "THREAD-O-LET", "THREADOLET", "SOCKLET", "SOCKET"],
|
||||
"subtypes": {
|
||||
"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"]
|
||||
"SOCKOLET": ["SOCK-O-LET", "SOCKOLET", "SOL", "SOCK O-LET", "SOCKET-O-LET", "SOCKLET"],
|
||||
"WELDOLET": ["WELD-O-LET", "WELDOLET", "WOL", "WELD O-LET", "WELDING-O-LET"],
|
||||
"THREADOLET": ["THREAD-O-LET", "THREADOLET", "TOL", "THREADED-O-LET"],
|
||||
"ELBOLET": ["ELB-O-LET", "ELBOLET", "EOL", "ELBOW-O-LET"],
|
||||
"NIPOLET": ["NIP-O-LET", "NIPOLET", "NOL", "NIPPLE-O-LET"],
|
||||
"COUPOLET": ["COUP-O-LET", "COUPOLET", "COL", "COUPLING-O-LET"]
|
||||
},
|
||||
"requires_two_sizes": True, # 주배관 x 분기관
|
||||
"common_connections": ["SOCKET_WELD", "THREADED", "BUTT_WELD"],
|
||||
@@ -189,7 +203,7 @@ def classify_fitting(dat_file: str, description: str, main_nom: str,
|
||||
dat_upper = dat_file.upper()
|
||||
|
||||
# 1. 명칭 우선 확인 (피팅 키워드가 있으면 피팅)
|
||||
fitting_keywords = ['ELBOW', 'TEE', 'REDUCER', 'CAP', 'NIPPLE', 'SWAGE', 'OLET', 'COUPLING', '엘보', '티', '리듀서', '캡', '니플', '스웨지', '올렛', '커플링', 'SOCK-O-LET', 'WELD-O-LET', 'SOCKOLET', 'WELDOLET']
|
||||
fitting_keywords = ['ELBOW', 'ELL', 'TEE', 'REDUCER', 'RED', 'CAP', 'NIPPLE', 'SWAGE', 'OLET', 'COUPLING', 'PLUG', 'SOCKLET', 'SOCKET', '엘보', '티', '리듀서', '캡', '니플', '스웨지', '올렛', '커플링', '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:
|
||||
|
||||
18
backend/scripts/06_add_main_red_nom_columns.sql
Normal file
18
backend/scripts/06_add_main_red_nom_columns.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- main_nom, red_nom 컬럼 추가 스크립트
|
||||
-- 2025.01.17 - MAIN_NOM/RED_NOM 별도 저장을 위한 스키마 개선
|
||||
|
||||
-- materials 테이블에 main_nom, red_nom 컬럼 추가
|
||||
ALTER TABLE materials ADD COLUMN IF NOT EXISTS main_nom VARCHAR(50);
|
||||
ALTER TABLE materials ADD COLUMN IF NOT EXISTS red_nom VARCHAR(50);
|
||||
|
||||
-- 인덱스 추가 (검색 성능 향상)
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_main_nom ON materials(main_nom);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_red_nom ON materials(red_nom);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_main_red_nom ON materials(main_nom, red_nom);
|
||||
|
||||
-- 기존 데이터에 대한 기본값 설정 (필요시)
|
||||
-- UPDATE materials SET main_nom = '', red_nom = '' WHERE main_nom IS NULL OR red_nom IS NULL;
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON COLUMN materials.main_nom IS 'MAIN_NOM 필드 - 주 사이즈 (예: 4", 150A)';
|
||||
COMMENT ON COLUMN materials.red_nom IS 'RED_NOM 필드 - 축소 사이즈 (Reducing 피팅/플랜지용)';
|
||||
68
backend/scripts/07_simplify_pipe_details_schema.sql
Normal file
68
backend/scripts/07_simplify_pipe_details_schema.sql
Normal file
@@ -0,0 +1,68 @@
|
||||
-- PIPE_DETAILS 테이블 간소화 스크립트
|
||||
-- 2025.01.17 - 실무 중심 구조 개선
|
||||
|
||||
-- 기존 테이블 백업
|
||||
CREATE TABLE IF NOT EXISTS pipe_details_backup AS SELECT * FROM pipe_details;
|
||||
|
||||
-- 새로운 간소화된 테이블 생성
|
||||
DROP TABLE IF EXISTS pipe_details_new;
|
||||
CREATE TABLE pipe_details_new (
|
||||
id SERIAL PRIMARY KEY,
|
||||
material_id INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
|
||||
-- 핵심 PIPE 정보 (실무 필수)
|
||||
outer_diameter VARCHAR(20), -- main_nom 기반 외경 정보
|
||||
schedule VARCHAR(10), -- SCH 40, SCH 80 등
|
||||
material_spec VARCHAR(100), -- 단일 재질 정보 (ASTM A106 GR B)
|
||||
manufacturing_method VARCHAR(20), -- SEAMLESS, WELDED, CAST
|
||||
end_preparation VARCHAR(20), -- POE, BOE, PEE, BEE 등
|
||||
length_mm DECIMAL(10,2), -- 길이 (mm)
|
||||
|
||||
-- 추가 정보 (cutting plan 용)
|
||||
area_number VARCHAR(10), -- 에리어 번호 (#01, #02)
|
||||
spool_number VARCHAR(10), -- 스풀 번호 (A, B, C)
|
||||
drawing_number VARCHAR(50), -- 도면 번호
|
||||
|
||||
-- 메타 정보
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 외래키
|
||||
FOREIGN KEY (material_id) REFERENCES materials(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 인덱스 추가 (성능)
|
||||
CREATE INDEX IF NOT EXISTS idx_pipe_details_material_id ON pipe_details_new(material_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipe_details_file_id ON pipe_details_new(file_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipe_details_outer_diameter ON pipe_details_new(outer_diameter);
|
||||
CREATE INDEX IF NOT EXISTS idx_pipe_details_schedule ON pipe_details_new(schedule);
|
||||
|
||||
-- 기존 데이터 마이그레이션 (가능한 것만)
|
||||
INSERT INTO pipe_details_new (
|
||||
material_id, file_id, outer_diameter, schedule, material_spec,
|
||||
manufacturing_method, length_mm
|
||||
)
|
||||
SELECT
|
||||
material_id,
|
||||
file_id,
|
||||
nominal_size as outer_diameter, -- nominal_size를 outer_diameter로
|
||||
schedule,
|
||||
COALESCE(material_standard, material_grade, material_type) as material_spec, -- 중복 필드 통합
|
||||
manufacturing_method,
|
||||
length_mm
|
||||
FROM pipe_details
|
||||
WHERE material_id IS NOT NULL;
|
||||
|
||||
-- 백업 테이블로 기존 테이블 교체
|
||||
DROP TABLE IF EXISTS pipe_details;
|
||||
ALTER TABLE pipe_details_new RENAME TO pipe_details;
|
||||
|
||||
-- 코멘트 추가
|
||||
COMMENT ON TABLE pipe_details IS '간소화된 PIPE 상세 정보 - 실무 중심 구조';
|
||||
COMMENT ON COLUMN pipe_details.outer_diameter IS '외경 정보 (main_nom에서 추출, 예: 4", 150A)';
|
||||
COMMENT ON COLUMN pipe_details.material_spec IS '통합 재질 정보 (예: ASTM A106 GR B)';
|
||||
COMMENT ON COLUMN pipe_details.end_preparation IS '끝단 가공 (POE-BOE, PEE, BEE 등)';
|
||||
COMMENT ON COLUMN pipe_details.area_number IS '에리어 번호 (cutting plan용)';
|
||||
COMMENT ON COLUMN pipe_details.spool_number IS '스풀 번호 (cutting plan용)';
|
||||
89
backend/test_main_red_nom.py
Normal file
89
backend/test_main_red_nom.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
main_nom, red_nom 기능 테스트 스크립트
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.services.fitting_classifier import classify_fitting
|
||||
from app.services.flange_classifier import classify_flange
|
||||
|
||||
def test_main_red_nom():
|
||||
"""main_nom과 red_nom 분류 테스트"""
|
||||
|
||||
print("🔧 main_nom/red_nom 분류 테스트 시작!")
|
||||
print("=" * 60)
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
"name": "일반 TEE (동일 사이즈)",
|
||||
"description": "TEE, SCH 40, ASTM A234 GR WPB",
|
||||
"main_nom": "4\"",
|
||||
"red_nom": None,
|
||||
"expected": "EQUAL TEE"
|
||||
},
|
||||
{
|
||||
"name": "리듀싱 TEE (다른 사이즈)",
|
||||
"description": "TEE RED, SCH 40 x SCH 40, ASTM A234 GR WPB",
|
||||
"main_nom": "4\"",
|
||||
"red_nom": "2\"",
|
||||
"expected": "REDUCING TEE"
|
||||
},
|
||||
{
|
||||
"name": "동심 리듀서",
|
||||
"description": "RED CONC, SCH 40 x SCH 40, ASTM A234 GR WPB",
|
||||
"main_nom": "6\"",
|
||||
"red_nom": "4\"",
|
||||
"expected": "CONCENTRIC REDUCER"
|
||||
},
|
||||
{
|
||||
"name": "리듀싱 플랜지",
|
||||
"description": "FLG REDUCING, 300LB, ASTM A105",
|
||||
"main_nom": "6\"",
|
||||
"red_nom": "4\"",
|
||||
"expected": "REDUCING FLANGE"
|
||||
}
|
||||
]
|
||||
|
||||
for i, test in enumerate(test_cases, 1):
|
||||
print(f"\n{i}. {test['name']}")
|
||||
print(f" 설명: {test['description']}")
|
||||
print(f" MAIN_NOM: {test['main_nom']}")
|
||||
print(f" RED_NOM: {test['red_nom']}")
|
||||
|
||||
# 피팅 분류 테스트
|
||||
fitting_result = classify_fitting(
|
||||
"",
|
||||
test['description'],
|
||||
test['main_nom'],
|
||||
test['red_nom']
|
||||
)
|
||||
|
||||
print(f" 🔧 FITTING 분류 결과:")
|
||||
print(f" 카테고리: {fitting_result.get('category')}")
|
||||
print(f" 타입: {fitting_result.get('fitting_type', {}).get('type')}")
|
||||
print(f" 서브타입: {fitting_result.get('fitting_type', {}).get('subtype')}")
|
||||
print(f" 신뢰도: {fitting_result.get('overall_confidence', 0):.2f}")
|
||||
|
||||
# 사이즈 정보 확인
|
||||
size_info = fitting_result.get('size_info', {})
|
||||
print(f" 메인 사이즈: {size_info.get('main_size')}")
|
||||
print(f" 축소 사이즈: {size_info.get('reduced_size')}")
|
||||
print(f" 사이즈 설명: {size_info.get('size_description')}")
|
||||
|
||||
# RED_NOM이 있는 경우 REDUCING 분류 확인
|
||||
if test['red_nom']:
|
||||
fitting_type = fitting_result.get('fitting_type', {})
|
||||
if 'REDUCING' in fitting_type.get('subtype', '').upper():
|
||||
print(f" ✅ REDUCING 타입 정상 인식!")
|
||||
else:
|
||||
print(f" ❌ REDUCING 타입 인식 실패")
|
||||
|
||||
print("-" * 50)
|
||||
|
||||
print("\n🎯 테스트 완료!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_main_red_nom()
|
||||
6
backend/test_sample.csv
Normal file
6
backend/test_sample.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
description,qty,main_nom,red_nom,length
|
||||
"TEE EQUAL, SCH 40, ASTM A234 GR WPB",2,4",,"
|
||||
"TEE RED, SCH 40 x SCH 40, ASTM A234 GR WPB",1,4",2","
|
||||
"RED CONC, SCH 40 x SCH 40, ASTM A234 GR WPB",1,6",4","
|
||||
"90 LR ELL, SCH 40, ASTM A234 GR WPB, SMLS",4,3",,"
|
||||
"PIPE SMLS, SCH 40, ASTM A106 GR B",1,2",,6000
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 42.
|
@@ -55,7 +55,7 @@ api.interceptors.response.use(
|
||||
export function uploadFile(formData, options = {}) {
|
||||
const config = {
|
||||
method: 'post',
|
||||
url: '/upload',
|
||||
url: '/files/upload',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
...options,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user