- 리비전 관리 라우터 및 서비스 추가 (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>
290 lines
11 KiB
Python
290 lines
11 KiB
Python
"""
|
|
리비전 세션 관리 서비스
|
|
- 리비전 세션 생성, 관리, 완료 처리
|
|
- 자재 변경 사항 추적 및 처리
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, List, Optional, Any
|
|
from datetime import datetime
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text, and_, or_
|
|
|
|
from ..models import File, Material
|
|
from ..database import get_db
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RevisionSessionService:
|
|
"""리비전 세션 관리 서비스"""
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
|
|
def create_revision_session(
|
|
self,
|
|
job_no: str,
|
|
current_file_id: int,
|
|
previous_file_id: int,
|
|
username: str
|
|
) -> Dict[str, Any]:
|
|
"""새로운 리비전 세션 생성"""
|
|
|
|
try:
|
|
# 파일 정보 조회
|
|
current_file = self.db.query(File).filter(File.id == current_file_id).first()
|
|
previous_file = self.db.query(File).filter(File.id == previous_file_id).first()
|
|
|
|
if not current_file or not previous_file:
|
|
raise ValueError("파일 정보를 찾을 수 없습니다")
|
|
|
|
# 기존 진행 중인 세션이 있는지 확인
|
|
existing_session = self.db.execute(text("""
|
|
SELECT id FROM revision_sessions
|
|
WHERE job_no = :job_no AND status = 'processing'
|
|
"""), {"job_no": job_no}).fetchone()
|
|
|
|
if existing_session:
|
|
logger.warning(f"기존 진행 중인 리비전 세션이 있습니다: {existing_session[0]}")
|
|
return {"session_id": existing_session[0], "status": "existing"}
|
|
|
|
# 새 세션 생성
|
|
session_data = {
|
|
"job_no": job_no,
|
|
"current_file_id": current_file_id,
|
|
"previous_file_id": previous_file_id,
|
|
"current_revision": current_file.revision,
|
|
"previous_revision": previous_file.revision,
|
|
"status": "processing",
|
|
"created_by": username
|
|
}
|
|
|
|
result = self.db.execute(text("""
|
|
INSERT INTO revision_sessions (
|
|
job_no, current_file_id, previous_file_id,
|
|
current_revision, previous_revision, status, created_by
|
|
) VALUES (
|
|
:job_no, :current_file_id, :previous_file_id,
|
|
:current_revision, :previous_revision, :status, :created_by
|
|
) RETURNING id
|
|
"""), session_data)
|
|
|
|
session_id = result.fetchone()[0]
|
|
self.db.commit()
|
|
|
|
logger.info(f"새 리비전 세션 생성: {session_id} (Job: {job_no})")
|
|
|
|
return {
|
|
"session_id": session_id,
|
|
"status": "created",
|
|
"job_no": job_no,
|
|
"current_revision": current_file.revision,
|
|
"previous_revision": previous_file.revision
|
|
}
|
|
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"리비전 세션 생성 실패: {e}")
|
|
raise
|
|
|
|
def get_session_status(self, session_id: int) -> Dict[str, Any]:
|
|
"""리비전 세션 상태 조회"""
|
|
|
|
try:
|
|
session_info = self.db.execute(text("""
|
|
SELECT
|
|
id, job_no, current_file_id, previous_file_id,
|
|
current_revision, previous_revision, status,
|
|
total_materials, processed_materials,
|
|
added_count, removed_count, changed_count, unchanged_count,
|
|
purchase_cancel_count, inventory_transfer_count, additional_purchase_count,
|
|
created_by, created_at, completed_at
|
|
FROM revision_sessions
|
|
WHERE id = :session_id
|
|
"""), {"session_id": session_id}).fetchone()
|
|
|
|
if not session_info:
|
|
raise ValueError(f"리비전 세션을 찾을 수 없습니다: {session_id}")
|
|
|
|
# 변경 사항 상세 조회
|
|
changes = self.db.execute(text("""
|
|
SELECT
|
|
category, change_type, revision_action, action_status,
|
|
COUNT(*) as count,
|
|
SUM(CASE WHEN purchase_status = 'purchased' THEN 1 ELSE 0 END) as purchased_count,
|
|
SUM(CASE WHEN purchase_status = 'not_purchased' THEN 1 ELSE 0 END) as unpurchased_count
|
|
FROM revision_material_changes
|
|
WHERE session_id = :session_id
|
|
GROUP BY category, change_type, revision_action, action_status
|
|
ORDER BY category, change_type
|
|
"""), {"session_id": session_id}).fetchall()
|
|
|
|
return {
|
|
"session_info": dict(session_info._mapping),
|
|
"changes_summary": [dict(change._mapping) for change in changes],
|
|
"progress_percentage": (
|
|
(session_info.processed_materials / session_info.total_materials * 100)
|
|
if session_info.total_materials > 0 else 0
|
|
)
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"리비전 세션 상태 조회 실패: {e}")
|
|
raise
|
|
|
|
def update_session_progress(
|
|
self,
|
|
session_id: int,
|
|
total_materials: int = None,
|
|
processed_materials: int = None,
|
|
**counts
|
|
) -> bool:
|
|
"""리비전 세션 진행 상황 업데이트"""
|
|
|
|
try:
|
|
update_fields = []
|
|
update_values = {"session_id": session_id}
|
|
|
|
if total_materials is not None:
|
|
update_fields.append("total_materials = :total_materials")
|
|
update_values["total_materials"] = total_materials
|
|
|
|
if processed_materials is not None:
|
|
update_fields.append("processed_materials = :processed_materials")
|
|
update_values["processed_materials"] = processed_materials
|
|
|
|
# 카운트 필드들 업데이트
|
|
count_fields = [
|
|
"added_count", "removed_count", "changed_count", "unchanged_count",
|
|
"purchase_cancel_count", "inventory_transfer_count", "additional_purchase_count"
|
|
]
|
|
|
|
for field in count_fields:
|
|
if field in counts:
|
|
update_fields.append(f"{field} = :{field}")
|
|
update_values[field] = counts[field]
|
|
|
|
if not update_fields:
|
|
return True # 업데이트할 내용이 없음
|
|
|
|
query = f"""
|
|
UPDATE revision_sessions
|
|
SET {', '.join(update_fields)}
|
|
WHERE id = :session_id
|
|
"""
|
|
|
|
self.db.execute(text(query), update_values)
|
|
self.db.commit()
|
|
|
|
logger.info(f"리비전 세션 진행 상황 업데이트: {session_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"리비전 세션 진행 상황 업데이트 실패: {e}")
|
|
raise
|
|
|
|
def complete_session(self, session_id: int, username: str) -> Dict[str, Any]:
|
|
"""리비전 세션 완료 처리"""
|
|
|
|
try:
|
|
# 세션 상태를 완료로 변경
|
|
self.db.execute(text("""
|
|
UPDATE revision_sessions
|
|
SET status = 'completed', completed_at = CURRENT_TIMESTAMP
|
|
WHERE id = :session_id AND status = 'processing'
|
|
"""), {"session_id": session_id})
|
|
|
|
# 완료 로그 기록
|
|
self.db.execute(text("""
|
|
INSERT INTO revision_action_logs (
|
|
session_id, action_type, action_description,
|
|
executed_by, result, result_message
|
|
) VALUES (
|
|
:session_id, 'session_complete', '리비전 세션 완료',
|
|
:username, 'success', '모든 리비전 처리 완료'
|
|
)
|
|
"""), {
|
|
"session_id": session_id,
|
|
"username": username
|
|
})
|
|
|
|
self.db.commit()
|
|
|
|
# 최종 상태 조회
|
|
final_status = self.get_session_status(session_id)
|
|
|
|
logger.info(f"리비전 세션 완료: {session_id}")
|
|
|
|
return {
|
|
"status": "completed",
|
|
"session_id": session_id,
|
|
"final_status": final_status
|
|
}
|
|
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"리비전 세션 완료 처리 실패: {e}")
|
|
raise
|
|
|
|
def cancel_session(self, session_id: int, username: str, reason: str = None) -> bool:
|
|
"""리비전 세션 취소"""
|
|
|
|
try:
|
|
# 세션 상태를 취소로 변경
|
|
self.db.execute(text("""
|
|
UPDATE revision_sessions
|
|
SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP
|
|
WHERE id = :session_id AND status = 'processing'
|
|
"""), {"session_id": session_id})
|
|
|
|
# 취소 로그 기록
|
|
self.db.execute(text("""
|
|
INSERT INTO revision_action_logs (
|
|
session_id, action_type, action_description,
|
|
executed_by, result, result_message
|
|
) VALUES (
|
|
:session_id, 'session_cancel', '리비전 세션 취소',
|
|
:username, 'cancelled', :reason
|
|
)
|
|
"""), {
|
|
"session_id": session_id,
|
|
"username": username,
|
|
"reason": reason or "사용자 요청에 의한 취소"
|
|
})
|
|
|
|
self.db.commit()
|
|
|
|
logger.info(f"리비전 세션 취소: {session_id}, 사유: {reason}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.db.rollback()
|
|
logger.error(f"리비전 세션 취소 실패: {e}")
|
|
raise
|
|
|
|
def get_job_revision_history(self, job_no: str) -> List[Dict[str, Any]]:
|
|
"""Job의 리비전 히스토리 조회"""
|
|
|
|
try:
|
|
sessions = self.db.execute(text("""
|
|
SELECT
|
|
rs.id, rs.current_revision, rs.previous_revision,
|
|
rs.status, rs.created_by, rs.created_at, rs.completed_at,
|
|
rs.added_count, rs.removed_count, rs.changed_count,
|
|
cf.filename as current_filename,
|
|
pf.filename as previous_filename
|
|
FROM revision_sessions rs
|
|
LEFT JOIN files cf ON rs.current_file_id = cf.id
|
|
LEFT JOIN files pf ON rs.previous_file_id = pf.id
|
|
WHERE rs.job_no = :job_no
|
|
ORDER BY rs.created_at DESC
|
|
"""), {"job_no": job_no}).fetchall()
|
|
|
|
return [dict(session._mapping) for session in sessions]
|
|
|
|
except Exception as e:
|
|
logger.error(f"리비전 히스토리 조회 실패: {e}")
|
|
raise
|