프론트엔드 작성중
This commit is contained in:
@@ -236,35 +236,86 @@ async def upload_file(
|
||||
async def get_materials(
|
||||
project_id: Optional[int] = None,
|
||||
file_id: Optional[int] = None,
|
||||
job_no: Optional[str] = None,
|
||||
filename: Optional[str] = None,
|
||||
revision: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
item_type: Optional[str] = None,
|
||||
material_grade: Optional[str] = None,
|
||||
size_spec: Optional[str] = None,
|
||||
file_filter: Optional[str] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""저장된 자재 목록 조회"""
|
||||
"""
|
||||
저장된 자재 목록 조회 (job_no, filename, revision 3가지로 필터링 가능)
|
||||
"""
|
||||
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.project_id,
|
||||
f.original_filename, f.project_id, f.job_no, f.revision,
|
||||
p.official_project_code, p.project_name
|
||||
FROM materials m
|
||||
LEFT JOIN files f ON m.file_id = f.id
|
||||
LEFT JOIN projects p ON f.project_id = p.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = {}
|
||||
|
||||
if project_id:
|
||||
query += " AND f.project_id = :project_id"
|
||||
params["project_id"] = project_id
|
||||
|
||||
if file_id:
|
||||
query += " AND m.file_id = :file_id"
|
||||
params["file_id"] = file_id
|
||||
if job_no:
|
||||
query += " AND f.job_no = :job_no"
|
||||
params["job_no"] = job_no
|
||||
if filename:
|
||||
query += " AND f.original_filename = :filename"
|
||||
params["filename"] = filename
|
||||
if revision:
|
||||
query += " AND f.revision = :revision"
|
||||
params["revision"] = revision
|
||||
if search:
|
||||
query += " AND (m.original_description ILIKE :search OR m.material_grade ILIKE :search)"
|
||||
params["search"] = f"%{search}%"
|
||||
if item_type:
|
||||
query += " AND m.classified_category = :item_type"
|
||||
params["item_type"] = item_type
|
||||
if material_grade:
|
||||
query += " AND m.material_grade ILIKE :material_grade"
|
||||
params["material_grade"] = f"%{material_grade}%"
|
||||
if size_spec:
|
||||
query += " AND m.size_spec ILIKE :size_spec"
|
||||
params["size_spec"] = f"%{size_spec}%"
|
||||
if file_filter:
|
||||
query += " AND f.original_filename ILIKE :file_filter"
|
||||
params["file_filter"] = f"%{file_filter}%"
|
||||
|
||||
# 정렬 처리
|
||||
if sort_by:
|
||||
if sort_by == "quantity_desc":
|
||||
query += " ORDER BY m.quantity DESC"
|
||||
elif sort_by == "quantity_asc":
|
||||
query += " ORDER BY m.quantity ASC"
|
||||
elif sort_by == "name_asc":
|
||||
query += " ORDER BY m.original_description ASC"
|
||||
elif sort_by == "name_desc":
|
||||
query += " ORDER BY m.original_description DESC"
|
||||
elif sort_by == "created_desc":
|
||||
query += " ORDER BY m.created_at DESC"
|
||||
elif sort_by == "created_asc":
|
||||
query += " ORDER BY m.created_at ASC"
|
||||
else:
|
||||
query += " ORDER BY m.line_number ASC"
|
||||
else:
|
||||
query += " ORDER BY m.line_number ASC"
|
||||
|
||||
query += " ORDER BY m.line_number ASC LIMIT :limit OFFSET :skip"
|
||||
query += " LIMIT :limit OFFSET :skip"
|
||||
params["limit"] = limit
|
||||
params["skip"] = skip
|
||||
|
||||
@@ -287,6 +338,26 @@ async def get_materials(
|
||||
if file_id:
|
||||
count_query += " AND m.file_id = :file_id"
|
||||
count_params["file_id"] = file_id
|
||||
|
||||
if search:
|
||||
count_query += " AND (m.original_description ILIKE :search OR m.material_grade ILIKE :search)"
|
||||
count_params["search"] = f"%{search}%"
|
||||
|
||||
if item_type:
|
||||
count_query += " AND m.classified_category = :item_type"
|
||||
count_params["item_type"] = item_type
|
||||
|
||||
if material_grade:
|
||||
count_query += " AND m.material_grade ILIKE :material_grade"
|
||||
count_params["material_grade"] = f"%{material_grade}%"
|
||||
|
||||
if size_spec:
|
||||
count_query += " AND m.size_spec ILIKE :size_spec"
|
||||
count_params["size_spec"] = f"%{size_spec}%"
|
||||
|
||||
if file_filter:
|
||||
count_query += " AND f.original_filename ILIKE :file_filter"
|
||||
count_params["file_filter"] = f"%{file_filter}%"
|
||||
|
||||
count_result = db.execute(text(count_query), count_params)
|
||||
total_count = count_result.fetchone()[0]
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from fastapi import FastAPI
|
||||
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(
|
||||
@@ -30,14 +37,514 @@ try:
|
||||
except ImportError:
|
||||
print("jobs 라우터를 찾을 수 없습니다")
|
||||
|
||||
# 파일 목록 조회 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.original_filename, # 파일명을 BOM 이름으로 사용
|
||||
"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": "파일을 찾을 수 없습니다"}
|
||||
|
||||
# 관련 자재 데이터 삭제
|
||||
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"]
|
||||
"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)}"}
|
||||
|
||||
# 파일 업로드 API
|
||||
@app.post("/upload")
|
||||
async def upload_file(
|
||||
file: UploadFile = File(...),
|
||||
job_no: str = Form(...), # project_id 대신 job_no 사용
|
||||
bom_name: str = Form(""), # BOM 이름 추가
|
||||
bom_type: str = Form(""),
|
||||
revision: str = Form("Rev.0"),
|
||||
parent_bom_id: Optional[int] = Form(None),
|
||||
description: str = Form(""),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""파일 업로드 및 자재 분류 (자동 리비전 관리)"""
|
||||
try:
|
||||
print("=== main.py 업로드 API 호출됨 ===")
|
||||
print(f"파일명: {file.filename}")
|
||||
print(f"job_no: {job_no}")
|
||||
print(f"bom_name: {bom_name}")
|
||||
print(f"bom_type: {bom_type}")
|
||||
|
||||
# job_no로 job 확인
|
||||
job_query = text("SELECT job_no FROM jobs WHERE job_no = :job_no AND is_active = true")
|
||||
job_result = db.execute(job_query, {"job_no": job_no})
|
||||
job = job_result.fetchone()
|
||||
|
||||
if not job:
|
||||
return {"error": f"Job No. '{job_no}'에 해당하는 작업을 찾을 수 없습니다."}
|
||||
|
||||
# 업로드 디렉토리 생성
|
||||
upload_dir = "uploads"
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
# 파일 저장
|
||||
if file.filename:
|
||||
file_path = os.path.join(upload_dir, file.filename)
|
||||
print(f"파일 저장 경로: {file_path}")
|
||||
print(f"원본 파일명: {file.filename}")
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
print(f"파일 저장 완료: {file_path}")
|
||||
else:
|
||||
return {"error": "파일명이 없습니다."}
|
||||
|
||||
# 파일 크기 계산
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
# 파일 타입 결정
|
||||
file_type = "excel" if file.filename.endswith(('.xls', '.xlsx')) else "csv" if file.filename.endswith('.csv') else "unknown"
|
||||
|
||||
# BOM 종류별 자동 리비전 관리
|
||||
if bom_type and not parent_bom_id:
|
||||
# 같은 job_no의 같은 파일명에 대한 최신 리비전 조회
|
||||
latest_revision_query = text("""
|
||||
SELECT revision FROM files
|
||||
WHERE job_no = :job_no AND original_filename = :filename
|
||||
ORDER BY revision DESC LIMIT 1
|
||||
""")
|
||||
|
||||
result = db.execute(latest_revision_query, {
|
||||
"job_no": job_no,
|
||||
"filename": file.filename
|
||||
})
|
||||
|
||||
latest_file = result.fetchone()
|
||||
if latest_file:
|
||||
# 기존 리비전이 있으면 다음 리비전 번호 생성
|
||||
current_rev = latest_file.revision
|
||||
if current_rev.startswith("Rev."):
|
||||
try:
|
||||
rev_num = int(current_rev.replace("Rev.", ""))
|
||||
revision = f"Rev.{rev_num + 1}"
|
||||
except ValueError:
|
||||
revision = "Rev.1"
|
||||
else:
|
||||
revision = "Rev.1"
|
||||
else:
|
||||
# 첫 번째 업로드인 경우 Rev.0
|
||||
revision = "Rev.0"
|
||||
|
||||
# 데이터베이스에 파일 정보 저장
|
||||
insert_query = text("""
|
||||
INSERT INTO files (
|
||||
job_no, filename, original_filename, file_path,
|
||||
file_size, upload_date, revision, file_type, uploaded_by
|
||||
) VALUES (
|
||||
:job_no, :filename, :original_filename, :file_path,
|
||||
:file_size, NOW(), :revision, :file_type, :uploaded_by
|
||||
) RETURNING id
|
||||
""")
|
||||
|
||||
result = db.execute(insert_query, {
|
||||
"job_no": job_no,
|
||||
"filename": file.filename,
|
||||
"original_filename": file.filename,
|
||||
"file_path": file_path,
|
||||
"file_size": file_size,
|
||||
"revision": revision,
|
||||
"file_type": file_type,
|
||||
"uploaded_by": "system"
|
||||
})
|
||||
|
||||
file_id = result.fetchone()[0]
|
||||
|
||||
# 1차: 파일 파싱 (CSV/Excel 파일 읽기)
|
||||
materials_data = parse_file(file_path)
|
||||
|
||||
# 2차: 각 자재를 분류기로 분류
|
||||
classified_materials = []
|
||||
|
||||
# 리비전 업로드인 경우 기존 분류 정보 가져오기
|
||||
existing_classifications = {}
|
||||
if parent_bom_id:
|
||||
parent_materials = db.execute(
|
||||
text("SELECT original_description, classified_category, classified_subcategory, material_grade, schedule, size_spec FROM materials WHERE file_id = :file_id"),
|
||||
{"file_id": parent_bom_id}
|
||||
).fetchall()
|
||||
|
||||
for material in parent_materials:
|
||||
existing_classifications[material.original_description] = {
|
||||
"classified_category": material.classified_category,
|
||||
"classified_subcategory": material.classified_subcategory,
|
||||
"material_grade": material.material_grade,
|
||||
"schedule": material.schedule,
|
||||
"size_spec": material.size_spec
|
||||
}
|
||||
|
||||
for material in materials_data:
|
||||
# 리비전 업로드인 경우 기존 분류 사용, 아니면 새로 분류
|
||||
if parent_bom_id and material.get("original_description") in existing_classifications:
|
||||
existing_class = existing_classifications[material.get("original_description")]
|
||||
classified_material = {
|
||||
**material,
|
||||
**existing_class,
|
||||
"classification_confidence": 1.0 # 기존 분류이므로 높은 신뢰도
|
||||
}
|
||||
else:
|
||||
classified_material = classify_material_item(material)
|
||||
classified_materials.append(classified_material)
|
||||
|
||||
# 3차: 분류된 자재를 데이터베이스에 저장
|
||||
for material in classified_materials:
|
||||
insert_material_query = text("""
|
||||
INSERT INTO materials (
|
||||
file_id, line_number, original_description,
|
||||
classified_category, classified_subcategory,
|
||||
material_grade, schedule, size_spec,
|
||||
quantity, unit, drawing_name, area_code, line_no,
|
||||
classification_confidence, is_verified, created_at
|
||||
) VALUES (
|
||||
:file_id, :line_number, :original_description,
|
||||
:classified_category, :classified_subcategory,
|
||||
:material_grade, :schedule, :size_spec,
|
||||
:quantity, :unit, :drawing_name, :area_code, :line_no,
|
||||
:classification_confidence, :is_verified, NOW()
|
||||
)
|
||||
""")
|
||||
|
||||
db.execute(insert_material_query, {
|
||||
"file_id": file_id,
|
||||
"line_number": material.get("line_number", 0),
|
||||
"original_description": material.get("original_description", ""),
|
||||
"classified_category": material.get("classified_category", ""),
|
||||
"classified_subcategory": material.get("classified_subcategory", ""),
|
||||
"material_grade": material.get("material_grade", ""),
|
||||
"schedule": material.get("schedule", ""),
|
||||
"size_spec": material.get("size_spec", ""),
|
||||
"quantity": material.get("quantity", 0),
|
||||
"unit": material.get("unit", ""),
|
||||
"drawing_name": material.get("drawing_name", ""),
|
||||
"area_code": material.get("area_code", ""),
|
||||
"line_no": material.get("line_no", ""),
|
||||
"classification_confidence": material.get("classification_confidence", 0.0),
|
||||
"is_verified": False
|
||||
})
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"file_id": file_id,
|
||||
"filename": file.filename,
|
||||
"materials_count": len(classified_materials),
|
||||
"revision": revision,
|
||||
"message": f"파일이 성공적으로 업로드되고 {len(classified_materials)}개의 자재가 분류되었습니다. (리비전: {revision})"
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"업로드 실패: {str(e)}")
|
||||
# HTTP 400 에러로 변경
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=400, detail=f"파일 업로드 및 분류 실패: {str(e)}")
|
||||
|
||||
def parse_file(file_path: str) -> List[Dict]:
|
||||
"""파일 파싱 (CSV/Excel)"""
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
try:
|
||||
print(f"parse_file 호출됨: {file_path}")
|
||||
print(f"파일 존재 여부: {os.path.exists(file_path)}")
|
||||
print(f"파일 확장자: {os.path.splitext(file_path)[1]}")
|
||||
|
||||
# 파일 확장자를 소문자로 변환하여 검증
|
||||
file_extension = os.path.splitext(file_path)[1].lower()
|
||||
print(f"소문자 변환된 확장자: {file_extension}")
|
||||
|
||||
if file_extension == '.csv':
|
||||
df = pd.read_csv(file_path)
|
||||
elif file_extension in ['.xls', '.xlsx']:
|
||||
df = pd.read_excel(file_path)
|
||||
else:
|
||||
print(f"지원되지 않는 파일 형식: {file_path}")
|
||||
print(f"파일 확장자: {file_extension}")
|
||||
raise ValueError("지원하지 않는 파일 형식입니다.")
|
||||
|
||||
print(f"파일 파싱 시작: {file_path}")
|
||||
print(f"데이터프레임 형태: {df.shape}")
|
||||
print(f"컬럼명: {list(df.columns)}")
|
||||
|
||||
# 컬럼명 매핑 (대소문자 구분 없이)
|
||||
column_mapping = {
|
||||
'description': ['DESCRIPTION', 'Description', 'description', 'DESC', 'Desc', 'desc', 'ITEM', 'Item', 'item'],
|
||||
'quantity': ['QTY', 'Quantity', 'quantity', 'QTY.', 'Qty', 'qty', 'AMOUNT', 'Amount', 'amount'],
|
||||
'unit': ['UNIT', 'Unit', 'unit', 'UOM', 'Uom', 'uom'],
|
||||
'drawing': ['DRAWING', 'Drawing', 'drawing', 'DWG', 'Dwg', 'dwg'],
|
||||
'area': ['AREA', 'Area', 'area', 'AREA_CODE', 'Area_Code', 'area_code'],
|
||||
'line': ['LINE', 'Line', 'line', 'LINE_NO', 'Line_No', 'line_no', 'PIPELINE', 'Pipeline', 'pipeline']
|
||||
}
|
||||
|
||||
# 실제 컬럼명 찾기
|
||||
found_columns = {}
|
||||
for target_col, possible_names in column_mapping.items():
|
||||
for col_name in possible_names:
|
||||
if col_name in df.columns:
|
||||
found_columns[target_col] = col_name
|
||||
break
|
||||
|
||||
print(f"찾은 컬럼 매핑: {found_columns}")
|
||||
|
||||
materials = []
|
||||
for index, row in df.iterrows():
|
||||
# 빈 행 건너뛰기
|
||||
if row.isna().all():
|
||||
continue
|
||||
|
||||
# 안전한 값 추출
|
||||
description = str(row.get(found_columns.get('description', ''), '') or '')
|
||||
quantity_raw = row.get(found_columns.get('quantity', 1), 1)
|
||||
quantity = float(quantity_raw) if quantity_raw is not None else 1.0
|
||||
unit = str(row.get(found_columns.get('unit', 'EA'), 'EA') or 'EA')
|
||||
drawing = str(row.get(found_columns.get('drawing', ''), '') or '')
|
||||
area = str(row.get(found_columns.get('area', ''), '') or '')
|
||||
line = str(row.get(found_columns.get('line', ''), '') or '')
|
||||
|
||||
material = {
|
||||
"line_number": index + 1,
|
||||
"original_description": description,
|
||||
"quantity": quantity,
|
||||
"unit": unit,
|
||||
"drawing_name": drawing,
|
||||
"area_code": area,
|
||||
"line_no": line
|
||||
}
|
||||
|
||||
# 빈 설명은 건너뛰기
|
||||
if not material["original_description"] or material["original_description"].strip() == '':
|
||||
continue
|
||||
|
||||
materials.append(material)
|
||||
|
||||
print(f"파싱된 자재 수: {len(materials)}")
|
||||
if materials:
|
||||
print(f"첫 번째 자재 예시: {materials[0]}")
|
||||
|
||||
return materials
|
||||
except Exception as e:
|
||||
print(f"파일 파싱 오류: {str(e)}")
|
||||
raise Exception(f"파일 파싱 실패: {str(e)}")
|
||||
|
||||
def classify_material_item(material: Dict) -> Dict:
|
||||
"""개별 자재 분류"""
|
||||
from .services import (
|
||||
pipe_classifier, fitting_classifier, bolt_classifier,
|
||||
valve_classifier, instrument_classifier, flange_classifier,
|
||||
gasket_classifier, material_classifier
|
||||
)
|
||||
|
||||
description = material.get("original_description", "")
|
||||
|
||||
# 각 분류기로 분류 시도
|
||||
classifiers = [
|
||||
("PIPE", pipe_classifier.classify_pipe),
|
||||
("FITTING", fitting_classifier.classify_fitting),
|
||||
("BOLT", bolt_classifier.classify_bolt),
|
||||
("VALVE", valve_classifier.classify_valve),
|
||||
("INSTRUMENT", instrument_classifier.classify_instrument),
|
||||
("FLANGE", flange_classifier.classify_flange),
|
||||
("GASKET", gasket_classifier.classify_gasket)
|
||||
]
|
||||
|
||||
best_result = None
|
||||
best_confidence = 0.0
|
||||
|
||||
for category, classifier_func in classifiers:
|
||||
try:
|
||||
result = classifier_func(description)
|
||||
if result and result.get("confidence", 0) > best_confidence:
|
||||
best_result = result
|
||||
best_confidence = result.get("confidence", 0)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 재질 분류
|
||||
material_result = material_classifier.classify_material(description)
|
||||
|
||||
# 최종 결과 조합
|
||||
final_result = {
|
||||
**material,
|
||||
"classified_category": best_result.get("category", "UNKNOWN") if best_result else "UNKNOWN",
|
||||
"classified_subcategory": best_result.get("subcategory", "") if best_result else "",
|
||||
"material_grade": material_result.get("grade", "") if material_result else "",
|
||||
"schedule": best_result.get("schedule", "") if best_result else "",
|
||||
"size_spec": best_result.get("size_spec", "") if best_result else "",
|
||||
"classification_confidence": best_confidence
|
||||
}
|
||||
|
||||
return final_result
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "timestamp": "2024-07-15"}
|
||||
|
||||
@@ -12,6 +12,14 @@ from pathlib import Path
|
||||
|
||||
from ..database import get_db
|
||||
from app.services.material_classifier import classify_material
|
||||
from app.services.bolt_classifier import classify_bolt
|
||||
from app.services.flange_classifier import classify_flange
|
||||
from app.services.fitting_classifier import classify_fitting
|
||||
from app.services.gasket_classifier import classify_gasket
|
||||
from app.services.instrument_classifier import classify_instrument
|
||||
from app.services.pipe_classifier import classify_pipe
|
||||
from app.services.valve_classifier import classify_valve
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
UPLOAD_DIR = Path("uploads")
|
||||
@@ -153,16 +161,21 @@ async def upload_file(
|
||||
file_path = UPLOAD_DIR / unique_filename
|
||||
|
||||
try:
|
||||
print("파일 저장 시작")
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
print(f"파일 저장 완료: {file_path}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}")
|
||||
|
||||
try:
|
||||
print("파일 저장 시작")
|
||||
materials_data = parse_file_data(str(file_path))
|
||||
parsed_count = len(materials_data)
|
||||
print(f"파싱 완료: {parsed_count}개 자재")
|
||||
|
||||
# 파일 정보 저장
|
||||
# 파일 정보 저장 (project_id 제거)
|
||||
print("DB 저장 시작")
|
||||
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)
|
||||
@@ -182,20 +195,40 @@ async def upload_file(
|
||||
})
|
||||
|
||||
file_id = file_result.fetchone()[0]
|
||||
print(f"파일 저장 완료: file_id = {file_id}")
|
||||
|
||||
# 자재 데이터 저장
|
||||
# 자재 데이터 저장 (분류 포함)
|
||||
print("자재 분류 및 저장 시작")
|
||||
materials_inserted = 0
|
||||
classification_stats = {
|
||||
'BOLT': 0, 'FLANGE': 0, 'FITTING': 0, 'GASKET': 0,
|
||||
'INSTRUMENT': 0, 'PIPE': 0, 'VALVE': 0, 'MATERIAL': 0, 'OTHER': 0
|
||||
}
|
||||
|
||||
for material_data in materials_data:
|
||||
# 자재 분류 실행
|
||||
classification_result = classify_material_item(
|
||||
material_data["original_description"],
|
||||
material_data["size_spec"]
|
||||
)
|
||||
|
||||
# 분류 통계 업데이트
|
||||
category = classification_result.get('category', 'OTHER')
|
||||
if category in classification_stats:
|
||||
classification_stats[category] += 1
|
||||
|
||||
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
|
||||
classification_confidence, is_verified, created_at,
|
||||
subcategory, standard, grade
|
||||
)
|
||||
VALUES (
|
||||
:file_id, :original_description, :quantity, :unit, :size_spec,
|
||||
:material_grade, :line_number, :row_number, :classified_category,
|
||||
:classification_confidence, :is_verified, :created_at
|
||||
:classification_confidence, :is_verified, :created_at,
|
||||
:subcategory, :standard, :grade
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -208,14 +241,20 @@ async def upload_file(
|
||||
"material_grade": material_data["material_grade"],
|
||||
"line_number": material_data["line_number"],
|
||||
"row_number": material_data["row_number"],
|
||||
"classified_category": get_major_category(material_data["original_description"]),
|
||||
"classification_confidence": 0.9,
|
||||
"classified_category": classification_result.get('category', 'OTHER'),
|
||||
"classification_confidence": classification_result.get('confidence', 0.0),
|
||||
"is_verified": False,
|
||||
"created_at": datetime.now()
|
||||
"created_at": datetime.now(),
|
||||
"subcategory": classification_result.get('subcategory', ''),
|
||||
"standard": classification_result.get('standard', ''),
|
||||
"grade": classification_result.get('grade', '')
|
||||
})
|
||||
materials_inserted += 1
|
||||
|
||||
print(f"자재 저장 완료: {materials_inserted}개")
|
||||
print("커밋 직전")
|
||||
db.commit()
|
||||
print("커밋 완료")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -224,6 +263,7 @@ async def upload_file(
|
||||
"file_id": file_id,
|
||||
"parsed_materials_count": parsed_count,
|
||||
"saved_materials_count": materials_inserted,
|
||||
"classification_stats": classification_stats,
|
||||
"sample_materials": materials_data[:3] if materials_data else []
|
||||
}
|
||||
|
||||
@@ -231,93 +271,260 @@ async def upload_file(
|
||||
db.rollback()
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
import traceback
|
||||
print(traceback.format_exc()) # 에러 전체 로그 출력
|
||||
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,
|
||||
project_id: Optional[int] = None,
|
||||
job_id: Optional[int] = None,
|
||||
revision: Optional[str] = None,
|
||||
grouping: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
search_value: Optional[str] = None,
|
||||
item_type: Optional[str] = None,
|
||||
material_grade: Optional[str] = None,
|
||||
size_spec: Optional[str] = None,
|
||||
file_filter: Optional[str] = None,
|
||||
sort_by: 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, m.classified_category, m.classification_confidence
|
||||
# 기본 쿼리 구성
|
||||
base_query = """
|
||||
SELECT
|
||||
m.id,
|
||||
m.original_description,
|
||||
m.quantity,
|
||||
m.unit,
|
||||
m.size_spec,
|
||||
m.material_grade,
|
||||
m.line_number,
|
||||
m.row_number,
|
||||
m.classified_category,
|
||||
m.classification_confidence,
|
||||
m.is_verified,
|
||||
m.created_at,
|
||||
f.job_no as job_number,
|
||||
f.revision,
|
||||
f.original_filename,
|
||||
f.project_id,
|
||||
p.project_name,
|
||||
COUNT(*) OVER() as total_count
|
||||
FROM materials m
|
||||
LEFT JOIN files f ON m.file_id = f.id
|
||||
LEFT JOIN jobs j ON f.job_no = j.job_no
|
||||
LEFT JOIN projects p ON f.project_id = p.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = {}
|
||||
conditions = []
|
||||
|
||||
if job_no:
|
||||
query += " AND f.job_no = :job_no"
|
||||
params["job_no"] = job_no
|
||||
# 프로젝트 필터
|
||||
if project_id:
|
||||
conditions.append("f.project_id = :project_id")
|
||||
params["project_id"] = project_id
|
||||
|
||||
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"
|
||||
# Job ID 필터
|
||||
if job_id:
|
||||
conditions.append("f.job_no = (SELECT job_no FROM jobs WHERE id = :job_id)")
|
||||
params["job_id"] = job_id
|
||||
|
||||
# 리비전 필터
|
||||
if revision:
|
||||
conditions.append("f.revision = :revision")
|
||||
params["revision"] = revision
|
||||
|
||||
# 검색 필터 (개선된 버전)
|
||||
if search and search_value:
|
||||
try:
|
||||
if search == "project":
|
||||
conditions.append("p.project_name ILIKE :search_value")
|
||||
elif search == "job":
|
||||
conditions.append("f.job_no ILIKE :search_value")
|
||||
elif search == "material":
|
||||
conditions.append("m.original_description ILIKE :search_value")
|
||||
elif search == "description":
|
||||
conditions.append("m.original_description ILIKE :search_value")
|
||||
elif search == "grade":
|
||||
conditions.append("m.material_grade ILIKE :search_value")
|
||||
elif search == "size":
|
||||
conditions.append("m.size_spec ILIKE :search_value")
|
||||
elif search == "filename":
|
||||
conditions.append("f.original_filename ILIKE :search_value")
|
||||
else:
|
||||
# 기본 검색 (기존 방식)
|
||||
conditions.append("(m.original_description ILIKE :search_value OR m.material_grade ILIKE :search_value)")
|
||||
|
||||
params["search_value"] = f"%{search_value}%"
|
||||
except Exception as e:
|
||||
print(f"검색 필터 처리 오류: {e}")
|
||||
# 오류 발생 시 기본 검색으로 fallback
|
||||
conditions.append("(m.original_description ILIKE :search_value OR m.material_grade ILIKE :search_value)")
|
||||
params["search_value"] = f"%{search_value}%"
|
||||
|
||||
# 품목 타입 필터
|
||||
if item_type:
|
||||
conditions.append("m.classified_category = :item_type")
|
||||
params["item_type"] = item_type
|
||||
|
||||
# 재질 필터
|
||||
if material_grade:
|
||||
conditions.append("m.material_grade ILIKE :material_grade")
|
||||
params["material_grade"] = f"%{material_grade}%"
|
||||
|
||||
# 사이즈 필터
|
||||
if size_spec:
|
||||
conditions.append("m.size_spec ILIKE :size_spec")
|
||||
params["size_spec"] = f"%{size_spec}%"
|
||||
|
||||
# 파일명 필터
|
||||
if file_filter:
|
||||
conditions.append("f.original_filename ILIKE :file_filter")
|
||||
params["file_filter"] = f"%{file_filter}%"
|
||||
|
||||
# 조건 추가
|
||||
if conditions:
|
||||
base_query += " AND " + " AND ".join(conditions)
|
||||
|
||||
# 그룹핑 처리
|
||||
if grouping:
|
||||
if grouping == "item":
|
||||
base_query += " GROUP BY m.classified_category, m.original_description, m.size_spec, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.revision, f.original_filename, f.project_id, p.project_name"
|
||||
elif grouping == "material":
|
||||
base_query += " GROUP BY m.material_grade, m.original_description, m.size_spec, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.revision, f.original_filename, f.project_id, p.project_name"
|
||||
elif grouping == "size":
|
||||
base_query += " GROUP BY m.size_spec, m.original_description, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.revision, f.original_filename, f.project_id, p.project_name"
|
||||
elif grouping == "job":
|
||||
base_query += " GROUP BY f.job_no, m.original_description, m.size_spec, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.revision, f.original_filename, f.project_id, p.project_name"
|
||||
elif grouping == "revision":
|
||||
base_query += " GROUP BY f.revision, m.original_description, m.size_spec, m.material_grade, m.id, m.quantity, m.unit, m.line_number, m.row_number, m.classified_category, m.classification_confidence, m.is_verified, m.created_at, f.job_no, f.original_filename, f.project_id, p.project_name"
|
||||
|
||||
# 정렬
|
||||
if sort_by:
|
||||
if sort_by == "quantity_desc":
|
||||
base_query += " ORDER BY SUM(m.quantity) DESC"
|
||||
elif sort_by == "quantity_asc":
|
||||
base_query += " ORDER BY SUM(m.quantity) ASC"
|
||||
elif sort_by == "name_asc":
|
||||
base_query += " ORDER BY m.original_description ASC"
|
||||
elif sort_by == "name_desc":
|
||||
base_query += " ORDER BY m.original_description DESC"
|
||||
elif sort_by == "created_desc":
|
||||
base_query += " ORDER BY m.created_at DESC"
|
||||
elif sort_by == "created_asc":
|
||||
base_query += " ORDER BY m.created_at ASC"
|
||||
else:
|
||||
base_query += " ORDER BY m.id DESC"
|
||||
else:
|
||||
base_query += " ORDER BY m.id DESC"
|
||||
|
||||
# 페이징
|
||||
base_query += " LIMIT :limit OFFSET :skip"
|
||||
params["limit"] = limit
|
||||
params["skip"] = skip
|
||||
|
||||
result = db.execute(text(query), params)
|
||||
result = db.execute(text(base_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 = {}
|
||||
# 리비전 비교 데이터 생성
|
||||
revision_comparison = None
|
||||
if revision and revision != "Rev.0":
|
||||
comparison_query = """
|
||||
SELECT
|
||||
m.original_description,
|
||||
m.size_spec,
|
||||
m.material_grade,
|
||||
SUM(CASE WHEN f.revision = :current_revision THEN m.quantity ELSE 0 END) as current_qty,
|
||||
SUM(CASE WHEN f.revision = :prev_revision THEN m.quantity ELSE 0 END) as prev_qty
|
||||
FROM materials m
|
||||
LEFT JOIN files f ON m.file_id = f.id
|
||||
WHERE f.project_id = :project_id
|
||||
AND f.revision IN (:current_revision, :prev_revision)
|
||||
GROUP BY m.original_description, m.size_spec, m.material_grade
|
||||
HAVING
|
||||
SUM(CASE WHEN f.revision = :current_revision THEN m.quantity ELSE 0 END) !=
|
||||
SUM(CASE WHEN f.revision = :prev_revision THEN m.quantity ELSE 0 END)
|
||||
"""
|
||||
|
||||
comparison_params = {
|
||||
"project_id": project_id,
|
||||
"current_revision": revision,
|
||||
"prev_revision": f"Rev.{int(revision.split('.')[-1]) - 1}"
|
||||
}
|
||||
|
||||
comparison_result = db.execute(text(comparison_query), comparison_params)
|
||||
comparison_data = comparison_result.fetchall()
|
||||
|
||||
if comparison_data:
|
||||
changes = []
|
||||
for row in comparison_data:
|
||||
change = row.current_qty - row.prev_qty
|
||||
if change != 0:
|
||||
changes.append({
|
||||
"description": row.original_description,
|
||||
"size_spec": row.size_spec,
|
||||
"material_grade": row.material_grade,
|
||||
"current_qty": row.current_qty,
|
||||
"prev_qty": row.prev_qty,
|
||||
"change": change
|
||||
})
|
||||
|
||||
revision_comparison = {
|
||||
"summary": f"{revision}에서 {len(changes)}개 항목이 변경되었습니다",
|
||||
"changes": changes
|
||||
}
|
||||
|
||||
if job_no:
|
||||
count_query += " AND f.job_no = :job_no"
|
||||
count_params["job_no"] = job_no
|
||||
# 결과 포맷팅
|
||||
formatted_materials = []
|
||||
for material in materials:
|
||||
# 라인 번호 문자열 생성
|
||||
line_numbers = [material.line_number] if material.line_number else []
|
||||
line_numbers_str = ", ".join(map(str, line_numbers)) if line_numbers else ""
|
||||
|
||||
# 수량 변경 계산 (리비전 비교)
|
||||
quantity_change = None
|
||||
if revision_comparison:
|
||||
for change in revision_comparison["changes"]:
|
||||
if (change["description"] == material.original_description and
|
||||
change["size_spec"] == material.size_spec and
|
||||
change["material_grade"] == material.material_grade):
|
||||
quantity_change = change["change"]
|
||||
break
|
||||
|
||||
formatted_material = {
|
||||
"id": material.id,
|
||||
"original_description": material.original_description,
|
||||
"quantity": float(material.quantity) if material.quantity else 0,
|
||||
"unit": material.unit or "EA",
|
||||
"size_spec": material.size_spec or "",
|
||||
"material_grade": material.material_grade or "",
|
||||
"line_number": material.line_number,
|
||||
"line_numbers_str": line_numbers_str,
|
||||
"line_count": len(line_numbers),
|
||||
"classified_category": material.classified_category or "OTHER",
|
||||
"classification_confidence": float(material.classification_confidence) if material.classification_confidence else 0,
|
||||
"is_verified": material.is_verified or False,
|
||||
"created_at": material.created_at.isoformat() if material.created_at else None,
|
||||
"job_number": material.job_number,
|
||||
"revision": material.revision or "Rev.0",
|
||||
"original_filename": material.original_filename,
|
||||
"project_id": material.project_id,
|
||||
"project_name": material.project_name,
|
||||
"quantity_change": quantity_change
|
||||
}
|
||||
|
||||
formatted_materials.append(formatted_material)
|
||||
|
||||
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]
|
||||
total_count = materials[0].total_count if materials else 0
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"materials": formatted_materials,
|
||||
"total_count": total_count,
|
||||
"returned_count": len(materials),
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
"materials": [
|
||||
{
|
||||
"id": m[0],
|
||||
"file_id": m[1],
|
||||
"filename": m[10],
|
||||
"job_no": m[12],
|
||||
"project_code": m[12],
|
||||
"project_name": "Job-" + str(m[11]) if m[11] else "Unknown",
|
||||
"original_description": m[2],
|
||||
"quantity": float(m[3]) if m.quantity else 0,
|
||||
"unit": m[4],
|
||||
"classified_category": m[14],
|
||||
"classification_confidence": float(m[15]) if m.classification_confidence else 0.0,
|
||||
"size_spec": m[5],
|
||||
"material_grade": m[6],
|
||||
"line_number": m[7],
|
||||
"row_number": m[8],
|
||||
"created_at": m[9]
|
||||
}
|
||||
for m in materials
|
||||
]
|
||||
"revision_comparison": revision_comparison
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -419,3 +626,123 @@ def get_major_category(description):
|
||||
return 'bolt'
|
||||
else:
|
||||
return 'other'
|
||||
|
||||
def classify_material_item(description: str, size_spec: str = "") -> dict:
|
||||
"""
|
||||
자재를 각 분류기로 보내서 분류하는 통합 함수
|
||||
|
||||
Args:
|
||||
description: 자재 설명
|
||||
size_spec: 사이즈 정보
|
||||
|
||||
Returns:
|
||||
분류 결과 딕셔너리
|
||||
"""
|
||||
desc_upper = description.upper().strip()
|
||||
|
||||
# 1. 볼트 분류
|
||||
bolt_result = classify_bolt(description)
|
||||
if bolt_result.get('confidence', 0) > 0.7:
|
||||
return {
|
||||
'category': 'BOLT',
|
||||
'subcategory': bolt_result.get('bolt_type', 'UNKNOWN'),
|
||||
'standard': bolt_result.get('standard', ''),
|
||||
'grade': bolt_result.get('grade', ''),
|
||||
'confidence': bolt_result.get('confidence', 0),
|
||||
'details': bolt_result
|
||||
}
|
||||
|
||||
# 2. 플랜지 분류
|
||||
flange_result = classify_flange(description)
|
||||
if flange_result.get('confidence', 0) > 0.7:
|
||||
return {
|
||||
'category': 'FLANGE',
|
||||
'subcategory': flange_result.get('flange_type', 'UNKNOWN'),
|
||||
'standard': flange_result.get('standard', ''),
|
||||
'grade': flange_result.get('grade', ''),
|
||||
'confidence': flange_result.get('confidence', 0),
|
||||
'details': flange_result
|
||||
}
|
||||
|
||||
# 3. 피팅 분류
|
||||
fitting_result = classify_fitting(description)
|
||||
if fitting_result.get('confidence', 0) > 0.7:
|
||||
return {
|
||||
'category': 'FITTING',
|
||||
'subcategory': fitting_result.get('fitting_type', 'UNKNOWN'),
|
||||
'standard': fitting_result.get('standard', ''),
|
||||
'grade': fitting_result.get('grade', ''),
|
||||
'confidence': fitting_result.get('confidence', 0),
|
||||
'details': fitting_result
|
||||
}
|
||||
|
||||
# 4. 가스켓 분류
|
||||
gasket_result = classify_gasket(description)
|
||||
if gasket_result.get('confidence', 0) > 0.7:
|
||||
return {
|
||||
'category': 'GASKET',
|
||||
'subcategory': gasket_result.get('gasket_type', 'UNKNOWN'),
|
||||
'standard': gasket_result.get('standard', ''),
|
||||
'grade': gasket_result.get('grade', ''),
|
||||
'confidence': gasket_result.get('confidence', 0),
|
||||
'details': gasket_result
|
||||
}
|
||||
|
||||
# 5. 계기 분류
|
||||
instrument_result = classify_instrument(description)
|
||||
if instrument_result.get('confidence', 0) > 0.7:
|
||||
return {
|
||||
'category': 'INSTRUMENT',
|
||||
'subcategory': instrument_result.get('instrument_type', 'UNKNOWN'),
|
||||
'standard': instrument_result.get('standard', ''),
|
||||
'grade': instrument_result.get('grade', ''),
|
||||
'confidence': instrument_result.get('confidence', 0),
|
||||
'details': instrument_result
|
||||
}
|
||||
|
||||
# 6. 파이프 분류
|
||||
pipe_result = classify_pipe(description)
|
||||
if pipe_result.get('confidence', 0) > 0.7:
|
||||
return {
|
||||
'category': 'PIPE',
|
||||
'subcategory': pipe_result.get('pipe_type', 'UNKNOWN'),
|
||||
'standard': pipe_result.get('standard', ''),
|
||||
'grade': pipe_result.get('grade', ''),
|
||||
'confidence': pipe_result.get('confidence', 0),
|
||||
'details': pipe_result
|
||||
}
|
||||
|
||||
# 7. 밸브 분류
|
||||
valve_result = classify_valve(description)
|
||||
if valve_result.get('confidence', 0) > 0.7:
|
||||
return {
|
||||
'category': 'VALVE',
|
||||
'subcategory': valve_result.get('valve_type', 'UNKNOWN'),
|
||||
'standard': valve_result.get('standard', ''),
|
||||
'grade': valve_result.get('grade', ''),
|
||||
'confidence': valve_result.get('confidence', 0),
|
||||
'details': valve_result
|
||||
}
|
||||
|
||||
# 8. 재질 분류 (기본)
|
||||
material_result = classify_material(description)
|
||||
if material_result.get('confidence', 0) > 0.5:
|
||||
return {
|
||||
'category': 'MATERIAL',
|
||||
'subcategory': material_result.get('material_type', 'UNKNOWN'),
|
||||
'standard': material_result.get('standard', ''),
|
||||
'grade': material_result.get('grade', ''),
|
||||
'confidence': material_result.get('confidence', 0),
|
||||
'details': material_result
|
||||
}
|
||||
|
||||
# 9. 기본 분류 (키워드 기반)
|
||||
category = get_major_category(description)
|
||||
return {
|
||||
'category': category.upper(),
|
||||
'subcategory': 'UNKNOWN',
|
||||
'standard': '',
|
||||
'grade': '',
|
||||
'confidence': 0.3,
|
||||
'details': {}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ async def get_jobs(
|
||||
search: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Job 목록 조회"""
|
||||
"""Job 목록 조회 (job_name을 프로젝트명으로 사용)"""
|
||||
try:
|
||||
query = """
|
||||
SELECT job_no, job_name, client_name, end_user, epc_company,
|
||||
@@ -68,7 +68,8 @@ async def get_jobs(
|
||||
"delivery_terms": job.delivery_terms,
|
||||
"status": job.status,
|
||||
"description": job.description,
|
||||
"created_at": job.created_at
|
||||
"created_at": job.created_at,
|
||||
"project_name": job.job_name # job_name을 프로젝트명으로 사용
|
||||
}
|
||||
for job in jobs
|
||||
]
|
||||
|
||||
@@ -10,6 +10,7 @@ alembic==1.13.1
|
||||
# 파일 처리
|
||||
pandas==2.1.4
|
||||
openpyxl==3.1.2
|
||||
xlrd>=2.0.1
|
||||
python-multipart==0.0.6
|
||||
|
||||
# 데이터 검증
|
||||
|
||||
18
backend/scripts/04_add_job_no_to_files.sql
Normal file
18
backend/scripts/04_add_job_no_to_files.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- files 테이블에 job_no 컬럼 추가
|
||||
-- 실행일: 2025.07.16
|
||||
|
||||
-- job_no 컬럼 추가
|
||||
ALTER TABLE files ADD COLUMN job_no VARCHAR(50);
|
||||
|
||||
-- job_no에 인덱스 추가
|
||||
CREATE INDEX idx_files_job_no ON files(job_no);
|
||||
|
||||
-- 기존 데이터가 있다면 project_id를 기반으로 job_no 설정
|
||||
-- (이 부분은 실제 데이터가 있을 때만 실행)
|
||||
-- UPDATE files SET job_no = (SELECT official_project_code FROM projects WHERE projects.id = files.project_id);
|
||||
|
||||
-- job_no를 NOT NULL로 설정 (데이터 마이그레이션 후)
|
||||
-- ALTER TABLE files ALTER COLUMN job_no SET NOT NULL;
|
||||
|
||||
-- project_id 컬럼 제거 (선택사항 - 기존 데이터 백업 후)
|
||||
-- ALTER TABLE files DROP COLUMN project_id;
|
||||
80
backend/scripts/05_add_classification_columns.sql
Normal file
80
backend/scripts/05_add_classification_columns.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
-- 분류 결과 저장을 위한 컬럼 추가
|
||||
-- 2024년 BOM 분류 시스템 개선
|
||||
|
||||
-- materials 테이블에 분류 관련 컬럼 추가
|
||||
ALTER TABLE materials ADD COLUMN IF NOT EXISTS subcategory VARCHAR(100);
|
||||
ALTER TABLE materials ADD COLUMN IF NOT EXISTS standard VARCHAR(200);
|
||||
ALTER TABLE materials ADD COLUMN IF NOT EXISTS grade VARCHAR(200);
|
||||
ALTER TABLE materials ADD COLUMN IF NOT EXISTS classification_details JSONB;
|
||||
|
||||
-- files 테이블에 분류 통계 컬럼 추가
|
||||
ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_stats JSONB;
|
||||
ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_completed BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE files ADD COLUMN IF NOT EXISTS classification_timestamp TIMESTAMP;
|
||||
|
||||
-- 인덱스 추가 (성능 향상)
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_classified_category ON materials(classified_category);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_subcategory ON materials(subcategory);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_standard ON materials(standard);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_grade ON materials(grade);
|
||||
CREATE INDEX IF NOT EXISTS idx_materials_classification_confidence ON materials(classification_confidence);
|
||||
|
||||
-- 기존 데이터에 대한 기본값 설정
|
||||
UPDATE materials SET
|
||||
subcategory = COALESCE(subcategory, ''),
|
||||
standard = COALESCE(standard, ''),
|
||||
grade = COALESCE(grade, ''),
|
||||
classification_details = COALESCE(classification_details, '{}'::jsonb)
|
||||
WHERE subcategory IS NULL OR standard IS NULL OR grade IS NULL OR classification_details IS NULL;
|
||||
|
||||
-- 분류 완료된 파일들 업데이트
|
||||
UPDATE files SET
|
||||
classification_completed = TRUE,
|
||||
classification_timestamp = created_at
|
||||
WHERE parsed_count > 0;
|
||||
|
||||
-- 통계 뷰 생성 (분류 결과 통계 조회용)
|
||||
CREATE OR REPLACE VIEW classification_summary AS
|
||||
SELECT
|
||||
f.job_no,
|
||||
f.original_filename,
|
||||
f.parsed_count,
|
||||
f.classification_completed,
|
||||
f.classification_timestamp,
|
||||
COUNT(*) as total_materials,
|
||||
COUNT(CASE WHEN m.classified_category = 'BOLT' THEN 1 END) as bolt_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'FLANGE' THEN 1 END) as flange_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'FITTING' THEN 1 END) as fitting_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'GASKET' THEN 1 END) as gasket_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'INSTRUMENT' THEN 1 END) as instrument_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'PIPE' THEN 1 END) as pipe_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'VALVE' THEN 1 END) as valve_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'MATERIAL' THEN 1 END) as material_count,
|
||||
COUNT(CASE WHEN m.classified_category = 'OTHER' THEN 1 END) as other_count,
|
||||
AVG(m.classification_confidence) as avg_confidence,
|
||||
COUNT(CASE WHEN m.is_verified = TRUE THEN 1 END) as verified_count
|
||||
FROM files f
|
||||
LEFT JOIN materials m ON f.id = m.file_id
|
||||
WHERE f.is_active = TRUE
|
||||
GROUP BY f.id, f.job_no, f.original_filename, f.parsed_count, f.classification_completed, f.classification_timestamp;
|
||||
|
||||
-- 분류 성능 통계 뷰
|
||||
CREATE OR REPLACE VIEW classification_performance AS
|
||||
SELECT
|
||||
classified_category,
|
||||
subcategory,
|
||||
standard,
|
||||
COUNT(*) as total_count,
|
||||
AVG(classification_confidence) as avg_confidence,
|
||||
COUNT(CASE WHEN is_verified = TRUE THEN 1 END) as verified_count,
|
||||
COUNT(CASE WHEN is_verified = FALSE THEN 1 END) as unverified_count,
|
||||
ROUND(
|
||||
(COUNT(CASE WHEN is_verified = TRUE THEN 1 END)::DECIMAL / COUNT(*) * 100), 2
|
||||
) as verification_rate
|
||||
FROM materials
|
||||
WHERE classified_category IS NOT NULL
|
||||
GROUP BY classified_category, subcategory, standard
|
||||
ORDER BY total_count DESC;
|
||||
|
||||
-- 변경사항 확인
|
||||
SELECT 'Database schema updated successfully' as status;
|
||||
Reference in New Issue
Block a user