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

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

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

View File

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

View File

@@ -440,6 +440,7 @@ def parse_file(file_path: str) -> List[Dict]:
column_mapping = { column_mapping = {
'description': ['DESCRIPTION', 'Description', 'description', 'DESC', 'Desc', 'desc', 'ITEM', 'Item', 'item'], 'description': ['DESCRIPTION', 'Description', 'description', 'DESC', 'Desc', 'desc', 'ITEM', 'Item', 'item'],
'quantity': ['QTY', 'Quantity', 'quantity', 'QTY.', 'Qty', 'qty', 'AMOUNT', 'Amount', 'amount'], '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'], 'unit': ['UNIT', 'Unit', 'unit', 'UOM', 'Uom', 'uom'],
'drawing': ['DRAWING', 'Drawing', 'drawing', 'DWG', 'Dwg', 'dwg'], 'drawing': ['DRAWING', 'Drawing', 'drawing', 'DWG', 'Dwg', 'dwg'],
'area': ['AREA', 'Area', 'area', 'AREA_CODE', 'Area_Code', 'area_code'], 'area': ['AREA', 'Area', 'area', 'AREA_CODE', 'Area_Code', 'area_code'],
@@ -466,6 +467,8 @@ def parse_file(file_path: str) -> List[Dict]:
description = str(row.get(found_columns.get('description', ''), '') or '') description = str(row.get(found_columns.get('description', ''), '') or '')
quantity_raw = row.get(found_columns.get('quantity', 1), 1) quantity_raw = row.get(found_columns.get('quantity', 1), 1)
quantity = float(quantity_raw) if quantity_raw is not None else 1.0 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') unit = str(row.get(found_columns.get('unit', 'EA'), 'EA') or 'EA')
drawing = str(row.get(found_columns.get('drawing', ''), '') or '') drawing = str(row.get(found_columns.get('drawing', ''), '') or '')
area = str(row.get(found_columns.get('area', ''), '') or '') area = str(row.get(found_columns.get('area', ''), '') or '')
@@ -475,6 +478,7 @@ def parse_file(file_path: str) -> List[Dict]:
"line_number": index + 1, "line_number": index + 1,
"original_description": description, "original_description": description,
"quantity": quantity, "quantity": quantity,
"length": length,
"unit": unit, "unit": unit,
"drawing_name": drawing, "drawing_name": drawing,
"area_code": area, "area_code": area,
@@ -505,42 +509,85 @@ def classify_material_item(material: Dict) -> Dict:
) )
description = material.get("original_description", "") description = material.get("original_description", "")
size_spec = material.get("size_spec", "")
length = material.get("length", 0.0) # 길이 정보 추가
# 각 분류기로 분류 시도 print(f"분류 시도: {description}")
classifiers = [
("PIPE", pipe_classifier.classify_pipe),
("FITTING", fitting_classifier.classify_fitting),
("BOLT", bolt_classifier.classify_bolt),
("VALVE", valve_classifier.classify_valve),
("INSTRUMENT", instrument_classifier.classify_instrument),
("FLANGE", flange_classifier.classify_flange),
("GASKET", gasket_classifier.classify_gasket)
]
best_result = None # 각 분류기로 분류 시도 (개선된 순서와 기준)
best_confidence = 0.0 desc_upper = description.upper()
for category, classifier_func in classifiers: # 1. 명확한 키워드 우선 확인 (높은 신뢰도)
try: if any(keyword in desc_upper for keyword in ['FLG', 'FLANGE', '플랜지', 'RF', 'WN', 'SO', 'BLIND']):
result = classifier_func(description) classification_result = flange_classifier.classify_flange("", description, size_spec, length)
if result and result.get("confidence", 0) > best_confidence: print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
best_result = result elif any(keyword in desc_upper for keyword in ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', '밸브', '게이트', '']):
best_confidence = result.get("confidence", 0) classification_result = valve_classifier.classify_valve("", description, size_spec, length)
except Exception: print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
continue 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) 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 = { final_result = {
**material, **material,
"classified_category": best_result.get("category", "UNKNOWN") if best_result else "UNKNOWN", "classified_category": classification_result.get("category", "UNKNOWN"),
"classified_subcategory": best_result.get("subcategory", "") if best_result else "", "classified_subcategory": classification_result.get("subcategory", ""),
"material_grade": material_result.get("grade", "") if material_result else "", "material_grade": material_result.get("grade", "") if material_result else "",
"schedule": best_result.get("schedule", "") if best_result else "", "schedule": schedule_value,
"size_spec": best_result.get("size_spec", "") if best_result else "", "size_spec": classification_result.get("size_spec", ""),
"classification_confidence": best_confidence "classification_confidence": classification_result.get("overall_confidence", 0.0),
"length": length # 길이 정보 추가
} }
return final_result return final_result

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
-- materials 테이블에 length 컬럼 추가
ALTER TABLE materials ADD COLUMN length NUMERIC(10, 3);
-- 기존 데이터의 length 컬럼을 NULL로 초기화
UPDATE materials SET length = NULL;

View File

