feat: 구매신청 기능 완성 및 SUPPORT/SPECIAL 카테고리 개선

- 모든 카테고리 구매신청 기능 완성 (PIPE, FITTING, VALVE, FLANGE, GASKET, BOLT, SUPPORT, SPECIAL, UNKNOWN)
- 구매신청 완료 항목: 회색 배경, 체크박스 비활성화, '구매신청완료' 배지 표시
- 전체 선택/구매신청 시 이미 구매신청된 항목 자동 제외
- 구매신청 quantity 타입 에러 수정 (문자열 -> 정수 변환)

SUPPORT 카테고리 (구 U-BOLT):
- U-BOLT -> SUPPORT로 카테고리명 변경
- 클램프, 유볼트, 우레탄블럭슈 분류 개선
- 테이블 헤더: 선택-종류-타입-크기-디스크립션-추가요구-사용자요구-수량
- 크기 정보 main_nom 필드에서 가져오기 (배관 인치)
- 엑셀 내보내기 형식 조정

SPECIAL 카테고리:
- SPECIAL 키워드 자재 자동 분류 (SPECIFICATION 제외)
- 파일 업로드 시 SPECIAL 카테고리 처리 로직 추가
- 도면번호 필드 추가 (drawing_name, line_no)
- 타입 필드: 크기/스케줄/재질 제외한 핵심 정보 표시
- 엑셀 DWG_NAME, LINE_NUM 컬럼 파싱 및 저장

FITTING 카테고리:
- 테이블 컬럼 너비 조정 (선택 2%, 종류 8.5%, 수량 12%)

구매신청 관리:
- 엑셀 재다운로드 형식 개선 (BOM 페이지와 동일한 형식)
- 그룹화된 자재 정보 포함하여 저장 및 다운로드
This commit is contained in:
Hyungi Ahn
2025-10-14 12:39:25 +09:00
parent e468663386
commit e27020ae9b
44 changed files with 13102 additions and 176 deletions

View File

