feat: 자재 관리 페이지 대규모 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- 파이프 수량 계산 로직 수정 (단관 개수가 아닌 실제 길이 기반 계산) - UI 전면 개편 (DevonThink 스타일의 간결하고 세련된 디자인) - 자재별 그룹핑 로직 개선: * 플랜지: 동일 사양별 그룹핑, WN 스케줄 표시, ORIFICE 풀네임 표시 * 피팅: 상세 타입 표시 (니플 길이, 엘보 각도/연결, 티 타입, 리듀서 타입 등) * 밸브: 동일 사양별 그룹핑, 타입/연결방식/압력 표시 * 볼트: 크기/재질/길이별 그룹핑 (8SET → 개별 집계) * 가스켓: 동일 사양별 그룹핑, 재질/상세내역/두께 분리 표시 * UNKNOWN: 원본 설명 전체 표시, 동일 항목 그룹핑 - 전체 카테고리 버튼 제거 (표시 복잡도 감소) - 카테고리별 동적 컬럼 헤더 및 레이아웃 적용
This commit is contained in:
427
backend/app/routers/dashboard.py
Normal file
427
backend/app/routers/dashboard.py
Normal file
@@ -0,0 +1,427 @@
|
||||
"""
|
||||
대시보드 API
|
||||
사용자별 맞춤형 대시보드 데이터 제공
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text, func
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ..database import get_db
|
||||
from ..auth.middleware import get_current_user
|
||||
from ..services.activity_logger import ActivityLogger
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_dashboard_stats(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
사용자별 맞춤형 대시보드 통계 데이터 조회
|
||||
|
||||
Returns:
|
||||
dict: 사용자 역할에 맞는 통계 데이터
|
||||
"""
|
||||
try:
|
||||
username = current_user.get('username')
|
||||
user_role = current_user.get('role', 'user')
|
||||
|
||||
# 역할별 맞춤 통계 생성
|
||||
if user_role == 'admin':
|
||||
stats = await get_admin_stats(db)
|
||||
elif user_role == 'manager':
|
||||
stats = await get_manager_stats(db, username)
|
||||
elif user_role == 'designer':
|
||||
stats = await get_designer_stats(db, username)
|
||||
elif user_role == 'purchaser':
|
||||
stats = await get_purchaser_stats(db, username)
|
||||
else:
|
||||
stats = await get_user_stats(db, username)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_role": user_role,
|
||||
"stats": stats
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Dashboard stats error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"대시보드 통계 조회 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/activities")
|
||||
async def get_user_activities(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
사용자 활동 이력 조회
|
||||
|
||||
Args:
|
||||
limit: 조회할 활동 수 (1-50)
|
||||
|
||||
Returns:
|
||||
dict: 사용자 활동 이력
|
||||
"""
|
||||
try:
|
||||
username = current_user.get('username')
|
||||
|
||||
activity_logger = ActivityLogger(db)
|
||||
activities = activity_logger.get_user_activities(
|
||||
username=username,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"activities": activities,
|
||||
"total": len(activities)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"User activities error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"활동 이력 조회 실패: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/recent-activities")
|
||||
async def get_recent_activities(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
days: int = Query(7, ge=1, le=30),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
최근 전체 활동 조회 (관리자/매니저용)
|
||||
|
||||
Args:
|
||||
days: 조회 기간 (일)
|
||||
limit: 조회할 활동 수
|
||||
|
||||
Returns:
|
||||
dict: 최근 활동 이력
|
||||
"""
|
||||
try:
|
||||
user_role = current_user.get('role', 'user')
|
||||
|
||||
# 관리자와 매니저만 전체 활동 조회 가능
|
||||
if user_role not in ['admin', 'manager']:
|
||||
raise HTTPException(status_code=403, detail="권한이 없습니다")
|
||||
|
||||
activity_logger = ActivityLogger(db)
|
||||
activities = activity_logger.get_recent_activities(
|
||||
days=days,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"activities": activities,
|
||||
"period_days": days,
|
||||
"total": len(activities)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Recent activities error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"최근 활동 조회 실패: {str(e)}")
|
||||
|
||||
|
||||
async def get_admin_stats(db: Session) -> Dict[str, Any]:
|
||||
"""관리자용 통계"""
|
||||
try:
|
||||
# 전체 프로젝트 수
|
||||
total_projects_query = text("SELECT COUNT(*) FROM jobs WHERE status != 'deleted'")
|
||||
total_projects = db.execute(total_projects_query).scalar()
|
||||
|
||||
# 활성 사용자 수 (최근 30일 로그인)
|
||||
active_users_query = text("""
|
||||
SELECT COUNT(DISTINCT username)
|
||||
FROM user_activity_logs
|
||||
WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '30 days'
|
||||
""")
|
||||
active_users = db.execute(active_users_query).scalar() or 0
|
||||
|
||||
# 오늘 업로드된 파일 수
|
||||
today_uploads_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM files
|
||||
WHERE DATE(upload_date) = CURRENT_DATE
|
||||
""")
|
||||
today_uploads = db.execute(today_uploads_query).scalar() or 0
|
||||
|
||||
# 전체 자재 수
|
||||
total_materials_query = text("SELECT COUNT(*) FROM materials")
|
||||
total_materials = db.execute(total_materials_query).scalar() or 0
|
||||
|
||||
return {
|
||||
"title": "시스템 관리자",
|
||||
"subtitle": "전체 시스템을 관리하고 모니터링합니다",
|
||||
"metrics": [
|
||||
{"label": "전체 프로젝트 수", "value": total_projects, "icon": "📋", "color": "#667eea"},
|
||||
{"label": "활성 사용자 수", "value": active_users, "icon": "👥", "color": "#48bb78"},
|
||||
{"label": "시스템 상태", "value": "정상", "icon": "🟢", "color": "#38b2ac"},
|
||||
{"label": "오늘 업로드", "value": today_uploads, "icon": "📤", "color": "#ed8936"}
|
||||
]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Admin stats error: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
async def get_manager_stats(db: Session, username: str) -> Dict[str, Any]:
|
||||
"""매니저용 통계"""
|
||||
try:
|
||||
# 담당 프로젝트 수 (향후 assigned_to 필드 활용)
|
||||
assigned_projects_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM jobs
|
||||
WHERE (assigned_to = :username OR created_by = :username)
|
||||
AND status != 'deleted'
|
||||
""")
|
||||
assigned_projects = db.execute(assigned_projects_query, {"username": username}).scalar() or 0
|
||||
|
||||
# 이번 주 완료된 작업 (활동 로그 기반)
|
||||
week_completed_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM user_activity_logs
|
||||
WHERE activity_type IN ('PROJECT_CREATE', 'PURCHASE_CONFIRM')
|
||||
AND created_at >= CURRENT_TIMESTAMP - INTERVAL '7 days'
|
||||
""")
|
||||
week_completed = db.execute(week_completed_query).scalar() or 0
|
||||
|
||||
# 승인 대기 (구매 확정 대기 등)
|
||||
pending_approvals_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM material_purchase_tracking
|
||||
WHERE purchase_status = 'PENDING'
|
||||
OR purchase_status = 'REQUESTED'
|
||||
""")
|
||||
pending_approvals = db.execute(pending_approvals_query).scalar() or 0
|
||||
|
||||
return {
|
||||
"title": "프로젝트 매니저",
|
||||
"subtitle": "팀 프로젝트를 관리하고 진행상황을 모니터링합니다",
|
||||
"metrics": [
|
||||
{"label": "담당 프로젝트", "value": assigned_projects, "icon": "📋", "color": "#667eea"},
|
||||
{"label": "팀 진행률", "value": "87%", "icon": "📈", "color": "#48bb78"},
|
||||
{"label": "승인 대기", "value": pending_approvals, "icon": "⏳", "color": "#ed8936"},
|
||||
{"label": "이번 주 완료", "value": week_completed, "icon": "✅", "color": "#38b2ac"}
|
||||
]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Manager stats error: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
async def get_designer_stats(db: Session, username: str) -> Dict[str, Any]:
|
||||
"""설계자용 통계"""
|
||||
try:
|
||||
# 내가 업로드한 BOM 파일 수
|
||||
my_files_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM files
|
||||
WHERE uploaded_by = :username
|
||||
AND is_active = true
|
||||
""")
|
||||
my_files = db.execute(my_files_query, {"username": username}).scalar() or 0
|
||||
|
||||
# 분류된 자재 수
|
||||
classified_materials_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM materials m
|
||||
JOIN files f ON m.file_id = f.id
|
||||
WHERE f.uploaded_by = :username
|
||||
AND m.classified_category IS NOT NULL
|
||||
""")
|
||||
classified_materials = db.execute(classified_materials_query, {"username": username}).scalar() or 0
|
||||
|
||||
# 검증 대기 자재 수
|
||||
pending_verification_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM materials m
|
||||
JOIN files f ON m.file_id = f.id
|
||||
WHERE f.uploaded_by = :username
|
||||
AND m.is_verified = false
|
||||
""")
|
||||
pending_verification = db.execute(pending_verification_query, {"username": username}).scalar() or 0
|
||||
|
||||
# 이번 주 업로드 수
|
||||
week_uploads_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM files
|
||||
WHERE uploaded_by = :username
|
||||
AND upload_date >= CURRENT_TIMESTAMP - INTERVAL '7 days'
|
||||
""")
|
||||
week_uploads = db.execute(week_uploads_query, {"username": username}).scalar() or 0
|
||||
|
||||
# 분류 완료율 계산
|
||||
total_materials_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM materials m
|
||||
JOIN files f ON m.file_id = f.id
|
||||
WHERE f.uploaded_by = :username
|
||||
""")
|
||||
total_materials = db.execute(total_materials_query, {"username": username}).scalar() or 1
|
||||
|
||||
classification_rate = f"{(classified_materials / total_materials * 100):.0f}%" if total_materials > 0 else "0%"
|
||||
|
||||
return {
|
||||
"title": "설계 담당자",
|
||||
"subtitle": "BOM 파일을 관리하고 자재를 분류합니다",
|
||||
"metrics": [
|
||||
{"label": "내 BOM 파일", "value": my_files, "icon": "📄", "color": "#667eea"},
|
||||
{"label": "분류 완료율", "value": classification_rate, "icon": "🎯", "color": "#48bb78"},
|
||||
{"label": "검증 대기", "value": pending_verification, "icon": "⏳", "color": "#ed8936"},
|
||||
{"label": "이번 주 업로드", "value": week_uploads, "icon": "📤", "color": "#9f7aea"}
|
||||
]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Designer stats error: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
async def get_purchaser_stats(db: Session, username: str) -> Dict[str, Any]:
|
||||
"""구매자용 통계"""
|
||||
try:
|
||||
# 구매 요청 수
|
||||
purchase_requests_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM material_purchase_tracking
|
||||
WHERE purchase_status IN ('PENDING', 'REQUESTED')
|
||||
""")
|
||||
purchase_requests = db.execute(purchase_requests_query).scalar() or 0
|
||||
|
||||
# 발주 완료 수
|
||||
orders_completed_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM material_purchase_tracking
|
||||
WHERE purchase_status = 'CONFIRMED'
|
||||
AND confirmed_by = :username
|
||||
""")
|
||||
orders_completed = db.execute(orders_completed_query, {"username": username}).scalar() or 0
|
||||
|
||||
# 입고 대기 수
|
||||
receiving_pending_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM material_purchase_tracking
|
||||
WHERE purchase_status = 'ORDERED'
|
||||
""")
|
||||
receiving_pending = db.execute(receiving_pending_query).scalar() or 0
|
||||
|
||||
# 이번 달 구매 금액 (임시 데이터)
|
||||
monthly_amount = "₩2.3M" # 실제로는 계산 필요
|
||||
|
||||
return {
|
||||
"title": "구매 담당자",
|
||||
"subtitle": "구매 요청을 처리하고 발주를 관리합니다",
|
||||
"metrics": [
|
||||
{"label": "구매 요청", "value": purchase_requests, "icon": "🛒", "color": "#667eea"},
|
||||
{"label": "발주 완료", "value": orders_completed, "icon": "✅", "color": "#48bb78"},
|
||||
{"label": "입고 대기", "value": receiving_pending, "icon": "📦", "color": "#ed8936"},
|
||||
{"label": "이번 달 금액", "value": monthly_amount, "icon": "💰", "color": "#9f7aea"}
|
||||
]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Purchaser stats error: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
async def get_user_stats(db: Session, username: str) -> Dict[str, Any]:
|
||||
"""일반 사용자용 통계"""
|
||||
try:
|
||||
# 내 활동 수 (최근 7일)
|
||||
my_activities_query = text("""
|
||||
SELECT COUNT(*)
|
||||
FROM user_activity_logs
|
||||
WHERE username = :username
|
||||
AND created_at >= CURRENT_TIMESTAMP - INTERVAL '7 days'
|
||||
""")
|
||||
my_activities = db.execute(my_activities_query, {"username": username}).scalar() or 0
|
||||
|
||||
# 접근 가능한 프로젝트 수 (임시)
|
||||
accessible_projects = 5
|
||||
|
||||
return {
|
||||
"title": "일반 사용자",
|
||||
"subtitle": "할당된 업무를 수행하고 프로젝트에 참여합니다",
|
||||
"metrics": [
|
||||
{"label": "내 업무", "value": 6, "icon": "📋", "color": "#667eea"},
|
||||
{"label": "완료율", "value": "75%", "icon": "📈", "color": "#48bb78"},
|
||||
{"label": "대기 중", "value": 2, "icon": "⏳", "color": "#ed8936"},
|
||||
{"label": "이번 주 활동", "value": my_activities, "icon": "🎯", "color": "#9f7aea"}
|
||||
]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"User stats error: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
@router.get("/quick-actions")
|
||||
async def get_quick_actions(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
사용자 역할별 빠른 작업 메뉴 조회
|
||||
|
||||
Returns:
|
||||
dict: 역할별 빠른 작업 목록
|
||||
"""
|
||||
try:
|
||||
user_role = current_user.get('role', 'user')
|
||||
|
||||
quick_actions = {
|
||||
"admin": [
|
||||
{"title": "사용자 관리", "icon": "👤", "path": "/admin/users", "color": "#667eea"},
|
||||
{"title": "시스템 설정", "icon": "⚙️", "path": "/admin/settings", "color": "#48bb78"},
|
||||
{"title": "백업 관리", "icon": "💾", "path": "/admin/backup", "color": "#ed8936"},
|
||||
{"title": "활동 로그", "icon": "📊", "path": "/admin/logs", "color": "#9f7aea"}
|
||||
],
|
||||
"manager": [
|
||||
{"title": "프로젝트 생성", "icon": "➕", "path": "/projects/new", "color": "#667eea"},
|
||||
{"title": "팀 관리", "icon": "👥", "path": "/team", "color": "#48bb78"},
|
||||
{"title": "진행 상황", "icon": "📊", "path": "/progress", "color": "#38b2ac"},
|
||||
{"title": "승인 처리", "icon": "✅", "path": "/approvals", "color": "#ed8936"}
|
||||
],
|
||||
"designer": [
|
||||
{"title": "BOM 업로드", "icon": "📤", "path": "/upload", "color": "#667eea"},
|
||||
{"title": "자재 분류", "icon": "🔧", "path": "/materials", "color": "#48bb78"},
|
||||
{"title": "리비전 관리", "icon": "🔄", "path": "/revisions", "color": "#38b2ac"},
|
||||
{"title": "분류 검증", "icon": "✅", "path": "/verify", "color": "#ed8936"}
|
||||
],
|
||||
"purchaser": [
|
||||
{"title": "구매 확정", "icon": "🛒", "path": "/purchase", "color": "#667eea"},
|
||||
{"title": "발주 관리", "icon": "📋", "path": "/orders", "color": "#48bb78"},
|
||||
{"title": "공급업체", "icon": "🏢", "path": "/suppliers", "color": "#38b2ac"},
|
||||
{"title": "입고 처리", "icon": "📦", "path": "/receiving", "color": "#ed8936"}
|
||||
],
|
||||
"user": [
|
||||
{"title": "내 업무", "icon": "📋", "path": "/my-tasks", "color": "#667eea"},
|
||||
{"title": "프로젝트 보기", "icon": "👁️", "path": "/projects", "color": "#48bb78"},
|
||||
{"title": "리포트 다운로드", "icon": "📊", "path": "/reports", "color": "#38b2ac"},
|
||||
{"title": "도움말", "icon": "❓", "path": "/help", "color": "#9f7aea"}
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_role": user_role,
|
||||
"quick_actions": quick_actions.get(user_role, quick_actions["user"])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Quick actions error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"빠른 작업 조회 실패: {str(e)}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,399 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from typing import List, Optional
|
||||
import os
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import pandas as pd
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
UPLOAD_DIR = Path("uploads")
|
||||
UPLOAD_DIR.mkdir(exist_ok=True)
|
||||
ALLOWED_EXTENSIONS = {".xlsx", ".xls", ".csv"}
|
||||
|
||||
@router.get("/")
|
||||
async def get_files_info():
|
||||
return {
|
||||
"message": "파일 관리 API",
|
||||
"allowed_extensions": list(ALLOWED_EXTENSIONS),
|
||||
"upload_directory": str(UPLOAD_DIR)
|
||||
}
|
||||
|
||||
@router.get("/test")
|
||||
async def test_endpoint():
|
||||
return {"status": "파일 API가 정상 작동합니다!"}
|
||||
|
||||
@router.post("/add-missing-columns")
|
||||
async def add_missing_columns(db: Session = Depends(get_db)):
|
||||
"""누락된 컬럼들 추가"""
|
||||
try:
|
||||
db.execute(text("ALTER TABLE files ADD COLUMN IF NOT EXISTS parsed_count INTEGER DEFAULT 0"))
|
||||
db.execute(text("ALTER TABLE materials ADD COLUMN IF NOT EXISTS row_number INTEGER"))
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "누락된 컬럼들이 추가되었습니다",
|
||||
"added_columns": ["files.parsed_count", "materials.row_number"]
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {"success": False, "error": f"컬럼 추가 실패: {str(e)}"}
|
||||
|
||||
def validate_file_extension(filename: str) -> bool:
|
||||
return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def generate_unique_filename(original_filename: str) -> str:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
stem = Path(original_filename).stem
|
||||
suffix = Path(original_filename).suffix
|
||||
return f"{stem}_{timestamp}_{unique_id}{suffix}"
|
||||
|
||||
def parse_dataframe(df):
|
||||
df = df.dropna(how='all')
|
||||
df.columns = df.columns.str.strip().str.lower()
|
||||
|
||||
column_mapping = {
|
||||
'description': ['description', 'item', 'material', '품명', '자재명'],
|
||||
'quantity': ['qty', 'quantity', 'ea', '수량'],
|
||||
'main_size': ['main_nom', 'nominal_diameter', 'nd', '주배관'],
|
||||
'red_size': ['red_nom', 'reduced_diameter', '축소배관'],
|
||||
'length': ['length', 'len', '길이'],
|
||||
'weight': ['weight', 'wt', '중량'],
|
||||
'dwg_name': ['dwg_name', 'drawing', '도면명'],
|
||||
'line_num': ['line_num', 'line_number', '라인번호']
|
||||
}
|
||||
|
||||
mapped_columns = {}
|
||||
for standard_col, possible_names in column_mapping.items():
|
||||
for possible_name in possible_names:
|
||||
if possible_name in df.columns:
|
||||
mapped_columns[standard_col] = possible_name
|
||||
break
|
||||
|
||||
materials = []
|
||||
for index, row in df.iterrows():
|
||||
description = str(row.get(mapped_columns.get('description', ''), ''))
|
||||
quantity_raw = row.get(mapped_columns.get('quantity', ''), 0)
|
||||
|
||||
try:
|
||||
quantity = float(quantity_raw) if pd.notna(quantity_raw) else 0
|
||||
except:
|
||||
quantity = 0
|
||||
|
||||
material_grade = ""
|
||||
if "ASTM" in description.upper():
|
||||
astm_match = re.search(r'ASTM\s+([A-Z0-9\s]+)', description.upper())
|
||||
if astm_match:
|
||||
material_grade = astm_match.group(0).strip()
|
||||
|
||||
main_size = str(row.get(mapped_columns.get('main_size', ''), ''))
|
||||
red_size = str(row.get(mapped_columns.get('red_size', ''), ''))
|
||||
|
||||
if main_size != 'nan' and red_size != 'nan' and red_size != '':
|
||||
size_spec = f"{main_size} x {red_size}"
|
||||
elif main_size != 'nan' and main_size != '':
|
||||
size_spec = main_size
|
||||
else:
|
||||
size_spec = ""
|
||||
|
||||
if description and description not in ['nan', 'None', '']:
|
||||
materials.append({
|
||||
'original_description': description,
|
||||
'quantity': quantity,
|
||||
'unit': "EA",
|
||||
'size_spec': size_spec,
|
||||
'material_grade': material_grade,
|
||||
'line_number': index + 1,
|
||||
'row_number': index + 1
|
||||
})
|
||||
|
||||
return materials
|
||||
|
||||
def parse_file_data(file_path):
|
||||
file_extension = Path(file_path).suffix.lower()
|
||||
|
||||
try:
|
||||
if file_extension == ".csv":
|
||||
df = pd.read_csv(file_path, encoding='utf-8')
|
||||
elif file_extension in [".xlsx", ".xls"]:
|
||||
df = pd.read_excel(file_path, sheet_name=0)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="지원하지 않는 파일 형식")
|
||||
|
||||
return parse_dataframe(df)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"파일 파싱 실패: {str(e)}")
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload_file(
|
||||
file: UploadFile = File(...),
|
||||
job_no: str = Form(...),
|
||||
revision: str = Form("Rev.0"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
if not validate_file_extension(file.filename):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"지원하지 않는 파일 형식입니다. 허용된 확장자: {', '.join(ALLOWED_EXTENSIONS)}"
|
||||
)
|
||||
|
||||
if file.size and file.size > 10 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="파일 크기는 10MB를 초과할 수 없습니다")
|
||||
|
||||
unique_filename = generate_unique_filename(file.filename)
|
||||
file_path = UPLOAD_DIR / unique_filename
|
||||
|
||||
try:
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}")
|
||||
|
||||
try:
|
||||
materials_data = parse_file_data(str(file_path))
|
||||
parsed_count = len(materials_data)
|
||||
|
||||
# 파일 정보 저장
|
||||
file_insert_query = text("""
|
||||
INSERT INTO files (filename, original_filename, file_path, job_no, revision, description, file_size, parsed_count, is_active)
|
||||
VALUES (:filename, :original_filename, :file_path, :job_no, :revision, :description, :file_size, :parsed_count, :is_active)
|
||||
RETURNING id
|
||||
""")
|
||||
|
||||
file_result = db.execute(file_insert_query, {
|
||||
"filename": unique_filename,
|
||||
"original_filename": file.filename,
|
||||
"file_path": str(file_path),
|
||||
"job_no": job_no,
|
||||
"revision": revision,
|
||||
"description": f"BOM 파일 - {parsed_count}개 자재",
|
||||
"file_size": file.size,
|
||||
"parsed_count": parsed_count,
|
||||
"is_active": True
|
||||
})
|
||||
|
||||
file_id = file_result.fetchone()[0]
|
||||
|
||||
# 자재 데이터 저장
|
||||
materials_inserted = 0
|
||||
for material_data in materials_data:
|
||||
material_insert_query = text("""
|
||||
INSERT INTO materials (
|
||||
file_id, original_description, quantity, unit, size_spec,
|
||||
material_grade, line_number, row_number, classified_category,
|
||||
classification_confidence, is_verified, created_at
|
||||
)
|
||||
VALUES (
|
||||
:file_id, :original_description, :quantity, :unit, :size_spec,
|
||||
:material_grade, :line_number, :row_number, :classified_category,
|
||||
:classification_confidence, :is_verified, :created_at
|
||||
)
|
||||
""")
|
||||
|
||||
db.execute(material_insert_query, {
|
||||
"file_id": file_id,
|
||||
"original_description": material_data["original_description"],
|
||||
"quantity": material_data["quantity"],
|
||||
"unit": material_data["unit"],
|
||||
"size_spec": material_data["size_spec"],
|
||||
"material_grade": material_data["material_grade"],
|
||||
"line_number": material_data["line_number"],
|
||||
"row_number": material_data["row_number"],
|
||||
"classified_category": None,
|
||||
"classification_confidence": None,
|
||||
"is_verified": False,
|
||||
"created_at": datetime.now()
|
||||
})
|
||||
materials_inserted += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"완전한 DB 저장 성공! {materials_inserted}개 자재 저장됨",
|
||||
"original_filename": file.filename,
|
||||
"file_id": file_id,
|
||||
"parsed_materials_count": parsed_count,
|
||||
"saved_materials_count": materials_inserted,
|
||||
"sample_materials": materials_data[:3] if materials_data else []
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
raise HTTPException(status_code=500, detail=f"파일 처리 실패: {str(e)}")
|
||||
@router.get("/materials")
|
||||
async def get_materials(
|
||||
job_no: Optional[str] = None,
|
||||
file_id: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""저장된 자재 목록 조회"""
|
||||
try:
|
||||
query = """
|
||||
SELECT m.id, m.file_id, m.original_description, m.quantity, m.unit,
|
||||
m.size_spec, m.material_grade, m.line_number, m.row_number,
|
||||
m.created_at,
|
||||
f.original_filename, f.job_no,
|
||||
j.job_no, j.job_name
|
||||
FROM materials m
|
||||
LEFT JOIN files f ON m.file_id = f.id
|
||||
LEFT JOIN jobs j ON f.job_no = j.job_no
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = {}
|
||||
|
||||
if job_no:
|
||||
query += " AND f.job_no = :job_no"
|
||||
params["job_no"] = job_no
|
||||
|
||||
if file_id:
|
||||
query += " AND m.file_id = :file_id"
|
||||
params["file_id"] = file_id
|
||||
|
||||
query += " ORDER BY m.line_number ASC LIMIT :limit OFFSET :skip"
|
||||
params["limit"] = limit
|
||||
params["skip"] = skip
|
||||
|
||||
result = db.execute(text(query), params)
|
||||
materials = result.fetchall()
|
||||
|
||||
# 전체 개수 조회
|
||||
count_query = """
|
||||
SELECT COUNT(*) as total
|
||||
FROM materials m
|
||||
LEFT JOIN files f ON m.file_id = f.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
count_params = {}
|
||||
|
||||
if job_no:
|
||||
count_query += " AND f.job_no = :job_no"
|
||||
count_params["job_no"] = job_no
|
||||
|
||||
if file_id:
|
||||
count_query += " AND m.file_id = :file_id"
|
||||
count_params["file_id"] = file_id
|
||||
|
||||
count_result = db.execute(text(count_query), count_params)
|
||||
total_count = count_result.fetchone()[0]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"total_count": total_count,
|
||||
"returned_count": len(materials),
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"materials": [
|
||||
{
|
||||
"id": m.id,
|
||||
"file_id": m.file_id,
|
||||
"filename": m.original_filename,
|
||||
"job_no": m.job_no,
|
||||
"project_code": m.official_project_code,
|
||||
"project_name": m.project_name,
|
||||
"original_description": m.original_description,
|
||||
"quantity": float(m.quantity) if m.quantity else 0,
|
||||
"unit": m.unit,
|
||||
"size_spec": m.size_spec,
|
||||
"material_grade": m.material_grade,
|
||||
"line_number": m.line_number,
|
||||
"row_number": m.row_number,
|
||||
"created_at": m.created_at
|
||||
}
|
||||
for m in materials
|
||||
]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"자재 조회 실패: {str(e)}")
|
||||
|
||||
@router.get("/materials/summary")
|
||||
async def get_materials_summary(
|
||||
job_no: Optional[str] = None,
|
||||
file_id: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""자재 요약 통계"""
|
||||
try:
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) as total_items,
|
||||
COUNT(DISTINCT m.original_description) as unique_descriptions,
|
||||
COUNT(DISTINCT m.size_spec) as unique_sizes,
|
||||
COUNT(DISTINCT m.material_grade) as unique_materials,
|
||||
SUM(m.quantity) as total_quantity,
|
||||
AVG(m.quantity) as avg_quantity,
|
||||
MIN(m.created_at) as earliest_upload,
|
||||
MAX(m.created_at) as latest_upload
|
||||
FROM materials m
|
||||
LEFT JOIN files f ON m.file_id = f.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = {}
|
||||
|
||||
if job_no:
|
||||
query += " AND f.job_no = :job_no"
|
||||
params["job_no"] = job_no
|
||||
|
||||
if file_id:
|
||||
query += " AND m.file_id = :file_id"
|
||||
params["file_id"] = file_id
|
||||
|
||||
result = db.execute(text(query), params)
|
||||
summary = result.fetchone()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"summary": {
|
||||
"total_items": summary.total_items,
|
||||
"unique_descriptions": summary.unique_descriptions,
|
||||
"unique_sizes": summary.unique_sizes,
|
||||
"unique_materials": summary.unique_materials,
|
||||
"total_quantity": float(summary.total_quantity) if summary.total_quantity else 0,
|
||||
"avg_quantity": round(float(summary.avg_quantity), 2) if summary.avg_quantity else 0,
|
||||
"earliest_upload": summary.earliest_upload,
|
||||
"latest_upload": summary.latest_upload
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"요약 조회 실패: {str(e)}")
|
||||
# Job 검증 함수 (파일 끝에 추가할 예정)
|
||||
async def validate_job_exists(job_no: str, db: Session):
|
||||
"""Job 존재 여부 및 활성 상태 확인"""
|
||||
try:
|
||||
query = text("SELECT job_no, job_name, status FROM jobs WHERE job_no = :job_no AND is_active = true")
|
||||
job = db.execute(query, {"job_no": job_no}).fetchone()
|
||||
|
||||
if not job:
|
||||
return {"valid": False, "error": f"Job No. '{job_no}'를 찾을 수 없습니다"}
|
||||
|
||||
if job.status == '완료':
|
||||
return {"valid": False, "error": f"완료된 Job '{job.job_name}'에는 파일을 업로드할 수 없습니다"}
|
||||
|
||||
return {
|
||||
"valid": True,
|
||||
"job": {
|
||||
"job_no": job.job_no,
|
||||
"job_name": job.job_name,
|
||||
"status": job.status
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"valid": False, "error": f"Job 검증 실패: {str(e)}"}
|
||||
@@ -157,6 +157,26 @@ async def confirm_material_purchase(
|
||||
]
|
||||
"""
|
||||
try:
|
||||
# 입력 데이터 검증
|
||||
if not job_no or not revision:
|
||||
raise HTTPException(status_code=400, detail="Job 번호와 리비전은 필수입니다")
|
||||
|
||||
if not confirmations:
|
||||
raise HTTPException(status_code=400, detail="확정할 자재가 없습니다")
|
||||
|
||||
# 각 확정 항목 검증
|
||||
for i, confirmation in enumerate(confirmations):
|
||||
if not confirmation.get("material_hash"):
|
||||
raise HTTPException(status_code=400, detail=f"{i+1}번째 항목의 material_hash가 없습니다")
|
||||
|
||||
confirmed_qty = confirmation.get("confirmed_quantity")
|
||||
if confirmed_qty is None or confirmed_qty < 0:
|
||||
raise HTTPException(status_code=400, detail=f"{i+1}번째 항목의 확정 수량이 유효하지 않습니다")
|
||||
|
||||
unit_price = confirmation.get("unit_price", 0)
|
||||
if unit_price < 0:
|
||||
raise HTTPException(status_code=400, detail=f"{i+1}번째 항목의 단가가 유효하지 않습니다")
|
||||
|
||||
confirmed_items = []
|
||||
|
||||
for confirmation in confirmations:
|
||||
@@ -470,7 +490,7 @@ async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]:
|
||||
"""파일의 자재를 해시별로 그룹화하여 조회"""
|
||||
import hashlib
|
||||
|
||||
print(f"🚨🚨🚨 get_materials_by_hash 호출됨! file_id={file_id} 🚨🚨🚨")
|
||||
# 로그 제거
|
||||
|
||||
query = text("""
|
||||
SELECT
|
||||
@@ -492,11 +512,7 @@ async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]:
|
||||
result = db.execute(query, {"file_id": file_id})
|
||||
materials = result.fetchall()
|
||||
|
||||
print(f"🔍 쿼리 결과 개수: {len(materials)}")
|
||||
if len(materials) > 0:
|
||||
print(f"🔍 첫 번째 자료 샘플: {materials[0]}")
|
||||
else:
|
||||
print(f"❌ 자료가 없음! file_id={file_id}")
|
||||
# 로그 제거
|
||||
|
||||
# 🔄 같은 파이프들을 Python에서 올바르게 그룹핑
|
||||
materials_dict = {}
|
||||
@@ -505,38 +521,41 @@ async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]:
|
||||
hash_source = f"{mat[1] or ''}|{mat[2] or ''}|{mat[3] or ''}"
|
||||
material_hash = hashlib.md5(hash_source.encode()).hexdigest()
|
||||
|
||||
print(f"📝 개별 자재: {mat[1][:50]}... ({mat[2]}) - 수량: {mat[4]}, 길이: {mat[7]}mm")
|
||||
# 개별 자재 로그 제거 (너무 많음)
|
||||
|
||||
if material_hash in materials_dict:
|
||||
# 🔄 기존 항목에 수량 합계
|
||||
existing = materials_dict[material_hash]
|
||||
existing["quantity"] += float(mat[4]) if mat[4] else 0.0
|
||||
# 파이프가 아닌 경우만 quantity 합산 (파이프는 개별 길이가 다르므로 합산하지 않음)
|
||||
if mat[5] != 'PIPE':
|
||||
existing["quantity"] += float(mat[4]) if mat[4] else 0.0
|
||||
existing["line_number"] += f", {mat[8]}" if mat[8] else ""
|
||||
|
||||
# 파이프인 경우 길이 정보 합산
|
||||
if mat[5] == 'PIPE' and mat[7] is not None:
|
||||
if "pipe_details" in existing:
|
||||
# 총길이 합산: 기존 총길이 + (현재 수량 × 현재 길이)
|
||||
# 총길이 합산: 기존 총길이 + 현재 파이프의 실제 길이 (DB에 저장된 개별 길이)
|
||||
current_total = existing["pipe_details"]["total_length_mm"]
|
||||
current_count = existing["pipe_details"]["pipe_count"]
|
||||
|
||||
new_length = float(mat[4]) * float(mat[7]) # 수량 × 단위길이
|
||||
existing["pipe_details"]["total_length_mm"] = current_total + new_length
|
||||
existing["pipe_details"]["pipe_count"] = current_count + float(mat[4])
|
||||
# ✅ DB에서 가져온 length_mm는 이미 개별 파이프의 실제 길이이므로 수량을 곱하지 않음
|
||||
individual_length = float(mat[7]) # 개별 파이프의 실제 길이
|
||||
existing["pipe_details"]["total_length_mm"] = current_total + individual_length
|
||||
existing["pipe_details"]["pipe_count"] = current_count + 1 # 파이프 개수는 1개씩 증가
|
||||
|
||||
# 평균 단위 길이 재계산
|
||||
total_length = existing["pipe_details"]["total_length_mm"]
|
||||
total_count = existing["pipe_details"]["pipe_count"]
|
||||
existing["pipe_details"]["length_mm"] = total_length / total_count
|
||||
|
||||
print(f"🔄 파이프 합산: {mat[1]} ({mat[2]}) - 총길이: {total_length}mm, 총개수: {total_count}개, 평균: {total_length/total_count:.1f}mm")
|
||||
# 파이프 합산 로그 제거 (너무 많음)
|
||||
else:
|
||||
# 첫 파이프 정보 설정
|
||||
pipe_length = float(mat[4]) * float(mat[7])
|
||||
individual_length = float(mat[7]) # 개별 파이프의 실제 길이
|
||||
existing["pipe_details"] = {
|
||||
"length_mm": float(mat[7]),
|
||||
"total_length_mm": pipe_length,
|
||||
"pipe_count": float(mat[4])
|
||||
"length_mm": individual_length,
|
||||
"total_length_mm": individual_length, # 첫 번째 파이프이므로 개별 길이와 동일
|
||||
"pipe_count": 1 # 첫 번째 파이프이므로 1개
|
||||
}
|
||||
else:
|
||||
# 🆕 새 항목 생성
|
||||
@@ -553,27 +572,22 @@ async def get_materials_by_hash(db: Session, file_id: int) -> Dict[str, Dict]:
|
||||
|
||||
# 파이프인 경우 pipe_details 정보 추가
|
||||
if mat[5] == 'PIPE' and mat[7] is not None:
|
||||
pipe_length = float(mat[4]) * float(mat[7]) # 수량 × 단위길이
|
||||
individual_length = float(mat[7]) # 개별 파이프의 실제 길이
|
||||
material_data["pipe_details"] = {
|
||||
"length_mm": float(mat[7]), # 단위 길이
|
||||
"total_length_mm": pipe_length, # 총 길이
|
||||
"pipe_count": float(mat[4]) # 파이프 개수
|
||||
"length_mm": individual_length, # 개별 파이프 길이
|
||||
"total_length_mm": individual_length, # 첫 번째 파이프이므로 개별 길이와 동일
|
||||
"pipe_count": 1 # 첫 번째 파이프이므로 1개
|
||||
}
|
||||
print(f"🆕 파이프 신규: {mat[1]} ({mat[2]}) - 단위: {mat[7]}mm, 총길이: {pipe_length}mm")
|
||||
# 파이프는 quantity를 1로 설정 (pipe_count와 동일)
|
||||
material_data["quantity"] = 1
|
||||
|
||||
materials_dict[material_hash] = material_data
|
||||
|
||||
# 파이프 데이터가 포함되었는지 확인
|
||||
# 파이프 데이터 요약만 출력
|
||||
pipe_count = sum(1 for data in materials_dict.values() if data.get('category') == 'PIPE')
|
||||
pipe_with_details = sum(1 for data in materials_dict.values()
|
||||
if data.get('category') == 'PIPE' and 'pipe_details' in data)
|
||||
print(f"🔍 반환 결과: 총 {len(materials_dict)}개 자재, 파이프 {pipe_count}개, pipe_details 있는 파이프 {pipe_with_details}개")
|
||||
|
||||
# 첫 번째 파이프 데이터 샘플 출력
|
||||
for hash_key, data in materials_dict.items():
|
||||
if data.get('category') == 'PIPE':
|
||||
print(f"🔍 파이프 샘플: {data}")
|
||||
break
|
||||
print(f"✅ 자재 처리 완료: 총 {len(materials_dict)}개, 파이프 {pipe_count}개 (길이정보: {pipe_with_details}개)")
|
||||
|
||||
return materials_dict
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
- 리비전 비교
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..services.purchase_calculator import (
|
||||
@@ -21,6 +23,28 @@ from ..services.purchase_calculator import (
|
||||
|
||||
router = APIRouter(prefix="/purchase", tags=["purchase"])
|
||||
|
||||
# Pydantic 모델 (최적화된 구조)
|
||||
class PurchaseItemMinimal(BaseModel):
|
||||
"""구매 확정용 최소 필수 데이터"""
|
||||
item_code: str
|
||||
category: str
|
||||
specification: str
|
||||
size: str = ""
|
||||
material: str = ""
|
||||
bom_quantity: float
|
||||
calculated_qty: float
|
||||
unit: str = "EA"
|
||||
safety_factor: float = 1.0
|
||||
|
||||
class PurchaseConfirmRequest(BaseModel):
|
||||
job_no: str
|
||||
file_id: int
|
||||
bom_name: Optional[str] = None # 선택적 필드로 변경
|
||||
revision: str
|
||||
purchase_items: List[PurchaseItemMinimal] # 최적화된 구조 사용
|
||||
confirmed_at: str
|
||||
confirmed_by: str
|
||||
|
||||
@router.get("/items/calculate")
|
||||
async def calculate_purchase_items(
|
||||
job_no: str = Query(..., description="Job 번호"),
|
||||
@@ -39,7 +63,7 @@ async def calculate_purchase_items(
|
||||
file_query = text("""
|
||||
SELECT id FROM files
|
||||
WHERE job_no = :job_no AND revision = :revision AND is_active = TRUE
|
||||
ORDER BY created_at DESC
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
file_result = db.execute(file_query, {"job_no": job_no, "revision": revision}).fetchone()
|
||||
@@ -62,6 +86,139 @@ async def calculate_purchase_items(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"구매 품목 계산 실패: {str(e)}")
|
||||
|
||||
@router.post("/confirm")
|
||||
async def confirm_purchase_quantities(
|
||||
request: PurchaseConfirmRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
구매 수량 확정
|
||||
- 계산된 구매 수량을 확정 상태로 저장
|
||||
- 자재별 확정 수량 및 상태 업데이트
|
||||
- 리비전 비교를 위한 기준 데이터 생성
|
||||
"""
|
||||
try:
|
||||
# 1. 기존 확정 데이터 확인 및 업데이트 또는 삽입
|
||||
existing_query = text("""
|
||||
SELECT id FROM purchase_confirmations
|
||||
WHERE file_id = :file_id
|
||||
""")
|
||||
existing_result = db.execute(existing_query, {"file_id": request.file_id}).fetchone()
|
||||
|
||||
if existing_result:
|
||||
# 기존 데이터 업데이트
|
||||
confirmation_id = existing_result[0]
|
||||
update_query = text("""
|
||||
UPDATE purchase_confirmations
|
||||
SET job_no = :job_no,
|
||||
bom_name = :bom_name,
|
||||
revision = :revision,
|
||||
confirmed_at = :confirmed_at,
|
||||
confirmed_by = :confirmed_by,
|
||||
is_active = TRUE,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = :confirmation_id
|
||||
""")
|
||||
db.execute(update_query, {
|
||||
"confirmation_id": confirmation_id,
|
||||
"job_no": request.job_no,
|
||||
"bom_name": request.bom_name or f"{request.job_no}_{request.revision}", # 기본값 제공
|
||||
"revision": request.revision,
|
||||
"confirmed_at": request.confirmed_at,
|
||||
"confirmed_by": request.confirmed_by
|
||||
})
|
||||
|
||||
# 기존 확정 품목들 삭제
|
||||
delete_items_query = text("""
|
||||
DELETE FROM confirmed_purchase_items
|
||||
WHERE confirmation_id = :confirmation_id
|
||||
""")
|
||||
db.execute(delete_items_query, {"confirmation_id": confirmation_id})
|
||||
else:
|
||||
# 새로운 확정 데이터 삽입
|
||||
confirm_query = text("""
|
||||
INSERT INTO purchase_confirmations (
|
||||
job_no, file_id, bom_name, revision,
|
||||
confirmed_at, confirmed_by, is_active, created_at
|
||||
) VALUES (
|
||||
:job_no, :file_id, :bom_name, :revision,
|
||||
:confirmed_at, :confirmed_by, TRUE, CURRENT_TIMESTAMP
|
||||
) RETURNING id
|
||||
""")
|
||||
|
||||
confirm_result = db.execute(confirm_query, {
|
||||
"job_no": request.job_no,
|
||||
"file_id": request.file_id,
|
||||
"bom_name": request.bom_name or f"{request.job_no}_{request.revision}", # 기본값 제공
|
||||
"revision": request.revision,
|
||||
"confirmed_at": request.confirmed_at,
|
||||
"confirmed_by": request.confirmed_by
|
||||
})
|
||||
|
||||
confirmation_id = confirm_result.fetchone()[0]
|
||||
|
||||
# 3. 확정된 구매 품목들 저장
|
||||
saved_items = 0
|
||||
for item in request.purchase_items:
|
||||
item_query = text("""
|
||||
INSERT INTO confirmed_purchase_items (
|
||||
confirmation_id, item_code, category, specification,
|
||||
size, material, bom_quantity, calculated_qty,
|
||||
unit, safety_factor, created_at
|
||||
) VALUES (
|
||||
:confirmation_id, :item_code, :category, :specification,
|
||||
:size, :material, :bom_quantity, :calculated_qty,
|
||||
:unit, :safety_factor, CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
db.execute(item_query, {
|
||||
"confirmation_id": confirmation_id,
|
||||
"item_code": item.item_code or f"{item.category}-{saved_items+1}",
|
||||
"category": item.category,
|
||||
"specification": item.specification,
|
||||
"size": item.size or "",
|
||||
"material": item.material or "",
|
||||
"bom_quantity": item.bom_quantity,
|
||||
"calculated_qty": item.calculated_qty,
|
||||
"unit": item.unit,
|
||||
"safety_factor": item.safety_factor
|
||||
})
|
||||
saved_items += 1
|
||||
|
||||
# 4. 파일 상태를 확정으로 업데이트
|
||||
file_update_query = text("""
|
||||
UPDATE files
|
||||
SET purchase_confirmed = TRUE,
|
||||
confirmed_at = :confirmed_at,
|
||||
confirmed_by = :confirmed_by,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = :file_id
|
||||
""")
|
||||
|
||||
db.execute(file_update_query, {
|
||||
"file_id": request.file_id,
|
||||
"confirmed_at": request.confirmed_at,
|
||||
"confirmed_by": request.confirmed_by
|
||||
})
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "구매 수량이 성공적으로 확정되었습니다",
|
||||
"confirmation_id": confirmation_id,
|
||||
"confirmed_items": saved_items,
|
||||
"job_no": request.job_no,
|
||||
"revision": request.revision,
|
||||
"confirmed_at": request.confirmed_at,
|
||||
"confirmed_by": request.confirmed_by
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"구매 수량 확정 실패: {str(e)}")
|
||||
|
||||
@router.post("/items/save")
|
||||
async def save_purchase_items(
|
||||
job_no: str,
|
||||
|
||||
Reference in New Issue
Block a user