166 lines
4.9 KiB
Python
166 lines
4.9 KiB
Python
"""
|
|
엑셀 파일 업로드 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)}"
|
|
)
|