feat: Add monthly stats excel upload endpoint
This commit is contained in:
parent
02a9149f55
commit
d9de2e939b
|
|
@ -298,3 +298,135 @@ async def upload_plant_data(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"DB 저장 실패: {str(e)}"
|
detail=f"DB 저장 실패: {str(e)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.post("/plants/{plant_id}/upload/monthly")
|
||||||
|
async def upload_plant_monthly_data(
|
||||||
|
plant_id: str,
|
||||||
|
file: UploadFile = File(..., description="월간 발전량 엑셀 파일 (year, month, kwh)"),
|
||||||
|
db: Client = Depends(get_db)
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
월간 발전 데이터 엑셀 업로드
|
||||||
|
|
||||||
|
엑셀 컬럼 형식:
|
||||||
|
- year: 연도 (2014년 등, 병합된 셀 지원)
|
||||||
|
- month: 월 (1월, 2월, 합계, 평균 등)
|
||||||
|
- kwh: 발전량 (1,234.56)
|
||||||
|
|
||||||
|
기능:
|
||||||
|
- '합계', '평균' 행은 자동으로 무시
|
||||||
|
- 'year' 컬럼이 비어있으면 이전 행의 값 사용 (ffill)
|
||||||
|
- 'month', 'kwh'의 특수문자(월, 콤마 등) 제거 및 숫자 변환
|
||||||
|
- monthly_stats 테이블에 저장
|
||||||
|
"""
|
||||||
|
# 1. 파일 확장자 검증
|
||||||
|
if not file.filename.endswith(('.xlsx', '.xls')):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="엑셀 파일(.xlsx, .xls)만 업로드 가능합니다."
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 발전소 존재 확인
|
||||||
|
try:
|
||||||
|
plant_check = db.table("plants").select("id").eq("id", plant_id).single().execute()
|
||||||
|
if not plant_check.data:
|
||||||
|
raise HTTPException(status_code=404, detail="발전소를 찾을 수 없습니다.")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"발전소 확인 실패: {e}")
|
||||||
|
|
||||||
|
# 3. 엑셀 파싱 및 전처리
|
||||||
|
try:
|
||||||
|
contents = await file.read()
|
||||||
|
df = pd.read_excel(io.BytesIO(contents))
|
||||||
|
|
||||||
|
# 컬럼명 정규화 (소문자, 공백제거)
|
||||||
|
df.columns = [str(col).lower().strip() for col in df.columns]
|
||||||
|
|
||||||
|
# 필수 컬럼 체크
|
||||||
|
# 사용자가 제공한 엑셀은 header가 없을 수도 있고, 1행이 header일 수도 있음.
|
||||||
|
# 일단 year, month, kwh가 포함되어 있다고 가정하거나, 첫 3열을 사용해야 할 수도 있음.
|
||||||
|
# 이미지에서는 header가 명확함 (year, month, kwh).
|
||||||
|
|
||||||
|
required = {'year', 'month', 'kwh'}
|
||||||
|
if not required.issubset(df.columns):
|
||||||
|
# 혹시 한글 헤더일 경우 매핑 시도
|
||||||
|
rename_map = {'년도': 'year', '월': 'month', '발전량': 'kwh', '발전량(kwh)': 'kwh'}
|
||||||
|
df.rename(columns=rename_map, inplace=True)
|
||||||
|
|
||||||
|
if not required.issubset(df.columns):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"필수 컬럼(year, month, kwh)이 누락되었습니다. 현재 컬럼: {list(df.columns)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A열(year) 병합된 셀 처리 (Forward Fill)
|
||||||
|
df['year'] = df['year'].fillna(method='ffill')
|
||||||
|
|
||||||
|
records = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
# 1. Year 파싱
|
||||||
|
y_raw = str(row['year']).replace('년', '').strip()
|
||||||
|
if not y_raw or y_raw.lower() == 'nan':
|
||||||
|
continue
|
||||||
|
year_val = int(float(y_raw)) # 2014.0 -> 2014
|
||||||
|
|
||||||
|
# 2. Month 파싱
|
||||||
|
m_raw = str(row['month']).strip()
|
||||||
|
if m_raw in ['합계', '평균', 'nan', 'None']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# '1월' -> 1
|
||||||
|
month_val_str = m_raw.replace('월', '').strip()
|
||||||
|
if not month_val_str.isdigit():
|
||||||
|
continue # '합계' 등이 걸러지지 않은 경우 대비
|
||||||
|
|
||||||
|
month_val = int(month_val_str)
|
||||||
|
if not (1 <= month_val <= 12):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3. Kwh 파싱
|
||||||
|
k_raw = str(row['kwh']).replace(',', '').strip()
|
||||||
|
if not k_raw or k_raw.lower() == 'nan':
|
||||||
|
kwh_val = 0.0
|
||||||
|
else:
|
||||||
|
kwh_val = float(k_raw)
|
||||||
|
|
||||||
|
# 포맷: YYYY-MM
|
||||||
|
month_key = f"{year_val}-{month_val:02d}"
|
||||||
|
|
||||||
|
records.append({
|
||||||
|
"plant_id": plant_id,
|
||||||
|
"month": month_key,
|
||||||
|
"total_generation": kwh_val,
|
||||||
|
"updated_at": datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Row {idx}: {e}")
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
raise HTTPException(status_code=400, detail="유효한 데이터가 없습니다.")
|
||||||
|
|
||||||
|
# 4. DB Upsert
|
||||||
|
# monthly_stats 테이블 생성 여부 확인이 필요하지만, 이미 되어있다고 가정
|
||||||
|
res = db.table("monthly_stats").upsert(
|
||||||
|
records,
|
||||||
|
on_conflict="plant_id, month"
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": f"총 {len(records)}건의 월간 데이터가 업로드되었습니다.",
|
||||||
|
"saved_count": len(records),
|
||||||
|
"errors": errors[:5]
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"처리 중 오류: {str(e)}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user