refactor(tkeg): print→logging 교체 + 레거시 파일 정리 (-5,447줄)

- 6개 파일 디버그 print문 102건 → logger.info/warning/error 교체
- DashboardPage.old.jsx 삭제 (미사용)
- NewMaterialsPage.jsx/css 삭제 (dead import)
- spool_manager_v2.py 삭제 (미사용)
- App.jsx dead import 제거
- main.py 구 주석 블록 제거

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-26 15:41:39 +09:00
parent d6dd03a52f
commit 1ceeef2a65
12 changed files with 62 additions and 5447 deletions

View File

@@ -104,13 +104,6 @@ try:
except ImportError: except ImportError:
logger.warning("dashboard 라우터를 찾을 수 없습니다") logger.warning("dashboard 라우터를 찾을 수 없습니다")
# 리비전 관리 라우터 (임시 비활성화)
# try:
# from .routers import revision_management
# app.include_router(revision_management.router, tags=["revision-management"])
# except ImportError:
# logger.warning("revision_management 라우터를 찾을 수 없습니다")
try: try:
from .routers import tubing from .routers import tubing
app.include_router(tubing.router, prefix="/tubing", tags=["tubing"]) app.include_router(tubing.router, prefix="/tubing", tags=["tubing"])

View File

@@ -201,26 +201,26 @@ async def upload_file(
# 🎯 트랜잭션 오류 방지: 완전한 트랜잭션 초기화 # 🎯 트랜잭션 오류 방지: 완전한 트랜잭션 초기화
# 🔍 디버깅: 업로드 파라미터 로깅 # 🔍 디버깅: 업로드 파라미터 로깅
print(f"🔍 [UPLOAD] job_no: {job_no}, revision: {revision}, parent_file_id: {parent_file_id}") logger.info(f"[UPLOAD] job_no: {job_no}, revision: {revision}, parent_file_id: {parent_file_id}")
print(f"🔍 [UPLOAD] bom_name: {bom_name}, filename: {file.filename}") logger.info(f"[UPLOAD] bom_name: {bom_name}, filename: {file.filename}")
print(f"🔍 [UPLOAD] revision != 'Rev.0': {revision != 'Rev.0'}") logger.info(f"[UPLOAD] revision != 'Rev.0': {revision != 'Rev.0'}")
try: try:
# 1. 현재 트랜잭션 완전 롤백 # 1. 현재 트랜잭션 완전 롤백
db.rollback() db.rollback()
print("🔄 1단계: 이전 트랜잭션 롤백 완료") logger.info("1단계: 이전 트랜잭션 롤백 완료")
# 2. 세션 상태 초기화 # 2. 세션 상태 초기화
db.close() db.close()
print("🔄 2단계: 세션 닫기 완료") logger.info("2단계: 세션 닫기 완료")
# 3. 새 세션 생성 # 3. 새 세션 생성
from ..database import get_db from ..database import get_db
db = next(get_db()) db = next(get_db())
print("🔄 3단계: 새 세션 생성 완료") logger.info("3단계: 새 세션 생성 완료")
except Exception as e: except Exception as e:
print(f"⚠️ 트랜잭션 초기화 중 오류: {e}") logger.warning(f"트랜잭션 초기화 중 오류: {e}")
# 오류 발생 시에도 계속 진행 # 오류 발생 시에도 계속 진행
# [변경] BOMParser 사용하여 확장자 검증 # [변경] BOMParser 사용하여 확장자 검증
@@ -245,10 +245,10 @@ async def upload_file(
try: try:
# [변경] BOMParser 사용하여 파일 파싱 (자동 양식 감지 포함) # [변경] BOMParser 사용하여 파일 파싱 (자동 양식 감지 포함)
print(f"🚀 파일 파싱 시작: {file_path}") logger.info(f"파일 파싱 시작: {file_path}")
materials_data = BOMParser.parse_file(str(file_path)) materials_data = BOMParser.parse_file(str(file_path))
parsed_count = len(materials_data) parsed_count = len(materials_data)
print(f"파싱 완료: {parsed_count}개 자재 추출됨") logger.info(f"파싱 완료: {parsed_count}개 자재 추출됨")
# 신규 자재 카운트 초기화 # 신규 자재 카운트 초기화
new_materials_count = 0 new_materials_count = 0
@@ -256,7 +256,7 @@ async def upload_file(
# 리비전 업로드인 경우만 자동 리비전 생성 및 기존 자재 조회 # 리비전 업로드인 경우만 자동 리비전 생성 및 기존 자재 조회
if parent_file_id is not None: if parent_file_id is not None:
print(f"🔄 리비전 업로드 감지: parent_file_id={parent_file_id}") logger.info(f"리비전 업로드 감지: parent_file_id={parent_file_id}")
# 부모 파일의 정보 조회 # 부모 파일의 정보 조회
parent_query = text(""" parent_query = text("""
SELECT original_filename, revision, bom_name FROM files SELECT original_filename, revision, bom_name FROM files
@@ -299,10 +299,10 @@ async def upload_file(
else: else:
revision = "Rev.1" revision = "Rev.1"
print(f"리비전 업로드: {latest_rev} {revision}") logger.info(f"리비전 업로드: {latest_rev} -> {revision}")
else: else:
revision = "Rev.1" revision = "Rev.1"
print(f"첫 번째 리비전: {revision}") logger.info(f"첫 번째 리비전: {revision}")
# 모든 이전 리비전의 누적 자재 목록 조회 (리비전 0부터 현재까지) # 모든 이전 리비전의 누적 자재 목록 조회 (리비전 0부터 현재까지)
existing_materials_query = text(""" existing_materials_query = text("""
@@ -327,24 +327,24 @@ async def upload_file(
existing_materials_descriptions.add(key) existing_materials_descriptions.add(key)
existing_materials_with_quantity[key] = float(row.total_quantity or 0) existing_materials_with_quantity[key] = float(row.total_quantity or 0)
print(f"📊 누적 자재 수 (Rev.0~현재): {len(existing_materials_descriptions)}") logger.info(f"누적 자재 수 (Rev.0~현재): {len(existing_materials_descriptions)}")
print(f"📊 누적 자재 총 수량: {sum(existing_materials_with_quantity.values())}") logger.info(f"누적 자재 총 수량: {sum(existing_materials_with_quantity.values())}")
if len(existing_materials_descriptions) > 0: if len(existing_materials_descriptions) > 0:
print(f"📝 기존 자재 샘플 (처음 3개): {list(existing_materials_descriptions)[:3]}") logger.info(f"기존 자재 샘플 (처음 3개): {list(existing_materials_descriptions)[:3]}")
# 수량이 있는 자재들 확인 # 수량이 있는 자재들 확인
quantity_samples = [(k, v) for k, v in list(existing_materials_with_quantity.items())[:3]] quantity_samples = [(k, v) for k, v in list(existing_materials_with_quantity.items())[:3]]
print(f"📊 기존 자재 수량 샘플: {quantity_samples}") logger.info(f"기존 자재 수량 샘플: {quantity_samples}")
else: else:
print(f"⚠️ 기존 자재가 없습니다! parent_file_id={parent_file_id}의 materials 테이블을 확인하세요.") logger.warning(f"기존 자재가 없습니다! parent_file_id={parent_file_id}의 materials 테이블을 확인하세요.")
# 파일명을 부모와 동일하게 유지 # 파일명을 부모와 동일하게 유지
file.filename = parent_file[0] file.filename = parent_file[0]
else: else:
# 일반 업로드 (새 BOM) # 일반 업로드 (새 BOM)
print(f"일반 업로드 모드: 새 BOM 파일 (Rev.0)") logger.info(f"일반 업로드 모드: 새 BOM 파일 (Rev.0)")
# 파일 정보 저장 (사용자 정보 포함) # 파일 정보 저장 (사용자 정보 포함)
print("DB 저장 시작") logger.info("DB 저장 시작")
username = current_user.get('username', 'unknown') username = current_user.get('username', 'unknown')
user_id = current_user.get('user_id') user_id = current_user.get('user_id')
@@ -370,7 +370,7 @@ async def upload_file(
file_id = file_result.fetchone()[0] file_id = file_result.fetchone()[0]
db.commit() # 파일 레코드 즉시 커밋 db.commit() # 파일 레코드 즉시 커밋
print(f"파일 저장 완료: file_id = {file_id}, uploaded_by = {username}") logger.info(f"파일 저장 완료: file_id = {file_id}, uploaded_by = {username}")
# 🔄 리비전 비교 수행 (RULES.md 코딩 컨벤션 준수) # 🔄 리비전 비교 수행 (RULES.md 코딩 컨벤션 준수)
revision_comparison = None revision_comparison = None
@@ -378,23 +378,23 @@ async def upload_file(
purchased_materials_map = {} # 구매확정된 자재 매핑 (키 -> 구매확정 정보) purchased_materials_map = {} # 구매확정된 자재 매핑 (키 -> 구매확정 정보)
if revision != "Rev.0": # 리비전 업로드인 경우만 비교 if revision != "Rev.0": # 리비전 업로드인 경우만 비교
print(f"🔍 [DEBUG] 리비전 비교 시작 - revision: {revision}, parent_file_id: {parent_file_id}") logger.info(f"[DEBUG] 리비전 비교 시작 - revision: {revision}, parent_file_id: {parent_file_id}")
try: try:
# 간단한 리비전 비교 로직 (purchase_confirmed 기반) # 간단한 리비전 비교 로직 (purchase_confirmed 기반)
print(f"🔍 [DEBUG] perform_simple_revision_comparison 호출 중...") logger.info(f"[DEBUG] perform_simple_revision_comparison 호출 중...")
revision_comparison = perform_simple_revision_comparison(db, job_no, parent_file_id, materials_data) revision_comparison = perform_simple_revision_comparison(db, job_no, parent_file_id, materials_data)
print(f"🔍 [DEBUG] 리비전 비교 완료: {revision_comparison.keys() if revision_comparison else 'None'}") logger.info(f"[DEBUG] 리비전 비교 완료: {revision_comparison.keys() if revision_comparison else 'None'}")
if revision_comparison.get("has_purchased_materials", False): if revision_comparison.get("has_purchased_materials", False):
print(f"📊 간단한 리비전 비교 결과:") logger.info(f"간단한 리비전 비교 결과:")
print(f" - 구매확정된 자재: {revision_comparison.get('purchased_count', 0)}") logger.info(f" - 구매확정된 자재: {revision_comparison.get('purchased_count', 0)}")
print(f" - 미구매 자재: {revision_comparison.get('unpurchased_count', 0)}") logger.info(f" - 미구매 자재: {revision_comparison.get('unpurchased_count', 0)}")
print(f" - 신규 자재: {revision_comparison.get('new_count', 0)}") logger.info(f" - 신규 자재: {revision_comparison.get('new_count', 0)}")
print(f" - 제외된 구매확정 자재: {revision_comparison.get('excluded_purchased_count', 0)}") logger.info(f" - 제외된 구매확정 자재: {revision_comparison.get('excluded_purchased_count', 0)}")
# 신규 및 변경된 자재만 분류 # 신규 및 변경된 자재만 분류
materials_to_classify = revision_comparison.get("materials_to_classify", []) materials_to_classify = revision_comparison.get("materials_to_classify", [])
print(f" - 분류 필요: {len(materials_to_classify)}") logger.info(f" - 분류 필요: {len(materials_to_classify)}")
# 🔥 구매확정된 자재 매핑 정보 저장 # 🔥 구매확정된 자재 매핑 정보 저장
purchased_materials_map = revision_comparison.get("purchased_materials_map", {}) purchased_materials_map = revision_comparison.get("purchased_materials_map", {})

View File

@@ -5,6 +5,8 @@
- 발주 상태 관리 - 발주 상태 관리
""" """
import logging
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import text from sqlalchemy import text
@@ -14,6 +16,8 @@ from datetime import datetime
from ..database import get_db from ..database import get_db
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/materials", tags=["material-comparison"]) router = APIRouter(prefix="/materials", tags=["material-comparison"])
@router.post("/compare-revisions") @router.post("/compare-revisions")
@@ -452,7 +456,7 @@ async def perform_material_comparison(
previous_total = previous_item["pipe_details"]["total_length_mm"] previous_total = previous_item["pipe_details"]["total_length_mm"]
length_change = current_total - previous_total length_change = current_total - previous_total
modified_item["length_change"] = length_change modified_item["length_change"] = length_change
print(f"🔢 실제 길이 변화: {current_item['description'][:50]} - 이전:{previous_total:.0f}mm 현재:{current_total:.0f}mm (변화:{length_change:+.0f}mm)") logger.info(f"실제 길이 변화: {current_item['description'][:50]} - 이전:{previous_total:.0f}mm -> 현재:{current_total:.0f}mm (변화:{length_change:+.0f}mm)")
modified_items.append(modified_item) modified_items.append(modified_item)
@@ -587,7 +591,7 @@ async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]:
pipe_count = sum(1 for data in materials_dict.values() if data.get('category') == 'PIPE') pipe_count = sum(1 for data in materials_dict.values() if data.get('category') == 'PIPE')
pipe_with_details = sum(1 for data in materials_dict.values() pipe_with_details = sum(1 for data in materials_dict.values()
if data.get('category') == 'PIPE' and 'pipe_details' in data) if data.get('category') == 'PIPE' and 'pipe_details' in data)
print(f"자재 처리 완료: 총 {len(materials_dict)}개, 파이프 {pipe_count}개 (길이정보: {pipe_with_details}개)") logger.info(f"자재 처리 완료: 총 {len(materials_dict)}개, 파이프 {pipe_count}개 (길이정보: {pipe_with_details}개)")
return materials_dict return materials_dict

View File

@@ -49,7 +49,7 @@ async def create_purchase_request(
if request_data.material_ids: if request_data.material_ids:
logger.info(f"🔍 material_ids 샘플: {request_data.material_ids[:5]}") logger.info(f"🔍 material_ids 샘플: {request_data.material_ids[:5]}")
print(f"🔍 [DEBUG] 구매신청 API 호출됨 - material_ids: {len(request_data.material_ids)}") logger.info(f"[DEBUG] 구매신청 API 호출됨 - material_ids: {len(request_data.material_ids)}")
# 구매신청 번호 생성 # 구매신청 번호 생성
today = datetime.now().strftime('%Y%m%d') today = datetime.now().strftime('%Y%m%d')
count_query = text(""" count_query = text("""
@@ -160,8 +160,8 @@ async def create_purchase_request(
# 🔥 중요: materials 테이블의 purchase_confirmed 업데이트 # 🔥 중요: materials 테이블의 purchase_confirmed 업데이트
if request_data.material_ids: if request_data.material_ids:
print(f"🔥 [PURCHASE] purchase_confirmed 업데이트 시작: {len(request_data.material_ids)}개 자재") logger.info(f"[PURCHASE] purchase_confirmed 업데이트 시작: {len(request_data.material_ids)}개 자재")
print(f"🔥 [PURCHASE] material_ids: {request_data.material_ids[:5]}...") # 처음 5개만 로그 logger.info(f"[PURCHASE] material_ids: {request_data.material_ids[:5]}...") # 처음 5개만 로그
update_materials_query = text(""" update_materials_query = text("""
UPDATE materials UPDATE materials
@@ -176,10 +176,10 @@ async def create_purchase_request(
"confirmed_by": current_user.get("username", "system") "confirmed_by": current_user.get("username", "system")
}) })
print(f"🔥 [PURCHASE] UPDATE 결과: {result.rowcount}개 행 업데이트됨") logger.info(f"[PURCHASE] UPDATE 결과: {result.rowcount}개 행 업데이트됨")
logger.info(f"{len(request_data.material_ids)}개 자재의 purchase_confirmed를 true로 업데이트") logger.info(f"{len(request_data.material_ids)}개 자재의 purchase_confirmed를 true로 업데이트")
else: else:
print(f"⚠️ [PURCHASE] material_ids가 비어있음!") logger.warning(f"[PURCHASE] material_ids가 비어있음!")
db.commit() db.commit()
@@ -330,7 +330,7 @@ async def get_request_materials(
data = json.load(f) data = json.load(f)
grouped_materials = data.get("grouped_materials", []) grouped_materials = data.get("grouped_materials", [])
except Exception as e: except Exception as e:
print(f"⚠️ JSON 파일 읽기 오류 (무시): {e}") logger.warning(f"JSON 파일 읽기 오류 (무시): {e}")
grouped_materials = [] grouped_materials = []
# 개별 자재 정보 조회 (기존 코드) # 개별 자재 정보 조회 (기존 코드)

View File

@@ -1,3 +1,4 @@
import logging
import pandas as pd import pandas as pd
import re import re
@@ -6,6 +7,8 @@ import uuid
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__)
# 허용된 확장자 # 허용된 확장자
ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"} ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"}
@@ -72,7 +75,7 @@ class BOMParser:
# 양식 감지 # 양식 감지
format_type = cls.detect_format(df) format_type = cls.detect_format(df)
print(f"📋 감지된 BOM 양식: {format_type}") logger.info(f"감지된 BOM 양식: {format_type}")
if format_type == 'INVENTOR': if format_type == 'INVENTOR':
return cls._parse_inventor_bom(df) return cls._parse_inventor_bom(df)
@@ -109,7 +112,7 @@ class BOMParser:
mapped_columns[standard_col] = possible_upper mapped_columns[standard_col] = possible_upper
break break
print(f"📋 [Standard] 컬럼 매핑 결과: {mapped_columns}") logger.info(f"[Standard] 컬럼 매핑 결과: {mapped_columns}")
materials = [] materials = []
for index, row in df.iterrows(): for index, row in df.iterrows():
@@ -190,7 +193,7 @@ class BOMParser:
헤더: NO., NAME, Q'ty, LENGTH & THICKNESS, WEIGHT, DESCIPTION, REMARK 헤더: NO., NAME, Q'ty, LENGTH & THICKNESS, WEIGHT, DESCIPTION, REMARK
특징: Size 컬럼 부재, NAME에 주요 정보 포함 특징: Size 컬럼 부재, NAME에 주요 정보 포함
""" """
print("⚠️ [Inventor] 인벤터 양식 파서를 사용합니다.") logger.warning("[Inventor] 인벤터 양식 파서를 사용합니다.")
# 컬럼명 전처리 (좌우 공백 제거 및 대문자화) # 컬럼명 전처리 (좌우 공백 제거 및 대문자화)
df.columns = df.columns.str.strip().str.upper() df.columns = df.columns.str.strip().str.upper()

View File

@@ -3,9 +3,12 @@
원본 설명에서 완전한 재질명을 추출하여 축약되지 않은 형태로 제공 원본 설명에서 완전한 재질명을 추출하여 축약되지 않은 형태로 제공
""" """
import logging
import re import re
from typing import Optional, Dict from typing import Optional, Dict
logger = logging.getLogger(__name__)
def extract_full_material_grade(description: str) -> str: def extract_full_material_grade(description: str) -> str:
""" """
원본 설명에서 전체 재질명 추출 원본 설명에서 전체 재질명 추출
@@ -196,7 +199,7 @@ def update_full_material_grades(db, batch_size: int = 1000) -> Dict:
count_query = text("SELECT COUNT(*) FROM materials WHERE full_material_grade IS NULL OR full_material_grade = ''") count_query = text("SELECT COUNT(*) FROM materials WHERE full_material_grade IS NULL OR full_material_grade = ''")
total_count = db.execute(count_query).scalar() total_count = db.execute(count_query).scalar()
print(f"📊 업데이트 대상 자재: {total_count}") logger.info(f"업데이트 대상 자재: {total_count}")
updated_count = 0 updated_count = 0
processed_count = 0 processed_count = 0
@@ -244,7 +247,7 @@ def update_full_material_grades(db, batch_size: int = 1000) -> Dict:
db.commit() db.commit()
offset += batch_size offset += batch_size
print(f"📈 진행률: {processed_count}/{total_count} ({processed_count/total_count*100:.1f}%)") logger.info(f"진행률: {processed_count}/{total_count} ({processed_count/total_count*100:.1f}%)")
return { return {
"total_processed": processed_count, "total_processed": processed_count,
@@ -254,7 +257,7 @@ def update_full_material_grades(db, batch_size: int = 1000) -> Dict:
except Exception as e: except Exception as e:
db.rollback() db.rollback()
print(f"업데이트 실패: {str(e)}") logger.error(f"업데이트 실패: {str(e)}")
return { return {
"total_processed": 0, "total_processed": 0,
"updated_count": 0, "updated_count": 0,

View File

@@ -1,3 +1,4 @@
import logging
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import text from sqlalchemy import text
@@ -18,6 +19,8 @@ from app.services.structural_classifier import classify_structural
from app.services.pipe_classifier import classify_pipe_for_purchase, extract_end_preparation_info from app.services.pipe_classifier import classify_pipe_for_purchase, extract_end_preparation_info
from app.services.material_grade_extractor import extract_full_material_grade from app.services.material_grade_extractor import extract_full_material_grade
logger = logging.getLogger(__name__)
class MaterialService: class MaterialService:
"""자재 처리 및 저장을 담당하는 서비스""" """자재 처리 및 저장을 담당하는 서비스"""
@@ -70,7 +73,7 @@ class MaterialService:
if revision_comparison and revision_comparison.get("materials_to_classify"): if revision_comparison and revision_comparison.get("materials_to_classify"):
materials_to_classify = revision_comparison.get("materials_to_classify") materials_to_classify = revision_comparison.get("materials_to_classify")
print(f"🔧 자재 분류 및 저장 시작: {len(materials_to_classify)}") logger.info(f"자재 분류 및 저장 시작: {len(materials_to_classify)}")
for material_data in materials_to_classify: for material_data in materials_to_classify:
MaterialService._classify_and_save_single_material( MaterialService._classify_and_save_single_material(
@@ -116,7 +119,7 @@ class MaterialService:
new_keys.add(new_key) new_keys.add(new_key)
except Exception as e: except Exception as e:
print(f"변경사항 분석 실패: {e}") logger.error(f"변경사항 분석 실패: {e}")
@staticmethod @staticmethod
def _generate_material_key(dwg, line, desc, size, grade): def _generate_material_key(dwg, line, desc, size, grade):
@@ -332,7 +335,7 @@ class MaterialService:
def inherit_purchase_requests(db: Session, current_file_id: int, parent_file_id: int): def inherit_purchase_requests(db: Session, current_file_id: int, parent_file_id: int):
"""이전 리비전의 구매신청 정보를 상속합니다.""" """이전 리비전의 구매신청 정보를 상속합니다."""
try: try:
print(f"🔄 구매신청 정보 상속 처리 시작...") logger.info(f"구매신청 정보 상속 처리 시작...")
# 1. 이전 리비전에서 그룹별 구매신청 수량 집계 # 1. 이전 리비전에서 그룹별 구매신청 수량 집계
prev_purchase_summary = text(""" prev_purchase_summary = text("""
@@ -396,14 +399,14 @@ class MaterialService:
inherited_count = len(new_materials) inherited_count = len(new_materials)
if inherited_count > 0: if inherited_count > 0:
print(f" {prev_purchase.original_description[:30]}... (도면: {prev_purchase.drawing_name or 'N/A'}) {inherited_count}/{purchased_count}개 상속") logger.info(f" {prev_purchase.original_description[:30]}... (도면: {prev_purchase.drawing_name or 'N/A'}) -> {inherited_count}/{purchased_count}개 상속")
# 커밋은 호출하는 쪽에서 일괄 처리하거나 여기서 처리 # 커밋은 호출하는 쪽에서 일괄 처리하거나 여기서 처리
# db.commit() # db.commit()
print(f"구매신청 정보 상속 완료") logger.info(f"구매신청 정보 상속 완료")
except Exception as e: except Exception as e:
print(f"구매신청 정보 상속 실패: {str(e)}") logger.error(f"구매신청 정보 상속 실패: {str(e)}")
# 상속 실패는 전체 프로세스를 중단하지 않음 # 상속 실패는 전체 프로세스를 중단하지 않음
@staticmethod @staticmethod

View File

@@ -1,229 +0,0 @@
"""
수정된 스풀 관리 시스템
도면별 스풀 넘버링 + 에리어는 별도 관리
"""
import re
from typing import Dict, List, Optional, Tuple
from datetime import datetime
# ========== 스풀 넘버링 규칙 ==========
SPOOL_NUMBERING_RULES = {
"SPOOL_NUMBER": {
"sequence": ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
"U", "V", "W", "X", "Y", "Z"],
"description": "도면별 스풀 넘버"
},
"AREA_NUMBER": {
"pattern": r"#(\d{2})", # #01, #02, #03...
"format": "#{:02d}", # 2자리 숫자
"range": (1, 99), # 01~99
"description": "물리적 구역 넘버 (별도 관리)"
}
}
class SpoolManagerV2:
"""수정된 스풀 관리 클래스"""
def __init__(self, project_id: int = None):
self.project_id = project_id
def generate_spool_identifier(self, dwg_name: str, spool_number: str) -> str:
"""
스풀 식별자 생성 (도면명 + 스풀넘버)
Args:
dwg_name: 도면명 (예: "A-1", "B-3")
spool_number: 스풀넘버 (예: "A", "B")
Returns:
스풀 식별자 (예: "A-1-A", "B-3-B")
"""
# 스풀 넘버 포맷 검증
spool_formatted = self.format_spool_number(spool_number)
# 조합: {도면명}-{스풀넘버}
return f"{dwg_name}-{spool_formatted}"
def parse_spool_identifier(self, spool_id: str) -> Dict:
"""스풀 식별자 파싱"""
# 패턴: DWG_NAME-SPOOL_NUMBER
# 예: A-1-A, B-3-B, 1-IAR-3B1D0-0129-N-A
# 마지막 '-' 기준으로 분리
parts = spool_id.rsplit('-', 1)
if len(parts) == 2:
dwg_base = parts[0]
spool_number = parts[1]
return {
"original_id": spool_id,
"dwg_name": dwg_base,
"spool_number": spool_number,
"is_valid": self.validate_spool_number(spool_number),
"format": "CORRECT"
}
else:
return {
"original_id": spool_id,
"dwg_name": None,
"spool_number": None,
"is_valid": False,
"format": "INVALID"
}
def format_spool_number(self, spool_input: str) -> str:
"""스풀 넘버 포맷팅 및 검증"""
spool_clean = spool_input.upper().strip()
# 유효한 스풀 넘버인지 확인 (A-Z 단일 문자)
if re.match(r'^[A-Z]$', spool_clean):
return spool_clean
raise ValueError(f"유효하지 않은 스풀 넘버: {spool_input} (A-Z 단일 문자만 가능)")
def validate_spool_number(self, spool_number: str) -> bool:
"""스풀 넘버 유효성 검증"""
return bool(re.match(r'^[A-Z]$', spool_number))
def get_next_spool_number(self, dwg_name: str, existing_spools: List[str] = None) -> str:
"""해당 도면의 다음 사용 가능한 스풀 넘버 추천"""
if not existing_spools:
return "A" # 첫 번째 스풀
# 해당 도면의 기존 스풀들 파싱
used_spools = set()
for spool_id in existing_spools:
parsed = self.parse_spool_identifier(spool_id)
if parsed["dwg_name"] == dwg_name and parsed["is_valid"]:
used_spools.add(parsed["spool_number"])
# 다음 사용 가능한 넘버 찾기
sequence = SPOOL_NUMBERING_RULES["SPOOL_NUMBER"]["sequence"]
for spool_num in sequence:
if spool_num not in used_spools:
return spool_num
raise ValueError(f"도면 {dwg_name}에서 사용 가능한 스풀 넘버가 없습니다")
def validate_spool_identifier(self, spool_id: str) -> Dict:
"""스풀 식별자 전체 유효성 검증"""
parsed = self.parse_spool_identifier(spool_id)
validation_result = {
"is_valid": True,
"errors": [],
"warnings": [],
"parsed": parsed
}
# 도면명 확인
if not parsed["dwg_name"]:
validation_result["is_valid"] = False
validation_result["errors"].append("도면명이 없습니다")
# 스풀 넘버 확인
if not parsed["spool_number"]:
validation_result["is_valid"] = False
validation_result["errors"].append("스풀 넘버가 없습니다")
elif not self.validate_spool_number(parsed["spool_number"]):
validation_result["is_valid"] = False
validation_result["errors"].append("스풀 넘버 형식이 잘못되었습니다 (A-Z 단일 문자)")
return validation_result
# ========== 에리어 관리 (별도 시스템) ==========
class AreaManager:
"""에리어 관리 클래스 (물리적 구역)"""
def __init__(self, project_id: int = None):
self.project_id = project_id
def format_area_number(self, area_input: str) -> str:
"""에리어 넘버 포맷팅"""
# 숫자만 추출
numbers = re.findall(r'\d+', area_input)
if numbers:
area_num = int(numbers[0])
if 1 <= area_num <= 99:
return f"#{area_num:02d}"
raise ValueError(f"유효하지 않은 에리어 넘버: {area_input} (#01-#99)")
def assign_drawings_to_area(self, area_number: str, drawing_names: List[str]) -> Dict:
"""도면들을 에리어에 할당"""
area_formatted = self.format_area_number(area_number)
return {
"area_number": area_formatted,
"assigned_drawings": drawing_names,
"assignment_count": len(drawing_names),
"assignment_date": datetime.now().isoformat()
}
def classify_pipe_with_corrected_spool(dat_file: str, description: str, main_nom: str,
length: float = None, dwg_name: str = None,
spool_number: str = None, area_number: str = None) -> Dict:
"""파이프 분류 + 수정된 스풀 정보"""
# 기본 파이프 분류
from .pipe_classifier import classify_pipe
pipe_result = classify_pipe(dat_file, description, main_nom, length)
# 스풀 관리자 생성
spool_manager = SpoolManagerV2()
area_manager = AreaManager()
# 스풀 정보 처리
spool_info = {
"dwg_name": dwg_name,
"spool_number": None,
"spool_identifier": None,
"area_number": None, # 별도 관리
"manual_input_required": True,
"validation": None
}
# 스풀 넘버가 제공된 경우
if dwg_name and spool_number:
try:
spool_identifier = spool_manager.generate_spool_identifier(dwg_name, spool_number)
spool_info.update({
"spool_number": spool_manager.format_spool_number(spool_number),
"spool_identifier": spool_identifier,
"manual_input_required": False,
"validation": {"is_valid": True, "errors": []}
})
except ValueError as e:
spool_info["validation"] = {
"is_valid": False,
"errors": [str(e)]
}
# 에리어 정보 처리 (별도)
if area_number:
try:
area_formatted = area_manager.format_area_number(area_number)
spool_info["area_number"] = area_formatted
except ValueError as e:
spool_info["area_validation"] = {
"is_valid": False,
"errors": [str(e)]
}
# 기존 결과에 스풀 정보 추가
pipe_result["spool_info"] = spool_info
return pipe_result

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import DashboardPage from './pages/dashboard/DashboardPage'; import DashboardPage from './pages/dashboard/DashboardPage';
import { UserMenu, ErrorBoundary } from './components/common'; import { UserMenu, ErrorBoundary } from './components/common';
import NewMaterialsPage from './pages/NewMaterialsPage';
import BOMManagementPage from './pages/BOMManagementPage'; import BOMManagementPage from './pages/BOMManagementPage';
import UnifiedBOMPage from './pages/UnifiedBOMPage'; import UnifiedBOMPage from './pages/UnifiedBOMPage';
import SystemSettingsPage from './pages/SystemSettingsPage'; import SystemSettingsPage from './pages/SystemSettingsPage';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff