diff --git a/app/main.py b/app/main.py index 63f244a..248a102 100644 --- a/app/main.py +++ b/app/main.py @@ -7,7 +7,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.core.config import get_settings -from app.routers import plants +from app.routers import plants, upload # 설정 로드 settings = get_settings() @@ -32,6 +32,7 @@ app.add_middleware( # 라우터 등록 app.include_router(plants.router) +app.include_router(upload.router) @app.get("/", tags=["Health"]) diff --git a/app/routers/upload.py b/app/routers/upload.py new file mode 100644 index 0000000..b1c1e08 --- /dev/null +++ b/app/routers/upload.py @@ -0,0 +1,165 @@ +""" +엑셀 파일 업로드 API +- 과거 발전 데이터(Excel)를 업로드하여 daily_stats 테이블에 저장 +""" + +import io +from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends +from supabase import Client +import pandas as pd + +from app.core.database import get_db + +router = APIRouter( + prefix="/upload", + tags=["Upload"] +) + + +@router.post("/history") +async def upload_history( + file: UploadFile = File(..., description="엑셀 파일 (.xlsx, .xls)"), + plant_id: str = Form(..., description="발전소 ID (예: nrems-01)"), + db: Client = Depends(get_db) +) -> dict: + """ + 과거 발전 데이터 엑셀 업로드 + + 엑셀 컬럼 형식: + - date: 날짜 (YYYY-MM-DD) + - generation: 발전량 (kWh) + + Args: + file: 엑셀 파일 + plant_id: 발전소 ID + + Returns: + 저장 결과 메시지 + """ + # 1. 파일 확장자 검증 + if not file.filename.endswith(('.xlsx', '.xls')): + raise HTTPException( + status_code=400, + detail="엑셀 파일(.xlsx, .xls)만 업로드 가능합니다." + ) + + # 2. 발전소 정보 조회 (capacity 필요) + try: + plant_response = db.table("plants") \ + .select("id, capacity") \ + .eq("id", plant_id) \ + .single() \ + .execute() + + if not plant_response.data: + raise HTTPException( + status_code=404, + detail=f"발전소 '{plant_id}'를 찾을 수 없습니다." + ) + + capacity = plant_response.data.get('capacity', 99.0) + if capacity <= 0: + capacity = 99.0 # 기본값 + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"발전소 조회 실패: {str(e)}" + ) + + # 3. 엑셀 파일 읽기 + try: + contents = await file.read() + df = pd.read_excel(io.BytesIO(contents)) + + # 컬럼 확인 + required_columns = ['date', 'generation'] + missing_columns = [col for col in required_columns if col not in df.columns] + if missing_columns: + raise HTTPException( + status_code=400, + detail=f"필수 컬럼이 없습니다: {missing_columns}. 엑셀에는 'date', 'generation' 컬럼이 필요합니다." + ) + + # 빈 데이터 체크 + if df.empty: + raise HTTPException( + status_code=400, + detail="엑셀 파일에 데이터가 없습니다." + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=400, + detail=f"엑셀 파일 읽기 실패: {str(e)}" + ) + + # 4. 데이터 변환 및 저장 준비 + records = [] + errors = [] + + for idx, row in df.iterrows(): + try: + # 날짜 파싱 + date_val = row['date'] + if pd.isna(date_val): + errors.append(f"행 {idx+2}: 날짜가 비어있습니다.") + continue + + if isinstance(date_val, str): + date_str = date_val.strip() + else: + date_str = pd.to_datetime(date_val).strftime('%Y-%m-%d') + + # 발전량 + generation = float(row['generation']) if pd.notna(row['generation']) else 0.0 + + # generation_hours 계산 + generation_hours = round(generation / capacity, 2) if capacity > 0 else 0.0 + + record = { + 'plant_id': plant_id, + 'date': date_str, + 'total_generation': round(generation, 2), + 'peak_kw': 0.0, # 과거 데이터에서는 알 수 없음 + 'generation_hours': generation_hours + } + records.append(record) + + except Exception as e: + errors.append(f"행 {idx+2}: {str(e)}") + + if not records: + raise HTTPException( + status_code=400, + detail=f"저장할 유효한 데이터가 없습니다. 오류: {errors[:5]}" + ) + + # 5. DB Upsert + try: + result = db.table("daily_stats").upsert( + records, + on_conflict="plant_id,date" + ).execute() + + response_msg = f"총 {len(records)}건의 데이터가 저장되었습니다." + if errors: + response_msg += f" (오류 {len(errors)}건 스킵)" + + return { + "status": "success", + "message": response_msg, + "saved_count": len(records), + "error_count": len(errors), + "errors": errors[:10] if errors else [] # 최대 10개 오류만 반환 + } + + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"DB 저장 실패: {str(e)}" + )