Initial commit: FastAPI middleware server for solar power monitoring

This commit is contained in:
haneulai 2026-01-22 14:22:47 +09:00
commit 8878303cac
12 changed files with 385 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# Environment
.env
.env.local
.env.*.local
# Virtual Environment
venv/
.venv/
env/
ENV/
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Distribution / Build
dist/
build/
*.egg-info/
# Logs
*.log

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
# Solar Power Monitoring API Server

1
app/core/__init__.py Normal file
View File

@ -0,0 +1 @@
# Core configuration module

33
app/core/config.py Normal file
View File

@ -0,0 +1,33 @@
"""
환경변수 설정 모듈
- python-dotenv를 사용하여 .env 파일에서 환경변수 로드
"""
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
"""애플리케이션 설정"""
# Supabase 설정
SUPABASE_URL: str
SUPABASE_KEY: str
# 앱 설정
APP_NAME: str = "Solar Power Monitoring API"
APP_VERSION: str = "1.0.0"
DEBUG: bool = False
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
@lru_cache()
def get_settings() -> Settings:
"""
설정 싱글톤 인스턴스 반환
lru_cache를 사용하여 번만 로드
"""
return Settings()

28
app/core/database.py Normal file
View File

@ -0,0 +1,28 @@
"""
Supabase 데이터베이스 클라이언트 모듈
- 싱글톤 패턴으로 클라이언트 인스턴스 관리
"""
from supabase import create_client, Client
from functools import lru_cache
from .config import get_settings
@lru_cache()
def get_supabase_client() -> Client:
"""
Supabase 클라이언트 싱글톤 인스턴스 반환
lru_cache를 사용하여 번만 생성
"""
settings = get_settings()
return create_client(
settings.SUPABASE_URL,
settings.SUPABASE_KEY
)
def get_db() -> Client:
"""
Supabase 클라이언트 반환 (의존성 주입용)
"""
return get_supabase_client()

75
app/main.py Normal file
View File

