✅ 백엔드 구조 개선: - DatabaseService: 공통 DB 쿼리 로직 통합 - FileUploadService: 파일 업로드 로직 모듈화 및 트랜잭션 관리 개선 - 서비스 레이어 패턴 도입으로 코드 재사용성 향상 ✅ 프론트엔드 컴포넌트 개선: - LoadingSpinner, ErrorMessage, ConfirmDialog 공통 컴포넌트 생성 - 재사용 가능한 컴포넌트 라이브러리 구축 - deprecated/backup 파일들 완전 제거 ✅ 성능 최적화: - optimize_database.py: 핵심 DB 인덱스 자동 생성 - 쿼리 최적화 및 통계 업데이트 자동화 - VACUUM ANALYZE 자동 실행 ✅ 코드 정리: - 개별 SQL 마이그레이션 파일들을 legacy/ 폴더로 정리 - 중복된 마이그레이션 스크립트 정리 - 깔끔하고 체계적인 프로젝트 구조 완성 ✅ 자동 마이그레이션 시스템 강화: - complete_migrate.py: SQLAlchemy 기반 완전한 마이그레이션 - analyze_and_fix_schema.py: 백엔드 코드 분석 기반 스키마 수정 - fix_missing_tables.py: 누락된 테이블/컬럼 자동 생성 - start.sh: 배포 시 자동 실행 순서 최적화
This commit is contained in:
@@ -338,48 +338,68 @@ async def upload_file(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
# 🎯 트랜잭션 오류 방지: 완전한 트랜잭션 초기화
|
||||
"""
|
||||
파일 업로드 API - 리팩토링된 서비스 레이어 사용
|
||||
"""
|
||||
from ..services.file_upload_service import FileUploadService
|
||||
|
||||
upload_service = FileUploadService(db)
|
||||
|
||||
try:
|
||||
# 1. 현재 트랜잭션 완전 롤백
|
||||
db.rollback()
|
||||
print("🔄 1단계: 이전 트랜잭션 롤백 완료")
|
||||
# 1. 업로드 요청 검증
|
||||
upload_service.validate_upload_request(file, job_no)
|
||||
|
||||
# 2. 세션 상태 초기화
|
||||
db.close()
|
||||
print("🔄 2단계: 세션 닫기 완료")
|
||||
# 2. 파일 저장
|
||||
unique_filename, file_path = upload_service.save_uploaded_file(file)
|
||||
|
||||
# 3. 새 세션 생성
|
||||
from ..database import get_db
|
||||
db = next(get_db())
|
||||
print("🔄 3단계: 새 세션 생성 완료")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 트랜잭션 초기화 중 오류: {e}")
|
||||
# 오류 발생 시에도 계속 진행
|
||||
# 로그 제거
|
||||
if not validate_file_extension(file.filename):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"지원하지 않는 파일 형식입니다. 허용된 확장자: {', '.join(ALLOWED_EXTENSIONS)}"
|
||||
# 3. 파일 레코드 생성
|
||||
file_record = upload_service.create_file_record(
|
||||
filename=unique_filename,
|
||||
original_filename=file.filename,
|
||||
file_path=str(file_path),
|
||||
job_no=job_no,
|
||||
revision=revision,
|
||||
bom_name=bom_name,
|
||||
file_size=file.size or 0,
|
||||
parsed_count=0, # 임시값, 파싱 후 업데이트
|
||||
uploaded_by=current_user.get('username', 'unknown'),
|
||||
parent_file_id=parent_file_id
|
||||
)
|
||||
|
||||
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)
|
||||
# 로그 제거
|
||||
|
||||
# 4. 자재 데이터 처리
|
||||
processing_result = upload_service.process_materials_data(
|
||||
file_path=file_path,
|
||||
file_id=file_record.id,
|
||||
job_no=job_no,
|
||||
revision=revision,
|
||||
parent_file_id=parent_file_id
|
||||
)
|
||||
|
||||
# 5. 파일 레코드 업데이트 (파싱된 자재 수)
|
||||
file_record.parsed_count = processing_result['materials_count']
|
||||
db.commit()
|
||||
|
||||
logger.info(f"File upload completed: {file_record.id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "파일 업로드 및 처리가 완료되었습니다.",
|
||||
"file_id": file_record.id,
|
||||
"filename": file_record.filename,
|
||||
"materials_count": processing_result['materials_count'],
|
||||
"classification_results": processing_result['classification_results']
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
# HTTP 예외는 그대로 전달
|
||||
upload_service.cleanup_failed_upload(file_path if 'file_path' in locals() else None)
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"파일 저장 실패: {str(e)}")
|
||||
|
||||
try:
|
||||
# 로그 제거
|
||||
materials_data = parse_file_data(str(file_path))
|
||||
# 기타 예외 처리
|
||||
db.rollback()
|
||||
upload_service.cleanup_failed_upload(file_path if 'file_path' in locals() else None)
|
||||
logger.error(f"File upload failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"파일 업로드 실패: {str(e)}")
|
||||
parsed_count = len(materials_data)
|
||||
# 로그 제거
|
||||
|
||||
@@ -1861,26 +1881,87 @@ async def get_files_stats(db: Session = Depends(get_db)):
|
||||
|
||||
@router.delete("/delete/{file_id}")
|
||||
async def delete_file(file_id: int, db: Session = Depends(get_db)):
|
||||
"""파일 삭제"""
|
||||
"""파일 삭제 - 모든 관련 데이터를 올바른 순서로 삭제"""
|
||||
try:
|
||||
# 자재 먼저 삭제
|
||||
db.execute(text("DELETE FROM materials WHERE file_id = :file_id"), {"file_id": file_id})
|
||||
|
||||
# 파일 삭제
|
||||
result = db.execute(text("DELETE FROM files WHERE id = :file_id"), {"file_id": file_id})
|
||||
|
||||
if result.rowcount == 0:
|
||||
# 파일 존재 확인
|
||||
file_check = db.execute(text("SELECT id FROM files WHERE id = :file_id"), {"file_id": file_id}).fetchone()
|
||||
if not file_check:
|
||||
raise HTTPException(status_code=404, detail="파일을 찾을 수 없습니다")
|
||||
|
||||
# 1. 자재 관련 상세 테이블들 먼저 삭제 (외래키 순서 고려)
|
||||
detail_tables = [
|
||||
"support_details", "fitting_details", "flange_details",
|
||||
"valve_details", "gasket_details", "bolt_details",
|
||||
"instrument_details", "user_requirements"
|
||||
]
|
||||
|
||||
for table in detail_tables:
|
||||
try:
|
||||
db.execute(text(f"DELETE FROM {table} WHERE file_id = :file_id"), {"file_id": file_id})
|
||||
except Exception as detail_error:
|
||||
# 테이블이 없거나 다른 오류가 있어도 계속 진행
|
||||
print(f"Warning: Failed to delete from {table}: {detail_error}")
|
||||
|
||||
# 2. 구매 관련 테이블 삭제
|
||||
try:
|
||||
# purchase_request_items 먼저 삭제 (purchase_requests를 참조하므로)
|
||||
db.execute(text("""
|
||||
DELETE FROM purchase_request_items
|
||||
WHERE request_id IN (
|
||||
SELECT request_id FROM purchase_requests WHERE file_id = :file_id
|
||||
)
|
||||
"""), {"file_id": file_id})
|
||||
|
||||
# purchase_requests 삭제
|
||||
db.execute(text("DELETE FROM purchase_requests WHERE file_id = :file_id"), {"file_id": file_id})
|
||||
except Exception as purchase_error:
|
||||
print(f"Warning: Failed to delete purchase data: {purchase_error}")
|
||||
|
||||
# 3. 기타 관련 테이블들 삭제
|
||||
try:
|
||||
# excel_exports 삭제
|
||||
db.execute(text("DELETE FROM excel_exports WHERE file_id = :file_id"), {"file_id": file_id})
|
||||
except Exception as excel_error:
|
||||
print(f"Warning: Failed to delete from excel_exports: {excel_error}")
|
||||
|
||||
try:
|
||||
# user_activity_logs 삭제 (target_id와 target_type 사용)
|
||||
db.execute(text("DELETE FROM user_activity_logs WHERE target_id = :file_id AND target_type = 'file'"), {"file_id": file_id})
|
||||
except Exception as activity_error:
|
||||
print(f"Warning: Failed to delete from user_activity_logs: {activity_error}")
|
||||
|
||||
try:
|
||||
# exported_materials 삭제 (먼저 export_id 조회 후 삭제)
|
||||
export_ids = db.execute(text("SELECT id FROM excel_exports WHERE file_id = :file_id"), {"file_id": file_id}).fetchall()
|
||||
for export_row in export_ids:
|
||||
db.execute(text("DELETE FROM exported_materials WHERE export_id = :export_id"), {"export_id": export_row[0]})
|
||||
except Exception as exported_error:
|
||||
print(f"Warning: Failed to delete from exported_materials: {exported_error}")
|
||||
|
||||
# 4. 자재 테이블 삭제
|
||||
materials_result = db.execute(text("DELETE FROM materials WHERE file_id = :file_id"), {"file_id": file_id})
|
||||
print(f"Deleted {materials_result.rowcount} materials")
|
||||
|
||||
# 5. 마지막으로 파일 삭제
|
||||
file_result = db.execute(text("DELETE FROM files WHERE id = :file_id"), {"file_id": file_id})
|
||||
|
||||
if file_result.rowcount == 0:
|
||||
raise HTTPException(status_code=404, detail="파일 삭제에 실패했습니다")
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "파일이 삭제되었습니다"
|
||||
"message": "파일과 모든 관련 데이터가 삭제되었습니다",
|
||||
"deleted_materials": materials_result.rowcount
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"File deletion error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"파일 삭제 실패: {str(e)}")
|
||||
|
||||
@router.get("/materials-v2") # 완전히 새로운 엔드포인트
|
||||
|
||||
Reference in New Issue
Block a user