diff --git a/backend/app/routers/files.py b/backend/app/routers/files.py index 096520c..8cbd861 100644 --- a/backend/app/routers/files.py +++ b/backend/app/routers/files.py @@ -11,7 +11,7 @@ import re from pathlib import Path from ..database import get_db - +from app.services.material_classifier import classify_material router = APIRouter() UPLOAD_DIR = Path("uploads") @@ -140,17 +140,6 @@ async def upload_file( revision: str = Form("Rev.0"), db: Session = Depends(get_db) ): - # 1. Job 검증 - job_validation = await validate_job_exists(job_no, db) - if not job_validation["valid"]: - raise HTTPException( - status_code=400, - detail=f"Job 오류: {job_validation['error']}" - ) - - job_info = job_validation["job"] - - # 2. 파일 검증 if not validate_file_extension(file.filename): raise HTTPException( status_code=400, @@ -160,7 +149,6 @@ async def upload_file( if file.size and file.size > 10 * 1024 * 1024: raise HTTPException(status_code=400, detail="파일 크기는 10MB를 초과할 수 없습니다") - # 3. 파일 저장 unique_filename = generate_unique_filename(file.filename) file_path = UPLOAD_DIR / unique_filename @@ -170,7 +158,6 @@ async def upload_file( except Exception as e: raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}") - # 4. 파일 파싱 및 자재 추출 try: materials_data = parse_file_data(str(file_path)) parsed_count = len(materials_data) @@ -188,7 +175,7 @@ async def upload_file( "file_path": str(file_path), "job_no": job_no, "revision": revision, - "description": f"BOM 파일 - {parsed_count}개 자재 ({job_info['job_name']})", + "description": f"BOM 파일 - {parsed_count}개 자재", "file_size": file.size, "parsed_count": parsed_count, "is_active": True @@ -221,8 +208,8 @@ async def upload_file( "material_grade": material_data["material_grade"], "line_number": material_data["line_number"], "row_number": material_data["row_number"], - "classified_category": None, - "classification_confidence": None, + "classified_category": get_major_category(material_data["original_description"]), + "classification_confidence": 0.9, "is_verified": False, "created_at": datetime.now() }) @@ -232,14 +219,11 @@ async def upload_file( return { "success": True, - "message": f"Job '{job_info['job_name']}'에 BOM 파일 업로드 완료!", - "job": job_info, - "file": { - "id": file_id, - "original_filename": file.filename, - "parsed_count": parsed_count, - "saved_count": materials_inserted - }, + "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 [] } @@ -248,6 +232,96 @@ async def upload_file( 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, m.classified_category, m.classification_confidence + 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": row.job_no, + "project_code": row.job_no, + "project_name": "Job-" + str(f.job_no) if f.job_no else "Unknown", + "original_description": row.original_description, + "quantity": float(m.quantity) if m.quantity else 0, + "unit": m.unit, + "classified_category": row.classified_category, + "classification_confidence": float(row.classification_confidence) if m.classification_confidence else 0.0, + "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( @@ -325,3 +399,23 @@ async def validate_job_exists(job_no: str, db: Session): except Exception as e: return {"valid": False, "error": f"Job 검증 실패: {str(e)}"} +def get_major_category(description): + """간단한 키워드 기반 대분류""" + desc_upper = description.upper() + + if 'PIPE' in desc_upper or 'TUBE' in desc_upper: + return 'pipe' + elif any(word in desc_upper for word in ['ELBOW', 'ELL', 'TEE', 'REDUCER', 'CAP', 'COUPLING']): + return 'fitting' + elif 'VALVE' in desc_upper: + return 'valve' + elif 'FLANGE' in desc_upper or 'FLG' in desc_upper: + return 'flange' + elif any(word in desc_upper for word in ['GAUGE', 'SENSOR', 'INSTRUMENT', 'TRANSMITTER']): + return 'instrument' + elif 'GASKET' in desc_upper or 'GASK' in desc_upper: + return 'gasket' + elif any(word in desc_upper for word in ['BOLT', 'STUD', 'NUT', 'SCREW']): + return 'bolt' + else: + return 'other' diff --git a/backend/temp_new_upload.py b/backend/temp_new_upload.py new file mode 100644 index 0000000..966a99d --- /dev/null +++ b/backend/temp_new_upload.py @@ -0,0 +1,120 @@ +@router.post("/upload") +async def upload_file( + file: UploadFile = File(...), + job_no: str = Form(...), + revision: str = Form("Rev.0"), + db: Session = Depends(get_db) +): + # 1. Job 검증 (새로 추가!) + job_validation = await validate_job_exists(job_no, db) + if not job_validation["valid"]: + raise HTTPException( + status_code=400, + detail=f"Job 오류: {job_validation['error']}" + ) + + job_info = job_validation["job"] + print(f"✅ Job 검증 완료: {job_info['job_no']} - {job_info['job_name']}") + + # 2. 파일 검증 + 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를 초과할 수 없습니다") + + # 3. 파일 저장 + 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)}") + + # 4. 파일 파싱 및 자재 추출 + try: + materials_data = parse_file_data(str(file_path)) + parsed_count = len(materials_data) + + # 파일 정보 저장 (job_no 사용!) + 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, # job_no 사용! + "revision": revision, + "description": f"BOM 파일 - {parsed_count}개 자재 ({job_info['job_name']})", + "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"Job '{job_info['job_name']}'에 BOM 파일 업로드 완료!", + "job": { + "job_no": job_info["job_no"], + "job_name": job_info["job_name"], + "status": job_info["status"] + }, + "file": { + "id": file_id, + "original_filename": file.filename, + "parsed_count": parsed_count, + "saved_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)}") diff --git a/backend/temp_upload_fix.py b/backend/temp_upload_fix.py new file mode 100644 index 0000000..5bbf33b --- /dev/null +++ b/backend/temp_upload_fix.py @@ -0,0 +1,13 @@ +# upload 함수에 추가할 Job 검증 로직 + +# Form 파라미터 받은 직후에 추가: +# Job 검증 +job_validation = await validate_job_exists(job_no, db) +if not job_validation["valid"]: + raise HTTPException( + status_code=400, + detail=f"Job 오류: {job_validation['error']}" + ) + +job_info = job_validation["job"] +print(f"✅ Job 검증 완료: {job_info['job_no']} - {job_info['job_name']}") diff --git a/backend/test_mixed_bom.csv b/backend/test_mixed_bom.csv new file mode 100644 index 0000000..ccab0b0 --- /dev/null +++ b/backend/test_mixed_bom.csv @@ -0,0 +1,6 @@ +Description,Quantity,Unit,Size +"PIPE ASTM A106 GR.B",10,EA,4" +"GATE VALVE ASTM A216",2,EA,4" +"FLANGE WELD NECK RF",8,EA,4" +"90 DEG ELBOW",5,EA,4" +"GASKET SPIRAL WOUND",4,EA,4"