feat: 리비전 관리 시스템 및 구매확정 기능 구현
- 리비전 관리 라우터 및 서비스 추가 (revision_management.py, revision_comparison_service.py, revision_session_service.py) - 구매확정 기능 구현: materials 테이블에 purchase_confirmed 필드 추가 및 업데이트 로직 - 리비전 비교 로직 구현: 구매확정된 자재 기반으로 신규/변경 자재 자동 분류 - 데이터베이스 스키마 확장: revision_sessions, revision_material_changes, inventory_transfers 테이블 추가 - 구매신청 생성 시 자재 상세 정보 저장 및 purchase_confirmed 자동 업데이트 - 프론트엔드: 리비전 관리 컴포넌트 및 hooks 추가 - 파일 목록 조회 API 추가 (/files/list) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -339,6 +339,12 @@ async def upload_file(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
# 🎯 트랜잭션 오류 방지: 완전한 트랜잭션 초기화
|
||||
|
||||
# 🔍 디버깅: 업로드 파라미터 로깅
|
||||
print(f"🔍 [UPLOAD] job_no: {job_no}, revision: {revision}, parent_file_id: {parent_file_id}")
|
||||
print(f"🔍 [UPLOAD] bom_name: {bom_name}, filename: {file.filename}")
|
||||
print(f"🔍 [UPLOAD] revision != 'Rev.0': {revision != 'Rev.0'}")
|
||||
|
||||
try:
|
||||
# 1. 현재 트랜잭션 완전 롤백
|
||||
db.rollback()
|
||||
@@ -502,6 +508,7 @@ async def upload_file(
|
||||
})
|
||||
|
||||
file_id = file_result.fetchone()[0]
|
||||
db.commit() # 파일 레코드 즉시 커밋
|
||||
print(f"파일 저장 완료: file_id = {file_id}, uploaded_by = {username}")
|
||||
|
||||
# 🔄 리비전 비교 수행 (RULES.md 코딩 컨벤션 준수)
|
||||
@@ -509,29 +516,30 @@ async def upload_file(
|
||||
materials_to_classify = materials_data
|
||||
|
||||
if revision != "Rev.0": # 리비전 업로드인 경우만 비교
|
||||
# 로그 제거
|
||||
print(f"🔍 [DEBUG] 리비전 비교 시작 - revision: {revision}, parent_file_id: {parent_file_id}")
|
||||
try:
|
||||
revision_comparison = get_revision_comparison(db, job_no, revision, materials_data)
|
||||
# 간단한 리비전 비교 로직 (purchase_confirmed 기반)
|
||||
print(f"🔍 [DEBUG] perform_simple_revision_comparison 호출 중...")
|
||||
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'}")
|
||||
|
||||
if revision_comparison.get("has_previous_confirmation", False):
|
||||
print(f"📊 리비전 비교 결과:")
|
||||
print(f" - 변경없음: {revision_comparison.get('unchanged_count', 0)}개")
|
||||
print(f" - 변경됨: {revision_comparison.get('changed_count', 0)}개")
|
||||
print(f" - 신규: {revision_comparison.get('new_count', 0)}개")
|
||||
print(f" - 삭제됨: {revision_comparison.get('removed_count', 0)}개")
|
||||
print(f" - 분류 필요: {revision_comparison.get('classification_needed', 0)}개")
|
||||
if revision_comparison.get("has_purchased_materials", False):
|
||||
print(f"📊 간단한 리비전 비교 결과:")
|
||||
print(f" - 구매확정된 자재: {revision_comparison.get('purchased_count', 0)}개")
|
||||
print(f" - 미구매 자재: {revision_comparison.get('unpurchased_count', 0)}개")
|
||||
print(f" - 신규 자재: {revision_comparison.get('new_count', 0)}개")
|
||||
print(f" - 변경된 자재: {revision_comparison.get('changed_count', 0)}개")
|
||||
|
||||
# 분류가 필요한 자재만 추출 (변경됨 + 신규)
|
||||
materials_to_classify = (
|
||||
revision_comparison.get("changed_materials", []) +
|
||||
revision_comparison.get("new_materials", [])
|
||||
)
|
||||
# 신규 및 변경된 자재만 분류
|
||||
materials_to_classify = revision_comparison.get("materials_to_classify", [])
|
||||
print(f" - 분류 필요: {len(materials_to_classify)}개")
|
||||
else:
|
||||
print("📝 이전 확정 자료 없음 - 전체 자재 분류")
|
||||
print("📝 이전 구매확정 자료 없음 - 전체 자재 분류")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"리비전 비교 실패: {str(e)}")
|
||||
print(f"⚠️ 리비전 비교 실패, 전체 자재 분류로 진행: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print(f"🔧 자재 분류 시작: {len(materials_to_classify)}개 자재")
|
||||
|
||||
@@ -1730,6 +1738,11 @@ async def upload_file(
|
||||
return response_data
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
print(f"❌ 파일 업로드 실패 - 상세 에러:")
|
||||
print(error_details)
|
||||
|
||||
db.rollback()
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
@@ -1779,6 +1792,56 @@ async def get_files(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"파일 목록 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/list")
|
||||
async def get_files_list(
|
||||
job_no: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""파일 목록 조회 (리비전 모드 확인용)"""
|
||||
try:
|
||||
query = """
|
||||
SELECT id, filename, original_filename, bom_name, job_no, revision,
|
||||
description, file_size, parsed_count, upload_date, is_active
|
||||
FROM files
|
||||
WHERE is_active = TRUE
|
||||
"""
|
||||
params = {}
|
||||
|
||||
if job_no:
|
||||
query += " AND job_no = :job_no"
|
||||
params["job_no"] = job_no
|
||||
|
||||
query += " ORDER BY upload_date ASC" # 업로드 순서대로 정렬
|
||||
|
||||
result = db.execute(text(query), params)
|
||||
files = result.fetchall()
|
||||
|
||||
files_list = [
|
||||
{
|
||||
"id": file.id,
|
||||
"filename": file.filename,
|
||||
"original_filename": file.original_filename,
|
||||
"bom_name": file.bom_name,
|
||||
"job_no": file.job_no,
|
||||
"revision": file.revision,
|
||||
"description": file.description,
|
||||
"file_size": file.file_size,
|
||||
"parsed_count": file.parsed_count,
|
||||
"upload_date": file.upload_date,
|
||||
"is_active": file.is_active
|
||||
}
|
||||
for file in files
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"files": files_list,
|
||||
"total_count": len(files_list)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"파일 목록 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/project/{project_code}")
|
||||
async def get_files_by_project(
|
||||
project_code: str,
|
||||
@@ -3957,4 +4020,156 @@ async def get_excel_exports(
|
||||
"success": False,
|
||||
"exports": [],
|
||||
"message": "엑셀 내보내기 목록을 조회할 수 없습니다."
|
||||
}
|
||||
|
||||
|
||||
def perform_simple_revision_comparison(db: Session, job_no: str, parent_file_id: int, new_materials: List[Dict]) -> Dict:
|
||||
"""
|
||||
간단한 리비전 비교 로직 (purchase_confirmed 기반)
|
||||
|
||||
Args:
|
||||
db: 데이터베이스 세션
|
||||
job_no: 프로젝트 번호
|
||||
parent_file_id: 이전 파일 ID
|
||||
new_materials: 신규 자재 목록
|
||||
|
||||
Returns:
|
||||
비교 결과
|
||||
"""
|
||||
try:
|
||||
# 1. 이전 파일의 구매확정된 자재 조회
|
||||
previous_materials_query = text("""
|
||||
SELECT original_description, classified_category, size_spec, material_grade,
|
||||
main_nom, red_nom, drawing_name, line_no,
|
||||
COALESCE(total_quantity, quantity, 0) as quantity, unit,
|
||||
purchase_confirmed
|
||||
FROM materials
|
||||
WHERE file_id = :parent_file_id
|
||||
ORDER BY id
|
||||
""")
|
||||
|
||||
previous_result = db.execute(previous_materials_query, {"parent_file_id": parent_file_id})
|
||||
previous_materials = previous_result.fetchall()
|
||||
|
||||
if not previous_materials:
|
||||
return {
|
||||
"has_purchased_materials": False,
|
||||
"message": "이전 자료가 없습니다."
|
||||
}
|
||||
|
||||
# 2. 이전 자재를 키별로 그룹화 (구매확정 상태 포함)
|
||||
previous_dict = {}
|
||||
purchased_count = 0
|
||||
unpurchased_count = 0
|
||||
|
||||
for material in previous_materials:
|
||||
# 자재 식별 키 생성 (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] = {
|
||||
"original_description": material.original_description,
|
||||
"classified_category": material.classified_category,
|
||||
"size_spec": material.size_spec,
|
||||
"material_grade": material.material_grade,
|
||||
"main_nom": material.main_nom,
|
||||
"red_nom": material.red_nom,
|
||||
"drawing_name": material.drawing_name,
|
||||
"line_no": material.line_no,
|
||||
"quantity": float(material.quantity or 0),
|
||||
"unit": material.unit,
|
||||
"purchase_confirmed": material.purchase_confirmed
|
||||
}
|
||||
|
||||
if material.purchase_confirmed:
|
||||
purchased_count += 1
|
||||
else:
|
||||
unpurchased_count += 1
|
||||
|
||||
# 3. 신규 자재를 키별로 그룹화
|
||||
new_dict = {}
|
||||
for material in new_materials:
|
||||
key = f"{material.get('original_description', '').strip().upper()}|{material.get('size_spec', '') or ''}"
|
||||
|
||||
# total_quantity 우선 사용
|
||||
quantity = float(material.get("total_quantity") or material.get("quantity", 0))
|
||||
|
||||
if key in new_dict:
|
||||
new_dict[key]["quantity"] += quantity
|
||||
else:
|
||||
new_dict[key] = material.copy()
|
||||
new_dict[key]["quantity"] = quantity
|
||||
|
||||
# 4. 비교 수행
|
||||
materials_to_classify = []
|
||||
new_count = 0
|
||||
changed_count = 0
|
||||
matched_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]}")
|
||||
|
||||
for key, new_material in new_dict.items():
|
||||
if key in previous_dict:
|
||||
previous_material = previous_dict[key]
|
||||
matched_count += 1
|
||||
|
||||
# 구매확정된 자재 매칭 로그
|
||||
if previous_material["purchase_confirmed"]:
|
||||
print(f"✅ 구매확정 자재 매칭: {key[:50]}...")
|
||||
|
||||
# 수량 비교
|
||||
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']}")
|
||||
|
||||
# 구매확정된 자재의 수량 변경은 특별 처리 필요
|
||||
if previous_material["purchase_confirmed"]:
|
||||
print(f"⚠️ 구매확정된 자재 수량 변경 감지!")
|
||||
|
||||
materials_to_classify.append(new_material)
|
||||
changed_count += 1
|
||||
else:
|
||||
# 수량이 동일하고 구매확정된 자재는 분류에서 제외
|
||||
if previous_material["purchase_confirmed"]:
|
||||
print(f"🚫 구매확정된 동일 자재 제외: {key[:50]}...")
|
||||
# else: 미구매 동일 자재는 분류 불필요 (기존 분류 유지)
|
||||
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}개")
|
||||
|
||||
has_purchased = any(mat["purchase_confirmed"] for mat in previous_dict.values())
|
||||
|
||||
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"]]),
|
||||
"new_count": new_count,
|
||||
"changed_count": changed_count,
|
||||
"materials_to_classify": materials_to_classify,
|
||||
"total_previous": len(previous_dict),
|
||||
"total_new": len(new_dict)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 간단한 리비전 비교 실패: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"has_purchased_materials": False,
|
||||
"message": f"비교 실패: {str(e)}"
|
||||
}
|
||||
@@ -42,6 +42,14 @@ async def create_purchase_request(
|
||||
구매신청 생성 (엑셀 내보내기 = 구매신청)
|
||||
"""
|
||||
try:
|
||||
# 🔍 디버깅: 요청 데이터 로깅
|
||||
logger.info(f"🔍 구매신청 생성 요청 - file_id: {request_data.file_id}, job_no: {request_data.job_no}")
|
||||
logger.info(f"🔍 material_ids 개수: {len(request_data.material_ids)}")
|
||||
logger.info(f"🔍 materials_data 개수: {len(request_data.materials_data)}")
|
||||
if request_data.material_ids:
|
||||
logger.info(f"🔍 material_ids 샘플: {request_data.material_ids[:5]}")
|
||||
|
||||
print(f"🔍 [DEBUG] 구매신청 API 호출됨 - material_ids: {len(request_data.material_ids)}개")
|
||||
# 구매신청 번호 생성
|
||||
today = datetime.now().strftime('%Y%m%d')
|
||||
count_query = text("""
|
||||
@@ -116,9 +124,13 @@ async def create_purchase_request(
|
||||
if not existing:
|
||||
insert_item = text("""
|
||||
INSERT INTO purchase_request_items (
|
||||
request_id, material_id, quantity, unit, user_requirement
|
||||
request_id, material_id, description, category, subcategory,
|
||||
material_grade, size_spec, quantity, unit, drawing_name,
|
||||
notes, user_requirement
|
||||
) VALUES (
|
||||
:request_id, :material_id, :quantity, :unit, :user_requirement
|
||||
:request_id, :material_id, :description, :category, :subcategory,
|
||||
:material_grade, :size_spec, :quantity, :unit, :drawing_name,
|
||||
:notes, :user_requirement
|
||||
)
|
||||
""")
|
||||
# quantity를 정수로 변환 (소수점 제거)
|
||||
@@ -131,14 +143,44 @@ async def create_purchase_request(
|
||||
db.execute(insert_item, {
|
||||
"request_id": request_id,
|
||||
"material_id": material_id,
|
||||
"description": material_data.get("description", material_data.get("original_description", "")),
|
||||
"category": material_data.get("category", material_data.get("classified_category", "")),
|
||||
"subcategory": material_data.get("subcategory", material_data.get("classified_subcategory", "")),
|
||||
"material_grade": material_data.get("material_grade", ""),
|
||||
"size_spec": material_data.get("size_spec", ""),
|
||||
"quantity": quantity,
|
||||
"unit": material_data.get("unit", ""),
|
||||
"unit": material_data.get("unit", "EA"),
|
||||
"drawing_name": material_data.get("drawing_name", ""),
|
||||
"notes": material_data.get("notes", ""),
|
||||
"user_requirement": material_data.get("user_requirement", "")
|
||||
})
|
||||
inserted_count += 1
|
||||
else:
|
||||
logger.warning(f"Material {material_id} already in another purchase request, skipping")
|
||||
|
||||
# 🔥 중요: materials 테이블의 purchase_confirmed 업데이트
|
||||
if request_data.material_ids:
|
||||
print(f"🔥 [PURCHASE] purchase_confirmed 업데이트 시작: {len(request_data.material_ids)}개 자재")
|
||||
print(f"🔥 [PURCHASE] material_ids: {request_data.material_ids[:5]}...") # 처음 5개만 로그
|
||||
|
||||
update_materials_query = text("""
|
||||
UPDATE materials
|
||||
SET purchase_confirmed = true,
|
||||
purchase_confirmed_at = NOW(),
|
||||
purchase_confirmed_by = :confirmed_by
|
||||
WHERE id = ANY(:material_ids)
|
||||
""")
|
||||
|
||||
result = db.execute(update_materials_query, {
|
||||
"material_ids": request_data.material_ids,
|
||||
"confirmed_by": current_user.get("username", "system")
|
||||
})
|
||||
|
||||
print(f"🔥 [PURCHASE] UPDATE 결과: {result.rowcount}개 행 업데이트됨")
|
||||
logger.info(f"✅ {len(request_data.material_ids)}개 자재의 purchase_confirmed를 true로 업데이트")
|
||||
else:
|
||||
print(f"⚠️ [PURCHASE] material_ids가 비어있음!")
|
||||
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Purchase request created: {request_no} with {inserted_count} materials (out of {len(request_data.material_ids)} requested)")
|
||||
@@ -150,6 +192,20 @@ async def create_purchase_request(
|
||||
verified_count = db.execute(verify_query, {"request_id": request_id}).fetchone().count
|
||||
logger.info(f"✅ DB 검증: purchase_request_items에 {verified_count}개 저장됨")
|
||||
|
||||
# purchase_requests 테이블의 total_items 필드 업데이트
|
||||
update_total_items = text("""
|
||||
UPDATE purchase_requests
|
||||
SET total_items = :total_items
|
||||
WHERE request_id = :request_id
|
||||
""")
|
||||
db.execute(update_total_items, {
|
||||
"request_id": request_id,
|
||||
"total_items": verified_count
|
||||
})
|
||||
db.commit()
|
||||
|
||||
logger.info(f"✅ total_items 업데이트 완료: {verified_count}개")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"request_no": request_no,
|
||||
@@ -224,7 +280,7 @@ async def get_purchase_requests(
|
||||
"job_no": row.job_no,
|
||||
"job_name": row.job_name,
|
||||
"category": "ALL", # 기본값
|
||||
"material_count": row.total_items or 0,
|
||||
"material_count": row.item_count or 0, # 실제 자재 개수 사용
|
||||
"item_count": row.item_count,
|
||||
"excel_file_path": None, # 현재 테이블에 없음
|
||||
"requested_at": row.request_date.isoformat() if row.request_date else None,
|
||||
|
||||
327
backend/app/routers/revision_management.py
Normal file
327
backend/app/routers/revision_management.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
간단한 리비전 관리 API
|
||||
- 리비전 세션 생성 및 관리
|
||||
- 자재 비교 및 변경사항 처리
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..database import get_db
|
||||
from ..auth.middleware import get_current_user
|
||||
from ..services.revision_session_service import RevisionSessionService
|
||||
from ..services.revision_comparison_service import RevisionComparisonService
|
||||
|
||||
router = APIRouter(prefix="/revision-management", tags=["revision-management"])
|
||||
|
||||
class RevisionSessionCreate(BaseModel):
|
||||
job_no: str
|
||||
current_file_id: int
|
||||
previous_file_id: int
|
||||
|
||||
@router.post("/sessions")
|
||||
async def create_revision_session(
|
||||
session_data: RevisionSessionCreate,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""리비전 세션 생성"""
|
||||
try:
|
||||
session_service = RevisionSessionService(db)
|
||||
|
||||
# 실제 DB에 세션 생성
|
||||
result = session_service.create_revision_session(
|
||||
job_no=session_data.job_no,
|
||||
current_file_id=session_data.current_file_id,
|
||||
previous_file_id=session_data.previous_file_id,
|
||||
username=current_user.get("username")
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result,
|
||||
"message": "리비전 세션이 생성되었습니다."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"리비전 세션 생성 실패: {str(e)}")
|
||||
|
||||
@router.get("/sessions/{session_id}")
|
||||
async def get_session_status(
|
||||
session_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""세션 상태 조회"""
|
||||
try:
|
||||
session_service = RevisionSessionService(db)
|
||||
|
||||
# 실제 DB에서 세션 상태 조회
|
||||
result = session_service.get_session_status(session_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"세션 상태 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/sessions/{session_id}/summary")
|
||||
async def get_revision_summary(
|
||||
session_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""리비전 요약 조회"""
|
||||
try:
|
||||
comparison_service = RevisionComparisonService(db)
|
||||
|
||||
# 세션의 모든 변경사항 조회
|
||||
changes = comparison_service.get_session_changes(session_id)
|
||||
|
||||
# 요약 통계 계산
|
||||
summary = {
|
||||
"session_id": session_id,
|
||||
"total_changes": len(changes),
|
||||
"new_materials": len([c for c in changes if c['change_type'] == 'added']),
|
||||
"changed_materials": len([c for c in changes if c['change_type'] == 'quantity_changed']),
|
||||
"removed_materials": len([c for c in changes if c['change_type'] == 'removed']),
|
||||
"categories": {}
|
||||
}
|
||||
|
||||
# 카테고리별 통계
|
||||
for change in changes:
|
||||
category = change['category']
|
||||
if category not in summary["categories"]:
|
||||
summary["categories"][category] = {
|
||||
"total_changes": 0,
|
||||
"added": 0,
|
||||
"changed": 0,
|
||||
"removed": 0
|
||||
}
|
||||
|
||||
summary["categories"][category]["total_changes"] += 1
|
||||
if change['change_type'] == 'added':
|
||||
summary["categories"][category]["added"] += 1
|
||||
elif change['change_type'] == 'quantity_changed':
|
||||
summary["categories"][category]["changed"] += 1
|
||||
elif change['change_type'] == 'removed':
|
||||
summary["categories"][category]["removed"] += 1
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": summary
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"리비전 요약 조회 실패: {str(e)}")
|
||||
|
||||
@router.post("/sessions/{session_id}/compare/{category}")
|
||||
async def compare_category(
|
||||
session_id: int,
|
||||
category: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""카테고리별 자재 비교"""
|
||||
try:
|
||||
# 세션 정보 조회
|
||||
session_service = RevisionSessionService(db)
|
||||
session_status = session_service.get_session_status(session_id)
|
||||
session_info = session_status["session_info"]
|
||||
|
||||
# 자재 비교 수행
|
||||
comparison_service = RevisionComparisonService(db)
|
||||
result = comparison_service.compare_materials_by_category(
|
||||
current_file_id=session_info["current_file_id"],
|
||||
previous_file_id=session_info["previous_file_id"],
|
||||
category=category,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"카테고리 비교 실패: {str(e)}")
|
||||
|
||||
@router.get("/history/{job_no}")
|
||||
async def get_revision_history(
|
||||
job_no: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""리비전 히스토리 조회"""
|
||||
try:
|
||||
session_service = RevisionSessionService(db)
|
||||
|
||||
# 실제 DB에서 리비전 히스토리 조회
|
||||
history = session_service.get_job_revision_history(job_no)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"job_no": job_no,
|
||||
"history": history
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"리비전 히스토리 조회 실패: {str(e)}")
|
||||
|
||||
# 세션 변경사항 조회
|
||||
@router.get("/sessions/{session_id}/changes")
|
||||
async def get_session_changes(
|
||||
session_id: int,
|
||||
category: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""세션의 변경사항 조회"""
|
||||
try:
|
||||
comparison_service = RevisionComparisonService(db)
|
||||
|
||||
# 세션의 변경사항 조회
|
||||
changes = comparison_service.get_session_changes(session_id, category)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"changes": changes,
|
||||
"total_count": len(changes)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"세션 변경사항 조회 실패: {str(e)}")
|
||||
|
||||
# 리비전 액션 처리
|
||||
@router.post("/changes/{change_id}/process")
|
||||
async def process_revision_action(
|
||||
change_id: int,
|
||||
action_data: Dict[str, Any],
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""리비전 액션 처리"""
|
||||
try:
|
||||
comparison_service = RevisionComparisonService(db)
|
||||
|
||||
# 액션 처리
|
||||
result = comparison_service.process_revision_action(
|
||||
change_id=change_id,
|
||||
action=action_data.get("action"),
|
||||
username=current_user.get("username"),
|
||||
notes=action_data.get("notes")
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"리비전 액션 처리 실패: {str(e)}")
|
||||
|
||||
# 세션 완료
|
||||
@router.post("/sessions/{session_id}/complete")
|
||||
async def complete_revision_session(
|
||||
session_id: int,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""리비전 세션 완료"""
|
||||
try:
|
||||
session_service = RevisionSessionService(db)
|
||||
|
||||
# 세션 완료 처리
|
||||
result = session_service.complete_session(
|
||||
session_id=session_id,
|
||||
username=current_user.get("username")
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"리비전 세션 완료 실패: {str(e)}")
|
||||
|
||||
# 세션 취소
|
||||
@router.post("/sessions/{session_id}/cancel")
|
||||
async def cancel_revision_session(
|
||||
session_id: int,
|
||||
reason: Optional[str] = Query(None),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""리비전 세션 취소"""
|
||||
try:
|
||||
session_service = RevisionSessionService(db)
|
||||
|
||||
# 세션 취소 처리
|
||||
result = session_service.cancel_session(
|
||||
session_id=session_id,
|
||||
username=current_user.get("username"),
|
||||
reason=reason
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {"cancelled": result}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"리비전 세션 취소 실패: {str(e)}")
|
||||
|
||||
@router.get("/categories")
|
||||
async def get_supported_categories():
|
||||
"""지원 카테고리 목록 조회"""
|
||||
try:
|
||||
categories = [
|
||||
{"key": "PIPE", "name": "배관", "description": "파이프 및 배관 자재"},
|
||||
{"key": "FITTING", "name": "피팅", "description": "배관 연결 부품"},
|
||||
{"key": "FLANGE", "name": "플랜지", "description": "플랜지 및 연결 부품"},
|
||||
{"key": "VALVE", "name": "밸브", "description": "각종 밸브류"},
|
||||
{"key": "GASKET", "name": "가스켓", "description": "씰링 부품"},
|
||||
{"key": "BOLT", "name": "볼트", "description": "체결 부품"},
|
||||
{"key": "SUPPORT", "name": "서포트", "description": "지지대 및 구조물"},
|
||||
{"key": "SPECIAL", "name": "특수자재", "description": "특수 목적 자재"},
|
||||
{"key": "UNCLASSIFIED", "name": "미분류", "description": "분류되지 않은 자재"}
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"categories": categories
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"지원 카테고리 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/actions")
|
||||
async def get_supported_actions():
|
||||
"""지원 액션 목록 조회"""
|
||||
try:
|
||||
actions = [
|
||||
{"key": "new_material", "name": "신규 자재", "description": "새로 추가된 자재"},
|
||||
{"key": "additional_purchase", "name": "추가 구매", "description": "구매된 자재의 수량 증가"},
|
||||
{"key": "inventory_transfer", "name": "재고 이관", "description": "구매된 자재의 수량 감소 또는 제거"},
|
||||
{"key": "purchase_cancel", "name": "구매 취소", "description": "미구매 자재의 제거"},
|
||||
{"key": "quantity_update", "name": "수량 업데이트", "description": "미구매 자재의 수량 변경"},
|
||||
{"key": "maintain", "name": "유지", "description": "변경사항 없음"}
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"actions": actions
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"지원 액션 조회 실패: {str(e)}")
|
||||
Reference in New Issue
Block a user