feat: PIPE 분석 기능 개선 및 자재 확인 페이지 UX 향상

- 자재 확인 페이지에 뒤로가기 버튼 추가
- 상세 목록 탭에 PIPE 분석 섹션 추가
  - 재질-외경-스케줄-제작방식별로 그룹화
  - 동일 속성 파이프들의 길이 합산 표시
  - 총 파이프 길이 및 규격 종류 수 요약
- 파일 삭제 기능 수정 (외래키 제약 조건 해결)
- MaterialsPage에서 전체 자재 목록 표시 (limit 10000)
- 길이 단위 변환 로직 수정 (mm 단위 유지)
- 파싱 로직에 디버그 출력 추가

TODO: MAIN_NOM/RED_NOM 별도 저장을 위한 스키마 개선 필요
This commit is contained in:
Hyungi Ahn
2025-07-17 15:55:40 +09:00
parent 5f7a6f0b3a
commit 82f057a0c9
8 changed files with 1433 additions and 156 deletions

View File

@@ -67,7 +67,10 @@ def generate_unique_filename(original_filename: str) -> str:
def parse_dataframe(df):
df = df.dropna(how='all')
# 원본 컬럼명 출력
print(f"원본 컬럼들: {list(df.columns)}")
df.columns = df.columns.str.strip().str.lower()
print(f"소문자 변환 후: {list(df.columns)}")
column_mapping = {
'description': ['description', 'item', 'material', '품명', '자재명'],
@@ -87,6 +90,8 @@ def parse_dataframe(df):
mapped_columns[standard_col] = possible_name
break
print(f"찾은 컬럼 매핑: {mapped_columns}")
materials = []
for index, row in df.iterrows():
description = str(row.get(mapped_columns.get('description', ''), ''))
@@ -269,6 +274,13 @@ async def upload_file(
RETURNING id
""")
# 첫 번째 자재에 대해서만 디버그 출력
if materials_inserted == 0:
print(f"첫 번째 자재 저장:")
print(f" size_spec: '{material_data['size_spec']}'")
print(f" original_description: {material_data['original_description']}")
print(f" category: {classification_result.get('category', 'UNKNOWN')}")
material_result = db.execute(material_insert_query, {
"file_id": file_id,
"original_description": material_data["original_description"],
@@ -291,20 +303,19 @@ async def upload_file(
if classification_result.get("category") == "PIPE":
print("PIPE 상세 정보 저장 시작")
# 길이 정보 추출
length_mm = None
if "length_info" in classification_result:
length_mm = classification_result["length_info"].get("length_mm")
# 길이 정보 추출 - material_data에서 직접 가져옴
length_mm = material_data.get("length", 0.0) if material_data.get("length") else None
# material_id도 함께 저장하도록 수정
pipe_detail_insert_query = text("""
INSERT INTO pipe_details (
file_id, material_standard, material_grade, material_type,
material_id, file_id, material_standard, material_grade, material_type,
manufacturing_method, end_preparation, schedule, wall_thickness,
nominal_size, length_mm, material_confidence, manufacturing_confidence,
end_prep_confidence, schedule_confidence
)
VALUES (
:file_id, :material_standard, :material_grade, :material_type,
:material_id, :file_id, :material_standard, :material_grade, :material_type,
:manufacturing_method, :end_preparation, :schedule, :wall_thickness,
:nominal_size, :length_mm, :material_confidence, :manufacturing_confidence,
:end_prep_confidence, :schedule_confidence
@@ -319,6 +330,7 @@ async def upload_file(
size_info = classification_result.get("size_info", {})
db.execute(pipe_detail_insert_query, {
"material_id": material_id,
"file_id": file_id,
"material_standard": material_info.get("standard"),
"material_grade": material_info.get("grade"),
@@ -327,7 +339,7 @@ async def upload_file(
"end_preparation": end_prep_info.get("type"),
"schedule": schedule_info.get("schedule"),
"wall_thickness": schedule_info.get("wall_thickness"),
"nominal_size": size_info.get("nominal_size"),
"nominal_size": material_data.get("size_spec", ""), # material_data에서 직접 가져옴
"length_mm": length_mm,
"material_confidence": material_info.get("confidence", 0.0),
"manufacturing_confidence": manufacturing_info.get("confidence", 0.0),
@@ -447,6 +459,7 @@ async def get_materials(
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.created_at, m.classified_category, m.classification_confidence,
m.classification_details,
f.original_filename, f.project_id, f.job_no, f.revision,
p.official_project_code, p.project_name
FROM materials m
@@ -552,33 +565,85 @@ async def get_materials(
count_result = db.execute(text(count_query), count_params)
total_count = count_result.fetchone()[0]
# 각 자재의 상세 정보도 가져오기
material_list = []
for m in materials:
material_dict = {
"id": m.id,
"file_id": m.file_id,
"filename": m.original_filename,
"project_id": m.project_id,
"project_code": m.official_project_code,
"project_name": m.project_name,
"original_description": m.original_description,
"quantity": float(m.quantity) if m.quantity else 0,
"unit": m.unit,
"size_spec": m.size_spec,
"material_grade": m.material_grade,
"line_number": m.line_number,
"row_number": m.row_number,
"classified_category": m.classified_category,
"classification_confidence": float(m.classification_confidence) if m.classification_confidence else 0.0,
"classification_details": m.classification_details,
"created_at": m.created_at
}
# 카테고리별 상세 정보 추가
if m.classified_category == 'PIPE':
pipe_query = text("SELECT * FROM pipe_details WHERE material_id = :material_id")
pipe_result = db.execute(pipe_query, {"material_id": m.id})
pipe_detail = pipe_result.fetchone()
if pipe_detail:
material_dict['pipe_details'] = {
"nominal_size": pipe_detail.nominal_size,
"schedule": pipe_detail.schedule,
"material_standard": pipe_detail.material_standard,
"material_grade": pipe_detail.material_grade,
"material_type": pipe_detail.material_type,
"manufacturing_method": pipe_detail.manufacturing_method,
"end_preparation": pipe_detail.end_preparation,
"wall_thickness": pipe_detail.wall_thickness,
"length_mm": float(pipe_detail.length_mm) if pipe_detail.length_mm else None
}
elif m.classified_category == 'FITTING':
fitting_query = text("SELECT * FROM fitting_details WHERE material_id = :material_id")
fitting_result = db.execute(fitting_query, {"material_id": m.id})
fitting_detail = fitting_result.fetchone()
if fitting_detail:
material_dict['fitting_details'] = {
"fitting_type": fitting_detail.fitting_type,
"fitting_subtype": fitting_detail.fitting_subtype,
"connection_method": fitting_detail.connection_method,
"pressure_rating": fitting_detail.pressure_rating,
"material_standard": fitting_detail.material_standard,
"material_grade": fitting_detail.material_grade,
"main_size": fitting_detail.main_size,
"reduced_size": fitting_detail.reduced_size
}
elif m.classified_category == 'VALVE':
valve_query = text("SELECT * FROM valve_details WHERE material_id = :material_id")
valve_result = db.execute(valve_query, {"material_id": m.id})
valve_detail = valve_result.fetchone()
if valve_detail:
material_dict['valve_details'] = {
"valve_type": valve_detail.valve_type,
"valve_subtype": valve_detail.valve_subtype,
"actuator_type": valve_detail.actuator_type,
"connection_method": valve_detail.connection_method,
"pressure_rating": valve_detail.pressure_rating,
"body_material": valve_detail.body_material,
"size_inches": valve_detail.size_inches
}
material_list.append(material_dict)
return {
"success": True,
"total_count": total_count,
"returned_count": len(materials),
"skip": skip,
"limit": limit,
"materials": [
{
"id": m.id,
"file_id": m.file_id,
"filename": m.original_filename,
"project_id": m.project_id,
"project_code": m.official_project_code,
"project_name": m.project_name,
"original_description": m.original_description,
"quantity": float(m.quantity) if m.quantity else 0,
"unit": m.unit,
"size_spec": m.size_spec,
"material_grade": m.material_grade,
"line_number": m.line_number,
"row_number": m.row_number,
"classified_category": m.classified_category,
"classification_confidence": float(m.classification_confidence) if m.classification_confidence else 0.0,
"created_at": m.created_at
}
for m in materials
]
"materials": material_list
}
except Exception as e:
@@ -863,6 +928,116 @@ async def get_pipe_details(
except Exception as e:
raise HTTPException(status_code=500, detail=f"PIPE 상세 정보 조회 실패: {str(e)}")
@router.get("/fitting-details")
async def get_fitting_details(
file_id: Optional[int] = None,
job_no: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
FITTING 상세 정보 조회
"""
try:
query = """
SELECT fd.*, f.original_filename, f.job_no, f.revision,
m.original_description, m.quantity, m.unit
FROM fitting_details fd
LEFT JOIN files f ON fd.file_id = f.id
LEFT JOIN materials m ON fd.material_id = m.id
WHERE 1=1
"""
params = {}
if file_id:
query += " AND fd.file_id = :file_id"
params["file_id"] = file_id
if job_no:
query += " AND f.job_no = :job_no"
params["job_no"] = job_no
query += " ORDER BY fd.created_at DESC"
result = db.execute(text(query), params)
fitting_details = result.fetchall()
return [
{
"id": fd.id,
"file_id": fd.file_id,
"fitting_type": fd.fitting_type,
"fitting_subtype": fd.fitting_subtype,
"connection_method": fd.connection_method,
"pressure_rating": fd.pressure_rating,
"material_standard": fd.material_standard,
"material_grade": fd.material_grade,
"main_size": fd.main_size,
"reduced_size": fd.reduced_size,
"classification_confidence": fd.classification_confidence,
"original_description": fd.original_description,
"quantity": fd.quantity
}
for fd in fitting_details
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"FITTING 상세 정보 조회 실패: {str(e)}")
@router.get("/valve-details")
async def get_valve_details(
file_id: Optional[int] = None,
job_no: Optional[str] = None,
db: Session = Depends(get_db)
):
"""
VALVE 상세 정보 조회
"""
try:
query = """
SELECT vd.*, f.original_filename, f.job_no, f.revision,
m.original_description, m.quantity, m.unit
FROM valve_details vd
LEFT JOIN files f ON vd.file_id = f.id
LEFT JOIN materials m ON vd.material_id = m.id
WHERE 1=1
"""
params = {}
if file_id:
query += " AND vd.file_id = :file_id"
params["file_id"] = file_id
if job_no:
query += " AND f.job_no = :job_no"
params["job_no"] = job_no
query += " ORDER BY vd.created_at DESC"
result = db.execute(text(query), params)
valve_details = result.fetchall()
return [
{
"id": vd.id,
"file_id": vd.file_id,
"valve_type": vd.valve_type,
"valve_subtype": vd.valve_subtype,
"actuator_type": vd.actuator_type,
"connection_method": vd.connection_method,
"pressure_rating": vd.pressure_rating,
"body_material": vd.body_material,
"size_inches": vd.size_inches,
"fire_safe": vd.fire_safe,
"classification_confidence": vd.classification_confidence,
"original_description": vd.original_description,
"quantity": vd.quantity
}
for vd in valve_details
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"VALVE 상세 정보 조회 실패: {str(e)}")
@router.get("/user-requirements")
async def get_user_requirements(
file_id: Optional[int] = None,