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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ GUEST_PASSWORD=guest

# 세션 로그인 시 사용되는 SECRET KEY
SECRET_KEY="CHANGE_ME!!!"

# allowed/trusted host 설정
ALLOWED_HOSTS="localhost,testserver,api.example.com"
TRUSTED_HOSTS="10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.1"
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ fastapi dev app
# 또는 python3 -m fastapi dev app
```

## Docker
## scripts

> [!NOTE]
> 도커 컨테이너 실행 방법은
> [team-overclock/monorepo#docker](https://github.com/team-overclock/monorepo#docker)
> 참고

내장 명령어 목록 및 설명
scripts 폴더 내 실행 가능한 스크립트 파일 목록 및 설명

### create user

Expand All @@ -68,14 +68,19 @@ fastapi dev app
docker exec -it <container_name> create-user <email> [name]
```

### insert data
### download data

> [!IMPORTANT]
> 도커 이미지 경량화를 위해 제거되었으며,
> 파이썬으로 직접 파일을 실행할 수 있습니다.
구글 드라이브 [공유폴더](https://drive.google.com/drive/folders/1w9TUhiq_XT-wl8xMRIegWvoWW65IyxC0?usp=sharing)
내 데이터 다운로드

```shell
python3 scripts/download_data.py
```

### insert data

> [!IMPORTANT]
> [url.txt](data/url.txt) 파일 내 구글 드라이브에서 모든 파일 다운로드
> `download data`를 먼저 실행하거나 직접 다운로드 필요

`data` 폴더 내 csv 파일 내 데이터를 데이터베이스에 삽입 (테이블 자동 생성)

Expand All @@ -87,7 +92,7 @@ python3 scripts/insert_data.py
### insert seeds

> [!IMPORTANT]
> `insert-data`가 먼저 수행되어야 함
> `insert data`가 먼저 수행되어야 함

> [!NOTE]
> 게스트 유저는 항상 생성되며,
Expand Down Expand Up @@ -132,10 +137,11 @@ docker exec -it <container_name> drop-tables [-y|--yes]

### drop to seeds

`drop-tables`, `insert-data`, `insert-seeds`를 연속으로 실행하는 스크립트
`drop tables`, `insert data`, `insert seeds`를 연속으로 실행하는 스크립트

```shell
docker exec -it <container_name> drop-to-seeds
python3 scripts/drop_to_seeds.py [num_of_recs] [num_of_users]
# docker exec -it <container_name> drop-to-seeds # deprecated
```

## 테스트
Expand Down
28 changes: 28 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import os


def parse_comma_separated(key: str, default_value: str):
value = os.getenv(key) or default_value
items = [x.strip() for x in value.split(",") if x.strip()]
return items


MODE = os.getenv("APP_ENV") or "development"
"""애플리케이션 실행 모드"""

PROD = MODE == "production"
"""운영 모드 여부"""


ALLOWED_HOSTS = parse_comma_separated("ALLOWED_HOSTS", "localhost")
"""허용된 호스트 목록 (도메인)"""

TRUSTED_HOSTS = parse_comma_separated("TRUSTED_HOSTS", "127.0.0.1")
"""신뢰할 수 있는 프록시 IP 목록"""


SEED_TASK_ID_PREFIX = "random_seed_"
SEED_USERNAME_PREFIX = "seed_user_"

DEFAULT_INSERT_SEED_RECOMMENDATIONS = 30
DEFAULT_INSERT_SEED_USERS = 0
4 changes: 2 additions & 2 deletions app/crud/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def fetch_regions(redis: Redis, hkey: str, sort: bool = True, **extra_dict):
if sort: result.sort(key=lambda x: x["id"])
return result

def get_regions(redis: Redis, include_depth: bool = True):
def get_regions(redis: Redis, include_depth: bool = True) -> list[dict]:
"""모든 동네 목록을 반환하는 함수"""
if not include_depth:
return fetch_regions(redis, REGIONS_ALL_KEY)
Expand All @@ -64,7 +64,7 @@ def get_regions(redis: Redis, include_depth: bool = True):
items.sort(key=lambda x: x["id"])
return items

def get_region_by_id(redis: Redis, region_id: int):
def get_region_by_id(redis: Redis, region_id: int) -> dict | None:
"""주어진 id에 해당하는 동네를 반환하는 함수"""
value = redis.hget(REGIONS_ALL_KEY, region_id)
if value is None:
Expand Down
2 changes: 1 addition & 1 deletion app/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

GUEST_LOGIN_ENABLE = (os.getenv("GUEST_LOGIN_ENABLE") or "false").lower() == "true"
GUEST_EMAIL = os.getenv("GUEST_EMAIL") or "guest@example.com"
GUEST_USERNAME = os.getenv("GUEST_USERNAME")
GUEST_USERNAME = os.getenv("GUEST_USERNAME") or None
GUEST_PASSWORD = os.getenv("GUEST_PASSWORD") or "guest"


Expand Down
25 changes: 21 additions & 4 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI, APIRouter, Request, Depends
from fastapi.responses import JSONResponse
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.sessions import SessionMiddleware

from .config import PROD, ALLOWED_HOSTS, TRUSTED_HOSTS
from .models import Base
from .redis import redis
from .database import engine, SessionLocal
Expand All @@ -24,6 +27,7 @@
recommendations,
)

