Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@
*.pyd

# 가상환경
venv
venv

# .env 파일
/.env
Empty file added app/__init__.py
Empty file.
44 changes: 44 additions & 0 deletions app/db/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# app/db/database.py
import os

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv

# 환경 변수 로드
load_dotenv()

# 데이터베이스 연결 정보
RDS_HOST = os.getenv("RDS_HOST")
RDS_PORT = os.getenv("RDS_PORT", "3306")
RDS_USER = os.getenv("RDS_USER")
RDS_PASSWORD = os.getenv("RDS_PASSWORD")
RDS_DB = os.getenv("RDS_DB")

# 모든 필수 환경 변수가 설정되었는지 확인
if not all([RDS_HOST, RDS_USER, RDS_PASSWORD, RDS_DB]):
raise ValueError("RDS 연결 정보가 불완전합니다. 환경 변수를 확인하세요.")

# 연결 문자열 구성
DATABASE_URL = f"mysql+pymysql://{RDS_USER}:{RDS_PASSWORD}@{RDS_HOST}:{RDS_PORT}/{RDS_DB}"

# 엔진 생성
engine = create_engine(
DATABASE_URL,
pool_size=5,
max_overflow=10,
pool_timeout=30,
pool_recycle=1800,
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# DB 세션 의존성
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
5 changes: 3 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from fastapi import FastAPI
from app.routers import stock, portfolio
from app.routers import member_route, stock, portfolio

app = FastAPI()

# 라우터 등록
app.include_router(member_route.router, prefix="/api", tags=["Members"])
app.include_router(stock.router, prefix="/stocks", tags=["Stocks"])
app.include_router(portfolio.router, prefix="/portfolio", tags=["Portfolio"])
app.include_router(portfolio.router, prefix="/portfolio", tags=["Portfolio"])
43 changes: 43 additions & 0 deletions app/models/member.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# app/models/member.py
from sqlalchemy import Column, BigInteger, String, DateTime, Enum, func
from app.db.database import Base

class Member(Base):
"""Member 데이터베이스 모델

'members' 테이블과 매핑되는 SQLAlchemy ORM 모델 클래스
사용자 계정 정보와 관련된 속성들을 포함
"""
__tablename__ = "members" # 데이터베이스 테이블 이름

# 회원 고유 식별자 (기본 키)
id = Column(BigInteger, primary_key=True, autoincrement=True)

# 회원 이메일 주소 (고유 값, 인덱스 생성, NULL 허용)
email = Column(String(255), unique=True, index=True, nullable=True)

# 회원 닉네임 (NULL 허용 안 함)
nickname = Column(String(255), nullable=False)

# 회원 역할 (ENUM 타입, ROLE_USER만 허용)
# 인증 및 권한 관리에 사용됨
role = Column(Enum('ROLE_USER'), nullable=False)

# 소셜 로그인 타입 (ENUM 타입, GOOGLE 또는 NAVER 허용)
# 소셜 로그인 제공자를 구분하는 데 사용됨
social_type = Column(Enum('GOOGLE', 'NAVER'), nullable=False)

# 회원 생성 일시 (자동으로 현재 시간 설정)
# 정밀도 6의 DateTime 타입 사용
created_at = Column(DateTime(6), nullable=False, default=func.now())

# 회원 정보 마지막 수정 일시 (생성 시 현재 시간, 업데이트 시 자동 갱신)
# 정밀도 6의 DateTime 타입 사용
update_at = Column(DateTime(6), nullable=False, default=func.now(), onupdate=func.now())

def __repr__(self):
"""
객체의 문자열 표현을 반환합니다.
디버깅 및 로깅에 유용합니다.
"""
return f"<Member(id={self.id}, email='{self.email}', nickname='{self.nickname}')>"
61 changes: 61 additions & 0 deletions app/routers/member_route.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# app/routers/member.py
from fastapi import APIRouter, Depends, HTTPException, Path
from sqlalchemy.orm import Session
from typing import List
from enum import Enum

from app.db.database import get_db
from app.services.member_service import MemberService
from app.schemas.member_sch import Member as MemberSchema

router = APIRouter()

# ENUM 검증을 위한 클래스
class RoleEnum(str, Enum):
ROLE_USER = "ROLE_USER"

class SocialTypeEnum(str, Enum):
GOOGLE = "GOOGLE"
NAVER = "NAVER"

@router.get("/members/", response_model = List[MemberSchema])
def read_members(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
"""전체 회원 목록 조회 API
:param skip: 건너뛸 레코드 수 (기본값 : 0)
:param limit: 최대 반환할 레코드 수 (기본값 : 100)
:param db: 데이터베이스 세션 (의존성 주입)
:return: List[MemberSchema] : 회원 객체 목록
"""
members = MemberService.get_all_members(db, skip=skip, limit=limit)
return members

@router.get("/members/{member_id}", response_model=MemberSchema)
def read_member(member_id: int, db: Session = Depends(get_db)):
"""특정 ID의 회원 정보 조회 API
:param member_id: (int) 조회할 회원의 ID
:param db: 데이터베이스 세션 (의존성 주입)
:return: MemberScheman : 회원 객체
:raise HTTPException: 회원을 찾을 수 없는 경우 404 에러 발생
"""
db_member = MemberService.get_member_by_id(db, member_id=member_id)

if db_member is None:
raise HTTPException(status_code=404, detail="Member not found")
return db_member

# 더 구체적인 경로부터 정의
@router.get("/members/email/{email}", response_model=MemberSchema)
def read_member_by_email(
email: str = Path(..., description="회원 이메일 주소"),
db: Session = Depends(get_db)
):
"""이메일로 회원 정보 조회 API
:param email: (str) 조회할 회원의 이메일 주소
:param db: 데이터베이스 세션(의존성 주입)
:return: MemberSchema: 회원 객체
:raise: HTTPException : 회원을 찾을 수 없는 경우 404 에러 발생
"""
db_member = MemberService.get_member_by_email(db, email=email)
if db_member is None:
raise HTTPException(status_code=404, detail="Member not found")
return db_member
32 changes: 32 additions & 0 deletions app/schemas/member_sch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# app/schemas/member_sch.py
'''
1. 데이터 검증
API 를 통해 받거나 반환하는 Member 데이터의 형식과 타입을 검증한다.
예를 들어, Email 필드는 EmailStr 타입이어야 하며, 유효한 이메일 형식인지 자동으로 검증한다.

2. API 문서화
FastAPI 의 자동 문서화 시스템(Swagger/OpenAPI)에서 API 요청/응답의 스키마를 생성한다.

3. 데이터 반환
데이터베이스 모델 (SQLAlchemy)과 API 요청/응답 사이의 데이터 변환을 담당한다.
'''
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional

class MemberBase(BaseModel):
email: EmailStr
nickname: str
role: str
social_type: str

class MemberCreate(MemberBase):
pass

class Member(MemberBase):
id: int
created_at: datetime
update_at: datetime

class Config:
orm_mode = True
51 changes: 51 additions & 0 deletions app/services/member_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# app/services/member_service.py
from sqlalchemy.orm import Session
from app.models.member import Member
from typing import Optional, Type


class MemberService:
"""
회원 정보 관련 서비스 클래스

- 이 클래스는 데이터베이스에서 회원 정보를 조회하는 메서드를 제공
- 모든 메서드는 정적 메서드 (staticmethod)로 구현되어 있어 인스턴스 생성 없이 직접 호출 가능
"""
@staticmethod
def get_member_by_id(db: Session, member_id: int) -> Optional[Member]:

"""ID 로 회원 정보를 조회
:param db: SQLAlchemy 데이터베이스 세션
:param member_id: 조회할 회원의 ID
:return: Optional[Member] : 해당 ID의 회원이 존재하면 Member 객체, 없으면 None
"""

return db.query(Member).filter(Member.id == member_id).first()

@staticmethod
def get_member_by_email(db: Session, email: str) -> Optional[Member]:

"""이메일로 회원 정보를 조회
:param db: SQLAlchemy 데이터베이스 세션
:param email: (str) 조회할 회원의 이메일 주소
:return: Optional[Member] : 해당 이메일의 회원이 존재하면 Member, 없으면 None
"""

return db.query(Member).filter(Member.email == email).first()

# 명시적 변환 방법
@staticmethod
def get_all_members(db: Session, skip: int = 0, limit: int = 100) -> list[Type[Member]]:
"""모든 회원 정보를 페이지네이션하여 조회
:param db: (Session) SQLAlchemy 데이터베이스 세션
:param skip: (int, optional) 건너뛸 레코드 수, 기본값은 0
:param limit: (int, optional) 최대 반영할 레코드 수, 기본값은 100
:return: sequence[Memnber] : 조회된 회원 목록
"""

result = db.query(Member).offset(skip).limit(limit).all()
return list(result) # 명시적으로 list로 변환

@staticmethod
def get_members_by_social_type(db: Session, social_type: str) -> list[Type[Member]]:
return db.query(Member).filter(Member.social_type == social_type).all()
1 change: 1 addition & 0 deletions app/services/portfolio_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,4 @@ def calculate_portfolio_performance(portfolio: Dict[str, Any]) -> Dict[str, Any]
"total_profit_loss_percent": round(total_profit_loss_percent, 2),
"stocks": stocks_data
}

6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ yfinance
pandas
requests
PyPortfolioOpt
sqlalchemy>=1.4.0
pymysql>=1.0.2
pydantic>=2.0.0
pydantic[email]>=2.0.0
email-validator>=2.0.0 # 이메일 검증을 위해 추가
python-dotenv>=1.0.0