feat: 리비전 관리 시스템 개선
주요 개선사항: 1. 구매확정된 자재 완전 제외 - 리비전 비교 시 구매확정된 자재는 수량 변경 여부와 무관하게 완전히 제외 2. 삭제된 항목 추적 - 이전 리비전에는 있었지만 신규 리비전에는 없는 자재를 removed_materials로 반환 3. PIPE 특별 처리 - 6,000mm(1본) 단위로 필요 본수를 계산하여 비교 - 4,500mm → 5,000mm: 둘 다 1본 필요 → 변경 없음 - 4,500mm → 7,000mm: 1본 → 2본 필요 → 분류 필요 4. 리비전 비교 결과 상세 정보 반환 - has_purchased_materials, purchased_count, unpurchased_count - new_count, removed_count, excluded_purchased_count - removed_materials 리스트 기술적 변경: - perform_simple_revision_comparison 함수 완전 재작성 - 구매확정/미구매 자재 별도 관리 (purchased_dict, unpurchased_dict) - PIPE 카테고리 자재는 math.ceil(수량/6000)로 필요 본수 계산 - 업로드 응답에 revision_comparison 필드 추가 설정 변경: - docker-compose.override.yml: 포트를 환경 변수로 관리 - .env.example 추가: 환경 변수 템플릿 제공 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
26
.env.example
Normal file
26
.env.example
Normal file
@@ -0,0 +1,26 @@
|
||||
# TK-MP-Project 환경 변수 설정 예시
|
||||
# 사용법: 이 파일을 .env로 복사한 후 필요한 값을 수정하세요
|
||||
# cp .env.example .env
|
||||
|
||||
# PostgreSQL 설정
|
||||
POSTGRES_DB=tk_mp_bom
|
||||
POSTGRES_USER=tkmp_user
|
||||
POSTGRES_PASSWORD=your_password_here
|
||||
POSTGRES_PORT=15432
|
||||
|
||||
# Redis 설정
|
||||
REDIS_PORT=16379
|
||||
|
||||
# 백엔드 설정
|
||||
BACKEND_PORT=18000
|
||||
ENVIRONMENT=development
|
||||
DEBUG=true
|
||||
|
||||
# 프론트엔드 설정
|
||||
FRONTEND_PORT=13000
|
||||
VITE_API_URL=http://localhost:18000
|
||||
|
||||
# pgAdmin 설정
|
||||
PGADMIN_EMAIL=admin@example.com
|
||||
PGADMIN_PASSWORD=admin_password_here
|
||||
PGADMIN_PORT=15050
|
||||
@@ -1735,6 +1735,20 @@ async def upload_file(
|
||||
if missing_drawings_info:
|
||||
response_data["missing_drawings"] = missing_drawings_info
|
||||
|
||||
# 🔄 리비전 비교 결과 추가
|
||||
if revision_comparison:
|
||||
response_data["revision_comparison"] = {
|
||||
"has_purchased_materials": revision_comparison.get("has_purchased_materials", False),
|
||||
"purchased_count": revision_comparison.get("purchased_count", 0),
|
||||
"unpurchased_count": revision_comparison.get("unpurchased_count", 0),
|
||||
"new_count": revision_comparison.get("new_count", 0),
|
||||
"removed_count": revision_comparison.get("removed_count", 0),
|
||||
"excluded_purchased_count": revision_comparison.get("excluded_purchased_count", 0),
|
||||
"removed_materials": revision_comparison.get("removed_materials", []),
|
||||
"total_previous": revision_comparison.get("total_previous", 0),
|
||||
"total_new": revision_comparison.get("total_new", 0)
|
||||
}
|
||||
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
@@ -4025,7 +4039,12 @@ async def get_excel_exports(
|
||||
|
||||
def perform_simple_revision_comparison(db: Session, job_no: str, parent_file_id: int, new_materials: List[Dict]) -> Dict:
|
||||
"""
|
||||
간단한 리비전 비교 로직 (purchase_confirmed 기반)
|
||||
개선된 리비전 비교 로직
|
||||
|
||||
요구사항:
|
||||
1. 구매확정된 자재는 완전히 제외 (수량 변경 여부와 무관)
|
||||
2. 삭제된 항목은 별도 리스트로 반환
|
||||
3. 추가된 항목만 분류 대상
|
||||
|
||||
Args:
|
||||
db: 데이터베이스 세션
|
||||
@@ -4034,10 +4053,20 @@ def perform_simple_revision_comparison(db: Session, job_no: str, parent_file_id:
|
||||
new_materials: 신규 자재 목록
|
||||
|
||||
Returns:
|
||||
비교 결과
|
||||
비교 결과 {
|
||||
has_purchased_materials: bool,
|
||||
purchased_count: int,
|
||||
unpurchased_count: int,
|
||||
new_count: int,
|
||||
removed_count: int,
|
||||
materials_to_classify: List[Dict], # 신규 자재만
|
||||
removed_materials: List[Dict], # 삭제된 자재
|
||||
total_previous: int,
|
||||
total_new: int
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 1. 이전 파일의 구매확정된 자재 조회
|
||||
# 1. 이전 파일의 모든 자재 조회
|
||||
previous_materials_query = text("""
|
||||
SELECT original_description, classified_category, size_spec, material_grade,
|
||||
main_nom, red_nom, drawing_name, line_no,
|
||||
@@ -4052,25 +4081,26 @@ def perform_simple_revision_comparison(db: Session, job_no: str, parent_file_id:
|
||||
previous_materials = previous_result.fetchall()
|
||||
|
||||
if not previous_materials:
|
||||
print("📝 이전 자료가 없음 - 전체 자재 분류")
|
||||
return {
|
||||
"has_purchased_materials": False,
|
||||
"message": "이전 자료가 없습니다."
|
||||
"message": "이전 자료가 없습니다.",
|
||||
"materials_to_classify": new_materials,
|
||||
"new_count": len(new_materials),
|
||||
"removed_materials": [],
|
||||
"removed_count": 0
|
||||
}
|
||||
|
||||
# 2. 이전 자재를 키별로 그룹화 (구매확정 상태 포함)
|
||||
# 2. 이전 자재를 키별로 그룹화
|
||||
previous_dict = {}
|
||||
purchased_count = 0
|
||||
unpurchased_count = 0
|
||||
purchased_dict = {} # 구매확정된 자재 별도 관리
|
||||
unpurchased_dict = {} # 미구매 자재 별도 관리
|
||||
|
||||
for material in previous_materials:
|
||||
# 자재 식별 키 생성 (description + size만 사용 - 도면 변경 시에도 매칭 가능)
|
||||
# 자재 식별 키 생성 (description + size)
|
||||
key = f"{material.original_description.strip().upper()}|{material.size_spec or ''}"
|
||||
|
||||
if key in previous_dict:
|
||||
# 동일 자재가 있으면 수량 합산
|
||||
previous_dict[key]["quantity"] += float(material.quantity or 0)
|
||||
else:
|
||||
previous_dict[key] = {
|
||||
material_data = {
|
||||
"original_description": material.original_description,
|
||||
"classified_category": material.classified_category,
|
||||
"size_spec": material.size_spec,
|
||||
@@ -4084,10 +4114,17 @@ def perform_simple_revision_comparison(db: Session, job_no: str, parent_file_id:
|
||||
"purchase_confirmed": material.purchase_confirmed
|
||||
}
|
||||
|
||||
if material.purchase_confirmed:
|
||||
purchased_count += 1
|
||||
if key in previous_dict:
|
||||
# 동일 자재가 있으면 수량 합산
|
||||
previous_dict[key]["quantity"] += float(material.quantity or 0)
|
||||
else:
|
||||
unpurchased_count += 1
|
||||
previous_dict[key] = material_data
|
||||
|
||||
# 구매확정 여부에 따라 분류
|
||||
if material.purchase_confirmed:
|
||||
purchased_dict[key] = previous_dict[key]
|
||||
else:
|
||||
unpurchased_dict[key] = previous_dict[key]
|
||||
|
||||
# 3. 신규 자재를 키별로 그룹화
|
||||
new_dict = {}
|
||||
@@ -4104,72 +4141,94 @@ def perform_simple_revision_comparison(db: Session, job_no: str, parent_file_id:
|
||||
new_dict[key]["quantity"] = quantity
|
||||
|
||||
# 4. 비교 수행
|
||||
materials_to_classify = []
|
||||
materials_to_classify = [] # 분류가 필요한 신규 자재만
|
||||
removed_materials = [] # 삭제된 자재 (이전에는 있었지만 신규에는 없는)
|
||||
new_count = 0
|
||||
changed_count = 0
|
||||
matched_count = 0
|
||||
excluded_purchased_count = 0
|
||||
|
||||
print(f"🔍 [DEBUG] 이전 자재 키 개수: {len(previous_dict)}")
|
||||
print(f"🔍 [DEBUG] 신규 자재 키 개수: {len(new_dict)}")
|
||||
|
||||
# 구매확정된 자재 키들 확인
|
||||
purchased_keys = [key for key, mat in previous_dict.items() if mat["purchase_confirmed"]]
|
||||
print(f"🔍 [DEBUG] 구매확정된 자재 키 개수: {len(purchased_keys)}")
|
||||
if purchased_keys:
|
||||
print(f"🔍 [DEBUG] 구매확정 자재 샘플: {purchased_keys[:3]}")
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🔍 리비전 비교 시작")
|
||||
print(f"{'='*60}")
|
||||
print(f"📊 이전 자재: {len(previous_dict)}개 (구매확정: {len(purchased_dict)}개, 미구매: {len(unpurchased_dict)}개)")
|
||||
print(f"📊 신규 자재: {len(new_dict)}개")
|
||||
|
||||
# 4-1. 신규 자재 확인 (구매확정된 자재는 제외)
|
||||
for key, new_material in new_dict.items():
|
||||
if key in previous_dict:
|
||||
previous_material = previous_dict[key]
|
||||
matched_count += 1
|
||||
if key in purchased_dict:
|
||||
# ✅ 구매확정된 자재는 완전히 제외
|
||||
excluded_purchased_count += 1
|
||||
print(f"🚫 구매확정 자재 제외: {new_material.get('original_description', '')[:50]}...")
|
||||
elif key in unpurchased_dict:
|
||||
# 미구매 자재인데 새 리비전에도 있음
|
||||
previous_material = unpurchased_dict[key]
|
||||
|
||||
# 구매확정된 자재 매칭 로그
|
||||
if previous_material["purchase_confirmed"]:
|
||||
print(f"✅ 구매확정 자재 매칭: {key[:50]}...")
|
||||
# 🔧 PIPE 특별 처리: 6,000mm(1본) 단위로 비교
|
||||
if previous_material.get("classified_category") == "PIPE":
|
||||
import math
|
||||
|
||||
# 수량 비교
|
||||
if abs(new_material["quantity"] - previous_material["quantity"]) > 0.001:
|
||||
# 수량이 변경된 경우
|
||||
print(f"🔄 수량 변경: {new_material.get('original_description', '')[:50]}... "
|
||||
f"{previous_material['quantity']} → {new_material['quantity']}")
|
||||
# 이전 수량으로 필요한 본수 계산
|
||||
prev_qty = previous_material.get("quantity", 0)
|
||||
new_qty = new_material.get("quantity", 0)
|
||||
|
||||
# 구매확정된 자재의 수량 변경은 특별 처리 필요
|
||||
if previous_material["purchase_confirmed"]:
|
||||
print(f"⚠️ 구매확정된 자재 수량 변경 감지!")
|
||||
prev_pipes_needed = math.ceil(prev_qty / 6000)
|
||||
new_pipes_needed = math.ceil(new_qty / 6000)
|
||||
|
||||
if prev_pipes_needed != new_pipes_needed:
|
||||
# 필요한 본수가 변경됨 - 분류 필요
|
||||
print(f"🔧 PIPE 본수 변경: {new_material.get('original_description', '')[:40]}... "
|
||||
f"{prev_qty}mm({prev_pipes_needed}본) → {new_qty}mm({new_pipes_needed}본)")
|
||||
materials_to_classify.append(new_material)
|
||||
changed_count += 1
|
||||
new_count += 1
|
||||
else:
|
||||
# 수량이 동일하고 구매확정된 자재는 분류에서 제외
|
||||
if previous_material["purchase_confirmed"]:
|
||||
print(f"🚫 구매확정된 동일 자재 제외: {key[:50]}...")
|
||||
# else: 미구매 동일 자재는 분류 불필요 (기존 분류 유지)
|
||||
# 필요한 본수 동일 - 기존 분류 유지
|
||||
print(f"♻️ PIPE 본수 유지: {new_material.get('original_description', '')[:40]}... "
|
||||
f"{prev_qty}mm → {new_qty}mm ({new_pipes_needed}본)")
|
||||
else:
|
||||
# 완전히 새로운 자재
|
||||
# 기타 자재: 기존 분류 유지
|
||||
print(f"♻️ 기존 미구매 자재 유지: {new_material.get('original_description', '')[:50]}...")
|
||||
else:
|
||||
# ✅ 완전히 새로운 자재 - 분류 필요
|
||||
print(f"➕ 신규 자재: {new_material.get('original_description', '')[:50]}...")
|
||||
materials_to_classify.append(new_material)
|
||||
new_count += 1
|
||||
|
||||
print(f"🔍 [DEBUG] 매칭된 자재: {matched_count}개, 신규: {new_count}개, 변경: {changed_count}개")
|
||||
# 4-2. 삭제된 자재 확인 (미구매 자재 중에서만)
|
||||
for key, previous_material in unpurchased_dict.items():
|
||||
if key not in new_dict:
|
||||
# ✅ 이전에는 있었지만 새 리비전에는 없음
|
||||
removed_materials.append(previous_material)
|
||||
print(f"➖ 삭제된 자재: {previous_material.get('original_description', '')[:50]}...")
|
||||
|
||||
has_purchased = any(mat["purchase_confirmed"] for mat in previous_dict.values())
|
||||
print(f"\n{'='*60}")
|
||||
print(f"📊 비교 결과")
|
||||
print(f"{'='*60}")
|
||||
print(f"✅ 신규 자재: {new_count}개 (분류 필요)")
|
||||
print(f"➖ 삭제 자재: {len(removed_materials)}개")
|
||||
print(f"🚫 구매확정 제외: {excluded_purchased_count}개")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return {
|
||||
"has_purchased_materials": has_purchased,
|
||||
"purchased_count": len([m for m in previous_dict.values() if m["purchase_confirmed"]]),
|
||||
"unpurchased_count": len([m for m in previous_dict.values() if not m["purchase_confirmed"]]),
|
||||
"has_purchased_materials": len(purchased_dict) > 0,
|
||||
"purchased_count": len(purchased_dict),
|
||||
"unpurchased_count": len(unpurchased_dict),
|
||||
"new_count": new_count,
|
||||
"changed_count": changed_count,
|
||||
"materials_to_classify": materials_to_classify,
|
||||
"removed_count": len(removed_materials),
|
||||
"excluded_purchased_count": excluded_purchased_count,
|
||||
"materials_to_classify": materials_to_classify, # 신규 자재만
|
||||
"removed_materials": removed_materials, # 삭제된 자재
|
||||
"total_previous": len(previous_dict),
|
||||
"total_new": len(new_dict)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 간단한 리비전 비교 실패: {str(e)}")
|
||||
print(f"❌ 리비전 비교 실패: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"has_purchased_materials": False,
|
||||
"message": f"비교 실패: {str(e)}"
|
||||
"message": f"비교 실패: {str(e)}",
|
||||
"materials_to_classify": new_materials,
|
||||
"removed_materials": [],
|
||||
"new_count": len(new_materials),
|
||||
"removed_count": 0
|
||||
}
|
||||
@@ -26,13 +26,13 @@ services:
|
||||
# 개발 환경에서는 모든 포트를 외부에 노출
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "${POSTGRES_PORT:-15432}:5432"
|
||||
|
||||
redis:
|
||||
ports:
|
||||
- "6379:6379"
|
||||
- "${REDIS_PORT:-16379}:6379"
|
||||
|
||||
pgadmin:
|
||||
ports:
|
||||
- "5050:80"
|
||||
- "${PGADMIN_PORT:-15050}:80"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user