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:
@@ -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"])
|
||||||
|
|||||||
@@ -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", {})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|
||||||
# 개별 자재 정보 조회 (기존 코드)
|
# 개별 자재 정보 조회 (기존 코드)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
Reference in New Issue
Block a user