@@ -0,0 +1,137 @@
-- 자재 규격/재질 기준표 테이블 생성 스크립트
-- 실행 순서: 1) material_standards, 2) material_categories, 3) material_specifications, 4) material_grades, 5) material_patterns
-- 특수 재질: 6) special_materials, 7) special_material_grades, 8) special_material_patterns
-- 1. 자재 규격 표준 테이블
CREATE TABLE IF NOT EXISTS material_standards (
id SERIAL PRIMARY KEY,
standard_code VARCHAR(20) UNIQUE NOT NULL,
standard_name VARCHAR(100) NOT NULL,
description TEXT,
country VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. 제조방식별 카테고리 테이블
CREATE TABLE IF NOT EXISTS material_categories (
id SERIAL PRIMARY KEY,
standard_id INTEGER REFERENCES material_standards(id),
category_code VARCHAR(50) NOT NULL,
category_name VARCHAR(100) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. 구체적인 규격 테이블
CREATE TABLE IF NOT EXISTS material_specifications (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES material_categories(id),
spec_code VARCHAR(20) NOT NULL,
spec_name VARCHAR(100) NOT NULL,
description TEXT,
material_type VARCHAR(50),
manufacturing VARCHAR(50),
pressure_rating VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. 등급별 상세 정보 테이블
CREATE TABLE IF NOT EXISTS material_grades (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
grade_code VARCHAR(20) NOT NULL,
grade_name VARCHAR(100),
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
temp_range VARCHAR(100),
yield_strength VARCHAR(50),
tensile_strength VARCHAR(50),
corrosion_resistance VARCHAR(50),
stabilizer VARCHAR(50),
base_grade VARCHAR(20),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. 정규식 패턴 테이블
CREATE TABLE IF NOT EXISTS material_patterns (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6. 특수 재질 테이블
CREATE TABLE IF NOT EXISTS special_materials (
id SERIAL PRIMARY KEY,
material_type VARCHAR(50) NOT NULL,
material_name VARCHAR(100) NOT NULL,
description TEXT,
composition VARCHAR(200),
applications TEXT,
temp_max VARCHAR(50),
manufacturing VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 7. 특수 재질 등급 테이블
CREATE TABLE IF NOT EXISTS special_material_grades (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
grade_code VARCHAR(20) NOT NULL,
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
strength VARCHAR(50),
purity VARCHAR(100),
corrosion VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 8. 특수 재질 정규식 패턴 테이블
CREATE TABLE IF NOT EXISTS special_material_patterns (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_material_standards_code ON material_standards(standard_code);
CREATE INDEX IF NOT EXISTS idx_material_categories_standard ON material_categories(standard_id);
CREATE INDEX IF NOT EXISTS idx_material_specifications_category ON material_specifications(category_id);
CREATE INDEX IF NOT EXISTS idx_material_grades_specification ON material_grades(specification_id);
CREATE INDEX IF NOT EXISTS idx_material_patterns_specification ON material_patterns(specification_id);
CREATE INDEX IF NOT EXISTS idx_special_materials_type ON special_materials(material_type);
CREATE INDEX IF NOT EXISTS idx_special_material_grades_material ON special_material_grades(material_id);
CREATE INDEX IF NOT EXISTS idx_special_material_patterns_material ON special_material_patterns(material_id);
-- 활성 상태 인덱스
CREATE INDEX IF NOT EXISTS idx_material_standards_active ON material_standards(is_active);
CREATE INDEX IF NOT EXISTS idx_material_categories_active ON material_categories(is_active);
CREATE INDEX IF NOT EXISTS idx_material_specifications_active ON material_specifications(is_active);
CREATE INDEX IF NOT EXISTS idx_material_grades_active ON material_grades(is_active);
CREATE INDEX IF NOT EXISTS idx_material_patterns_active ON material_patterns(is_active);
CREATE INDEX IF NOT EXISTS idx_special_materials_active ON special_materials(is_active);
CREATE INDEX IF NOT EXISTS idx_special_material_grades_active ON special_material_grades(is_active);
CREATE INDEX IF NOT EXISTS idx_special_material_patterns_active ON special_material_patterns(is_active);

View File

@@ -0,0 +1,109 @@
-- 파이프 상세 정보 및 사용자 요구사항 테이블 생성
-- 2024-01-XX
-- 파이프 상세 정보 테이블
CREATE TABLE IF NOT EXISTS pipe_details (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL,
-- 재질 정보
material_standard TEXT, -- ASTM, KS, JIS 등
material_grade TEXT, -- A106, A53, STPG370 등
material_type TEXT, -- CARBON, STAINLESS 등
-- 파이프 특화 정보
manufacturing_method TEXT, -- SEAMLESS, WELDED, CAST
end_preparation TEXT, -- BOTH_ENDS_BEVELED, ONE_END_BEVELED, NO_BEVEL
schedule TEXT, -- SCH 10, 20, 40, 80 등
wall_thickness TEXT, -- 벽두께 정보
-- 치수 정보
nominal_size TEXT, -- MAIN_NOM (인치, 직경)
length_mm REAL, -- LENGTH (길이)
-- 신뢰도
material_confidence REAL,
manufacturing_confidence REAL,
end_prep_confidence REAL,
schedule_confidence REAL,
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 요구사항 타입 마스터 테이블
CREATE TABLE IF NOT EXISTS requirement_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type_code TEXT UNIQUE NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT' 등
type_name TEXT NOT NULL, -- '임팩테스트', '열처리' 등
category TEXT NOT NULL, -- 'TEST', 'TREATMENT', 'CERTIFICATION', 'CUSTOM' 등
description TEXT, -- 타입 설명
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 사용자 추가 요구사항 테이블
CREATE TABLE IF NOT EXISTS user_requirements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL,
-- 요구사항 타입
requirement_type TEXT NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
-- 요구사항 내용
requirement_title TEXT NOT NULL, -- '임팩테스트', '열처리', '인증서' 등
requirement_description TEXT, -- 상세 설명
requirement_spec TEXT, -- 구체적 스펙 (예: "Charpy V-notch -20°C")
-- 상태 관리
status TEXT DEFAULT 'PENDING', -- 'PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'
priority TEXT DEFAULT 'NORMAL', -- 'LOW', 'NORMAL', 'HIGH', 'URGENT'
-- 담당자 정보
assigned_to TEXT, -- 담당자명
due_date DATE, -- 완료 예정일
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_pipe_details_file_id ON pipe_details(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_file_id ON user_requirements(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_status ON user_requirements(status);
CREATE INDEX IF NOT EXISTS idx_user_requirements_type ON user_requirements(requirement_type);
-- 기본 요구사항 타입 데이터 삽입
INSERT OR IGNORE INTO requirement_types (type_code, type_name, category, description) VALUES
('IMPACT_TEST', '임팩테스트', 'TEST', 'Charpy V-notch, Izod 등의 충격 시험'),
('HEAT_TREATMENT', '열처리', 'TREATMENT', '정규화, 어닐링, 템퍼링 등의 열처리'),
('CERTIFICATION', '인증서', 'CERTIFICATION', '재질증명서, 시험성적서 등'),
('CUSTOM_SPEC', '특수사양', 'CUSTOM', '고객 특별 요구사항'),
('NDT_TEST', '비파괴검사', 'TEST', '초음파, 방사선, 자분탐상 등'),
('CHEMICAL_ANALYSIS', '화학분석', 'TEST', '화학성분 분석'),
('MECHANICAL_TEST', '기계적성질', 'TEST', '인장, 압축, 굽힘 시험'),
('SURFACE_TREATMENT', '표면처리', 'TREATMENT', '도금, 도장, 패시베이션 등'),
('DIMENSIONAL_CHECK', '치수검사', 'INSPECTION', '치수, 형상 검사'),
('WELDING_SPEC', '용접사양', 'SPEC', '용접 방법, 용접재 등');
-- 트리거 생성 (updated_at 자동 업데이트)
CREATE TRIGGER IF NOT EXISTS update_pipe_details_timestamp
AFTER UPDATE ON pipe_details
FOR EACH ROW
BEGIN
UPDATE pipe_details SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS update_user_requirements_timestamp
AFTER UPDATE ON user_requirements
FOR EACH ROW
BEGIN
UPDATE user_requirements SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END;

View File

@@ -0,0 +1,115 @@
-- 파이프 상세 정보 및 사용자 요구사항 테이블 생성 (PostgreSQL)
-- 2024-01-XX
-- 파이프 상세 정보 테이블
CREATE TABLE IF NOT EXISTS pipe_details (
id SERIAL PRIMARY KEY,
file_id INTEGER NOT NULL,
-- 재질 정보
material_standard VARCHAR(50), -- ASTM, KS, JIS 등
material_grade VARCHAR(50), -- A106, A53, STPG370 등
material_type VARCHAR(50), -- CARBON, STAINLESS 등
-- 파이프 특화 정보
manufacturing_method VARCHAR(50), -- SEAMLESS, WELDED, CAST
end_preparation VARCHAR(50), -- BOTH_ENDS_BEVELED, ONE_END_BEVELED, NO_BEVEL
schedule VARCHAR(50), -- SCH 10, 20, 40, 80 등
wall_thickness VARCHAR(50), -- 벽두께 정보
-- 치수 정보
nominal_size VARCHAR(50), -- MAIN_NOM (인치, 직경)
length_mm DECIMAL(10, 3), -- LENGTH (길이)
-- 신뢰도
material_confidence DECIMAL(3, 2),
manufacturing_confidence DECIMAL(3, 2),
end_prep_confidence DECIMAL(3, 2),
schedule_confidence DECIMAL(3, 2),
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 요구사항 타입 마스터 테이블
CREATE TABLE IF NOT EXISTS requirement_types (
id SERIAL PRIMARY KEY,
type_code VARCHAR(50) UNIQUE NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT' 등
type_name VARCHAR(100) NOT NULL, -- '임팩테스트', '열처리' 등
category VARCHAR(50) NOT NULL, -- 'TEST', 'TREATMENT', 'CERTIFICATION', 'CUSTOM' 등
description TEXT, -- 타입 설명
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 사용자 추가 요구사항 테이블
CREATE TABLE IF NOT EXISTS user_requirements (
id SERIAL PRIMARY KEY,
file_id INTEGER NOT NULL,
-- 요구사항 타입
requirement_type VARCHAR(50) NOT NULL, -- 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
-- 요구사항 내용
requirement_title VARCHAR(200) NOT NULL, -- '임팩테스트', '열처리', '인증서' 등
requirement_description TEXT, -- 상세 설명
requirement_spec TEXT, -- 구체적 스펙 (예: "Charpy V-notch -20°C")
-- 상태 관리
status VARCHAR(20) DEFAULT 'PENDING', -- 'PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'
priority VARCHAR(20) DEFAULT 'NORMAL', -- 'LOW', 'NORMAL', 'HIGH', 'URGENT'
-- 담당자 정보
assigned_to VARCHAR(100), -- 담당자명
due_date DATE, -- 완료 예정일
-- 메타데이터
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_pipe_details_file_id ON pipe_details(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_file_id ON user_requirements(file_id);
CREATE INDEX IF NOT EXISTS idx_user_requirements_status ON user_requirements(status);
CREATE INDEX IF NOT EXISTS idx_user_requirements_type ON user_requirements(requirement_type);
-- 기본 요구사항 타입 데이터 삽입
INSERT INTO requirement_types (type_code, type_name, category, description) VALUES
('IMPACT_TEST', '임팩테스트', 'TEST', 'Charpy V-notch, Izod 등의 충격 시험'),
('HEAT_TREATMENT', '열처리', 'TREATMENT', '정규화, 어닐링, 템퍼링 등의 열처리'),
('CERTIFICATION', '인증서', 'CERTIFICATION', '재질증명서, 시험성적서 등'),
('CUSTOM_SPEC', '특수사양', 'CUSTOM', '고객 특별 요구사항'),
('NDT_TEST', '비파괴검사', 'TEST', '초음파, 방사선, 자분탐상 등'),
('CHEMICAL_ANALYSIS', '화학분석', 'TEST', '화학성분 분석'),
('MECHANICAL_TEST', '기계적성질', 'TEST', '인장, 압축, 굽힘 시험'),
('SURFACE_TREATMENT', '표면처리', 'TREATMENT', '도금, 도장, 패시베이션 등'),
('DIMENSIONAL_CHECK', '치수검사', 'INSPECTION', '치수, 형상 검사'),
('WELDING_SPEC', '용접사양', 'SPEC', '용접 방법, 용접재 등')
ON CONFLICT (type_code) DO NOTHING;
-- 트리거 함수 생성 (updated_at 자동 업데이트)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- 트리거 생성
CREATE TRIGGER update_pipe_details_timestamp
BEFORE UPDATE ON pipe_details
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_requirements_timestamp
BEFORE UPDATE ON user_requirements
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,462 @@
#!/usr/bin/env python3
"""
자재 규격/재질 기준표 데이터를 DB에 삽입하는 스크립트
기존 materials_schema.py의 딕셔너리 데이터를 DB 테이블로 변환
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.database import DATABASE_URL
from app.models import (
MaterialStandard, MaterialCategory, MaterialSpecification,
MaterialGrade, MaterialPattern, SpecialMaterial,
SpecialMaterialGrade, SpecialMaterialPattern
)
# 기존 materials_schema.py의 데이터 (일부만 예시로 포함)
MATERIAL_STANDARDS_DATA = {
"ASTM_ASME": {
"name": "미국재질학회",
"country": "USA",
"categories": {
"FORGED_GRADES": {
"name": "단조품",
"specifications": {
"A182": {
"name": "탄소강 단조품",
"material_type": "carbon_alloy",
"manufacturing": "FORGED",
"subtypes": {
"carbon_alloy": {
"manufacturing": "FORGED",
"grades": {
"F1": {
"composition": "0.5Mo",
"temp_max": "482°C",
"applications": "중온용"
},
"F5": {
"composition": "5Cr-0.5Mo",
"temp_max": "649°C",
"applications": "고온용"
},
"F11": {
"composition": "1.25Cr-0.5Mo",
"temp_max": "593°C",
"applications": "일반 고온용"
},
"F22": {
"composition": "2.25Cr-1Mo",
"temp_max": "649°C",
"applications": "고온 고압용"
},
"F91": {
"composition": "9Cr-1Mo-V",
"temp_max": "649°C",
"applications": "초고온용"
}
},
"patterns": [
r"ASTM\s+A182\s+(?:GR\s*)?F(\d+)",
r"A182\s+(?:GR\s*)?F(\d+)",
r"ASME\s+SA182\s+(?:GR\s*)?F(\d+)"
]
},
"stainless": {
"manufacturing": "FORGED",
"grades": {
"F304": {
"composition": "18Cr-8Ni",
"applications": "일반용",
"corrosion_resistance": "보통"
},
"F304L": {
"composition": "18Cr-8Ni-저탄소",
"applications": "용접용",
"corrosion_resistance": "보통"
},
"F316": {
"composition": "18Cr-10Ni-2Mo",
"applications": "내식성",
"corrosion_resistance": "우수"
},
"F316L": {
"composition": "18Cr-10Ni-2Mo-저탄소",
"applications": "용접+내식성",
"corrosion_resistance": "우수"
},
"F321": {
"composition": "18Cr-8Ni-Ti",
"applications": "고온안정화",
"stabilizer": "Titanium"
},
"F347": {
"composition": "18Cr-8Ni-Nb",
"applications": "고온안정화",
"stabilizer": "Niobium"
}
},
"patterns": [
r"ASTM\s+A182\s+F(\d{3}[LH]*)",
r"A182\s+F(\d{3}[LH]*)",
r"ASME\s+SA182\s+F(\d{3}[LH]*)"
]
}
}
},
"A105": {
"name": "탄소강 단조품",
"description": "탄소강 단조품",
"composition": "탄소강",
"applications": "일반 압력용 단조품",
"manufacturing": "FORGED",
"pressure_rating": "150LB ~ 9000LB",
"patterns": [
r"ASTM\s+A105(?:\s+(?:GR\s*)?([ABC]))?",
r"A105(?:\s+(?:GR\s*)?([ABC]))?",
r"ASME\s+SA105"
]
}
}
},
"WELDED_GRADES": {
"name": "용접품",
"specifications": {
"A234": {
"name": "탄소강 용접 피팅",
"material_type": "carbon",
"manufacturing": "WELDED_FABRICATED",
"subtypes": {
"carbon": {
"manufacturing": "WELDED_FABRICATED",
"grades": {
"WPA": {
"yield_strength": "30 ksi",
"applications": "저압용",
"temp_range": "-29°C ~ 400°C"
},
"WPB": {
"yield_strength": "35 ksi",
"applications": "일반용",
"temp_range": "-29°C ~ 400°C"
},
"WPC": {
"yield_strength": "40 ksi",
"applications": "고압용",
"temp_range": "-29°C ~ 400°C"
}
},
"patterns": [
r"ASTM\s+A234\s+(?:GR\s*)?WP([ABC])",
r"A234\s+(?:GR\s*)?WP([ABC])",
r"ASME\s+SA234\s+(?:GR\s*)?WP([ABC])"
]
}
}
}
}
}
}
},
"KS": {
"name": "한국산업표준",
"country": "KOREA",
"categories": {
"PIPE_GRADES": {
"name": "배관용",
"specifications": {
"D3507": {
"name": "배관용 탄소강관",
"description": "배관용 탄소강관",
"manufacturing": "SEAMLESS",
"patterns": [
r"KS\s+D\s*3507\s+SPPS\s*(\d+)"
]
},
"D3583": {
"name": "압력배관용 탄소강관",
"description": "압력배관용 탄소강관",
"manufacturing": "SEAMLESS",
"patterns": [
r"KS\s+D\s*3583\s+STPG\s*(\d+)"
]
}
}
}
}
},
"JIS": {
"name": "일본공업규격",
"country": "JAPAN",
"categories": {
"PIPE_GRADES": {
"name": "배관용",
"specifications": {
"G3452": {
"name": "배관용 탄소강관",
"description": "배관용 탄소강관",
"manufacturing": "WELDED",
"patterns": [
r"JIS\s+G\s*3452\s+SGP"
]
}
}
}
}
}
}
SPECIAL_MATERIALS_DATA = {
"SUPER_ALLOYS": {
"INCONEL": {
"description": "니켈 기반 초합금",
"composition": "Ni-Cr",
"applications": "고온 산화 환경",
"temp_max": "1177°C",
"manufacturing": "FORGED_OR_CAST",
"grades": {
"600": {
"composition": "Ni-Cr",
"temp_max": "1177°C",
"applications": "고온 산화 환경"
},
"625": {
"composition": "Ni-Cr-Mo",
"temp_max": "982°C",
"applications": "고온 부식 환경"
}
},
"patterns": [
r"INCONEL\s*(\d+)"
]
}
},
"TITANIUM": {
"TITANIUM": {
"description": "티타늄 및 티타늄 합금",
"composition": "Ti",
"applications": "화학공정, 항공우주",
"temp_max": "1177°C",
"manufacturing": "FORGED_OR_SEAMLESS",
"grades": {
"1": {
"purity": "상업용 순티타늄",
"strength": "낮음",
"applications": "화학공정"
},
"2": {
"purity": "상업용 순티타늄 (일반)",
"strength": "보통",
"applications": "일반용"
},
"5": {
"composition": "Ti-6Al-4V",
"strength": "고강도",
"applications": "항공우주"
}
},
"patterns": [
r"TITANIUM(?:\s+(?:GR|GRADE)\s*(\d+))?",
r"Ti(?:\s+(?:GR|GRADE)\s*(\d+))?",
r"ASTM\s+B\d+\s+(?:GR|GRADE)\s*(\d+)"
]
}
}
}
def insert_material_standards():
"""자재 규격 데이터를 DB에 삽입"""
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session()
try:
print("자재 규격 데이터 삽입 시작...")
# 1. 자재 규격 표준 삽입
for standard_code, standard_data in MATERIAL_STANDARDS_DATA.items():
standard = MaterialStandard(
standard_code=standard_code,
standard_name=standard_data["name"],
country=standard_data["country"],
description=f"{standard_data['name']} 규격"
)
session.add(standard)
session.flush() # ID 생성
print(f" - {standard_code} ({standard_data['name']}) 추가됨")
# 2. 카테고리 삽입
for category_code, category_data in standard_data["categories"].items():
category = MaterialCategory(
standard_id=standard.id,
category_code=category_code,
category_name=category_data["name"],
description=f"{category_data['name']} 분류"
)
session.add(category)
session.flush()
print(f" - {category_code} ({category_data['name']}) 추가됨")
# 3. 규격 삽입
for spec_code, spec_data in category_data["specifications"].items():
specification = MaterialSpecification(
category_id=category.id,
spec_code=spec_code,
spec_name=spec_data["name"],
description=spec_data.get("description", ""),
material_type=spec_data.get("material_type"),
manufacturing=spec_data.get("manufacturing"),
pressure_rating=spec_data.get("pressure_rating")
)
session.add(specification)
session.flush()
print(f" - {spec_code} ({spec_data['name']}) 추가됨")
# 4. 패턴 삽입
if "patterns" in spec_data:
for i, pattern in enumerate(spec_data["patterns"]):
pattern_obj = MaterialPattern(
specification_id=specification.id,
pattern=pattern,
description=f"{spec_code} 패턴 {i+1}",
priority=i+1
)
session.add(pattern_obj)
# 5. 등급 삽입 (subtypes가 있는 경우)
if "subtypes" in spec_data:
for subtype_name, subtype_data in spec_data["subtypes"].items():
# subtypes의 grades 처리
if "grades" in subtype_data:
for grade_code, grade_data in subtype_data["grades"].items():
grade = MaterialGrade(
specification_id=specification.id,
grade_code=grade_code,
composition=grade_data.get("composition"),
applications=grade_data.get("applications"),
temp_max=grade_data.get("temp_max"),
temp_range=grade_data.get("temp_range"),
yield_strength=grade_data.get("yield_strength"),
corrosion_resistance=grade_data.get("corrosion_resistance"),
stabilizer=grade_data.get("stabilizer"),
base_grade=grade_data.get("base_grade")
)
session.add(grade)
print(f" - {grade_code} 등급 추가됨")
# 5. 등급 삽입 (직접 grades가 있는 경우)
elif "grades" in spec_data:
for grade_code, grade_data in spec_data["grades"].items():
grade = MaterialGrade(
specification_id=specification.id,
grade_code=grade_code,
composition=grade_data.get("composition"),
applications=grade_data.get("applications"),
temp_max=grade_data.get("temp_max"),
temp_range=grade_data.get("temp_range"),
yield_strength=grade_data.get("yield_strength"),
corrosion_resistance=grade_data.get("corrosion_resistance"),
stabilizer=grade_data.get("stabilizer"),
base_grade=grade_data.get("base_grade")
)
session.add(grade)
print(f" - {grade_code} 등급 추가됨")
session.commit()
print("자재 규격 데이터 삽입 완료!")
except Exception as e:
session.rollback()
print(f"오류 발생: {e}")
raise
finally:
session.close()
def insert_special_materials():
"""특수 재질 데이터를 DB에 삽입"""
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session()
try:
print("특수 재질 데이터 삽입 시작...")
for material_type, materials in SPECIAL_MATERIALS_DATA.items():
for material_name, material_data in materials.items():
# 특수 재질 추가
special_material = SpecialMaterial(
material_type=material_type,
material_name=material_name,
description=material_data.get("description", ""),
composition=material_data.get("composition"),
applications=material_data.get("applications"),
temp_max=material_data.get("temp_max"),
manufacturing=material_data.get("manufacturing")
)
session.add(special_material)
session.flush()
print(f" - {material_name} ({material_type}) 추가됨")
# 등급 추가
if "grades" in material_data:
for grade_code, grade_data in material_data["grades"].items():
grade = SpecialMaterialGrade(
material_id=special_material.id,
grade_code=grade_code,
composition=grade_data.get("composition"),
applications=grade_data.get("applications"),
temp_max=grade_data.get("temp_max"),
strength=grade_data.get("strength"),
purity=grade_data.get("purity"),
corrosion=grade_data.get("corrosion")
)
session.add(grade)
print(f" - {grade_code} 등급 추가됨")
# 패턴 추가
if "patterns" in material_data:
for i, pattern in enumerate(material_data["patterns"]):
pattern_obj = SpecialMaterialPattern(
material_id=special_material.id,
pattern=pattern,
description=f"{material_name} 패턴 {i+1}",
priority=i+1
)
session.add(pattern_obj)
session.commit()
print("특수 재질 데이터 삽입 완료!")
except Exception as e:
session.rollback()
print(f"오류 발생: {e}")
raise
finally:
session.close()
def main():
"""메인 실행 함수"""
print("자재 규격/재질 기준표 DB 데이터 삽입 시작")
print("=" * 50)
# 1. 자재 규격 데이터 삽입
insert_material_standards()
print("\n" + "=" * 50)
# 2. 특수 재질 데이터 삽입
insert_special_materials()
print("\n" + "=" * 50)
print("모든 데이터 삽입 완료!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
자재 규격/재질 기준표 테이블 생성 및 데이터 삽입 스크립트
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine, text
from app.database import DATABASE_URL
def create_tables():
"""테이블 생성"""
engine = create_engine(DATABASE_URL)
with engine.connect() as conn:
print("자재 규격/재질 기준표 테이블 생성 시작...")
# 1. 자재 규격 표준 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_standards (
id SERIAL PRIMARY KEY,
standard_code VARCHAR(20) UNIQUE NOT NULL,
standard_name VARCHAR(100) NOT NULL,
description TEXT,
country VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_standards 테이블 생성됨")
# 2. 제조방식별 카테고리 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_categories (
id SERIAL PRIMARY KEY,
standard_id INTEGER REFERENCES material_standards(id),
category_code VARCHAR(50) NOT NULL,
category_name VARCHAR(100) NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_categories 테이블 생성됨")
# 3. 구체적인 규격 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_specifications (
id SERIAL PRIMARY KEY,
category_id INTEGER REFERENCES material_categories(id),
spec_code VARCHAR(20) NOT NULL,
spec_name VARCHAR(100) NOT NULL,
description TEXT,
material_type VARCHAR(50),
manufacturing VARCHAR(50),
pressure_rating VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_specifications 테이블 생성됨")
# 4. 등급별 상세 정보 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_grades (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
grade_code VARCHAR(20) NOT NULL,
grade_name VARCHAR(100),
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
temp_range VARCHAR(100),
yield_strength VARCHAR(50),
tensile_strength VARCHAR(50),
corrosion_resistance VARCHAR(50),
stabilizer VARCHAR(50),
base_grade VARCHAR(20),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_grades 테이블 생성됨")
# 5. 정규식 패턴 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS material_patterns (
id SERIAL PRIMARY KEY,
specification_id INTEGER REFERENCES material_specifications(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - material_patterns 테이블 생성됨")
# 6. 특수 재질 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS special_materials (
id SERIAL PRIMARY KEY,
material_type VARCHAR(50) NOT NULL,
material_name VARCHAR(100) NOT NULL,
description TEXT,
composition VARCHAR(200),
applications TEXT,
temp_max VARCHAR(50),
manufacturing VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - special_materials 테이블 생성됨")
# 7. 특수 재질 등급 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS special_material_grades (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
grade_code VARCHAR(20) NOT NULL,
composition VARCHAR(200),
applications VARCHAR(200),
temp_max VARCHAR(50),
strength VARCHAR(50),
purity VARCHAR(100),
corrosion VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - special_material_grades 테이블 생성됨")
# 8. 특수 재질 정규식 패턴 테이블
conn.execute(text("""
CREATE TABLE IF NOT EXISTS special_material_patterns (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES special_materials(id),
pattern TEXT NOT NULL,
description VARCHAR(200),
priority INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""))
print(" - special_material_patterns 테이블 생성됨")
# 인덱스 생성
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_standards_code ON material_standards(standard_code);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_categories_standard ON material_categories(standard_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_specifications_category ON material_specifications(category_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_grades_specification ON material_grades(specification_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_patterns_specification ON material_patterns(specification_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_materials_type ON special_materials(material_type);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_grades_material ON special_material_grades(material_id);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_patterns_material ON special_material_patterns(material_id);"))
# 활성 상태 인덱스
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_standards_active ON material_standards(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_categories_active ON material_categories(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_specifications_active ON material_specifications(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_grades_active ON material_grades(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_material_patterns_active ON material_patterns(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_materials_active ON special_materials(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_grades_active ON special_material_grades(is_active);"))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_special_material_patterns_active ON special_material_patterns(is_active);"))
conn.commit()
print("모든 테이블 및 인덱스 생성 완료!")
def main():
"""메인 실행 함수"""
print("자재 규격/재질 기준표 DB 마이그레이션 시작")
print("=" * 50)
create_tables()
print("\n" + "=" * 50)
print("마이그레이션 완료!")
print("\n다음 단계: python scripts/06_insert_material_standards_data.py")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,237 @@
-- 1. FITTING 상세 테이블
CREATE TABLE IF NOT EXISTS fitting_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 피팅 타입 정보
fitting_type VARCHAR(50),
fitting_subtype VARCHAR(50),
-- 연결 방식
connection_method VARCHAR(50),
connection_code VARCHAR(50),
-- 압력 등급
pressure_rating VARCHAR(50),
max_pressure VARCHAR(50),
-- 제작 방법
manufacturing_method VARCHAR(50),
-- 재질 정보
material_standard VARCHAR(100),
material_grade VARCHAR(100),
material_type VARCHAR(50),
-- 사이즈 정보
main_size VARCHAR(50),
reduced_size VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 2. VALVE 상세 테이블
CREATE TABLE IF NOT EXISTS valve_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 밸브 타입 정보
valve_type VARCHAR(50),
valve_subtype VARCHAR(50),
actuator_type VARCHAR(50),
-- 연결 방식
connection_method VARCHAR(50),
-- 압력 등급
pressure_rating VARCHAR(50),
pressure_class VARCHAR(50),
-- 재질 정보
body_material VARCHAR(100),
trim_material VARCHAR(100),
-- 사이즈 정보
size_inches VARCHAR(50),
-- 특수 사양
fire_safe BOOLEAN,
low_temp_service BOOLEAN,
special_features JSONB,
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 3. FLANGE 상세 테이블
CREATE TABLE IF NOT EXISTS flange_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 플랜지 타입
flange_type VARCHAR(50),
facing_type VARCHAR(50),
-- 압력 등급
pressure_rating VARCHAR(50),
-- 재질 정보
material_standard VARCHAR(100),
material_grade VARCHAR(100),
-- 사이즈 정보
size_inches VARCHAR(50),
bolt_hole_count INTEGER,
bolt_hole_size VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 4. BOLT 상세 테이블
CREATE TABLE IF NOT EXISTS bolt_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 볼트 타입
bolt_type VARCHAR(50),
thread_type VARCHAR(50),
-- 사양 정보
diameter VARCHAR(50),
length VARCHAR(50),
-- 재질 정보
material_standard VARCHAR(100),
material_grade VARCHAR(100),
coating_type VARCHAR(100),
-- 너트/와셔 정보
includes_nut BOOLEAN,
includes_washer BOOLEAN,
nut_type VARCHAR(50),
washer_type VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 5. GASKET 상세 테이블
CREATE TABLE IF NOT EXISTS gasket_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 가스켓 타입
gasket_type VARCHAR(50),
gasket_subtype VARCHAR(50),
-- 재질 정보
material_type VARCHAR(100),
filler_material VARCHAR(100),
-- 사이즈 및 등급
size_inches VARCHAR(50),
pressure_rating VARCHAR(50),
thickness VARCHAR(50),
-- 특수 사양
temperature_range VARCHAR(100),
fire_safe BOOLEAN,
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6. INSTRUMENT 상세 테이블
CREATE TABLE IF NOT EXISTS instrument_details (
id SERIAL PRIMARY KEY,
material_id INTEGER REFERENCES materials(id) ON DELETE CASCADE,
file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
-- 계장품 타입
instrument_type VARCHAR(50),
instrument_subtype VARCHAR(50),
-- 측정 사양
measurement_type VARCHAR(50),
measurement_range VARCHAR(100),
accuracy VARCHAR(50),
-- 연결 정보
connection_type VARCHAR(50),
connection_size VARCHAR(50),
-- 재질 정보
body_material VARCHAR(100),
wetted_parts_material VARCHAR(100),
-- 전기 사양
electrical_rating VARCHAR(100),
output_signal VARCHAR(50),
-- 신뢰도
classification_confidence FLOAT,
-- 추가 정보
additional_info JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 인덱스 생성
CREATE INDEX idx_fitting_details_material_id ON fitting_details(material_id);
CREATE INDEX idx_fitting_details_file_id ON fitting_details(file_id);
CREATE INDEX idx_fitting_details_type ON fitting_details(fitting_type);
CREATE INDEX idx_valve_details_material_id ON valve_details(material_id);
CREATE INDEX idx_valve_details_file_id ON valve_details(file_id);
CREATE INDEX idx_valve_details_type ON valve_details(valve_type);
CREATE INDEX idx_flange_details_material_id ON flange_details(material_id);
CREATE INDEX idx_flange_details_file_id ON flange_details(file_id);
CREATE INDEX idx_bolt_details_material_id ON bolt_details(material_id);
CREATE INDEX idx_bolt_details_file_id ON bolt_details(file_id);
CREATE INDEX idx_gasket_details_material_id ON gasket_details(material_id);
CREATE INDEX idx_gasket_details_file_id ON gasket_details(file_id);
CREATE INDEX idx_instrument_details_material_id ON instrument_details(material_id);
CREATE INDEX idx_instrument_details_file_id ON instrument_details(file_id);

View File

@@ -81,6 +81,7 @@ CREATE TABLE materials (
-- 분류 신뢰도 및 검증 -- 분류 신뢰도 및 검증
classification_confidence DECIMAL(3,2), -- 0.00-1.00 분류 신뢰도 classification_confidence DECIMAL(3,2), -- 0.00-1.00 분류 신뢰도
classification_details JSONB, -- 분류 상세 정보 (JSON)
is_verified BOOLEAN DEFAULT false, -- 사용자 검증 여부 is_verified BOOLEAN DEFAULT false, -- 사용자 검증 여부
verified_by VARCHAR(100), verified_by VARCHAR(100),
verified_at TIMESTAMP, verified_at TIMESTAMP,

View File

@@ -0,0 +1,8 @@
-- classification_details 컬럼 추가 마이그레이션
-- 생성일: 2025.01.27
-- materials 테이블에 classification_details 컬럼 추가
ALTER TABLE materials ADD COLUMN IF NOT EXISTS classification_details JSONB;
-- 인덱스 추가 (JSONB 컬럼 검색 최적화)
CREATE INDEX IF NOT EXISTS idx_materials_classification_details ON materials USING GIN (classification_details);

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import JobSelectionPage from './pages/JobSelectionPage'; import JobSelectionPage from './pages/JobSelectionPage';
import BOMManagerPage from './pages/BOMManagerPage'; import BOMStatusPage from './pages/BOMStatusPage';
import MaterialsPage from './pages/MaterialsPage'; import MaterialsPage from './pages/MaterialsPage';
function App() { function App() {
@@ -9,7 +9,7 @@ function App() {
<Router> <Router>
<Routes> <Routes>
<Route path="/" element={<JobSelectionPage />} /> <Route path="/" element={<JobSelectionPage />} />
<Route path="/bom-manager" element={<BOMManagerPage />} /> <Route path="/bom-status" element={<BOMStatusPage />} />
<Route path="/materials" element={<MaterialsPage />} /> <Route path="/materials" element={<MaterialsPage />} />
<Route path="*" element={<Navigate to="/" />} /> <Route path="*" element={<Navigate to="/" />} />
</Routes> </Routes>

View File

@@ -94,6 +94,18 @@ export function createJob(data) {
return api.post('/jobs', data); return api.post('/jobs', data);
} }
// 리비전 비교
export function compareRevisions(jobNo, filename, oldRevision, newRevision) {
return api.get('/files/materials/compare-revisions', {
params: {
job_no: jobNo,
filename: filename,
old_revision: oldRevision,
new_revision: newRevision
}
});
}
// 프로젝트 수정 // 프로젝트 수정
export function updateProject(projectId, data) { export function updateProject(projectId, data) {
return api.put(`/projects/${projectId}`, data); return api.put(`/projects/${projectId}`, data);

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Card, CardContent, Typography, Box } from '@mui/material';
const PipeDetailsCard = ({ material, fileId }) => {
// 간단한 테스트 버전
return (
<Card sx={{ mt: 2, backgroundColor: '#f5f5f5' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
PIPE 상세 정보 (테스트)
</Typography>
<Box>
<Typography variant="body2">
자재명: {material.original_description}
</Typography>
<Typography variant="body2">
분류: {material.classified_category}
</Typography>
<Typography variant="body2">
사이즈: {material.size_spec || '정보 없음'}
</Typography>
<Typography variant="body2">
수량: {material.quantity} {material.unit}
</Typography>
</Box>
</CardContent>
</Card>
);
};
export default PipeDetailsCard;

View File

@@ -17,7 +17,7 @@ const BOMStatusPage = () => {
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
let url = '/files'; let url = 'http://localhost:8000/files';
if (jobNo) { if (jobNo) {
url += `?job_no=${jobNo}`; url += `?job_no=${jobNo}`;
} }
@@ -28,6 +28,7 @@ const BOMStatusPage = () => {
else setFiles([]); else setFiles([]);
} catch (e) { } catch (e) {
setError('파일 목록을 불러오지 못했습니다.'); setError('파일 목록을 불러오지 못했습니다.');
console.error('파일 목록 로드 에러:', e);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -48,7 +49,7 @@ const BOMStatusPage = () => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('project_id', 1); // 예시: 실제 프로젝트 ID로 대체 필요 formData.append('project_id', 1); // 예시: 실제 프로젝트 ID로 대체 필요
const res = await fetch('/files/upload', { const res = await fetch('http://localhost:8000/upload', {
method: 'POST', method: 'POST',
body: formData body: formData
}); });
@@ -98,7 +99,7 @@ const BOMStatusPage = () => {
<TableCell>{file.original_filename || file.filename}</TableCell> <TableCell>{file.original_filename || file.filename}</TableCell>
<TableCell>{file.revision}</TableCell> <TableCell>{file.revision}</TableCell>
<TableCell> <TableCell>
<Button size="small" variant="outlined" onClick={() => alert(`자재확인: ${file.original_filename}`)}> <Button size="small" variant="outlined" onClick={() => navigate(`/materials?fileId=${file.id}`)}>
자재확인 자재확인
</Button> </Button>
</TableCell> </TableCell>
@@ -108,7 +109,20 @@ const BOMStatusPage = () => {
</Button> </Button>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button size="small" variant="outlined" color="error" onClick={() => alert(`삭제: ${file.original_filename}`)}> <Button size="small" variant="outlined" color="error" onClick={async () => {
if (window.confirm(`정말로 ${file.original_filename}을 삭제하시겠습니까?`)) {
try {
const res = await fetch(`http://localhost:8000/files/${file.id}`, { method: 'DELETE' });
if (res.ok) {
fetchFiles();
} else {
alert('삭제에 실패했습니다.');
}
} catch (e) {
alert('삭제 중 오류가 발생했습니다.');
}
}
}}>
삭제 삭제
</Button> </Button>
</TableCell> </TableCell>

View File

@@ -40,7 +40,7 @@ const JobSelectionPage = () => {
const handleConfirm = () => { const handleConfirm = () => {
if (selectedJobNo && selectedJobName) { if (selectedJobNo && selectedJobName) {
navigate(`/bom-manager?job_no=${selectedJobNo}&job_name=${encodeURIComponent(selectedJobName)}`); navigate(`/bom-status?job_no=${selectedJobNo}&job_name=${encodeURIComponent(selectedJobName)}`);
} }
}; };

View File

@@ -1,221 +0,0 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Typography,
FormControl,
InputLabel,
Select,
MenuItem,
Button,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
CircularProgress,
Alert
} from '@mui/material';
// API 함수는 기존 api.js의 fetchJobs, fetchFiles, fetchMaterials를 활용한다고 가정
import { fetchJobs, fetchMaterials } from '../api';
const MaterialLookupPage = () => {
const [jobs, setJobs] = useState([]);
const [files, setFiles] = useState([]);
const [revisions, setRevisions] = useState([]);
const [selectedJobNo, setSelectedJobNo] = useState('');
const [selectedFilename, setSelectedFilename] = useState('');
const [selectedRevision, setSelectedRevision] = useState('');
const [materials, setMaterials] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// 1. Job 목록 불러오기 (최초 1회)
useEffect(() => {
async function loadJobs() {
try {
const res = await fetchJobs({});
if (res.data && res.data.jobs) setJobs(res.data.jobs);
} catch (e) {
setError('Job 목록을 불러오지 못했습니다.');
}
}
loadJobs();
}, []);
// 2. Job 선택 시 해당 도면(파일) 목록 불러오기
useEffect(() => {
async function loadFiles() {
if (!selectedJobNo) {
setFiles([]);
setRevisions([]);
setSelectedFilename('');
setSelectedRevision('');
return;
}
try {
const res = await fetch(`/files?job_no=${selectedJobNo}`);
const data = await res.json();
if (Array.isArray(data)) setFiles(data);
else if (data && Array.isArray(data.files)) setFiles(data.files);
else setFiles([]);
setSelectedFilename('');
setSelectedRevision('');
setRevisions([]);
} catch (e) {
setFiles([]);
setRevisions([]);
setError('도면 목록을 불러오지 못했습니다.');
}
}
loadFiles();
}, [selectedJobNo]);
// 3. 도면 선택 시 해당 리비전 목록 추출
useEffect(() => {
if (!selectedFilename) {
setRevisions([]);
setSelectedRevision('');
return;
}
const filtered = files.filter(f => f.original_filename === selectedFilename);
setRevisions(filtered.map(f => f.revision));
setSelectedRevision('');
}, [selectedFilename, files]);
// 4. 조회 버튼 클릭 시 자재 목록 불러오기
const handleLookup = async () => {
setLoading(true);
setError('');
setMaterials([]);
try {
const params = {
job_no: selectedJobNo,
filename: selectedFilename,
revision: selectedRevision
};
const res = await fetchMaterials(params);
if (res.data && Array.isArray(res.data.materials)) {
setMaterials(res.data.materials);
} else {
setMaterials([]);
}
} catch (e) {
setError('자재 목록을 불러오지 못했습니다.');
} finally {
setLoading(false);
}
};
// 5. 3개 모두 선택 시 자동 조회 (원하면 주석 해제)
// useEffect(() => {
// if (selectedJobNo && selectedFilename && selectedRevision) {
// handleLookup();
// }
// }, [selectedJobNo, selectedFilename, selectedRevision]);
return (
<Box sx={{ maxWidth: 900, mx: 'auto', mt: 4 }}>
<Typography variant="h4" gutterBottom>
자재 상세 조회 (Job No + 도면명 + 리비전)
</Typography>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
{/* Job No 드롭다운 */}
<FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel>Job No</InputLabel>
<Select
value={selectedJobNo}
label="Job No"
onChange={e => setSelectedJobNo(e.target.value)}
displayEmpty
>
<MenuItem value="">선택</MenuItem>
{jobs.map(job => (
<MenuItem key={job.job_number} value={job.job_number}>
{job.job_number}
</MenuItem>
))}
</Select>
</FormControl>
{/* 도면명 드롭다운 */}
<FormControl size="small" sx={{ minWidth: 200 }} disabled={!selectedJobNo}>
<InputLabel>도면명(파일명)</InputLabel>
<Select
value={selectedFilename}
label="도면명(파일명)"
onChange={e => setSelectedFilename(e.target.value)}
displayEmpty
>
<MenuItem value="">선택</MenuItem>
{files.map(file => (
<MenuItem key={file.id} value={file.original_filename}>
{file.bom_name || file.original_filename || file.filename}
</MenuItem>
))}
</Select>
</FormControl>
{/* 리비전 드롭다운 */}
<FormControl size="small" sx={{ minWidth: 120 }} disabled={!selectedFilename}>
<InputLabel>리비전</InputLabel>
<Select
value={selectedRevision}
label="리비전"
onChange={e => setSelectedRevision(e.target.value)}
displayEmpty
>
<MenuItem value="">선택</MenuItem>
{revisions.map(rev => (
<MenuItem key={rev} value={rev}>{rev}</MenuItem>
))}
</Select>
</FormControl>
<Button
variant="contained"
onClick={handleLookup}
disabled={!(selectedJobNo && selectedFilename && selectedRevision) || loading}
>
조회
</Button>
</Box>
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
{loading && <CircularProgress sx={{ mt: 4 }} />}
{!loading && materials.length > 0 && (
<TableContainer component={Paper} sx={{ mt: 2 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>품명</TableCell>
<TableCell>수량</TableCell>
<TableCell>단위</TableCell>
<TableCell>사이즈</TableCell>
<TableCell>재질</TableCell>
<TableCell>라인번호</TableCell>
</TableRow>
</TableHead>
<TableBody>
{materials.map(mat => (
<TableRow key={mat.id}>
<TableCell>{mat.original_description}</TableCell>
<TableCell>{mat.quantity}</TableCell>
<TableCell>{mat.unit}</TableCell>
<TableCell>{mat.size_spec}</TableCell>
<TableCell>{mat.material_grade}</TableCell>
<TableCell>{mat.line_number}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{!loading && materials.length === 0 && (selectedJobNo && selectedFilename && selectedRevision) && (
<Alert severity="info" sx={{ mt: 4 }}>
해당 조건에 맞는 자재가 없습니다.
</Alert>
)}
</Box>
);
};
export default MaterialLookupPage;

File diff suppressed because it is too large Load Diff