feat: 자재 분류 시스템 개선 및 상세 테이블 추가
- 모든 자재 카테고리별 상세 테이블 생성 (fitting, valve, flange, bolt, gasket, instrument) - PIPE, FITTING, VALVE 분류 결과를 각 상세 테이블에 저장하는 로직 구현 - 프론트엔드 라우팅 정리 및 BOM 현황 페이지 기능 개선 - 자재확인 페이지 에러 처리 개선 TODO: FLANGE, BOLT, GASKET, INSTRUMENT 저장 로직 추가 필요
This commit is contained in:
@@ -8,9 +8,18 @@ from datetime import datetime
|
||||
import uuid
|
||||
import pandas as pd
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from ..database import get_db
|
||||
from app.services.material_classifier import classify_material
|
||||
from app.services.bolt_classifier import classify_bolt
|
||||
from app.services.flange_classifier import classify_flange
|
||||
from app.services.fitting_classifier import classify_fitting
|
||||
from app.services.gasket_classifier import classify_gasket
|
||||
from app.services.instrument_classifier import classify_instrument
|
||||
from app.services.pipe_classifier import classify_pipe
|
||||
from app.services.valve_classifier import classify_valve
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -59,7 +68,8 @@ def generate_unique_filename(original_filename: str) -> str:
|
||||
|
||||
def parse_dataframe(df):
|
||||
df = df.dropna(how='all')
|
||||
df.columns = df.columns.str.strip().str.lower()
|
||||
# 원본 컬럼명 유지 (소문자 변환하지 않음)
|
||||
df.columns = df.columns.str.strip()
|
||||
|
||||
column_mapping = {
|
||||
'description': ['description', 'item', 'material', '품명', '자재명'],
|
||||
@@ -75,10 +85,16 @@ def parse_dataframe(df):
|
||||
mapped_columns = {}
|
||||
for standard_col, possible_names in column_mapping.items():
|
||||
for possible_name in possible_names:
|
||||
if possible_name in df.columns:
|
||||
mapped_columns[standard_col] = possible_name
|
||||
# 대소문자 구분 없이 매핑
|
||||
for col in df.columns:
|
||||
if possible_name.lower() == col.lower():
|
||||
mapped_columns[standard_col] = col
|
||||
break
|
||||
if standard_col in mapped_columns:
|
||||
break
|
||||
|
||||
print(f"찾은 컬럼 매핑: {mapped_columns}")
|
||||
|
||||
materials = []
|
||||
for index, row in df.iterrows():
|
||||
description = str(row.get(mapped_columns.get('description', ''), ''))
|
||||
@@ -89,6 +105,15 @@ def parse_dataframe(df):
|
||||
except:
|
||||
quantity = 0
|
||||
|
||||
# 길이 정보 파싱
|
||||
length_raw = row.get(mapped_columns.get('length', ''), None)
|
||||
length_value = None
|
||||
if pd.notna(length_raw) and length_raw != '':
|
||||
try:
|
||||
length_value = float(length_raw)
|
||||
except:
|
||||
length_value = None
|
||||
|
||||
material_grade = ""
|
||||
if "ASTM" in description.upper():
|
||||
astm_match = re.search(r'ASTM\s+([A-Z0-9\s]+)', description.upper())
|
||||
@@ -112,6 +137,7 @@ def parse_dataframe(df):
|
||||
'unit': "EA",
|
||||
'size_spec': size_spec,
|
||||
'material_grade': material_grade,
|
||||
'length': length_value,
|
||||
'line_number': index + 1,
|
||||
'row_number': index + 1
|
||||
})
|
||||
@@ -140,7 +166,7 @@ async def upload_file(
|
||||
revision: str = Form("Rev.0"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
if not validate_file_extension(file.filename):
|
||||
if not validate_file_extension(str(file.filename)):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"지원하지 않는 파일 형식입니다. 허용된 확장자: {', '.join(ALLOWED_EXTENSIONS)}"
|
||||
@@ -149,7 +175,7 @@ async def upload_file(
|
||||
if file.size and file.size > 10 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="파일 크기는 10MB를 초과할 수 없습니다")
|
||||
|
||||
unique_filename = generate_unique_filename(file.filename)
|
||||
unique_filename = generate_unique_filename(str(file.filename))
|
||||
file_path = UPLOAD_DIR / unique_filename
|
||||
|
||||
try:
|
||||
@@ -183,19 +209,133 @@ async def upload_file(
|
||||
|
||||
file_id = file_result.fetchone()[0]
|
||||
|
||||
# 자재 데이터 저장
|
||||
# 자재 데이터 저장 (분류 포함)
|
||||
materials_inserted = 0
|
||||
for material_data in materials_data:
|
||||
# 자재 타입 분류기 적용 (PIPE, FITTING, VALVE 등)
|
||||
description = material_data["original_description"]
|
||||
size_spec = material_data["size_spec"]
|
||||
|
||||
# 각 분류기로 시도 (올바른 매개변수 사용)
|
||||
print(f"분류 시도: {description}")
|
||||
|
||||
# 분류기 호출 시 타임아웃 및 예외 처리
|
||||
classification_result = None
|
||||
try:
|
||||
# 파이프 분류기 호출 시 length 매개변수 전달
|
||||
length_value = None
|
||||
if 'length' in material_data:
|
||||
try:
|
||||
length_value = float(material_data['length'])
|
||||
except:
|
||||
length_value = None
|
||||
# None이면 0.0으로 대체
|
||||
if length_value is None:
|
||||
length_value = 0.0
|
||||
|
||||
# 타임아웃 설정 (10초)
|
||||
import signal
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError("분류기 실행 시간 초과")
|
||||
|
||||
signal.signal(signal.SIGALRM, timeout_handler)
|
||||
signal.alarm(10) # 10초 타임아웃
|
||||
|
||||
try:
|
||||
classification_result = classify_pipe("", description, size_spec, length_value)
|
||||
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
finally:
|
||||
signal.alarm(0) # 타임아웃 해제
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
signal.alarm(10)
|
||||
try:
|
||||
classification_result = classify_fitting("", description, size_spec)
|
||||
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
signal.alarm(10)
|
||||
try:
|
||||
classification_result = classify_valve("", description, size_spec)
|
||||
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
signal.alarm(10)
|
||||
try:
|
||||
classification_result = classify_flange("", description, size_spec)
|
||||
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
signal.alarm(10)
|
||||
try:
|
||||
classification_result = classify_bolt("", description, size_spec)
|
||||
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
signal.alarm(10)
|
||||
try:
|
||||
classification_result = classify_gasket("", description, size_spec)
|
||||
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
signal.alarm(10)
|
||||
try:
|
||||
classification_result = classify_instrument("", description, size_spec)
|
||||
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
except (TimeoutError, Exception) as e:
|
||||
print(f"분류기 실행 중 오류 발생: {e}")
|
||||
# 기본 분류 결과 생성
|
||||
classification_result = {
|
||||
"category": "UNKNOWN",
|
||||
"overall_confidence": 0.0,
|
||||
"reason": f"분류기 오류: {str(e)}"
|
||||
}
|
||||
|
||||
print(f"최종 분류 결과: {classification_result.get('category', 'UNKNOWN')}")
|
||||
|
||||
# 분류 결과에서 상세 정보 추출
|
||||
if classification_result.get('category') == 'PIPE':
|
||||
classification_details = classification_result
|
||||
elif classification_result.get('category') == 'FITTING':
|
||||
classification_details = classification_result
|
||||
elif classification_result.get('category') == 'VALVE':
|
||||
classification_details = classification_result
|
||||
else:
|
||||
classification_details = {}
|
||||
# DB에 저장 시 JSON 직렬화
|
||||
classification_details = json.dumps(classification_details, ensure_ascii=False)
|
||||
|
||||
# 디버깅: 저장 직전 데이터 확인
|
||||
print(f"=== 자재[{materials_inserted + 1}] 저장 직전 ===")
|
||||
print(f"자재명: {material_data['original_description']}")
|
||||
print(f"분류결과: {classification_result.get('category')}")
|
||||
print(f"신뢰도: {classification_result.get('overall_confidence', 0)}")
|
||||
print(f"classification_details 길이: {len(classification_details)}")
|
||||
print(f"classification_details 샘플: {classification_details[:200]}...")
|
||||
print("=" * 50)
|
||||
material_insert_query = text("""
|
||||
INSERT INTO materials (
|
||||
file_id, original_description, quantity, unit, size_spec,
|
||||
material_grade, line_number, row_number, classified_category,
|
||||
classification_confidence, is_verified, created_at
|
||||
classification_confidence, classification_details, is_verified, created_at
|
||||
)
|
||||
VALUES (
|
||||
:file_id, :original_description, :quantity, :unit, :size_spec,
|
||||
:material_grade, :line_number, :row_number, :classified_category,
|
||||
:classification_confidence, :is_verified, :created_at
|
||||
:classification_confidence, :classification_details, :is_verified, :created_at
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -208,11 +348,155 @@ async def upload_file(
|
||||
"material_grade": material_data["material_grade"],
|
||||
"line_number": material_data["line_number"],
|
||||
"row_number": material_data["row_number"],
|
||||
"classified_category": None,
|
||||
"classification_confidence": None,
|
||||
"classified_category": classification_result.get("category", "UNKNOWN"),
|
||||
"classification_confidence": classification_result.get("overall_confidence", 0.0),
|
||||
"classification_details": classification_details,
|
||||
"is_verified": False,
|
||||
"created_at": datetime.now()
|
||||
})
|
||||
|
||||
# 각 카테고리별로 상세 테이블에 저장
|
||||
category = classification_result.get('category')
|
||||
confidence = classification_result.get('overall_confidence', 0)
|
||||
|
||||
if category == 'PIPE' and confidence >= 0.5:
|
||||
try:
|
||||
# 분류 결과에서 파이프 상세 정보 추출
|
||||
pipe_info = classification_result
|
||||
|
||||
# cutting_dimensions에서 length 정보 가져오기
|
||||
cutting_dims = pipe_info.get('cutting_dimensions', {})
|
||||
length_mm = cutting_dims.get('length_mm')
|
||||
|
||||
# length_mm가 없으면 원본 데이터의 length 사용
|
||||
if not length_mm and material_data.get('length'):
|
||||
length_mm = material_data['length']
|
||||
|
||||
pipe_insert_query = text("""
|
||||
INSERT INTO pipe_details (
|
||||
material_id, file_id, size_inches, schedule_type, material_spec,
|
||||
manufacturing_method, length_mm, outer_diameter_mm, wall_thickness_mm,
|
||||
weight_per_meter_kg, classification_confidence, additional_info
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM materials WHERE file_id = :file_id AND original_description = :description AND row_number = :row_number),
|
||||
:file_id, :size_inches, :schedule_type, :material_spec,
|
||||
:manufacturing_method, :length_mm, :outer_diameter_mm, :wall_thickness_mm,
|
||||
:weight_per_meter_kg, :classification_confidence, :additional_info
|
||||
)
|
||||
""")
|
||||
|
||||
db.execute(pipe_insert_query, {
|
||||
"file_id": file_id,
|
||||
"description": material_data["original_description"],
|
||||
"row_number": material_data["row_number"],
|
||||
"size_inches": pipe_info.get('nominal_diameter', ''),
|
||||
"schedule_type": pipe_info.get('schedule', ''),
|
||||
"material_spec": pipe_info.get('material_spec', ''),
|
||||
"manufacturing_method": pipe_info.get('manufacturing_method', ''),
|
||||
"length_mm": length_mm,
|
||||
"outer_diameter_mm": pipe_info.get('outer_diameter_mm'),
|
||||
"wall_thickness_mm": pipe_info.get('wall_thickness_mm'),
|
||||
"weight_per_meter_kg": pipe_info.get('weight_per_meter_kg'),
|
||||
"classification_confidence": classification_result.get('overall_confidence', 0.0),
|
||||
"additional_info": json.dumps(pipe_info, ensure_ascii=False)
|
||||
})
|
||||
|
||||
print(f"PIPE 상세정보 저장 완료: {material_data['original_description']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"PIPE 상세정보 저장 실패: {e}")
|
||||
# 에러가 발생해도 전체 프로세스는 계속 진행
|
||||
|
||||
elif category == 'FITTING' and confidence >= 0.5:
|
||||
try:
|
||||
fitting_info = classification_result
|
||||
|
||||
fitting_insert_query = text("""
|
||||
INSERT INTO fitting_details (
|
||||
material_id, file_id, fitting_type, fitting_subtype,
|
||||
connection_method, connection_code, pressure_rating, max_pressure,
|
||||
manufacturing_method, material_standard, material_grade, material_type,
|
||||
main_size, reduced_size, classification_confidence, additional_info
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM materials WHERE file_id = :file_id AND original_description = :description AND row_number = :row_number),
|
||||
:file_id, :fitting_type, :fitting_subtype,
|
||||
:connection_method, :connection_code, :pressure_rating, :max_pressure,
|
||||
:manufacturing_method, :material_standard, :material_grade, :material_type,
|
||||
:main_size, :reduced_size, :classification_confidence, :additional_info
|
||||
)
|
||||
""")
|
||||
|
||||
db.execute(fitting_insert_query, {
|
||||
"file_id": file_id,
|
||||
"description": material_data["original_description"],
|
||||
"row_number": material_data["row_number"],
|
||||
"fitting_type": fitting_info.get('fitting_type', {}).get('type', ''),
|
||||
"fitting_subtype": fitting_info.get('fitting_type', {}).get('subtype', ''),
|
||||
"connection_method": fitting_info.get('connection_method', {}).get('method', ''),
|
||||
"connection_code": fitting_info.get('connection_method', {}).get('matched_code', ''),
|
||||
"pressure_rating": fitting_info.get('pressure_rating', {}).get('rating', ''),
|
||||
"max_pressure": fitting_info.get('pressure_rating', {}).get('max_pressure', ''),
|
||||
"manufacturing_method": fitting_info.get('manufacturing', {}).get('method', ''),
|
||||
"material_standard": fitting_info.get('material', {}).get('standard', ''),
|
||||
"material_grade": fitting_info.get('material', {}).get('grade', ''),
|
||||
"material_type": fitting_info.get('material', {}).get('material_type', ''),
|
||||
"main_size": fitting_info.get('size_info', {}).get('main_size', ''),
|
||||
"reduced_size": fitting_info.get('size_info', {}).get('reduced_size', ''),
|
||||
"classification_confidence": confidence,
|
||||
"additional_info": json.dumps(fitting_info, ensure_ascii=False)
|
||||
})
|
||||
|
||||
print(f"FITTING 상세정보 저장 완료: {material_data['original_description']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"FITTING 상세정보 저장 실패: {e}")
|
||||
|
||||
elif category == 'VALVE' and confidence >= 0.5:
|
||||
try:
|
||||
valve_info = classification_result
|
||||
|
||||
valve_insert_query = text("""
|
||||
INSERT INTO valve_details (
|
||||
material_id, file_id, valve_type, valve_subtype, actuator_type,
|
||||
connection_method, pressure_rating, pressure_class,
|
||||
body_material, trim_material, size_inches,
|
||||
fire_safe, low_temp_service, classification_confidence, additional_info
|
||||
)
|
||||
VALUES (
|
||||
(SELECT id FROM materials WHERE file_id = :file_id AND original_description = :description AND row_number = :row_number),
|
||||
:file_id, :valve_type, :valve_subtype, :actuator_type,
|
||||
:connection_method, :pressure_rating, :pressure_class,
|
||||
:body_material, :trim_material, :size_inches,
|
||||
:fire_safe, :low_temp_service, :classification_confidence, :additional_info
|
||||
)
|
||||
""")
|
||||
|
||||
db.execute(valve_insert_query, {
|
||||
"file_id": file_id,
|
||||
"description": material_data["original_description"],
|
||||
"row_number": material_data["row_number"],
|
||||
"valve_type": valve_info.get('valve_type', ''),
|
||||
"valve_subtype": valve_info.get('valve_subtype', ''),
|
||||
"actuator_type": valve_info.get('actuator_type', ''),
|
||||
"connection_method": valve_info.get('connection_method', ''),
|
||||
"pressure_rating": valve_info.get('pressure_rating', ''),
|
||||
"pressure_class": valve_info.get('pressure_class', ''),
|
||||
"body_material": valve_info.get('body_material', ''),
|
||||
"trim_material": valve_info.get('trim_material', ''),
|
||||
"size_inches": valve_info.get('size', ''),
|
||||
"fire_safe": valve_info.get('fire_safe', False),
|
||||
"low_temp_service": valve_info.get('low_temp_service', False),
|
||||
"classification_confidence": confidence,
|
||||
"additional_info": json.dumps(valve_info, ensure_ascii=False)
|
||||
})
|
||||
|
||||
print(f"VALVE 상세정보 저장 완료: {material_data['original_description']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"VALVE 상세정보 저장 실패: {e}")
|
||||
|
||||
materials_inserted += 1
|
||||
|
||||
db.commit()
|
||||
@@ -256,6 +540,7 @@ async def get_materials(
|
||||
query = """
|
||||
SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit,
|
||||
m.size_spec, m.material_grade, m.line_number, m.row_number,
|
||||
m.classified_category, m.classification_confidence, m.classification_details,
|
||||
m.created_at,
|
||||
f.original_filename, f.project_id, f.job_no, f.revision,
|
||||
p.official_project_code, p.project_name
|
||||
@@ -383,6 +668,9 @@ async def get_materials(
|
||||
"material_grade": m.material_grade,
|
||||
"line_number": m.line_number,
|
||||
"row_number": m.row_number,
|
||||
"classified_category": m.classified_category,
|
||||
"classification_confidence": float(m.classification_confidence) if m.classification_confidence else 0,
|
||||
"classification_details": json.loads(m.classification_details) if m.classification_details else None,
|
||||
"created_at": m.created_at
|
||||
}
|
||||
for m in materials
|
||||
@@ -444,3 +732,267 @@ async def get_materials_summary(
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"요약 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/materials/compare-revisions")
|
||||
async def compare_revisions(
|
||||
job_no: str,
|
||||
filename: str,
|
||||
old_revision: str,
|
||||
new_revision: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
리비전 간 자재 비교
|
||||
"""
|
||||
try:
|
||||
# 기존 리비전 자재 조회
|
||||
old_materials_query = text("""
|
||||
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
|
||||
m.material_grade, m.classified_category, m.classification_confidence
|
||||
FROM materials m
|
||||
JOIN files f ON m.file_id = f.id
|
||||
WHERE f.job_no = :job_no
|
||||
AND f.original_filename = :filename
|
||||
AND f.revision = :old_revision
|
||||
""")
|
||||
|
||||
old_result = db.execute(old_materials_query, {
|
||||
"job_no": job_no,
|
||||
"filename": filename,
|
||||
"old_revision": old_revision
|
||||
})
|
||||
old_materials = old_result.fetchall()
|
||||
|
||||
# 새 리비전 자재 조회
|
||||
new_materials_query = text("""
|
||||
SELECT m.original_description, m.quantity, m.unit, m.size_spec,
|
||||
m.material_grade, m.classified_category, m.classification_confidence
|
||||
FROM materials m
|
||||
JOIN files f ON m.file_id = f.id
|
||||
WHERE f.job_no = :job_no
|
||||
AND f.original_filename = :filename
|
||||
AND f.revision = :new_revision
|
||||
""")
|
||||
|
||||
new_result = db.execute(new_materials_query, {
|
||||
"job_no": job_no,
|
||||
"filename": filename,
|
||||
"new_revision": new_revision
|
||||
})
|
||||
new_materials = new_result.fetchall()
|
||||
|
||||
# 자재 키 생성 함수
|
||||
def create_material_key(material):
|
||||
return f"{material.original_description}_{material.size_spec}_{material.material_grade}"
|
||||
|
||||
# 기존 자재를 딕셔너리로 변환
|
||||
old_materials_dict = {}
|
||||
for material in old_materials:
|
||||
key = create_material_key(material)
|
||||
old_materials_dict[key] = {
|
||||
"original_description": material.original_description,
|
||||
"quantity": float(material.quantity) if material.quantity else 0,
|
||||
"unit": material.unit,
|
||||
"size_spec": material.size_spec,
|
||||
"material_grade": material.material_grade,
|
||||
"classified_category": material.classified_category,
|
||||
"classification_confidence": material.classification_confidence
|
||||
}
|
||||
|
||||
# 새 자재를 딕셔너리로 변환
|
||||
new_materials_dict = {}
|
||||
for material in new_materials:
|
||||
key = create_material_key(material)
|
||||
new_materials_dict[key] = {
|
||||
"original_description": material.original_description,
|
||||
"quantity": float(material.quantity) if material.quantity else 0,
|
||||
"unit": material.unit,
|
||||
"size_spec": material.size_spec,
|
||||
"material_grade": material.material_grade,
|
||||
"classified_category": material.classified_category,
|
||||
"classification_confidence": material.classification_confidence
|
||||
}
|
||||
|
||||
# 변경 사항 분석
|
||||
all_keys = set(old_materials_dict.keys()) | set(new_materials_dict.keys())
|
||||
|
||||
added_items = []
|
||||
removed_items = []
|
||||
changed_items = []
|
||||
|
||||
for key in all_keys:
|
||||
old_item = old_materials_dict.get(key)
|
||||
new_item = new_materials_dict.get(key)
|
||||
|
||||
if old_item and not new_item:
|
||||
# 삭제된 항목
|
||||
removed_items.append({
|
||||
"key": key,
|
||||
"item": old_item,
|
||||
"change_type": "removed"
|
||||
})
|
||||
elif not old_item and new_item:
|
||||
# 추가된 항목
|
||||
added_items.append({
|
||||
"key": key,
|
||||
"item": new_item,
|
||||
"change_type": "added"
|
||||
})
|
||||
elif old_item and new_item:
|
||||
# 수량 변경 확인
|
||||
if old_item["quantity"] != new_item["quantity"]:
|
||||
changed_items.append({
|
||||
"key": key,
|
||||
"old_item": old_item,
|
||||
"new_item": new_item,
|
||||
"quantity_change": new_item["quantity"] - old_item["quantity"],
|
||||
"change_type": "quantity_changed"
|
||||
})
|
||||
|
||||
# 분류별 통계
|
||||
def calculate_category_stats(items):
|
||||
stats = {}
|
||||
for item in items:
|
||||
category = item.get("item", {}).get("classified_category", "OTHER")
|
||||
if category not in stats:
|
||||
stats[category] = {"count": 0, "total_quantity": 0}
|
||||
stats[category]["count"] += 1
|
||||
stats[category]["total_quantity"] += item.get("item", {}).get("quantity", 0)
|
||||
return stats
|
||||
|
||||
added_stats = calculate_category_stats(added_items)
|
||||
removed_stats = calculate_category_stats(removed_items)
|
||||
changed_stats = calculate_category_stats(changed_items)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"comparison": {
|
||||
"old_revision": old_revision,
|
||||
"new_revision": new_revision,
|
||||
"filename": filename,
|
||||
"job_no": job_no,
|
||||
"summary": {
|
||||
"added_count": len(added_items),
|
||||
"removed_count": len(removed_items),
|
||||
"changed_count": len(changed_items),
|
||||
"total_changes": len(added_items) + len(removed_items) + len(changed_items)
|
||||
},
|
||||
"changes": {
|
||||
"added": added_items,
|
||||
"removed": removed_items,
|
||||
"changed": changed_items
|
||||
},
|
||||
"category_stats": {
|
||||
"added": added_stats,
|
||||
"removed": removed_stats,
|
||||
"changed": changed_stats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"리비전 비교 실패: {str(e)}")
|
||||
|
||||
@router.post("/materials/update-classification-details")
|
||||
async def update_classification_details(
|
||||
file_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""기존 자재들의 classification_details 업데이트"""
|
||||
try:
|
||||
# 업데이트할 자재들 조회
|
||||
query = """
|
||||
SELECT id, original_description, size_spec, classified_category
|
||||
FROM materials
|
||||
WHERE classification_details IS NULL OR classification_details = '{}'
|
||||
"""
|
||||
params = {}
|
||||
if file_id:
|
||||
query += " AND file_id = :file_id"
|
||||
params["file_id"] = file_id
|
||||
|
||||
query += " ORDER BY id"
|
||||
result = db.execute(text(query), params)
|
||||
materials = result.fetchall()
|
||||
|
||||
if not materials:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "업데이트할 자재가 없습니다.",
|
||||
"updated_count": 0
|
||||
}
|
||||
|
||||
updated_count = 0
|
||||
for material in materials:
|
||||
material_id = material.id
|
||||
description = material.original_description
|
||||
size_spec = material.size_spec
|
||||
category = material.classified_category
|
||||
|
||||
print(f"자재 {material_id} 재분류 중: {description}")
|
||||
|
||||
# 카테고리별로 적절한 분류기 호출
|
||||
classification_result = None
|
||||
|
||||
if category == 'PIPE':
|
||||
classification_result = classify_pipe("", description, size_spec, 0.0)
|
||||
elif category == 'FITTING':
|
||||
classification_result = classify_fitting("", description, size_spec)
|
||||
elif category == 'VALVE':
|
||||
classification_result = classify_valve("", description, size_spec)
|
||||
elif category == 'FLANGE':
|
||||
classification_result = classify_flange("", description, size_spec)
|
||||
elif category == 'BOLT':
|
||||
classification_result = classify_bolt("", description, size_spec)
|
||||
elif category == 'GASKET':
|
||||
classification_result = classify_gasket("", description, size_spec)
|
||||
elif category == 'INSTRUMENT':
|
||||
classification_result = classify_instrument("", description, size_spec)
|
||||
else:
|
||||
# 카테고리가 없으면 모든 분류기 시도
|
||||
classification_result = classify_pipe("", description, size_spec, 0.0)
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_fitting("", description, size_spec)
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_valve("", description, size_spec)
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_flange("", description, size_spec)
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_bolt("", description, size_spec)
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_gasket("", description, size_spec)
|
||||
if classification_result.get("overall_confidence", 0) < 0.5:
|
||||
classification_result = classify_instrument("", description, size_spec)
|
||||
|
||||
if classification_result:
|
||||
# classification_details를 JSON으로 직렬화
|
||||
classification_details = json.dumps(classification_result, ensure_ascii=False)
|
||||
|
||||
# DB 업데이트
|
||||
update_query = text("""
|
||||
UPDATE materials
|
||||
SET classification_details = :classification_details,
|
||||
updated_at = NOW()
|
||||
WHERE id = :material_id
|
||||
""")
|
||||
|
||||
db.execute(update_query, {
|
||||
"material_id": material_id,
|
||||
"classification_details": classification_details
|
||||
})
|
||||
|
||||
updated_count += 1
|
||||
print(f"자재 {material_id} 업데이트 완료")
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"{updated_count}개 자재의 분류 상세정보가 업데이트되었습니다.",
|
||||
"updated_count": updated_count,
|
||||
"total_materials": len(materials)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"분류 상세정보 업데이트 실패: {str(e)}")
|
||||
|
||||
@@ -440,6 +440,7 @@ def parse_file(file_path: str) -> List[Dict]:
|
||||
column_mapping = {
|
||||
'description': ['DESCRIPTION', 'Description', 'description', 'DESC', 'Desc', 'desc', 'ITEM', 'Item', 'item'],
|
||||
'quantity': ['QTY', 'Quantity', 'quantity', 'QTY.', 'Qty', 'qty', 'AMOUNT', 'Amount', 'amount'],
|
||||
'length': ['LENGTH', 'Length', 'length', 'LG', 'Lg', 'lg', 'LENGTH_MM', 'Length_mm', 'length_mm'],
|
||||
'unit': ['UNIT', 'Unit', 'unit', 'UOM', 'Uom', 'uom'],
|
||||
'drawing': ['DRAWING', 'Drawing', 'drawing', 'DWG', 'Dwg', 'dwg'],
|
||||
'area': ['AREA', 'Area', 'area', 'AREA_CODE', 'Area_Code', 'area_code'],
|
||||
@@ -466,6 +467,8 @@ def parse_file(file_path: str) -> List[Dict]:
|
||||
description = str(row.get(found_columns.get('description', ''), '') or '')
|
||||
quantity_raw = row.get(found_columns.get('quantity', 1), 1)
|
||||
quantity = float(quantity_raw) if quantity_raw is not None else 1.0
|
||||
length_raw = row.get(found_columns.get('length', 0), 0)
|
||||
length = float(length_raw) if length_raw is not None else 0.0
|
||||
unit = str(row.get(found_columns.get('unit', 'EA'), 'EA') or 'EA')
|
||||
drawing = str(row.get(found_columns.get('drawing', ''), '') or '')
|
||||
area = str(row.get(found_columns.get('area', ''), '') or '')
|
||||
@@ -475,6 +478,7 @@ def parse_file(file_path: str) -> List[Dict]:
|
||||
"line_number": index + 1,
|
||||
"original_description": description,
|
||||
"quantity": quantity,
|
||||
"length": length,
|
||||
"unit": unit,
|
||||
"drawing_name": drawing,
|
||||
"area_code": area,
|
||||
@@ -505,42 +509,85 @@ def classify_material_item(material: Dict) -> Dict:
|
||||
)
|
||||
|
||||
description = material.get("original_description", "")
|
||||
size_spec = material.get("size_spec", "")
|
||||
length = material.get("length", 0.0) # 길이 정보 추가
|
||||
|
||||
# 각 분류기로 분류 시도
|
||||
classifiers = [
|
||||
("PIPE", pipe_classifier.classify_pipe),
|
||||
("FITTING", fitting_classifier.classify_fitting),
|
||||
("BOLT", bolt_classifier.classify_bolt),
|
||||
("VALVE", valve_classifier.classify_valve),
|
||||
("INSTRUMENT", instrument_classifier.classify_instrument),
|
||||
("FLANGE", flange_classifier.classify_flange),
|
||||
("GASKET", gasket_classifier.classify_gasket)
|
||||
]
|
||||
print(f"분류 시도: {description}")
|
||||
|
||||
best_result = None
|
||||
best_confidence = 0.0
|
||||
# 각 분류기로 분류 시도 (개선된 순서와 기준)
|
||||
desc_upper = description.upper()
|
||||
|
||||
for category, classifier_func in classifiers:
|
||||
try:
|
||||
result = classifier_func(description)
|
||||
if result and result.get("confidence", 0) > best_confidence:
|
||||
best_result = result
|
||||
best_confidence = result.get("confidence", 0)
|
||||
except Exception:
|
||||
continue
|
||||
# 1. 명확한 키워드 우선 확인 (높은 신뢰도)
|
||||
if any(keyword in desc_upper for keyword in ['FLG', 'FLANGE', '플랜지', 'RF', 'WN', 'SO', 'BLIND']):
|
||||
classification_result = flange_classifier.classify_flange("", description, size_spec, length)
|
||||
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
elif any(keyword in desc_upper for keyword in ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', '밸브', '게이트', '볼']):
|
||||
classification_result = valve_classifier.classify_valve("", description, size_spec, length)
|
||||
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
elif any(keyword in desc_upper for keyword in ['ELBOW', 'ELL', 'TEE', 'REDUCER', 'CAP', 'COUPLING', '엘보', '티', '리듀서', '캡']):
|
||||
classification_result = fitting_classifier.classify_fitting("", description, size_spec, length)
|
||||
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
elif any(keyword in desc_upper for keyword in ['BOLT', 'STUD', 'NUT', 'SCREW', '볼트', '너트', '스터드']):
|
||||
classification_result = bolt_classifier.classify_bolt("", description, size_spec, length)
|
||||
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
elif any(keyword in desc_upper for keyword in ['GASKET', 'GASK', '가스켓']):
|
||||
classification_result = gasket_classifier.classify_gasket("", description, size_spec, length)
|
||||
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
elif any(keyword in desc_upper for keyword in ['GAUGE', 'SENSOR', 'TRANSMITTER', 'INSTRUMENT', '계기', '게이지']):
|
||||
classification_result = instrument_classifier.classify_instrument("", description, size_spec, length)
|
||||
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
elif any(keyword in desc_upper for keyword in ['PIPE', 'TUBE', '파이프', '배관']):
|
||||
classification_result = pipe_classifier.classify_pipe("", description, size_spec, length)
|
||||
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
else:
|
||||
# 2. 일반적인 분류 시도 (낮은 신뢰도 임계값)
|
||||
classification_result = flange_classifier.classify_flange("", description, size_spec, length)
|
||||
print(f"FLANGE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.3:
|
||||
classification_result = valve_classifier.classify_valve("", description, size_spec, length)
|
||||
print(f"VALVE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.3:
|
||||
classification_result = fitting_classifier.classify_fitting("", description, size_spec, length)
|
||||
print(f"FITTING 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.3:
|
||||
classification_result = pipe_classifier.classify_pipe("", description, size_spec, length)
|
||||
print(f"PIPE 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.3:
|
||||
classification_result = bolt_classifier.classify_bolt("", description, size_spec, length)
|
||||
print(f"BOLT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.3:
|
||||
classification_result = gasket_classifier.classify_gasket("", description, size_spec, length)
|
||||
print(f"GASKET 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
if classification_result.get("overall_confidence", 0) < 0.3:
|
||||
classification_result = instrument_classifier.classify_instrument("", description, size_spec, length)
|
||||
print(f"INSTRUMENT 분류 결과: {classification_result.get('category', 'UNKNOWN')} (신뢰도: {classification_result.get('overall_confidence', 0)})")
|
||||
|
||||
print(f"최종 분류 결과: {classification_result.get('category', 'UNKNOWN')}")
|
||||
|
||||
# 재질 분류
|
||||
material_result = material_classifier.classify_material(description)
|
||||
|
||||
# 최종 결과 조합
|
||||
# schedule이 딕셔너리인 경우 문자열로 변환
|
||||
schedule_value = classification_result.get("schedule", "")
|
||||
if isinstance(schedule_value, dict):
|
||||
schedule_value = schedule_value.get("schedule", "")
|
||||
|
||||
final_result = {
|
||||
**material,
|
||||
"classified_category": best_result.get("category", "UNKNOWN") if best_result else "UNKNOWN",
|
||||
"classified_subcategory": best_result.get("subcategory", "") if best_result else "",
|
||||
"classified_category": classification_result.get("category", "UNKNOWN"),
|
||||
"classified_subcategory": classification_result.get("subcategory", ""),
|
||||
"material_grade": material_result.get("grade", "") if material_result else "",
|
||||
"schedule": best_result.get("schedule", "") if best_result else "",
|
||||
"size_spec": best_result.get("size_spec", "") if best_result else "",
|
||||
"classification_confidence": best_confidence
|
||||
"schedule": schedule_value,
|
||||
"size_spec": classification_result.get("size_spec", ""),
|
||||
"classification_confidence": classification_result.get("overall_confidence", 0.0),
|
||||
"length": length # 길이 정보 추가
|
||||
}
|
||||
|
||||
return final_result
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, Numeric, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, Numeric, ForeignKey, JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
@@ -59,12 +59,13 @@ class Material(Base):
|
||||
size_spec = Column(String(50))
|
||||
quantity = Column(Numeric(10, 3), nullable=False)
|
||||
unit = Column(String(10), nullable=False)
|
||||
# length = Column(Numeric(10, 3)) # 임시로 주석 처리
|
||||
drawing_name = Column(String(100))
|
||||
area_code = Column(String(20))
|
||||
line_no = Column(String(50))
|
||||
classification_confidence = Column(Numeric(3, 2))
|
||||
is_verified = Column(Boolean, default=False)
|
||||
verified_by = Column(String(100))
|
||||
verified_by = Column(String(50))
|
||||
verified_at = Column(DateTime)
|
||||
drawing_reference = Column(String(100))
|
||||
notes = Column(Text)
|
||||
@@ -72,3 +73,239 @@ class Material(Base):
|
||||
|
||||
# 관계 설정
|
||||
file = relationship("File", back_populates="materials")
|
||||
|
||||
# ========== 자재 규격/재질 기준표 테이블들 ==========
|
||||
|
||||
class MaterialStandard(Base):
|
||||
"""자재 규격 표준 (ASTM, KS, JIS 등)"""
|
||||
__tablename__ = "material_standards"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
standard_code = Column(String(20), unique=True, nullable=False, index=True) # ASTM_ASME, KS, JIS
|
||||
standard_name = Column(String(100), nullable=False) # 미국재질학회, 한국산업표준, 일본공업규격
|
||||
description = Column(Text)
|
||||
country = Column(String(50)) # USA, KOREA, JAPAN
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
categories = relationship("MaterialCategory", back_populates="standard")
|
||||
|
||||
class MaterialCategory(Base):
|
||||
"""제조방식별 카테고리 (FORGED, WELDED, CAST 등)"""
|
||||
__tablename__ = "material_categories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
standard_id = Column(Integer, ForeignKey("material_standards.id"))
|
||||
category_code = Column(String(50), nullable=False) # FORGED_GRADES, WELDED_GRADES, CAST_GRADES
|
||||
category_name = Column(String(100), nullable=False) # 단조품, 용접품, 주조품
|
||||
description = Column(Text)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
standard = relationship("MaterialStandard", back_populates="categories")
|
||||
specifications = relationship("MaterialSpecification", back_populates="category")
|
||||
|
||||
class MaterialSpecification(Base):
|
||||
"""구체적인 규격 (A182, A105, D3507 등)"""
|
||||
__tablename__ = "material_specifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
category_id = Column(Integer, ForeignKey("material_categories.id"))
|
||||
spec_code = Column(String(20), nullable=False) # A182, A105, D3507
|
||||
spec_name = Column(String(100), nullable=False) # 탄소강 단조품, 배관용 탄소강관
|
||||
description = Column(Text)
|
||||
material_type = Column(String(50)) # carbon_alloy, stainless, carbon
|
||||
manufacturing = Column(String(50)) # FORGED, WELDED_FABRICATED, CAST, SEAMLESS
|
||||
pressure_rating = Column(String(100)) # 150LB ~ 9000LB
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
category = relationship("MaterialCategory", back_populates="specifications")
|
||||
grades = relationship("MaterialGrade", back_populates="specification")
|
||||
patterns = relationship("MaterialPattern", back_populates="specification")
|
||||
|
||||
class MaterialGrade(Base):
|
||||
"""등급별 상세 정보 (F1, F5, WPA, WPB 등)"""
|
||||
__tablename__ = "material_grades"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
specification_id = Column(Integer, ForeignKey("material_specifications.id"))
|
||||
grade_code = Column(String(20), nullable=False) # F1, F5, WPA, WPB
|
||||
grade_name = Column(String(100))
|
||||
composition = Column(String(200)) # 0.5Mo, 5Cr-0.5Mo, 18Cr-8Ni
|
||||
applications = Column(String(200)) # 중온용, 고온용, 저압용
|
||||
temp_max = Column(String(50)) # 482°C, 649°C
|
||||
temp_range = Column(String(100)) # -29°C ~ 400°C
|
||||
yield_strength = Column(String(50)) # 30 ksi, 35 ksi
|
||||
tensile_strength = Column(String(50))
|
||||
corrosion_resistance = Column(String(50)) # 보통, 우수
|
||||
stabilizer = Column(String(50)) # Titanium, Niobium
|
||||
base_grade = Column(String(20)) # 304, 316
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
specification = relationship("MaterialSpecification", back_populates="grades")
|
||||
|
||||
class MaterialPattern(Base):
|
||||
"""정규식 패턴들"""
|
||||
__tablename__ = "material_patterns"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
specification_id = Column(Integer, ForeignKey("material_specifications.id"))
|
||||
pattern = Column(Text, nullable=False) # 정규식 패턴
|
||||
description = Column(String(200))
|
||||
priority = Column(Integer, default=1) # 패턴 우선순위
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
specification = relationship("MaterialSpecification", back_populates="patterns")
|
||||
|
||||
class SpecialMaterial(Base):
|
||||
"""특수 재질 (INCONEL, HASTELLOY, TITANIUM 등)"""
|
||||
__tablename__ = "special_materials"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
material_type = Column(String(50), nullable=False) # SUPER_ALLOYS, TITANIUM, COPPER_ALLOYS
|
||||
material_name = Column(String(100), nullable=False) # INCONEL, HASTELLOY, TITANIUM
|
||||
description = Column(Text)
|
||||
composition = Column(String(200))
|
||||
applications = Column(Text)
|
||||
temp_max = Column(String(50))
|
||||
manufacturing = Column(String(50))
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
grades = relationship("SpecialMaterialGrade", back_populates="material")
|
||||
patterns = relationship("SpecialMaterialPattern", back_populates="material")
|
||||
|
||||
class SpecialMaterialGrade(Base):
|
||||
"""특수 재질 등급"""
|
||||
__tablename__ = "special_material_grades"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
material_id = Column(Integer, ForeignKey("special_materials.id"))
|
||||
grade_code = Column(String(20), nullable=False) # 600, 625, C276
|
||||
composition = Column(String(200))
|
||||
applications = Column(String(200))
|
||||
temp_max = Column(String(50))
|
||||
strength = Column(String(50))
|
||||
purity = Column(String(100))
|
||||
corrosion = Column(String(50))
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
material = relationship("SpecialMaterial", back_populates="grades")
|
||||
|
||||
class SpecialMaterialPattern(Base):
|
||||
"""특수 재질 정규식 패턴"""
|
||||
__tablename__ = "special_material_patterns"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
material_id = Column(Integer, ForeignKey("special_materials.id"))
|
||||
pattern = Column(Text, nullable=False)
|
||||
description = Column(String(200))
|
||||
priority = Column(Integer, default=1)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
material = relationship("SpecialMaterial", back_populates="patterns")
|
||||
|
||||
# ========== 파이프 상세 정보 및 사용자 요구사항 테이블 ==========
|
||||
|
||||
class PipeDetail(Base):
|
||||
"""파이프 상세 정보"""
|
||||
__tablename__ = "pipe_details"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
file_id = Column(Integer, ForeignKey("files.id"), nullable=False)
|
||||
|
||||
# 재질 정보
|
||||
material_standard = Column(String(50)) # ASTM, KS, JIS 등
|
||||
material_grade = Column(String(50)) # A106, A53, STPG370 등
|
||||
material_type = Column(String(50)) # CARBON, STAINLESS 등
|
||||
|
||||
# 파이프 특화 정보
|
||||
manufacturing_method = Column(String(50)) # SEAMLESS, WELDED, CAST
|
||||
end_preparation = Column(String(50)) # BOTH_ENDS_BEVELED, ONE_END_BEVELED, NO_BEVEL
|
||||
schedule = Column(String(50)) # SCH 10, 20, 40, 80 등
|
||||
wall_thickness = Column(String(50)) # 벽두께 정보
|
||||
|
||||
# 치수 정보
|
||||
nominal_size = Column(String(50)) # MAIN_NOM (인치, 직경)
|
||||
length_mm = Column(Numeric(10, 3)) # LENGTH (길이)
|
||||
|
||||
# 신뢰도
|
||||
material_confidence = Column(Numeric(3, 2))
|
||||
manufacturing_confidence = Column(Numeric(3, 2))
|
||||
end_prep_confidence = Column(Numeric(3, 2))
|
||||
schedule_confidence = Column(Numeric(3, 2))
|
||||
|
||||
# 메타데이터
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
file = relationship("File", backref="pipe_details")
|
||||
|
||||
class RequirementType(Base):
|
||||
"""요구사항 타입 마스터"""
|
||||
__tablename__ = "requirement_types"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
type_code = Column(String(50), unique=True, nullable=False) # 'IMPACT_TEST', 'HEAT_TREATMENT' 등
|
||||
type_name = Column(String(100), nullable=False) # '임팩테스트', '열처리' 등
|
||||
category = Column(String(50), nullable=False) # 'TEST', 'TREATMENT', 'CERTIFICATION', 'CUSTOM' 등
|
||||
description = Column(Text) # 타입 설명
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
requirements = relationship("UserRequirement", back_populates="requirement_type")
|
||||
|
||||
class UserRequirement(Base):
|
||||
"""사용자 추가 요구사항"""
|
||||
__tablename__ = "user_requirements"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
file_id = Column(Integer, ForeignKey("files.id"), nullable=False)
|
||||
|
||||
# 요구사항 타입
|
||||
requirement_type = Column(String(50), nullable=False) # 'IMPACT_TEST', 'HEAT_TREATMENT', 'CUSTOM_SPEC', 'CERTIFICATION' 등
|
||||
|
||||
# 요구사항 내용
|
||||
requirement_title = Column(String(200), nullable=False) # '임팩테스트', '열처리', '인증서' 등
|
||||
requirement_description = Column(Text) # 상세 설명
|
||||
requirement_spec = Column(Text) # 구체적 스펙 (예: "Charpy V-notch -20°C")
|
||||
|
||||
# 상태 관리
|
||||
status = Column(String(20), default='PENDING') # 'PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'
|
||||
priority = Column(String(20), default='NORMAL') # 'LOW', 'NORMAL', 'HIGH', 'URGENT'
|
||||
|
||||
# 담당자 정보
|
||||
assigned_to = Column(String(100)) # 담당자명
|
||||
due_date = Column(DateTime) # 완료 예정일
|
||||
|
||||
# 메타데이터
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 관계 설정
|
||||
file = relationship("File", backref="user_requirements")
|
||||
requirement_type_rel = relationship("RequirementType", back_populates="requirements")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 분류
|
||||
|
||||
|
||||
@@ -86,11 +86,11 @@ FITTING_TYPES = {
|
||||
},
|
||||
|
||||
"OLET": {
|
||||
"dat_file_patterns": ["SOL_", "WOL_", "TOL_", "OLET_"],
|
||||
"description_keywords": ["OLET", "올렛", "O-LET"],
|
||||
"dat_file_patterns": ["SOL_", "WOL_", "TOL_", "OLET_", "SOCK-O-LET", "WELD-O-LET"],
|
||||
"description_keywords": ["OLET", "올렛", "O-LET", "SOCK-O-LET", "WELD-O-LET", "SOCKOLET", "WELDOLET"],
|
||||
"subtypes": {
|
||||
"SOCKOLET": ["SOCK-O-LET", "SOCKOLET", "SOL"],
|
||||
"WELDOLET": ["WELD-O-LET", "WELDOLET", "WOL"],
|
||||
"SOCKOLET": ["SOCK-O-LET", "SOCKOLET", "SOL", "SOCK O-LET"],
|
||||
"WELDOLET": ["WELD-O-LET", "WELDOLET", "WOL", "WELD O-LET"],
|
||||
"THREADOLET": ["THREAD-O-LET", "THREADOLET", "TOL"],
|
||||
"ELBOLET": ["ELB-O-LET", "ELBOLET", "EOL"]
|
||||
},
|
||||
@@ -171,7 +171,7 @@ PRESSURE_RATINGS = {
|
||||
}
|
||||
|
||||
def classify_fitting(dat_file: str, description: str, main_nom: str,
|
||||
red_nom: str = None) -> Dict:
|
||||
red_nom: str = None, length: float = None) -> Dict:
|
||||
"""
|
||||
완전한 FITTING 분류
|
||||
|
||||
@@ -185,7 +185,21 @@ def classify_fitting(dat_file: str, description: str, main_nom: str,
|
||||
완전한 피팅 분류 결과
|
||||
"""
|
||||
|
||||
# 1. 재질 분류 (공통 모듈 사용)
|
||||
desc_upper = description.upper()
|
||||
dat_upper = dat_file.upper()
|
||||
|
||||
# 1. 명칭 우선 확인 (피팅 키워드가 있으면 피팅)
|
||||
fitting_keywords = ['ELBOW', 'TEE', 'REDUCER', 'CAP', 'NIPPLE', 'SWAGE', 'OLET', 'COUPLING', '엘보', '티', '리듀서', '캡', '니플', '스웨지', '올렛', '커플링', 'SOCK-O-LET', 'WELD-O-LET', 'SOCKOLET', 'WELDOLET']
|
||||
is_fitting = any(keyword in desc_upper or keyword in dat_upper for keyword in fitting_keywords)
|
||||
|
||||
if not is_fitting:
|
||||
return {
|
||||
"category": "UNKNOWN",
|
||||
"overall_confidence": 0.0,
|
||||
"reason": "피팅 키워드 없음"
|
||||
}
|
||||
|
||||
# 2. 재질 분류 (공통 모듈 사용)
|
||||
material_result = classify_material(description)
|
||||
|
||||
# 2. 피팅 타입 분류
|
||||
@@ -328,7 +342,7 @@ def classify_fitting_subtype(fitting_type: str, description: str,
|
||||
|
||||
# 2. 사이즈 분석이 필요한 경우 (TEE, REDUCER 등)
|
||||
if type_data.get("size_analysis"):
|
||||
if red_nom and red_nom.strip() and red_nom != main_nom:
|
||||
if red_nom and str(red_nom).strip() and red_nom != main_nom:
|
||||
return {
|
||||
"subtype": "REDUCING",
|
||||
"confidence": 0.85,
|
||||
@@ -343,7 +357,7 @@ def classify_fitting_subtype(fitting_type: str, description: str,
|
||||
|
||||
# 3. 두 사이즈가 필요한 경우 확인
|
||||
if type_data.get("requires_two_sizes"):
|
||||
if red_nom and red_nom.strip():
|
||||
if red_nom and str(red_nom).strip():
|
||||
confidence = 0.8
|
||||
evidence = [f"TWO_SIZES_PROVIDED: {main_nom} x {red_nom}"]
|
||||
else:
|
||||
@@ -511,11 +525,12 @@ def determine_fitting_manufacturing(material_result: Dict, connection_result: Di
|
||||
|
||||
def format_fitting_size(main_nom: str, red_nom: str = None) -> str:
|
||||
"""피팅 사이즈 표기 포맷팅"""
|
||||
|
||||
if red_nom and red_nom.strip() and red_nom != main_nom:
|
||||
return f"{main_nom} x {red_nom}"
|
||||
main_nom_str = str(main_nom) if main_nom is not None else ""
|
||||
red_nom_str = str(red_nom) if red_nom is not None else ""
|
||||
if red_nom_str.strip() and red_nom_str != main_nom_str:
|
||||
return f"{main_nom_str} x {red_nom_str}"
|
||||
else:
|
||||
return main_nom
|
||||
return main_nom_str
|
||||
|
||||
def calculate_fitting_confidence(confidence_scores: Dict) -> float:
|
||||
"""피팅 분류 전체 신뢰도 계산"""
|
||||
|
||||
@@ -10,8 +10,8 @@ from .material_classifier import classify_material, get_manufacturing_method_fro
|
||||
# ========== SPECIAL FLANGE 타입 ==========
|
||||
SPECIAL_FLANGE_TYPES = {
|
||||
"ORIFICE": {
|
||||
"dat_file_patterns": ["FLG_ORI_", "ORI_"],
|
||||
"description_keywords": ["ORIFICE", "오리피스", "유량측정"],
|
||||
"dat_file_patterns": ["FLG_ORI_", "ORI_", "ORIFICE_"],
|
||||
"description_keywords": ["ORIFICE", "오리피스", "유량측정", "구멍"],
|
||||
"characteristics": "유량 측정용 구멍",
|
||||
"special_features": ["BORE_SIZE", "FLOW_MEASUREMENT"]
|
||||
},
|
||||
@@ -164,7 +164,7 @@ FLANGE_PRESSURE_RATINGS = {
|
||||
}
|
||||
|
||||
def classify_flange(dat_file: str, description: str, main_nom: str,
|
||||
red_nom: str = None) -> Dict:
|
||||
red_nom: str = None, length: float = None) -> Dict:
|
||||
"""
|
||||
완전한 FLANGE 분류
|
||||
|
||||
@@ -178,7 +178,21 @@ def classify_flange(dat_file: str, description: str, main_nom: str,
|
||||
완전한 플랜지 분류 결과
|
||||
"""
|
||||
|
||||
# 1. 재질 분류 (공통 모듈 사용)
|
||||
desc_upper = description.upper()
|
||||
dat_upper = dat_file.upper()
|
||||
|
||||
# 1. 명칭 우선 확인 (플랜지 키워드가 있으면 플랜지)
|
||||
flange_keywords = ['FLG', 'FLANGE', '플랜지']
|
||||
is_flange = any(keyword in desc_upper or keyword in dat_upper for keyword in flange_keywords)
|
||||
|
||||
if not is_flange:
|
||||
return {
|
||||
"category": "UNKNOWN",
|
||||
"overall_confidence": 0.0,
|
||||
"reason": "플랜지 키워드 없음"
|
||||
}
|
||||
|
||||
# 2. 재질 분류 (공통 모듈 사용)
|
||||
material_result = classify_material(description)
|
||||
|
||||
# 2. SPECIAL vs STANDARD 분류
|
||||
@@ -490,11 +504,12 @@ def determine_flange_manufacturing(material_result: Dict, flange_type_result: Di
|
||||
|
||||
def format_flange_size(main_nom: str, red_nom: str = None) -> str:
|
||||
"""플랜지 사이즈 표기 포맷팅"""
|
||||
|
||||
if red_nom and red_nom.strip() and red_nom != main_nom:
|
||||
return f"{main_nom} x {red_nom}"
|
||||
main_nom_str = str(main_nom) if main_nom is not None else ""
|
||||
red_nom_str = str(red_nom) if red_nom is not None else ""
|
||||
if red_nom_str.strip() and red_nom_str != main_nom_str:
|
||||
return f"{main_nom_str} x {red_nom_str}"
|
||||
else:
|
||||
return main_nom
|
||||
return main_nom_str
|
||||
|
||||
def calculate_flange_confidence(confidence_scores: Dict) -> float:
|
||||
"""플랜지 분류 전체 신뢰도 계산"""
|
||||
|
||||
@@ -160,7 +160,7 @@ GASKET_SIZE_PATTERNS = {
|
||||
"thickness": r"THK?\s*(\d+(?:\.\d+)?)\s*MM"
|
||||
}
|
||||
|
||||
def classify_gasket(dat_file: str, description: str, main_nom: str) -> Dict:
|
||||
def classify_gasket(dat_file: str, description: str, main_nom: str, length: float = None) -> Dict:
|
||||
"""
|
||||
완전한 GASKET 분류
|
||||
|
||||
|
||||
@@ -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 분류
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def classify_material(description: str) -> Dict:
|
||||
재질 분류 결과 딕셔너리
|
||||
"""
|
||||
|
||||
desc_upper = description.upper().strip()
|
||||
desc_upper = str(description).upper().strip() if description is not None else ""
|
||||
|
||||
# 1단계: 특수 재질 우선 확인 (가장 구체적)
|
||||
special_result = check_special_materials(desc_upper)
|
||||
|
||||
@@ -63,7 +63,7 @@ PIPE_SCHEDULE = {
|
||||
}
|
||||
|
||||
def classify_pipe(dat_file: str, description: str, main_nom: str,
|
||||
length: float = None) -> Dict:
|
||||
length: Optional[float] = None) -> Dict:
|
||||
"""
|
||||
완전한 PIPE 분류
|
||||
|
||||
@@ -77,7 +77,38 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
|
||||
완전한 파이프 분류 결과
|
||||
"""
|
||||
|
||||
# 1. 재질 분류 (공통 모듈 사용)
|
||||
desc_upper = description.upper()
|
||||
|
||||
# 1. 명칭 우선 확인 (다른 자재 타입 키워드가 있으면 파이프가 아님)
|
||||
other_material_keywords = [
|
||||
'FLG', 'FLANGE', '플랜지', # 플랜지
|
||||
'ELBOW', 'ELL', 'TEE', 'REDUCER', 'CAP', 'COUPLING', '엘보', '티', '리듀서', # 피팅
|
||||
'VALVE', 'BALL', 'GATE', 'GLOBE', 'CHECK', '밸브', # 밸브
|
||||
'BOLT', 'STUD', 'NUT', 'SCREW', '볼트', '너트', # 볼트
|
||||
'GASKET', 'GASK', '가스켓', # 가스켓
|
||||
'GAUGE', 'SENSOR', 'TRANSMITTER', 'INSTRUMENT', '계기' # 계기
|
||||
]
|
||||
|
||||
for keyword in other_material_keywords:
|
||||
if keyword in desc_upper:
|
||||
return {
|
||||
"category": "UNKNOWN",
|
||||
"overall_confidence": 0.0,
|
||||
"reason": f"다른 자재 키워드 발견: {keyword}"
|
||||
}
|
||||
|
||||
# 2. 파이프 키워드 확인
|
||||
pipe_keywords = ['PIPE', 'TUBE', '파이프', '배관']
|
||||
is_pipe = any(keyword in desc_upper for keyword in pipe_keywords)
|
||||
|
||||
if not is_pipe:
|
||||
return {
|
||||
"category": "UNKNOWN",
|
||||
"overall_confidence": 0.0,
|
||||
"reason": "파이프 키워드 없음"
|
||||
}
|
||||
|
||||
# 3. 재질 분류 (공통 모듈 사용)
|
||||
material_result = classify_material(description)
|
||||
|
||||
# 2. 제조 방법 분류
|
||||
@@ -89,8 +120,8 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
|
||||
# 4. 스케줄 분류
|
||||
schedule_result = classify_pipe_schedule(description)
|
||||
|
||||
# 5. 절단 치수 처리
|
||||
cutting_dimensions = extract_pipe_cutting_dimensions(length, description)
|
||||
# 5. 길이(절단 치수) 처리
|
||||
length_info = extract_pipe_length_info(length, description)
|
||||
|
||||
# 6. 최종 결과 조합
|
||||
return {
|
||||
@@ -124,11 +155,11 @@ def classify_pipe(dat_file: str, description: str, main_nom: str,
|
||||
"confidence": schedule_result.get('confidence', 0.0)
|
||||
},
|
||||
|
||||
"cutting_dimensions": cutting_dimensions,
|
||||
"length_info": length_info,
|
||||
|
||||
"size_info": {
|
||||
"nominal_size": main_nom,
|
||||
"length_mm": cutting_dimensions.get('length_mm')
|
||||
"length_mm": length_info.get('length_mm')
|
||||
},
|
||||
|
||||
# 전체 신뢰도
|
||||
@@ -234,10 +265,10 @@ def classify_pipe_schedule(description: str) -> Dict:
|
||||
"confidence": 0.0
|
||||
}
|
||||
|
||||
def extract_pipe_cutting_dimensions(length: float, description: str) -> Dict:
|
||||
"""파이프 절단 치수 정보 추출"""
|
||||
def extract_pipe_length_info(length: Optional[float], description: str) -> Dict:
|
||||
"""파이프 길이(절단 치수) 정보 추출"""
|
||||
|
||||
cutting_info = {
|
||||
length_info = {
|
||||
"length_mm": None,
|
||||
"source": None,
|
||||
"confidence": 0.0,
|
||||
@@ -246,31 +277,31 @@ def extract_pipe_cutting_dimensions(length: float, description: str) -> Dict:
|
||||
|
||||
# 1. LENGTH 필드에서 추출 (우선)
|
||||
if length and length > 0:
|
||||
cutting_info.update({
|
||||
length_info.update({
|
||||
"length_mm": round(length, 1),
|
||||
"source": "LENGTH_FIELD",
|
||||
"confidence": 0.95,
|
||||
"note": f"도면 명기 치수: {length}mm"
|
||||
"note": f"도면 명기 길이: {length}mm"
|
||||
})
|
||||
|
||||
# 2. DESCRIPTION에서 백업 추출
|
||||
else:
|
||||
desc_length = extract_length_from_description(description)
|
||||
if desc_length:
|
||||
cutting_info.update({
|
||||
length_info.update({
|
||||
"length_mm": desc_length,
|
||||
"source": "DESCRIPTION_PARSED",
|
||||
"confidence": 0.8,
|
||||
"note": f"설명란에서 추출: {desc_length}mm"
|
||||
})
|
||||
else:
|
||||
cutting_info.update({
|
||||
length_info.update({
|
||||
"source": "NO_LENGTH_INFO",
|
||||
"confidence": 0.0,
|
||||
"note": "절단 치수 정보 없음 - 도면 확인 필요"
|
||||
"note": "길이 정보 없음 - 도면 확인 필요"
|
||||
})
|
||||
|
||||
return cutting_info
|
||||
return length_info
|
||||
|
||||
def extract_length_from_description(description: str) -> Optional[float]:
|
||||
"""DESCRIPTION에서 길이 정보 추출"""
|
||||
@@ -318,7 +349,7 @@ def generate_pipe_cutting_plan(pipe_data: Dict) -> Dict:
|
||||
|
||||
cutting_plan = {
|
||||
"material_spec": f"{pipe_data['material']['grade']} {pipe_data['schedule']['schedule']}",
|
||||
"length_mm": pipe_data['cutting_dimensions']['length_mm'],
|
||||
"length_mm": pipe_data['length_info']['length_mm'],
|
||||
"end_preparation": pipe_data['end_preparation']['cutting_note'],
|
||||
"machining_required": pipe_data['end_preparation']['machining_required']
|
||||
}
|
||||
@@ -332,6 +363,6 @@ def generate_pipe_cutting_plan(pipe_data: Dict) -> Dict:
|
||||
가공여부: {'베벨가공 필요' if cutting_plan['machining_required'] else '직각절단만'}
|
||||
""".strip()
|
||||
else:
|
||||
cutting_plan["cutting_instruction"] = "도면 확인 후 절단 치수 입력 필요"
|
||||
cutting_plan["cutting_instruction"] = "도면 확인 후 길이 정보 입력 필요"
|
||||
|
||||
return cutting_plan
|
||||
|
||||
@@ -66,7 +66,7 @@ VALVE_TYPES = {
|
||||
|
||||
"RELIEF_VALVE": {
|
||||
"dat_file_patterns": ["RELIEF_", "RV_", "PSV_"],
|
||||
"description_keywords": ["RELIEF VALVE", "PRESSURE RELIEF", "SAFETY VALVE", "릴리프"],
|
||||
"description_keywords": ["RELIEF VALVE", "PRESSURE RELIEF", "SAFETY VALVE", "PSV", "압력 안전", "릴리프"],
|
||||
"characteristics": "안전 압력 방출용",
|
||||
"typical_connections": ["FLANGED", "THREADED"],
|
||||
"pressure_range": "150LB ~ 2500LB",
|
||||
@@ -196,20 +196,34 @@ VALVE_PRESSURE_RATINGS = {
|
||||
}
|
||||
}
|
||||
|
||||
def classify_valve(dat_file: str, description: str, main_nom: str) -> Dict:
|
||||
def classify_valve(dat_file: str, description: str, main_nom: str, length: float = None) -> Dict:
|
||||
"""
|
||||
완전한 VALVE 분류
|
||||
|
||||
Args:
|
||||
dat_file: DAT_FILE 필드
|
||||
description: DESCRIPTION 필드
|
||||
main_nom: MAIN_NOM 필드 (밸브 사이즈)
|
||||
main_nom: MAIN_NOM 필드 (사이즈)
|
||||
|
||||
Returns:
|
||||
완전한 밸브 분류 결과
|
||||
"""
|
||||
|
||||
# 1. 재질 분류 (공통 모듈 사용)
|
||||
desc_upper = description.upper()
|
||||
dat_upper = dat_file.upper()
|
||||
|
||||
# 1. 명칭 우선 확인 (밸브 키워드가 있으면 밸브)
|
||||
valve_keywords = ['VALVE', 'GATE', 'BALL', 'GLOBE', 'CHECK', 'BUTTERFLY', 'NEEDLE', 'RELIEF', 'SOLENOID', 'PLUG', '밸브', '게이트', '볼', '글로브', '체크', '버터플라이', '니들', '릴리프', '솔레노이드', '플러그']
|
||||
is_valve = any(keyword in desc_upper or keyword in dat_upper for keyword in valve_keywords)
|
||||
|
||||
if not is_valve:
|
||||
return {
|
||||
"category": "UNKNOWN",
|
||||
"overall_confidence": 0.0,
|
||||
"reason": "밸브 키워드 없음"
|
||||
}
|
||||
|
||||
# 2. 재질 분류 (공통 모듈 사용)
|
||||
material_result = classify_material(description)
|
||||
|
||||
# 2. 밸브 타입 분류
|
||||
|
||||
5
backend/scripts/05_add_length_to_materials.sql
Normal file
5
backend/scripts/05_add_length_to_materials.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- materials 테이블에 length 컬럼 추가
|
||||
ALTER TABLE materials ADD COLUMN length NUMERIC(10, 3);
|
||||
|
||||
-- 기존 데이터의 length 컬럼을 NULL로 초기화
|
||||
UPDATE materials SET length = NULL;
|
||||
137
backend/scripts/05_create_material_standards_tables.sql
Normal file
137
backend/scripts/05_create_material_standards_tables.sql
Normal 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);
|
||||
109
backend/scripts/05_create_pipe_details_and_requirements.sql
Normal file
109
backend/scripts/05_create_pipe_details_and_requirements.sql
Normal 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;
|
||||
@@ -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();
|
||||
462
backend/scripts/06_insert_material_standards_data.py
Normal file
462
backend/scripts/06_insert_material_standards_data.py
Normal 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()
|
||||
193
backend/scripts/07_execute_material_standards_migration.py
Normal file
193
backend/scripts/07_execute_material_standards_migration.py
Normal 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()
|
||||
237
backend/scripts/create_material_detail_tables.sql
Normal file
237
backend/scripts/create_material_detail_tables.sql
Normal 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);
|
||||
@@ -81,6 +81,7 @@ CREATE TABLE materials (
|
||||
|
||||
-- 분류 신뢰도 및 검증
|
||||
classification_confidence DECIMAL(3,2), -- 0.00-1.00 분류 신뢰도
|
||||
classification_details JSONB, -- 분류 상세 정보 (JSON)
|
||||
is_verified BOOLEAN DEFAULT false, -- 사용자 검증 여부
|
||||
verified_by VARCHAR(100),
|
||||
verified_at TIMESTAMP,
|
||||
|
||||
8
database/init/02_add_classification_details.sql
Normal file
8
database/init/02_add_classification_details.sql
Normal 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);
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import JobSelectionPage from './pages/JobSelectionPage';
|
||||
import BOMManagerPage from './pages/BOMManagerPage';
|
||||
import BOMStatusPage from './pages/BOMStatusPage';
|
||||
import MaterialsPage from './pages/MaterialsPage';
|
||||
|
||||
function App() {
|
||||
@@ -9,7 +9,7 @@ function App() {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<JobSelectionPage />} />
|
||||
<Route path="/bom-manager" element={<BOMManagerPage />} />
|
||||
<Route path="/bom-status" element={<BOMStatusPage />} />
|
||||
<Route path="/materials" element={<MaterialsPage />} />
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
|
||||
@@ -94,6 +94,18 @@ export function createJob(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) {
|
||||
return api.put(`/projects/${projectId}`, data);
|
||||
|
||||
@@ -388,20 +388,20 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
📊 업로드 결과
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Description />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="파일명"
|
||||
secondary={uploadResult.original_filename}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircle />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
<ListItemText
|
||||
primary="파싱된 자재 수"
|
||||
secondary={`${uploadResult.parsed_materials_count}개`}
|
||||
/>
|
||||
@@ -413,9 +413,9 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
<ListItemText
|
||||
primary="저장된 자재 수"
|
||||
secondary={`${uploadResult.saved_materials_count}개`}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
@@ -434,11 +434,11 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
/>
|
||||
<Typography variant="body2">
|
||||
{stat.count}개 ({stat.percentage}%)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -470,67 +470,67 @@ function FileUpload({ selectedProject, onUploadSuccess }) {
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Paper
|
||||
{...getRootProps()}
|
||||
) : (
|
||||
<>
|
||||
<Paper
|
||||
{...getRootProps()}
|
||||
onClick={() => console.log('드래그 앤 드롭 영역 클릭됨')}
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
border: 2,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: isDragActive ? 'primary.main' : 'grey.300',
|
||||
bgcolor: isDragActive ? 'primary.50' : 'grey.50',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.50'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<CloudUpload sx={{
|
||||
fontSize: 64,
|
||||
color: isDragActive ? 'primary.main' : 'grey.400',
|
||||
mb: 2
|
||||
}} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{isDragActive
|
||||
? "파일을 여기에 놓으세요!"
|
||||
: "Excel 파일을 드래그하거나 클릭하여 선택"
|
||||
}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
||||
지원 형식: .xlsx, .xls, .csv (최대 10MB)
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AttachFile />}
|
||||
component="span"
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
border: 2,
|
||||
borderStyle: 'dashed',
|
||||
borderColor: isDragActive ? 'primary.main' : 'grey.300',
|
||||
bgcolor: isDragActive ? 'primary.50' : 'grey.50',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
borderColor: 'primary.main',
|
||||
bgcolor: 'primary.50'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<CloudUpload sx={{
|
||||
fontSize: 64,
|
||||
color: isDragActive ? 'primary.main' : 'grey.400',
|
||||
mb: 2
|
||||
}} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{isDragActive
|
||||
? "파일을 여기에 놓으세요!"
|
||||
: "Excel 파일을 드래그하거나 클릭하여 선택"
|
||||
}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
||||
지원 형식: .xlsx, .xls, .csv (최대 10MB)
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AttachFile />}
|
||||
component="span"
|
||||
disabled={uploading}
|
||||
onClick={() => console.log('파일 선택 버튼 클릭됨')}
|
||||
>
|
||||
>
|
||||
{uploading ? '업로드 중...' : '파일 선택'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
💡 <strong>업로드 및 분류 프로세스:</strong>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• BOM(Bill of Materials) 파일을 업로드하면 자동으로 자재 정보를 추출합니다
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• 각 자재는 8가지 분류기(볼트, 플랜지, 피팅, 가스켓, 계기, 파이프, 밸브, 재질)로 자동 분류됩니다
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
• 분류 결과는 신뢰도 점수와 함께 저장되며, 필요시 수동 검증이 가능합니다
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
31
frontend/src/components/PipeDetailsCard.jsx
Normal file
31
frontend/src/components/PipeDetailsCard.jsx
Normal 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;
|
||||
@@ -17,7 +17,7 @@ const BOMStatusPage = () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
let url = '/files';
|
||||
let url = 'http://localhost:8000/files';
|
||||
if (jobNo) {
|
||||
url += `?job_no=${jobNo}`;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ const BOMStatusPage = () => {
|
||||
else setFiles([]);
|
||||
} catch (e) {
|
||||
setError('파일 목록을 불러오지 못했습니다.');
|
||||
console.error('파일 목록 로드 에러:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -45,10 +46,10 @@ const BOMStatusPage = () => {
|
||||
setUploading(true);
|
||||
setError('');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('project_id', 1); // 예시: 실제 프로젝트 ID로 대체 필요
|
||||
const res = await fetch('/files/upload', {
|
||||
const res = await fetch('http://localhost:8000/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
@@ -74,49 +75,62 @@ const BOMStatusPage = () => {
|
||||
accept=".csv,.xlsx,.xls"
|
||||
onChange={e => setFile(e.target.files[0])}
|
||||
disabled={uploading}
|
||||
/>
|
||||
/>
|
||||
<Button type="submit" variant="contained" disabled={!file || uploading} sx={{ ml: 2 }}>
|
||||
업로드
|
||||
</Button>
|
||||
</Button>
|
||||
</form>
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{loading && <CircularProgress sx={{ mt: 4 }} />}
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>파일명</TableCell>
|
||||
<TableCell>리비전</TableCell>
|
||||
<TableCell>세부내역</TableCell>
|
||||
<TableCell>리비전</TableCell>
|
||||
<TableCell>삭제</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{files.map(file => (
|
||||
<TableRow key={file.id}>
|
||||
<TableCell>{file.original_filename || file.filename}</TableCell>
|
||||
<TableCell>{file.revision}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" onClick={() => alert(`자재확인: ${file.original_filename}`)}>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" onClick={() => navigate(`/materials?fileId=${file.id}`)}>
|
||||
자재확인
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" color="info" onClick={() => alert(`리비전 관리: ${file.original_filename}`)}>
|
||||
리비전
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="small" variant="outlined" color="error" onClick={() => alert(`삭제: ${file.original_filename}`)}>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ const JobSelectionPage = () => {
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selectedJobNo && selectedJobName) {
|
||||
navigate(`/bom-manager?job_no=${selectedJobNo}&job_name=${encodeURIComponent(selectedJobName)}`);
|
||||
navigate(`/bom-status?job_no=${selectedJobNo}&job_name=${encodeURIComponent(selectedJobName)}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -14,13 +14,13 @@ const ProjectSelectionPage = () => {
|
||||
async function loadJobs() {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
try {
|
||||
const res = await fetchJobs({});
|
||||
if (res.data && Array.isArray(res.data.jobs)) {
|
||||
setJobs(res.data.jobs);
|
||||
} else {
|
||||
setJobs([]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError('프로젝트 목록을 불러오지 못했습니다.');
|
||||
} finally {
|
||||
@@ -37,8 +37,8 @@ const ProjectSelectionPage = () => {
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
<FormControl size="small" fullWidth sx={{ mt: 3 }}>
|
||||
<InputLabel>Job No</InputLabel>
|
||||
<Select
|
||||
value={selectedJob}
|
||||
<Select
|
||||
value={selectedJob}
|
||||
label="Job No"
|
||||
onChange={e => setSelectedJob(e.target.value)}
|
||||
displayEmpty
|
||||
@@ -47,23 +47,23 @@ const ProjectSelectionPage = () => {
|
||||
{jobs.map(job => (
|
||||
<MenuItem key={job.job_no} value={job.job_no}>
|
||||
{job.job_no} ({job.job_name})
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedJob && (
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedJob && (
|
||||
<Alert severity="info" sx={{ mt: 3 }}>
|
||||
선택된 Job No: <b>{selectedJob}</b>
|
||||
</Alert>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ mt: 4, minWidth: 120 }}
|
||||
disabled={!selectedJob}
|
||||
onClick={() => navigate(`/bom?job_no=${selectedJob}`)}
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
>
|
||||
확인
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user