Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,34 +1,65 @@
# Serena
.serena

# Python
# ── Python ────────────────────────────────────────────────────────────
__pycache__/
*.py[cod]
*.pyo
.eggs/
*.pyd
*.so
*.egg
*.egg-info/
dist/
build/
.eggs/
MANIFEST

# Virtual environments
# ── 가상환경 ──────────────────────────────────────────────────────────
.venv/
venv/
env/
.env/

# ── 의존성 잠금 파일 ──────────────────────────────────────────────────
uv.lock

# Environment variables
# ── 환경변수 ──────────────────────────────────────────────────────────
.env
.env.local
.env.*.local

# pytest
# ── 테스트 / 커버리지 ─────────────────────────────────────────────────
.pytest_cache/
.coverage
.coverage.*
htmlcov/
coverage.xml

# mypy
# ── 타입 체크 / 린트 ──────────────────────────────────────────────────
.mypy_cache/
.ruff_cache/

# ── Jupyter Notebook ──────────────────────────────────────────────────
.ipynb_checkpoints/

# IDE
# ── Docker / 인프라 ───────────────────────────────────────────────────
postgres_data/
neo4j_data/
qdrant_data/

# ── Alembic ───────────────────────────────────────────────────────────
alembic/versions/*.pyc

# ── IDE / 에디터 ──────────────────────────────────────────────────────
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# ── 로그 ─────────────────────────────────────────────────────────────
*.log
logs/

# ── pyenv ─────────────────────────────────────────────────────────────
.python-version
5 changes: 5 additions & 0 deletions app/apis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""API 라우터 패키지."""

from app.apis import health, stocks

__all__ = ["health", "stocks"]
33 changes: 33 additions & 0 deletions app/apis/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""FastAPI 의존성 주입 설정.

⚠️ DB 교체 지점:
현재는 StubStockRepository(메모리 stub)를 사용합니다.
DB 담당자가 PostgresStockRepository 를 구현하면
아래 get_stock_service() 의 repo 인스턴스만 교체하면 됩니다.
"""

from __future__ import annotations

from typing import Annotated

from fastapi import Depends

from app.repositories.postgres.stub_stock_repository import StubStockRepository
from app.services.stock_service import StockService


def get_stock_service() -> StockService:
"""StockService 인스턴스를 제공합니다.

── DB 교체 방법 ──────────────────────────────────────────────────
from app.repositories.postgres.postgres_stock_repository import PostgresStockRepository
repo = PostgresStockRepository(session=...) # DB 세션 주입
return StockService(repo=repo)
──────────────────────────────────────────────────────────────────
"""
repo = StubStockRepository()
return StockService(repo=repo)


# 라우터에서 Annotated 타입으로 간결하게 사용할 수 있도록 별칭 제공
StockServiceDep = Annotated[StockService, Depends(get_stock_service)]
21 changes: 21 additions & 0 deletions app/apis/dtos/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""API 응답 DTO 패키지."""

from app.apis.dtos.common import DateRangeParams, Page, PaginationParams
from app.apis.dtos.stock import (
DividendResponse,
FinancialStatementResponse,
MetricResponse,
PriceResponse,
SecurityResponse,
)

__all__ = [
"Page",
"PaginationParams",
"DateRangeParams",
"SecurityResponse",
"PriceResponse",
"FinancialStatementResponse",
"DividendResponse",
"MetricResponse",
]
48 changes: 48 additions & 0 deletions app/apis/dtos/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""공통 쿼리 파라미터 및 제네릭 페이지 응답 모델."""

from __future__ import annotations

from datetime import date
from typing import Generic, TypeVar

from fastapi import Query
from pydantic import BaseModel, Field

T = TypeVar("T")


class Page(BaseModel, Generic[T]):
"""페이지네이션된 응답 래퍼."""

items: list[T]
total: int = Field(description="전체 항목 수")
limit: int = Field(description="페이지 크기")
offset: int = Field(description="시작 오프셋")


class PaginationParams:
"""공통 페이지네이션 쿼리 파라미터 (FastAPI Depends용)."""

def __init__(
self,
limit: int = Query(default=50, ge=1, le=500, description="반환 최대 건수"),
offset: int = Query(default=0, ge=0, description="시작 오프셋"),
) -> None:
self.limit = limit
self.offset = offset


class DateRangeParams:
"""날짜 범위 쿼리 파라미터 (FastAPI Depends용)."""

def __init__(
self,
from_date: date | None = Query(
default=None, alias="from", description="시작일 (YYYY-MM-DD)"
),
to_date: date | None = Query(
default=None, alias="to", description="종료일 (YYYY-MM-DD)"
),
) -> None:
self.from_date = from_date
self.to_date = to_date
84 changes: 84 additions & 0 deletions app/apis/dtos/stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""주가·재무·종목 관련 API 응답 DTO.

기존 pipeline/collectors/dtos/stock.py 의 OhlcvRecord 필드 구조를 API 계약으로 계승합니다.

Note: `from __future__ import annotations` 미사용.
Pydantic v2에서 `date: date` 처럼 필드명과 타입명이 같으면 "unevaluable type annotation" 오류가
발생하므로, datetime.date 를 Date 별칭으로 임포트합니다.
"""

from datetime import date as Date # noqa: N812
from typing import Literal

from pydantic import BaseModel, Field


class SecurityResponse(BaseModel):
"""종목/기업 마스터 정보."""

symbol: str = Field(description="티커 심볼 (예: AAPL, 005930.KS)")
name: str = Field(description="회사명")
exchange: str = Field(description="거래소 (예: NASDAQ, KRX)")
sector: str | None = Field(default=None, description="섹터")
industry: str | None = Field(default=None, description="산업")
currency: str = Field(description="거래 통화 (예: USD, KRW)")
country: str | None = Field(default=None, description="국가 코드 (예: US, KR)")
market_cap: float | None = Field(default=None, description="시가총액")

model_config = {"from_attributes": True}


class PriceResponse(BaseModel):
"""일봉(OHLCV) 주가 데이터.

pipeline/collectors/dtos/stock.py 의 OhlcvRecord와 동일한 스키마입니다.
"""

symbol: str = Field(description="티커 심볼")
date: Date = Field(description="거래일")
open: float = Field(ge=0, description="시가")
high: float = Field(ge=0, description="고가")
low: float = Field(ge=0, description="저가")
close: float = Field(ge=0, description="종가")
volume: int = Field(ge=0, description="거래량")

model_config = {"from_attributes": True}


class FinancialStatementResponse(BaseModel):
"""재무제표 (손익계산서·재무상태표·현금흐름표).

line_items에 항목명 → 금액(KRW 또는 USD) 딕셔너리로 제공합니다.
"""

symbol: str = Field(description="티커 심볼")
statement_type: Literal["income", "balance", "cashflow"] = Field(description="재무제표 종류")
period_type: Literal["annual", "quarterly"] = Field(description="기간 종류")
fiscal_date: Date = Field(description="회계 기준일")
line_items: dict[str, float | None] = Field(description="재무 항목 딕셔너리")

model_config = {"from_attributes": True}


class DividendResponse(BaseModel):
"""배당 이력."""

symbol: str = Field(description="티커 심볼")
ex_date: Date = Field(description="배당락일")
amount: float = Field(ge=0, description="주당 배당금")

model_config = {"from_attributes": True}


class MetricResponse(BaseModel):
"""주요 밸류에이션 지표."""

symbol: str = Field(description="티커 심볼")
as_of_date: Date = Field(description="기준일")
per: float | None = Field(default=None, description="주가수익비율(PER)")
pbr: float | None = Field(default=None, description="주가순자산비율(PBR)")
eps: float | None = Field(default=None, description="주당순이익(EPS)")
dividend_yield: float | None = Field(default=None, description="배당수익률 (0~1)")
market_cap: float | None = Field(default=None, description="시가총액")

model_config = {"from_attributes": True}
20 changes: 20 additions & 0 deletions app/apis/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""헬스체크 라우터."""

from fastapi import APIRouter
from pydantic import BaseModel

router = APIRouter(tags=["health"])


class HealthResponse(BaseModel):
status: str
version: str


@router.get("/health", response_model=HealthResponse, summary="헬스체크")
async def health() -> HealthResponse:
"""서비스 상태를 반환합니다."""
from app.core.config import get_settings

settings = get_settings()
return HealthResponse(status="ok", version=settings.app_version)
Loading