- 리비전 관리 라우터 및 서비스 추가 (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>
327 lines
11 KiB
Python
327 lines
11 KiB
Python
"""
|
|
간단한 리비전 관리 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)}") |