🔧 재질 정보 표시 개선 및 UI 확장
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
✅ 주요 수정사항: - 재질 GRADE 전체 표기: ASTM A106 B 완전 표시 (A10 잘림 현상 해결) - material_grade_extractor.py 정규표현식 패턴 개선 - files.py 파일 업로드 시 재질 추출 로직 수정 - CSS 그리드 너비 확장으로 텍스트 잘림 현상 해결 - 사용자 요구사항 엑셀 다운로드 기능 완료 🎯 해결된 문제: 1. ASTM A106 B → ASTM A10 잘림 문제 2. 재질 컬럼 너비 부족으로 인한 표시 문제 3. 사용자 요구사항이 엑셀에 반영되지 않는 문제 📋 다음 단계 준비: - 파이프 끝단 정보 제외 취합 로직 개선 - 플랜지 타입 정보 확장 - 자재 분류 필터 기능 추가
This commit is contained in:
@@ -31,6 +31,130 @@ from app.services.revision_comparator import get_revision_comparison
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def extract_enhanced_material_grade(description: str, original_grade: str, category: str) -> str:
|
||||
"""
|
||||
원본 설명에서 개선된 재질 정보를 추출
|
||||
|
||||
Args:
|
||||
description: 원본 설명
|
||||
original_grade: 기존 재질 정보
|
||||
category: 자재 카테고리 (PIPE, FITTING, FLANGE 등)
|
||||
|
||||
Returns:
|
||||
개선된 재질 정보
|
||||
"""
|
||||
if not description:
|
||||
return original_grade or '-'
|
||||
|
||||
desc_upper = description.upper()
|
||||
|
||||
# PIPE 재질 패턴
|
||||
if category == 'PIPE':
|
||||
pipe_patterns = [
|
||||
(r'A312\s*(TP\d+[A-Z]*)', lambda m: f'A312 {m.group(1)}'),
|
||||
(r'A106\s*(GR\.?\s*[A-Z]+)', lambda m: f'A106 {m.group(1)}'), # A106 GR.B, A106 GR B 등 전체 보존
|
||||
(r'A106\s*([A-Z]+)', lambda m: f'A106 GR.{m.group(1)}'), # A106 B → A106 GR.B
|
||||
(r'A333\s*(GR\.?\s*[A-Z0-9]+)', lambda m: f'A333 {m.group(1)}'), # A333 GR.6 등 전체 보존
|
||||
(r'A333\s*([A-Z0-9]+)', lambda m: f'A333 GR.{m.group(1)}'), # A333 6 → A333 GR.6
|
||||
(r'A53\s*(GR\.?\s*[A-Z]+)', lambda m: f'A53 {m.group(1)}'), # A53 GR.B 등 전체 보존
|
||||
(r'A53\s*([A-Z]+)', lambda m: f'A53 GR.{m.group(1)}'), # A53 B → A53 GR.B
|
||||
(r'A335\s*(P\d+[A-Z]*)', lambda m: f'A335 {m.group(1)}'),
|
||||
(r'STPG\s*(\d+)', lambda m: f'STPG {m.group(1)}'),
|
||||
(r'STS\s*(\d+[A-Z]*)', lambda m: f'STS {m.group(1)}')
|
||||
]
|
||||
|
||||
for pattern, formatter in pipe_patterns:
|
||||
match = re.search(pattern, desc_upper)
|
||||
if match:
|
||||
return formatter(match)
|
||||
|
||||
# FITTING 재질 패턴
|
||||
elif category == 'FITTING':
|
||||
fitting_patterns = [
|
||||
(r'A403\s*(WP\d+[A-Z]*)', lambda m: f'A403 {m.group(1)}'),
|
||||
(r'A234\s*(WP[A-Z]+)', lambda m: f'A234 {m.group(1)}'),
|
||||
(r'A420\s*(WPL\d+)', lambda m: f'A420 {m.group(1)}'),
|
||||
(r'A105\s*(GR\.?\s*[N])', lambda m: f'A105 {m.group(1)}'), # A105 GR.N 전체 보존
|
||||
(r'A105\s*([N])', lambda m: f'A105 GR.{m.group(1)}'), # A105 N → A105 GR.N
|
||||
(r'A105(?!\s*[A-Z])', lambda m: f'A105'), # A105만 있는 경우
|
||||
(r'A106\s*(GR\.?\s*[A-Z]+)', lambda m: f'A106 {m.group(1)}'), # A106 GR.B 전체 보존
|
||||
(r'A106\s*([A-Z]+)', lambda m: f'A106 GR.{m.group(1)}'), # A106 B → A106 GR.B
|
||||
(r'A182\s*(F\d+[A-Z]*)', lambda m: f'A182 {m.group(1)}'),
|
||||
(r'A350\s*(LF\d+)', lambda m: f'A350 {m.group(1)}')
|
||||
]
|
||||
|
||||
for pattern, formatter in fitting_patterns:
|
||||
match = re.search(pattern, desc_upper)
|
||||
if match:
|
||||
return formatter(match)
|
||||
|
||||
# FLANGE 재질 패턴
|
||||
elif category == 'FLANGE':
|
||||
flange_patterns = [
|
||||
(r'A182\s*(F\d+[A-Z]*)', lambda m: f'A182 {m.group(1)}'),
|
||||
(r'A105\s*(GR\.?\s*[N])', lambda m: f'A105 {m.group(1)}'), # A105 GR.N 전체 보존
|
||||
(r'A105\s*([N])', lambda m: f'A105 GR.{m.group(1)}'), # A105 N → A105 GR.N
|
||||
(r'A105(?!\s*[A-Z])', lambda m: f'A105'), # A105만 있는 경우
|
||||
(r'A350\s*(LF\d+)', lambda m: f'A350 {m.group(1)}'),
|
||||
(r'A694\s*(F\d+)', lambda m: f'A694 {m.group(1)}')
|
||||
]
|
||||
|
||||
for pattern, formatter in flange_patterns:
|
||||
match = re.search(pattern, desc_upper)
|
||||
if match:
|
||||
return formatter(match)
|
||||
|
||||
# 패턴이 매치되지 않으면 기존 값 반환
|
||||
return original_grade or '-'
|
||||
|
||||
def extract_enhanced_flange_type(description: str, original_type: str) -> str:
|
||||
"""
|
||||
FLANGE 타입에 PIPE측 연결면 정보 추가
|
||||
|
||||
Args:
|
||||
description: 원본 설명
|
||||
original_type: 기존 플랜지 타입
|
||||
|
||||
Returns:
|
||||
개선된 플랜지 타입 (예: WN RF, SO FF 등)
|
||||
"""
|
||||
if not description:
|
||||
return original_type or '-'
|
||||
|
||||
desc_upper = description.upper()
|
||||
|
||||
# 기본 플랜지 타입 매핑
|
||||
flange_type_map = {
|
||||
'WELD_NECK': 'WN',
|
||||
'SLIP_ON': 'SO',
|
||||
'BLIND': 'BL',
|
||||
'SOCKET_WELD': 'SW',
|
||||
'LAP_JOINT': 'LJ',
|
||||
'THREADED': 'TH',
|
||||
'ORIFICE': 'ORIFICE'
|
||||
}
|
||||
|
||||
display_type = flange_type_map.get(original_type, original_type) if original_type else '-'
|
||||
|
||||
# PIPE측 연결면 타입 추출
|
||||
pipe_end_type = ''
|
||||
if ' RF' in desc_upper or 'RAISED FACE' in desc_upper:
|
||||
pipe_end_type = ' RF'
|
||||
elif ' FF' in desc_upper or 'FLAT FACE' in desc_upper:
|
||||
pipe_end_type = ' FF'
|
||||
elif ' RTJ' in desc_upper or 'RING TYPE JOINT' in desc_upper:
|
||||
pipe_end_type = ' RTJ'
|
||||
elif ' MSF' in desc_upper or 'MALE AND FEMALE' in desc_upper:
|
||||
pipe_end_type = ' MSF'
|
||||
elif ' T&G' in desc_upper or 'TONGUE AND GROOVE' in desc_upper:
|
||||
pipe_end_type = ' T&G'
|
||||
|
||||
# 최종 타입 조합
|
||||
if pipe_end_type and display_type != '-':
|
||||
return display_type + pipe_end_type
|
||||
|
||||
return display_type
|
||||
|
||||
UPLOAD_DIR = Path("uploads")
|
||||
UPLOAD_DIR.mkdir(exist_ok=True)
|
||||
ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"}
|
||||
@@ -108,7 +232,8 @@ def parse_dataframe(df):
|
||||
material_grade = ""
|
||||
if "ASTM" in description.upper():
|
||||
# ASTM 표준과 등급만 추출, end_preparation(BOE, POE, BBE 등)은 제외
|
||||
astm_match = re.search(r'ASTM\s+([A-Z0-9]+(?:\s+GR\s+[A-Z0-9]+)?)', description.upper())
|
||||
# A\d{3,4} 패턴으로 3-4자리 숫자 보장, 등급도 포함
|
||||
astm_match = re.search(r'ASTM\s+(A\d{3,4}[A-Z]*(?:\s+(?:GR\s+)?[A-Z0-9]+)?)', description.upper())
|
||||
if astm_match:
|
||||
material_grade = astm_match.group(0).strip()
|
||||
|
||||
@@ -511,6 +636,9 @@ async def upload_file(
|
||||
classification_result = classify_gasket("", description, main_nom or "")
|
||||
elif material_type == "INSTRUMENT":
|
||||
classification_result = classify_instrument("", description, main_nom or "")
|
||||
elif material_type == "SUPPORT":
|
||||
from ..services.support_classifier import classify_support
|
||||
classification_result = classify_support("", description, main_nom or "")
|
||||
else:
|
||||
# UNKNOWN 처리
|
||||
classification_result = {
|
||||
@@ -528,16 +656,22 @@ async def upload_file(
|
||||
|
||||
print(f"최종 분류 결과: {classification_result.get('category', 'UNKNOWN')}")
|
||||
|
||||
# 전체 재질명 추출
|
||||
from ..services.material_grade_extractor import extract_full_material_grade
|
||||
full_material_grade = extract_full_material_grade(description)
|
||||
if not full_material_grade and material_data.get("material_grade"):
|
||||
full_material_grade = material_data["material_grade"]
|
||||
|
||||
# 기본 자재 정보 저장
|
||||
material_insert_query = text("""
|
||||
INSERT INTO materials (
|
||||
file_id, original_description, quantity, unit, size_spec,
|
||||
main_nom, red_nom, material_grade, line_number, row_number,
|
||||
main_nom, red_nom, material_grade, full_material_grade, line_number, row_number,
|
||||
classified_category, classification_confidence, is_verified, created_at
|
||||
)
|
||||
VALUES (
|
||||
:file_id, :original_description, :quantity, :unit, :size_spec,
|
||||
:main_nom, :red_nom, :material_grade, :line_number, :row_number,
|
||||
:main_nom, :red_nom, :material_grade, :full_material_grade, :line_number, :row_number,
|
||||
:classified_category, :classification_confidence, :is_verified, :created_at
|
||||
)
|
||||
RETURNING id
|
||||
@@ -559,6 +693,7 @@ async def upload_file(
|
||||
"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"],
|
||||
"row_number": material_data["row_number"],
|
||||
"classified_category": classification_result.get("category", "UNKNOWN"),
|
||||
@@ -1040,6 +1175,79 @@ async def upload_file(
|
||||
|
||||
print(f"BOLT 상세 정보 저장 완료: {bolt_type} - {material_standard} {material_grade}")
|
||||
|
||||
# SUPPORT 분류 결과인 경우 상세 정보 저장
|
||||
if classification_result.get("category") == "SUPPORT":
|
||||
print("SUPPORT 상세 정보 저장 시작")
|
||||
|
||||
support_type = classification_result.get("support_type", "UNKNOWN")
|
||||
support_subtype = classification_result.get("support_subtype", "")
|
||||
load_rating = classification_result.get("load_rating", "")
|
||||
load_capacity = classification_result.get("load_capacity", "")
|
||||
|
||||
# 재질 정보
|
||||
material_info = classification_result.get("material", {})
|
||||
material_standard = material_info.get("standard", "UNKNOWN")
|
||||
material_grade = material_info.get("grade", "UNKNOWN")
|
||||
|
||||
# 사이즈 정보
|
||||
size_info = classification_result.get("size_info", {})
|
||||
pipe_size = size_info.get("pipe_size", "")
|
||||
dimensions = size_info.get("dimensions", {})
|
||||
|
||||
length_mm = None
|
||||
width_mm = None
|
||||
height_mm = None
|
||||
|
||||
if dimensions:
|
||||
length_str = dimensions.get("length", "")
|
||||
width_str = dimensions.get("width", "")
|
||||
height_str = dimensions.get("height", "")
|
||||
|
||||
# mm 단위 추출
|
||||
import re
|
||||
if length_str:
|
||||
length_match = re.search(r'(\d+(?:\.\d+)?)', length_str)
|
||||
if length_match:
|
||||
length_mm = float(length_match.group(1))
|
||||
|
||||
if width_str:
|
||||
width_match = re.search(r'(\d+(?:\.\d+)?)', width_str)
|
||||
if width_match:
|
||||
width_mm = float(width_match.group(1))
|
||||
|
||||
if height_str:
|
||||
height_match = re.search(r'(\d+(?:\.\d+)?)', height_str)
|
||||
if height_match:
|
||||
height_mm = float(height_match.group(1))
|
||||
|
||||
db.execute(text("""
|
||||
INSERT INTO support_details (
|
||||
material_id, file_id, support_type, support_subtype,
|
||||
load_rating, load_capacity, material_standard, material_grade,
|
||||
pipe_size, length_mm, width_mm, height_mm, classification_confidence
|
||||
) VALUES (
|
||||
:material_id, :file_id, :support_type, :support_subtype,
|
||||
:load_rating, :load_capacity, :material_standard, :material_grade,
|
||||
:pipe_size, :length_mm, :width_mm, :height_mm, :classification_confidence
|
||||
)
|
||||
"""), {
|
||||
"material_id": material_id,
|
||||
"file_id": file_id,
|
||||
"support_type": support_type,
|
||||
"support_subtype": support_subtype,
|
||||
"load_rating": load_rating,
|
||||
"load_capacity": load_capacity,
|
||||
"material_standard": material_standard,
|
||||
"material_grade": material_grade,
|
||||
"pipe_size": pipe_size,
|
||||
"length_mm": length_mm,
|
||||
"width_mm": width_mm,
|
||||
"height_mm": height_mm,
|
||||
"classification_confidence": classification_result.get("overall_confidence", 0.0)
|
||||
})
|
||||
|
||||
print(f"SUPPORT 상세 정보 저장 완료: {support_type} - {material_standard} {material_grade}")
|
||||
|
||||
# VALVE 분류 결과인 경우 상세 정보 저장
|
||||
if classification_result.get("category") == "VALVE":
|
||||
print("VALVE 상세 정보 저장 시작")
|
||||
@@ -1331,7 +1539,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.line_number, m.row_number,
|
||||
m.size_spec, m.main_nom, m.red_nom, m.material_grade, m.full_material_grade, m.line_number, m.row_number,
|
||||
m.created_at, m.classified_category, m.classification_confidence,
|
||||
m.classification_details,
|
||||
m.is_verified, m.verified_by, m.verified_at,
|
||||
@@ -1503,6 +1711,14 @@ async def get_materials(
|
||||
# 로그 제거
|
||||
pass
|
||||
|
||||
# 개선된 재질 정보 추출
|
||||
final_category = m.final_classified_category or m.classified_category
|
||||
enhanced_material_grade = extract_enhanced_material_grade(
|
||||
m.original_description,
|
||||
m.material_grade,
|
||||
final_category
|
||||
)
|
||||
|
||||
material_dict = {
|
||||
"id": m.id,
|
||||
"file_id": m.file_id,
|
||||
@@ -1516,11 +1732,13 @@ async def get_materials(
|
||||
"size_spec": m.size_spec,
|
||||
"main_nom": m.main_nom, # 추가
|
||||
"red_nom": m.red_nom, # 추가
|
||||
"material_grade": m.material_grade,
|
||||
"material_grade": m.full_material_grade or enhanced_material_grade, # 전체 재질명 우선 사용
|
||||
"original_material_grade": m.material_grade, # 원본 재질 정보도 보존
|
||||
"full_material_grade": m.full_material_grade, # 전체 재질명
|
||||
"line_number": m.line_number,
|
||||
"row_number": m.row_number,
|
||||
# 구매수량 계산에서 분류된 정보를 우선 사용
|
||||
"classified_category": m.final_classified_category or m.classified_category,
|
||||
"classified_category": final_category,
|
||||
"classification_confidence": float(m.classification_confidence) if m.classification_confidence else 0.0,
|
||||
"classification_details": m.classification_details,
|
||||
"is_verified": m.final_is_verified if m.final_is_verified is not None else m.is_verified,
|
||||
@@ -1664,8 +1882,15 @@ async def get_materials(
|
||||
flange_result = db.execute(flange_query, {"material_id": m.id})
|
||||
flange_detail = flange_result.fetchone()
|
||||
if flange_detail:
|
||||
# 개선된 플랜지 타입 (PIPE측 연결면 포함)
|
||||
enhanced_flange_type = extract_enhanced_flange_type(
|
||||
m.original_description,
|
||||
flange_detail.flange_type
|
||||
)
|
||||
|
||||
material_dict['flange_details'] = {
|
||||
"flange_type": flange_detail.flange_type,
|
||||
"flange_type": enhanced_flange_type, # 개선된 타입 사용
|
||||
"original_flange_type": flange_detail.flange_type, # 원본 타입 보존
|
||||
"facing_type": flange_detail.facing_type,
|
||||
"pressure_rating": flange_detail.pressure_rating,
|
||||
"material_standard": flange_detail.material_standard,
|
||||
@@ -2431,6 +2656,7 @@ async def create_user_requirement(
|
||||
file_id: int,
|
||||
requirement_type: str,
|
||||
requirement_title: str,
|
||||
material_id: Optional[int] = None,
|
||||
requirement_description: Optional[str] = None,
|
||||
requirement_spec: Optional[str] = None,
|
||||
priority: str = "NORMAL",
|
||||
@@ -2444,11 +2670,11 @@ async def create_user_requirement(
|
||||
try:
|
||||
insert_query = text("""
|
||||
INSERT INTO user_requirements (
|
||||
file_id, requirement_type, requirement_title, requirement_description,
|
||||
file_id, material_id, requirement_type, requirement_title, requirement_description,
|
||||
requirement_spec, priority, assigned_to, due_date
|
||||
)
|
||||
VALUES (
|
||||
:file_id, :requirement_type, :requirement_title, :requirement_description,
|
||||
:file_id, :material_id, :requirement_type, :requirement_title, :requirement_description,
|
||||
:requirement_spec, :priority, :assigned_to, :due_date
|
||||
)
|
||||
RETURNING id
|
||||
@@ -2456,6 +2682,7 @@ async def create_user_requirement(
|
||||
|
||||
result = db.execute(insert_query, {
|
||||
"file_id": file_id,
|
||||
"material_id": material_id,
|
||||
"requirement_type": requirement_type,
|
||||
"requirement_title": requirement_title,
|
||||
"requirement_description": requirement_description,
|
||||
@@ -2478,6 +2705,41 @@ async def create_user_requirement(
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"요구사항 생성 실패: {str(e)}")
|
||||
|
||||
@router.delete("/user-requirements")
|
||||
async def delete_user_requirements(
|
||||
file_id: Optional[int] = None,
|
||||
material_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
사용자 요구사항 삭제 (파일별 또는 자재별)
|
||||
"""
|
||||
try:
|
||||
if file_id:
|
||||
# 파일별 삭제
|
||||
delete_query = text("DELETE FROM user_requirements WHERE file_id = :file_id")
|
||||
result = db.execute(delete_query, {"file_id": file_id})
|
||||
deleted_count = result.rowcount
|
||||
elif material_id:
|
||||
# 자재별 삭제
|
||||
delete_query = text("DELETE FROM user_requirements WHERE material_id = :material_id")
|
||||
result = db.execute(delete_query, {"material_id": material_id})
|
||||
deleted_count = result.rowcount
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="file_id 또는 material_id가 필요합니다")
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"{deleted_count}개의 요구사항이 삭제되었습니다",
|
||||
"deleted_count": deleted_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"요구사항 삭제 실패: {str(e)}")
|
||||
|
||||
@router.post("/materials/{material_id}/verify")
|
||||
async def verify_material_classification(
|
||||
material_id: int,
|
||||
|
||||
Reference in New Issue
Block a user