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:
289
backend/app/services/revision_session_service.py
Normal file
289
backend/app/services/revision_session_service.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
리비전 세션 관리 서비스
|
||||
- 리비전 세션 생성, 관리, 완료 처리
|
||||
- 자재 변경 사항 추적 및 처리
|
||||
"""
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user