from .seeds import router as seeds
from . import demo


Expand Down Expand Up @@ -53,12 +57,22 @@ def create_app() -> FastAPI:
"""FastAPI 앱 생성 및 라우터를 등록 후 반환."""
app = FastAPI(
title="fastapi",
docs_url="/docs",
docs_url=None if PROD else "/docs",
redoc_url="/redoc",
redirect_slashes=False,
lifespan=lifespan,
)

app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=ALLOWED_HOSTS,
)

app.add_middleware(
ProxyHeadersMiddleware,
trusted_hosts=TRUSTED_HOSTS,
)

app.add_middleware(
CORSMiddleware,
allow_credentials=True,
Expand All @@ -73,7 +87,7 @@ def create_app() -> FastAPI:
secret_key=os.getenv("SECRET_KEY", "x"),
session_cookie="session",
same_site="lax", # CSRF 방어
https_only=os.getenv("APP_ENV") == "production", # 운영 모드에서만 True
https_only=PROD, # 운영 모드에서만 True
max_age=60 * 60 * 24, # 1일 동안 세션 유지
)

Expand Down Expand Up @@ -119,10 +133,13 @@ def require_auth(router: APIRouter):
)

# Scalar 문서는 OpenAPI 목록에서 숨김
app.include_router(scalar.router, prefix="/scalar", include_in_schema=False)
if not PROD:
app.include_router(scalar.router, prefix="/scalar", include_in_schema=False)
app.include_router(seeds.router)
app.include_router(health.router)
app.include_router(public.router)
app.include_router(demo.router)
if not PROD:
app.include_router(demo.router)
app.include_router(auth.router)
require_auth(users.router)
require_auth(recommendations.router)
Expand Down
4 changes: 2 additions & 2 deletions app/schemas/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pydantic import BaseModel, EmailStr

from .common import PK_STR, UserName, Password
from .common import PK_STR, OptionalStr, Password
from .user import UserInfo


Expand All @@ -15,7 +15,7 @@ class UserLoginRequest(BaseModel):


class UserCreateRequest(BaseModel):
name: UserName | None = None
name: OptionalStr = None
email: EmailStr
password: Password

Expand Down
8 changes: 6 additions & 2 deletions app/schemas/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ def empty_to_none(v: str | None):
return None
return v

UserName = Annotated[str, Field(min_length=1)]
Password = Annotated[str, Field(min_length=4)]
OptionalStr = Annotated[
Union[str, None],
BeforeValidator(empty_to_none)
]
OptionalEmail = Annotated[
Union[EmailStr, None],
BeforeValidator(empty_to_none)
]

Password = Annotated[str, Field(min_length=4)]

PK_AI = Annotated[int, Field(description="Primary Key (Auto Increment)", ge=1)]
PK_STR = Annotated[str, Field(description="Primary Key (String)", examples=["abc123"], min_length=1)]

Expand Down
4 changes: 2 additions & 2 deletions app/schemas/user.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime

from .common import PK_STR, Password, OptionalEmail
from .common import PK_STR, Password, OptionalStr, OptionalEmail


class UserInfo(BaseModel):
Expand All @@ -12,7 +12,7 @@ class UserInfo(BaseModel):


class UserInfoUpdateRequest(BaseModel):
name: str | None = None
name: OptionalStr = None
email: OptionalEmail = None

class UserPasswordChangeRequest(BaseModel):
Expand Down
17 changes: 17 additions & 0 deletions app/seeds/drop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from ..database import SessionLocal
from ..models import User, Recommendation
from ..config import SEED_TASK_ID_PREFIX, SEED_USERNAME_PREFIX


def run():
db = SessionLocal()
try:
db.query(User).filter(User.name.like(f"{SEED_USERNAME_PREFIX}%")).delete()
db.query(Recommendation).filter(Recommendation.task_id.like(f"{SEED_TASK_ID_PREFIX}%")).delete()
except Exception as e:
db.rollback()
raise e
else:
db.commit()
finally:
db.close()
Loading