@@ -218,7 +218,8 @@ def parse_dataframe(df):
mapped_columns[standard_col] = possible_name
break
# 로그 제거
print(f"📋 엑셀 컬럼 매핑 결과: {mapped_columns}")
print(f"📋 원본 컬럼명들: {list(df.columns)}")
materials = []
for index, row in df.iterrows():
@@ -262,16 +263,34 @@ def parse_dataframe(df):
except (ValueError, TypeError):
length_value = None
# DWG_NAME 정보 추출
dwg_name_raw = row.get(mapped_columns.get('dwg_name', ''), '')
dwg_name = None
if pd.notna(dwg_name_raw) and str(dwg_name_raw).strip() not in ['', 'nan', 'None']:
dwg_name = str(dwg_name_raw).strip()
if index < 3: # 처음 3개만 로그
print(f"📐 도면번호 파싱: {dwg_name}")
# LINE_NUM 정보 추출
line_num_raw = row.get(mapped_columns.get('line_num', ''), '')
line_num = None
if pd.notna(line_num_raw) and str(line_num_raw).strip() not in ['', 'nan', 'None']:
line_num = str(line_num_raw).strip()
if index < 3: # 처음 3개만 로그
print(f"📍 라인번호 파싱: {line_num}")
if description and description not in ['nan', 'None', '']:
materials.append({
'original_description': description,
'quantity': quantity,
'unit': "EA",
'size_spec': size_spec,
'main_nom': main_nom, # 추가
'red_nom': red_nom, # 추가
'main_nom': main_nom,
'red_nom': red_nom,
'material_grade': material_grade,
'length': length_value,
'dwg_name': dwg_name,
'line_num': line_num,
'line_number': index + 1,
'row_number': index + 1
})
@@ -651,6 +670,18 @@ async def upload_file(
elif material_type == "SUPPORT":
from ..services.support_classifier import classify_support
classification_result = classify_support("", description, main_nom or "")
elif material_type == "SPECIAL":
# SPECIAL 카테고리는 별도 분류기 없이 통합 분류 결과 사용
classification_result = {
"category": "SPECIAL",
"overall_confidence": integrated_result.get('confidence', 1.0),
"reason": integrated_result.get('reason', 'SPECIAL 키워드 발견'),
"details": {
"description": description,
"main_nom": main_nom or "",
"drawing_required": True # 도면 필요
}
}
else:
# UNKNOWN 처리
classification_result = {
@@ -679,12 +710,14 @@ async def upload_file(
INSERT INTO materials (
file_id, original_description, quantity, unit, size_spec,
main_nom, red_nom, material_grade, full_material_grade, line_number, row_number,
classified_category, classification_confidence, is_verified, created_at
classified_category, classification_confidence, is_verified,
drawing_name, line_no, created_at
)
VALUES (
:file_id, :original_description, :quantity, :unit, :size_spec,
:main_nom, :red_nom, :material_grade, :full_material_grade, :line_number, :row_number,
:classified_category, :classification_confidence, :is_verified, :created_at
:classified_category, :classification_confidence, :is_verified,
:drawing_name, :line_no, :created_at
)
RETURNING id
""")
@@ -702,8 +735,8 @@ async def upload_file(
"quantity": material_data["quantity"],
"unit": material_data["unit"],
"size_spec": material_data["size_spec"],
"main_nom": material_data.get("main_nom"), # 추가
"red_nom": material_data.get("red_nom"), # 추가
"main_nom": material_data.get("main_nom"),
"red_nom": material_data.get("red_nom"),
"material_grade": material_data["material_grade"],
"full_material_grade": full_material_grade,
"line_number": material_data["line_number"],
@@ -711,6 +744,8 @@ async def upload_file(
"classified_category": classification_result.get("category", "UNKNOWN"),
"classification_confidence": classification_result.get("overall_confidence", 0.0),
"is_verified": False,
"drawing_name": material_data.get("dwg_name"),
"line_no": material_data.get("line_num"),
"created_at": datetime.now()
})
@@ -1565,6 +1600,8 @@ async def get_materials(
size_spec: Optional[str] = None,
file_filter: Optional[str] = None,
sort_by: Optional[str] = None,
exclude_requested: bool = True, # 구매신청된 자재 제외 여부
group_by_spec: bool = False, # 같은 사양끼리 그룹화
db: Session = Depends(get_db)
):
"""
@@ -1575,6 +1612,7 @@ async def get_materials(
query = """
SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit,
m.size_spec, m.main_nom, m.red_nom, m.material_grade, m.full_material_grade, m.line_number, m.row_number,
m.drawing_name, m.line_no,
m.created_at, m.classified_category, m.classification_confidence,
m.classification_details,
m.is_verified, m.verified_by, m.verified_at,
@@ -1612,6 +1650,8 @@ async def get_materials(
FROM materials m
LEFT JOIN files f ON m.file_id = f.id
LEFT JOIN projects p ON f.project_id = p.id
-- 구매신청된 자재 제외
LEFT JOIN purchase_request_items pri ON m.id = pri.material_id
LEFT JOIN pipe_details pd ON m.id = pd.material_id
LEFT JOIN pipe_end_preparations pep ON m.id = pep.material_id
LEFT JOIN fitting_details fd ON m.id = fd.material_id
@@ -1625,6 +1665,11 @@ async def get_materials(
WHERE 1=1
"""
params = {}
# 구매신청된 자재 제외
if exclude_requested:
query += " AND pri.material_id IS NULL"
if project_id:
query += " AND f.project_id = :project_id"
params["project_id"] = project_id
@@ -1769,11 +1814,13 @@ async def get_materials(
"quantity": float(m.quantity) if m.quantity else 0,
"unit": m.unit,
"size_spec": m.size_spec,
"main_nom": m.main_nom, # 추가
"red_nom": m.red_nom, # 추가
"material_grade": m.full_material_grade or enhanced_material_grade, # 전체 재질명 우선 사용
"original_material_grade": m.material_grade, # 원본 재질 정보도 보존
"full_material_grade": m.full_material_grade, # 전체 재질명
"main_nom": m.main_nom,
"red_nom": m.red_nom,
"material_grade": m.full_material_grade or enhanced_material_grade,
"original_material_grade": m.material_grade,
"full_material_grade": m.full_material_grade,
"drawing_name": m.drawing_name,
"line_no": m.line_no,
"line_number": m.line_number,
"row_number": m.row_number,
# 구매수량 계산에서 분류된 정보를 우선 사용
@@ -2093,6 +2140,20 @@ async def get_materials(
# 평균 단위 길이 계산
if group_info["total_quantity"] > 0:
representative_pipe['pipe_details']['avg_length_mm'] = group_info["total_length_mm"] / group_info["total_quantity"]
# 개별 파이프 길이 정보 수집
individual_pipes = []
for mat in group_info["materials"]:
if 'pipe_details' in mat and mat['pipe_details'].get('length_mm'):
individual_pipes.append({
'length': mat['pipe_details']['length_mm'],
'quantity': 1,
'id': mat['id']
})
representative_pipe['pipe_details']['individual_pipes'] = individual_pipes
# 그룹화된 모든 자재 ID 저장
representative_pipe['grouped_ids'] = [mat['id'] for mat in group_info["materials"]]
material_list.append(representative_pipe)