🧮 구매 수량 계산 로직: - PIPE: 절단 손실(3mm/절단) + 6M 단위 올림 계산 - 일반 자재: 여유율 + 최소 주문 수량 적용 - 자재별 차별화된 여유율 (VALVE 50%, BOLT 20% 등) 🛒 구매 관리 API: - /purchase/items/calculate: 실시간 구매 수량 계산 - /purchase/items/save: 구매 품목 DB 저장 - /purchase/revision-diff: 리비전간 차이 계산 - /purchase/orders/create: 구매 주문 생성 🧪 테스트 검증: - PIPE 절단 손실 계산: 25,000mm → 5본 (정확) - 여유율 적용: VALVE 2개 → 3개 (50% 예비) - 최소 주문: BOLT 24개 → 50개 (박스 단위) 📱 프론트엔드: - PurchaseConfirmationPage 라우팅 추가 - 구매확정 버튼 → 구매 페이지 이동
256 lines
8.7 KiB
Python
256 lines
8.7 KiB
Python
from fastapi import FastAPI, UploadFile, File, Form
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from sqlalchemy import text
|
|
from .database import get_db
|
|
from sqlalchemy.orm import Session
|
|
from fastapi import Depends
|
|
from typing import Optional, List, Dict
|
|
import os
|
|
import shutil
|
|
|
|
# FastAPI 앱 생성
|
|
app = FastAPI(
|
|
title="TK-MP BOM Management API",
|
|
description="자재 분류 및 프로젝트 관리 시스템",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# CORS 설정
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# 라우터들 import 및 등록
|
|
try:
|
|
from .routers import files
|
|
app.include_router(files.router, prefix="/files", tags=["files"])
|
|
except ImportError:
|
|
print("files 라우터를 찾을 수 없습니다")
|
|
|
|
try:
|
|
from .routers import jobs
|
|
app.include_router(jobs.router, prefix="/jobs", tags=["jobs"])
|
|
except ImportError:
|
|
print("jobs 라우터를 찾을 수 없습니다")
|
|
|
|
try:
|
|
from .routers import purchase
|
|
app.include_router(purchase.router, tags=["purchase"])
|
|
except ImportError:
|
|
print("purchase 라우터를 찾을 수 없습니다")
|
|
|
|
# 파일 목록 조회 API
|
|
@app.get("/files")
|
|
async def get_files(
|
|
job_no: Optional[str] = None, # project_id 대신 job_no 사용
|
|
show_history: bool = False, # 이력 표시 여부
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""파일 목록 조회 (BOM별 그룹화)"""
|
|
try:
|
|
if show_history:
|
|
# 전체 이력 표시
|
|
query = "SELECT * FROM files"
|
|
params = {}
|
|
|
|
if job_no:
|
|
query += " WHERE job_no = :job_no"
|
|
params["job_no"] = job_no
|
|
|
|
query += " ORDER BY original_filename, revision DESC"
|
|
else:
|
|
# 최신 리비전만 표시
|
|
if job_no:
|
|
query = """
|
|
SELECT f1.* FROM files f1
|
|
INNER JOIN (
|
|
SELECT original_filename, MAX(revision) as max_revision
|
|
FROM files
|
|
WHERE job_no = :job_no
|
|
GROUP BY original_filename
|
|
) f2 ON f1.original_filename = f2.original_filename
|
|
AND f1.revision = f2.max_revision
|
|
WHERE f1.job_no = :job_no
|
|
ORDER BY f1.upload_date DESC
|
|
"""
|
|
params = {"job_no": job_no}
|
|
else:
|
|
# job_no가 없으면 전체 파일 조회
|
|
query = "SELECT * FROM files ORDER BY upload_date DESC"
|
|
params = {}
|
|
|
|
result = db.execute(text(query), params)
|
|
files = result.fetchall()
|
|
|
|
return [
|
|
{
|
|
"id": f.id,
|
|
"filename": f.original_filename,
|
|
"original_filename": f.original_filename,
|
|
"name": f.original_filename,
|
|
"job_no": f.job_no, # job_no 사용
|
|
"bom_name": f.bom_name or f.original_filename, # 실제 bom_name 값 사용, 없으면 파일명
|
|
"bom_type": f.file_type or "unknown", # file_type을 BOM 종류로 사용
|
|
"status": "active" if f.is_active else "inactive", # is_active 상태
|
|
"file_size": f.file_size,
|
|
"created_at": f.upload_date,
|
|
"upload_date": f.upload_date,
|
|
"revision": f.revision or "Rev.0", # 실제 리비전 또는 기본값
|
|
"description": f"파일: {f.original_filename}"
|
|
}
|
|
for f in files
|
|
]
|
|
except Exception as e:
|
|
print(f"파일 목록 조회 에러: {str(e)}")
|
|
return {"error": f"파일 목록 조회 실패: {str(e)}"}
|
|
|
|
# 파일 삭제 API
|
|
@app.delete("/files/{file_id}")
|
|
async def delete_file(
|
|
file_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""파일 삭제"""
|
|
try:
|
|
# 먼저 파일 정보 조회
|
|
file_query = text("SELECT * FROM files WHERE id = :file_id")
|
|
file_result = db.execute(file_query, {"file_id": file_id})
|
|
file = file_result.fetchone()
|
|
|
|
if not file:
|
|
return {"error": "파일을 찾을 수 없습니다"}
|
|
|
|
# 먼저 상세 테이블의 데이터 삭제 (외래 키 제약 조건 때문)
|
|
# 각 자재 타입별 상세 테이블 데이터 삭제
|
|
detail_tables = [
|
|
'pipe_details', 'fitting_details', 'valve_details',
|
|
'flange_details', 'bolt_details', 'gasket_details',
|
|
'instrument_details'
|
|
]
|
|
|
|
# 해당 파일의 materials ID 조회
|
|
material_ids_query = text("SELECT id FROM materials WHERE file_id = :file_id")
|
|
material_ids_result = db.execute(material_ids_query, {"file_id": file_id})
|
|
material_ids = [row[0] for row in material_ids_result]
|
|
|
|
if material_ids:
|
|
# 각 상세 테이블에서 관련 데이터 삭제
|
|
for table in detail_tables:
|
|
delete_detail_query = text(f"DELETE FROM {table} WHERE material_id = ANY(:material_ids)")
|
|
db.execute(delete_detail_query, {"material_ids": material_ids})
|
|
|
|
# materials 테이블 데이터 삭제
|
|
materials_query = text("DELETE FROM materials WHERE file_id = :file_id")
|
|
db.execute(materials_query, {"file_id": file_id})
|
|
|
|
# 파일 삭제
|
|
delete_query = text("DELETE FROM files WHERE id = :file_id")
|
|
db.execute(delete_query, {"file_id": file_id})
|
|
|
|
db.commit()
|
|
return {"success": True, "message": "파일과 관련 데이터가 삭제되었습니다"}
|
|
except Exception as e:
|
|
db.rollback()
|
|
return {"error": f"파일 삭제 실패: {str(e)}"}
|
|
|
|
# 프로젝트 관리 API (비활성화 - jobs 테이블 사용)
|
|
# projects 테이블은 더 이상 사용하지 않음
|
|
# ):
|
|
# """프로젝트 수정"""
|
|
# try:
|
|
# update_query = text("""
|
|
# UPDATE projects
|
|
# SET project_name = :project_name, status = :status
|
|
# WHERE id = :project_id
|
|
# """)
|
|
#
|
|
# db.execute(update_query, {
|
|
# "project_id": project_id,
|
|
# "project_name": project_data["project_name"],
|
|
# "status": project_data["status"]
|
|
# })
|
|
#
|
|
# db.commit()
|
|
# return {"success": True}
|
|
# except Exception as e:
|
|
# db.rollback()
|
|
# return {"error": f"프로젝트 수정 실패: {str(e)}"}
|
|
|
|
# @app.delete("/projects/{project_id}")
|
|
# async def delete_project(
|
|
# project_id: int,
|
|
# db: Session = Depends(get_db)
|
|
# ):
|
|
# """프로젝트 삭제"""
|
|
# try:
|
|
# delete_query = text("DELETE FROM projects WHERE id = :project_id")
|
|
# db.execute(delete_query, {"project_id": project_id})
|
|
# db.commit()
|
|
# return {"success": True}
|
|
# except Exception as e:
|
|
# db.rollback()
|
|
# return {"error": f"프로젝트 삭제 실패: {str(e)}"}
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {
|
|
"message": "TK-MP BOM Management API",
|
|
"version": "1.0.0",
|
|
"endpoints": ["/docs", "/jobs", "/files", "/projects"]
|
|
}
|
|
|
|
# Jobs API
|
|
# @app.get("/jobs")
|
|
# async def get_jobs(db: Session = Depends(get_db)):
|
|
# """Jobs 목록 조회"""
|
|
# try:
|
|
# # jobs 테이블에서 데이터 조회
|
|
# query = text("""
|
|
# SELECT
|
|
# job_no,
|
|
# job_name,
|
|
# client_name,
|
|
# end_user,
|
|
# epc_company,
|
|
# status,
|
|
# created_at
|
|
# FROM jobs
|
|
# WHERE is_active = true
|
|
# ORDER BY created_at DESC
|
|
# """)
|
|
#
|
|
# result = db.execute(query)
|
|
# jobs = result.fetchall()
|
|
#
|
|
# return [
|
|
# {
|
|
# "job_no": job.job_no,
|
|
# "job_name": job.job_name,
|
|
# "client_name": job.client_name,
|
|
# "end_user": job.end_user,
|
|
# "epc_company": job.epc_company,
|
|
# "status": job.status or "진행중",
|
|
# "created_at": job.created_at
|
|
# }
|
|
# for job in jobs
|
|
# ]
|
|
# except Exception as e:
|
|
# print(f"Jobs 조회 에러: {str(e)}")
|
|
# return {"error": f"Jobs 조회 실패: {str(e)}"}
|
|
|
|
# 파일 업로드는 /files/upload 엔드포인트를 사용하세요 (routers/files.py)
|
|
|
|
# parse_file과 classify_material_item 함수는 routers/files.py로 이동되었습니다
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
return {"status": "healthy", "timestamp": "2024-07-15"}
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|