Initial commit: FastAPI middleware server for solar power monitoring
This commit is contained in:
commit
8878303cac
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal 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
1
app/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Solar Power Monitoring API Server
|
||||||
1
app/core/__init__.py
Normal file
1
app/core/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Core configuration module
|
||||||
33
app/core/config.py
Normal file
33
app/core/config.py
Normal 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
28
app/core/database.py
Normal 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
75
app/main.py
Normal 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
1
app/routers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# API Routers module
|
||||||
18
app/routers/auth.py
Normal file
18
app/routers/auth.py
Normal 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
140
app/routers/plants.py
Normal 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
2
app/schemas/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Pydantic Schemas module
|
||||||
|
from .plant import PlantBase, PlantResponse, PlantWithLatestLog, PlantsListResponse
|
||||||
46
app/schemas/plant.py
Normal file
46
app/schemas/plant.py
Normal 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
6
requirements.txt
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user