@ -0,0 +1,75 @@
"""
FastAPI 애플리케이션 진입점
- 태양광 발전 관제 시스템 미들웨어 서버
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import get_settings
from app.routers import plants
# 설정 로드
settings = get_settings()
# FastAPI 앱 생성
app = FastAPI(
title=settings.APP_NAME,
version=settings.APP_VERSION,
description="태양광 발전 관제 시스템을 위한 미들웨어 API 서버",
docs_url="/docs",
redoc_url="/redoc"
)
# CORS 미들웨어 설정 (모든 도메인 허용 - 개발용)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 라우터 등록
app.include_router(plants.router)
@app.get("/", tags=["Health"])
async def health_check() -> dict:
"""
서버 상태 확인 (Health Check)
Returns:
서버 상태 버전 정보
"""
return {
"status": "healthy",
"app_name": settings.APP_NAME,
"version": settings.APP_VERSION
}
@app.get("/health", tags=["Health"])
async def detailed_health_check() -> dict:
"""
상세 서버 상태 확인
Returns:
서버 상태 연결 정보
"""
return {
"status": "healthy",
"app_name": settings.APP_NAME,
"version": settings.APP_VERSION,
"supabase_connected": bool(settings.SUPABASE_URL)
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8000,
reload=settings.DEBUG
)

1
app/routers/__init__.py Normal file
View File

@ -0,0 +1 @@
# API Routers module

18
app/routers/auth.py Normal file
View File

@ -0,0 +1,18 @@
"""
인증 관련 API 엔드포인트 (추후 예정)
- 로그인/로그아웃
- 토큰 관리
"""
from fastapi import APIRouter
router = APIRouter(
prefix="/auth",
tags=["Authentication"]
)
# TODO: 추후 구현 예정
# - POST /auth/login
# - POST /auth/logout
# - POST /auth/refresh

140
app/routers/plants.py Normal file
View File

@ -0,0 +1,140 @@
"""
발전소 관련 API 엔드포인트
- 발전소 목록 조회
- 발전 현황 조회
"""
from fastapi import APIRouter, HTTPException, Depends
from supabase import Client
from typing import List
from app.core.database import get_db
from app.schemas.plant import PlantsListResponse, PlantWithLatestLog, SolarLogBase
router = APIRouter(
prefix="/plants",
tags=["Plants"]
)
@router.get("/{company_id}", response_model=PlantsListResponse)
async def get_plants_by_company(
company_id: int,
db: Client = Depends(get_db)
) -> PlantsListResponse:
"""
특정 업체의 모든 발전소 목록과 최신 발전 현황을 조회합니다.
Args:
company_id: 업체 ID
Returns:
발전소 목록 발전소의 최신 발전 로그
"""
try:
# 1. 해당 업체의 모든 발전소 조회
plants_response = db.table("plants") \
.select("*") \
.eq("company_id", company_id) \
.execute()
plants = plants_response.data
if not plants:
return PlantsListResponse(
status="success",
data=[],
total_count=0
)
# 2. 각 발전소의 최신 발전 로그 조회
result: List[PlantWithLatestLog] = []
for plant in plants:
# 해당 발전소의 최신 로그 1건 조회
log_response = db.table("solar_logs") \
.select("*") \
.eq("plant_id", plant["id"]) \
.order("timestamp", desc=True) \
.limit(1) \
.execute()
latest_log = None
if log_response.data:
latest_log = SolarLogBase(**log_response.data[0])
plant_with_log = PlantWithLatestLog(
**plant,
latest_log=latest_log
)
result.append(plant_with_log)
return PlantsListResponse(
status="success",
data=result,
total_count=len(result)
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"데이터베이스 조회 중 오류가 발생했습니다: {str(e)}"
)
@router.get("/{company_id}/{plant_id}", response_model=dict)
async def get_plant_detail(
company_id: int,
plant_id: int,
db: Client = Depends(get_db)
) -> dict:
"""
특정 발전소의 상세 정보를 조회합니다.
Args:
company_id: 업체 ID
plant_id: 발전소 ID
Returns:
발전소 상세 정보 최근 발전 로그
"""
try:
# 발전소 조회
plant_response = db.table("plants") \
.select("*") \
.eq("id", plant_id) \
.eq("company_id", company_id) \
.single() \
.execute()
if not plant_response.data:
raise HTTPException(
status_code=404,
detail="발전소를 찾을 수 없습니다."
)
plant = plant_response.data
# 최근 발전 로그 10건 조회
logs_response = db.table("solar_logs") \
.select("*") \
.eq("plant_id", plant_id) \
.order("timestamp", desc=True) \
.limit(10) \
.execute()
return {
"status": "success",
"data": {
"plant": plant,
"recent_logs": logs_response.data or []
}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"데이터베이스 조회 중 오류가 발생했습니다: {str(e)}"
)

2
app/schemas/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Pydantic Schemas module
from .plant import PlantBase, PlantResponse, PlantWithLatestLog, PlantsListResponse

46
app/schemas/plant.py Normal file
View File

@ -0,0 +1,46 @@
"""
발전소 관련 Pydantic 스키마
- 요청/응답 데이터 검증
"""
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
class PlantBase(BaseModel):
"""발전소 기본 정보 스키마"""
id: int
company_id: int
name: str
capacity: Optional[float] = Field(None, description="발전 용량 (kW)")
location: Optional[str] = Field(None, description="발전소 위치")
created_at: Optional[datetime] = None
class SolarLogBase(BaseModel):
"""발전 로그 기본 정보 스키마"""
id: int
plant_id: int
timestamp: datetime
power_output: Optional[float] = Field(None, description="현재 발전량 (kW)")
daily_generation: Optional[float] = Field(None, description="일일 발전량 (kWh)")
total_generation: Optional[float] = Field(None, description="누적 발전량 (kWh)")
class PlantWithLatestLog(PlantBase):
"""발전소 정보 + 최신 발전 현황"""
latest_log: Optional[SolarLogBase] = Field(None, description="최신 발전 로그")
class PlantResponse(BaseModel):
"""단일 발전소 응답 스키마"""
status: str = "success"
data: PlantWithLatestLog
class PlantsListResponse(BaseModel):
"""발전소 목록 응답 스키마"""
status: str = "success"
data: List[PlantWithLatestLog]
total_count: int = Field(description="총 발전소 수")

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
fastapi>=0.104.0
uvicorn[standard]>=0.24.0
python-dotenv>=1.0.0
supabase>=2.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0