first commit
parent
f24ff46b09
commit
dd16013816
|
|
@ -10,6 +10,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.database.session import get_session
|
from app.database.session import get_session
|
||||||
from app.sns.schemas.sns_schema import InstagramUploadRequest, InstagramUploadResponse
|
from app.sns.schemas.sns_schema import InstagramUploadRequest, InstagramUploadResponse
|
||||||
|
from app.user.dependencies.auth import get_current_user
|
||||||
|
from app.user.models import Platform, SocialAccount, User
|
||||||
|
from app.utils.instagram import ErrorState, InstagramClient, parse_instagram_error
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
from app.video.models import Video
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -80,13 +87,7 @@ class InstagramContainerError(SNSException):
|
||||||
|
|
||||||
def __init__(self, message: str = "Instagram 미디어 처리에 실패했습니다."):
|
def __init__(self, message: str = "Instagram 미디어 처리에 실패했습니다."):
|
||||||
super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, "INSTAGRAM_CONTAINER_ERROR", message)
|
super().__init__(status.HTTP_500_INTERNAL_SERVER_ERROR, "INSTAGRAM_CONTAINER_ERROR", message)
|
||||||
from app.user.dependencies.auth import get_current_user
|
|
||||||
from app.user.models import Platform, SocialAccount, User
|
|
||||||
from app.utils.instagram import ErrorState, InstagramClient, parse_instagram_error
|
|
||||||
from app.utils.logger import get_logger
|
|
||||||
from app.video.models import Video
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/sns", tags=["SNS"])
|
router = APIRouter(prefix="/sns", tags=["SNS"])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ Social Account Service
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.utils.timezone import now
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
|
|
@ -405,7 +407,7 @@ class SocialAccountService:
|
||||||
"""
|
"""
|
||||||
# 만료 시간 확인 (만료 10분 전이면 갱신)
|
# 만료 시간 확인 (만료 10분 전이면 갱신)
|
||||||
if account.token_expires_at:
|
if account.token_expires_at:
|
||||||
buffer_time = datetime.now() + timedelta(minutes=10)
|
buffer_time = now() + timedelta(minutes=10)
|
||||||
if account.token_expires_at <= buffer_time:
|
if account.token_expires_at <= buffer_time:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[SOCIAL] 토큰 만료 임박, 갱신 시작 - account_id: {account.id}"
|
f"[SOCIAL] 토큰 만료 임박, 갱신 시작 - account_id: {account.id}"
|
||||||
|
|
@ -445,7 +447,7 @@ class SocialAccountService:
|
||||||
if token_response.refresh_token:
|
if token_response.refresh_token:
|
||||||
account.refresh_token = token_response.refresh_token
|
account.refresh_token = token_response.refresh_token
|
||||||
if token_response.expires_in:
|
if token_response.expires_in:
|
||||||
account.token_expires_at = datetime.now() + timedelta(
|
account.token_expires_at = now() + timedelta(
|
||||||
seconds=token_response.expires_in
|
seconds=token_response.expires_in
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -506,7 +508,7 @@ class SocialAccountService:
|
||||||
# 토큰 만료 시간 계산
|
# 토큰 만료 시간 계산
|
||||||
token_expires_at = None
|
token_expires_at = None
|
||||||
if token_response.expires_in:
|
if token_response.expires_in:
|
||||||
token_expires_at = datetime.now() + timedelta(
|
token_expires_at = now() + timedelta(
|
||||||
seconds=token_response.expires_in
|
seconds=token_response.expires_in
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -559,7 +561,7 @@ class SocialAccountService:
|
||||||
if token_response.refresh_token:
|
if token_response.refresh_token:
|
||||||
account.refresh_token = token_response.refresh_token
|
account.refresh_token = token_response.refresh_token
|
||||||
if token_response.expires_in:
|
if token_response.expires_in:
|
||||||
account.token_expires_at = datetime.now() + timedelta(
|
account.token_expires_at = now() + timedelta(
|
||||||
seconds=token_response.expires_in
|
seconds=token_response.expires_in
|
||||||
)
|
)
|
||||||
if token_response.scope:
|
if token_response.scope:
|
||||||
|
|
@ -575,7 +577,7 @@ class SocialAccountService:
|
||||||
|
|
||||||
# 재연결 시 연결 시간 업데이트
|
# 재연결 시 연결 시간 업데이트
|
||||||
if update_connected_at:
|
if update_connected_at:
|
||||||
account.connected_at = datetime.now()
|
account.connected_at = now()
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(account)
|
await session.refresh(account)
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ Social Upload Background Task
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
|
|
||||||
|
from app.utils.timezone import now
|
||||||
import httpx
|
import httpx
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
@ -70,7 +71,7 @@ async def _update_upload_status(
|
||||||
if error_message:
|
if error_message:
|
||||||
upload.error_message = error_message
|
upload.error_message = error_message
|
||||||
if status == UploadStatus.COMPLETED:
|
if status == UploadStatus.COMPLETED:
|
||||||
upload.uploaded_at = datetime.now()
|
upload.uploaded_at = now()
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||||
|
|
||||||
|
from app.utils.timezone import now
|
||||||
from fastapi.responses import RedirectResponse, Response
|
from fastapi.responses import RedirectResponse, Response
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
@ -434,7 +435,7 @@ async def generate_test_token(
|
||||||
session.add(db_refresh_token)
|
session.add(db_refresh_token)
|
||||||
|
|
||||||
# 마지막 로그인 시간 업데이트
|
# 마지막 로그인 시간 업데이트
|
||||||
user.last_login_at = datetime.now(timezone.utc)
|
user.last_login_at = now()
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -522,7 +522,7 @@ class SocialAccount(Base):
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
# 플랫폼 계정 식별 정보
|
# 플랫폼 계정 식별 정보
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
platform_user_id: Mapped[str] = mapped_column(
|
platform_user_id: Mapped[Optional[str]] = mapped_column(
|
||||||
String(100),
|
String(100),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
comment="플랫폼 내 사용자 고유 ID",
|
comment="플랫폼 내 사용자 고유 ID",
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
from app.utils.timezone import now
|
||||||
from sqlalchemy import select, update
|
from sqlalchemy import select, update
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
@ -167,7 +168,7 @@ class AuthService:
|
||||||
logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}, user_uuid: {user.user_uuid}")
|
logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}, user_uuid: {user.user_uuid}")
|
||||||
|
|
||||||
# 7. 마지막 로그인 시간 업데이트
|
# 7. 마지막 로그인 시간 업데이트
|
||||||
user.last_login_at = datetime.now()
|
user.last_login_at = now()
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
redirect_url = f"{prj_settings.PROJECT_DOMAIN}"
|
redirect_url = f"{prj_settings.PROJECT_DOMAIN}"
|
||||||
|
|
@ -222,7 +223,7 @@ class AuthService:
|
||||||
if db_token.is_revoked:
|
if db_token.is_revoked:
|
||||||
raise TokenRevokedError()
|
raise TokenRevokedError()
|
||||||
|
|
||||||
if db_token.expires_at < datetime.now():
|
if db_token.expires_at < now():
|
||||||
raise TokenExpiredError()
|
raise TokenExpiredError()
|
||||||
|
|
||||||
# 4. 사용자 확인
|
# 4. 사용자 확인
|
||||||
|
|
@ -482,7 +483,7 @@ class AuthService:
|
||||||
.where(RefreshToken.token_hash == token_hash)
|
.where(RefreshToken.token_hash == token_hash)
|
||||||
.values(
|
.values(
|
||||||
is_revoked=True,
|
is_revoked=True,
|
||||||
revoked_at=datetime.now(),
|
revoked_at=now(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
@ -507,7 +508,7 @@ class AuthService:
|
||||||
)
|
)
|
||||||
.values(
|
.values(
|
||||||
is_revoked=True,
|
is_revoked=True,
|
||||||
revoked_at=datetime.now(),
|
revoked_at=now(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from typing import Optional
|
||||||
|
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
|
|
||||||
|
from app.utils.timezone import now
|
||||||
from config import jwt_settings
|
from config import jwt_settings
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,7 +24,7 @@ def create_access_token(user_uuid: str) -> str:
|
||||||
Returns:
|
Returns:
|
||||||
JWT 액세스 토큰 문자열
|
JWT 액세스 토큰 문자열
|
||||||
"""
|
"""
|
||||||
expire = datetime.now() + timedelta(
|
expire = now() + timedelta(
|
||||||
minutes=jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
minutes=jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
)
|
)
|
||||||
to_encode = {
|
to_encode = {
|
||||||
|
|
@ -48,7 +49,7 @@ def create_refresh_token(user_uuid: str) -> str:
|
||||||
Returns:
|
Returns:
|
||||||
JWT 리프레시 토큰 문자열
|
JWT 리프레시 토큰 문자열
|
||||||
"""
|
"""
|
||||||
expire = datetime.now() + timedelta(
|
expire = now() + timedelta(
|
||||||
days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||||
)
|
)
|
||||||
to_encode = {
|
to_encode = {
|
||||||
|
|
@ -106,7 +107,7 @@ def get_refresh_token_expires_at() -> datetime:
|
||||||
Returns:
|
Returns:
|
||||||
리프레시 토큰 만료 datetime (로컬 시간)
|
리프레시 토큰 만료 datetime (로컬 시간)
|
||||||
"""
|
"""
|
||||||
return datetime.now() + timedelta(
|
return now() + timedelta(
|
||||||
days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ Django 로거 구조를 참고하여 FastAPI에 최적화된 로깅 시스템.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
from app.utils.timezone import today_str
|
||||||
from config import log_settings
|
from config import log_settings
|
||||||
|
|
||||||
# 로그 디렉토리 설정 (config.py의 LogSettings에서 관리)
|
# 로그 디렉토리 설정 (config.py의 LogSettings에서 관리)
|
||||||
|
|
@ -86,7 +86,7 @@ def _get_shared_file_handler() -> RotatingFileHandler:
|
||||||
global _shared_file_handler
|
global _shared_file_handler
|
||||||
|
|
||||||
if _shared_file_handler is None:
|
if _shared_file_handler is None:
|
||||||
today = datetime.today().strftime("%Y-%m-%d")
|
today = today_str()
|
||||||
log_file = LOG_DIR / f"{today}_app.log"
|
log_file = LOG_DIR / f"{today}_app.log"
|
||||||
|
|
||||||
_shared_file_handler = RotatingFileHandler(
|
_shared_file_handler = RotatingFileHandler(
|
||||||
|
|
@ -116,7 +116,7 @@ def _get_shared_error_handler() -> RotatingFileHandler:
|
||||||
global _shared_error_handler
|
global _shared_error_handler
|
||||||
|
|
||||||
if _shared_error_handler is None:
|
if _shared_error_handler is None:
|
||||||
today = datetime.today().strftime("%Y-%m-%d")
|
today = today_str()
|
||||||
log_file = LOG_DIR / f"{today}_error.log"
|
log_file = LOG_DIR / f"{today}_error.log"
|
||||||
|
|
||||||
_shared_error_handler = RotatingFileHandler(
|
_shared_error_handler = RotatingFileHandler(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""
|
||||||
|
타임존 유틸리티
|
||||||
|
|
||||||
|
프로젝트 전역에서 일관된 서울 타임존(Asia/Seoul) 시간을 사용하기 위한 유틸리티입니다.
|
||||||
|
모든 datetime.now() 호출은 이 모듈의 함수로 대체해야 합니다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from config import TIMEZONE
|
||||||
|
|
||||||
|
|
||||||
|
def now() -> datetime:
|
||||||
|
"""
|
||||||
|
서울 타임존(Asia/Seoul) 기준 현재 시간을 반환합니다.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
datetime: 서울 타임존이 적용된 현재 시간 (aware datetime)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from app.utils.timezone import now
|
||||||
|
>>> current_time = now() # 2024-01-15 15:30:00+09:00
|
||||||
|
"""
|
||||||
|
return datetime.now(TIMEZONE)
|
||||||
|
|
||||||
|
|
||||||
|
def today_str(fmt: str = "%Y-%m-%d") -> str:
|
||||||
|
"""
|
||||||
|
서울 타임존 기준 오늘 날짜를 문자열로 반환합니다.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fmt: 날짜 포맷 (기본값: YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 포맷된 날짜 문자열
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from app.utils.timezone import today_str
|
||||||
|
>>> today_str() # "2024-01-15"
|
||||||
|
>>> today_str("%Y/%m/%d") # "2024/01/15"
|
||||||
|
"""
|
||||||
|
return datetime.now(TIMEZONE).strftime(fmt)
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
# 프로젝트 전역 타임존 설정 (서울)
|
||||||
|
TIMEZONE = ZoneInfo("Asia/Seoul")
|
||||||
|
|
||||||
PROJECT_DIR = Path(__file__).resolve().parent
|
PROJECT_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
# 미디어 파일 저장 디렉토리
|
# 미디어 파일 저장 디렉토리
|
||||||
|
|
@ -157,7 +161,7 @@ class CreatomateSettings(BaseSettings):
|
||||||
)
|
)
|
||||||
DEBUG_AUTO_LYRIC: bool = Field(
|
DEBUG_AUTO_LYRIC: bool = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description = "Creatomate 자동 가사 생성 기능 사용 여부"
|
description="Creatomate 자동 가사 생성 기능 사용 여부",
|
||||||
)
|
)
|
||||||
|
|
||||||
model_config = _base_config
|
model_config = _base_config
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
# 타임존 설정 작업 계획
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
프로젝트 전체에 서울 타임존(Asia/Seoul, UTC+9)을 일관되게 적용합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
### 완료된 설정
|
||||||
|
| 구성 요소 | 상태 | 비고 |
|
||||||
|
|----------|------|------|
|
||||||
|
| **MySQL 데이터베이스** | ✅ 완료 | DB 생성 시 Asia/Seoul로 설정됨 |
|
||||||
|
| **config.py** | ✅ 완료 | `TIMEZONE = ZoneInfo("Asia/Seoul")` 설정됨 |
|
||||||
|
| **SQLAlchemy 모델** | ✅ 정상 | `server_default=func.now()` → DB 타임존(서울) 따름 |
|
||||||
|
|
||||||
|
### 문제점
|
||||||
|
- Python 코드에서 `datetime.now()` (naive) 또는 `datetime.now(timezone.utc)` (UTC) 혼용
|
||||||
|
- 타임존이 적용된 현재 시간을 얻는 공통 유틸리티 없음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 수정 대상 파일
|
||||||
|
|
||||||
|
| 파일 | 라인 | 현재 코드 | 문제 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| `app/user/services/jwt.py` | 26, 51, 109 | `datetime.now()` | naive datetime |
|
||||||
|
| `app/user/services/auth.py` | 170, 225, 485, 510 | `datetime.now()` | naive datetime |
|
||||||
|
| `app/user/api/routers/v1/auth.py` | 437 | `datetime.now(timezone.utc)` | UTC 사용 (서울 아님) |
|
||||||
|
| `app/social/services.py` | 408, 448, 509, 562, 578 | `datetime.now()` | naive datetime |
|
||||||
|
| `app/social/worker/upload_task.py` | 73 | `datetime.now()` | naive datetime |
|
||||||
|
| `app/utils/logger.py` | 89, 119 | `datetime.today()` | naive datetime |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 단계
|
||||||
|
|
||||||
|
### 1단계: 타임존 유틸리티 생성 (신규 파일)
|
||||||
|
|
||||||
|
**파일**: `app/utils/timezone.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
타임존 유틸리티
|
||||||
|
|
||||||
|
프로젝트 전역에서 일관된 서울 타임존(Asia/Seoul) 시간을 사용하기 위한 유틸리티입니다.
|
||||||
|
모든 datetime.now() 호출은 이 모듈의 함수로 대체해야 합니다.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from config import TIMEZONE
|
||||||
|
|
||||||
|
|
||||||
|
def now() -> datetime:
|
||||||
|
"""
|
||||||
|
서울 타임존(Asia/Seoul) 기준 현재 시간을 반환합니다.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
datetime: 서울 타임존이 적용된 현재 시간 (aware datetime)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from app.utils.timezone import now
|
||||||
|
>>> current_time = now() # 2024-01-15 15:30:00+09:00
|
||||||
|
"""
|
||||||
|
return datetime.now(TIMEZONE)
|
||||||
|
|
||||||
|
|
||||||
|
def today_str(fmt: str = "%Y-%m-%d") -> str:
|
||||||
|
"""
|
||||||
|
서울 타임존 기준 오늘 날짜를 문자열로 반환합니다.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fmt: 날짜 포맷 (기본값: YYYY-MM-DD)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 포맷된 날짜 문자열
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from app.utils.timezone import today_str
|
||||||
|
>>> today_str() # "2024-01-15"
|
||||||
|
>>> today_str("%Y/%m/%d") # "2024/01/15"
|
||||||
|
"""
|
||||||
|
return datetime.now(TIMEZONE).strftime(fmt)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2단계: 기존 코드 수정
|
||||||
|
|
||||||
|
모든 `datetime.now()`, `datetime.today()`, `datetime.now(timezone.utc)`를 타임존 유틸리티 함수로 교체합니다.
|
||||||
|
|
||||||
|
#### 2.1 app/user/services/jwt.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
expire = datetime.now() + timedelta(...)
|
||||||
|
|
||||||
|
# After
|
||||||
|
from datetime import timedelta
|
||||||
|
from app.utils.timezone import now
|
||||||
|
|
||||||
|
expire = now() + timedelta(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 app/user/services/auth.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
user.last_login_at = datetime.now()
|
||||||
|
if db_token.expires_at < datetime.now():
|
||||||
|
revoked_at=datetime.now()
|
||||||
|
|
||||||
|
# After
|
||||||
|
from app.utils.timezone import now
|
||||||
|
|
||||||
|
user.last_login_at = now()
|
||||||
|
if db_token.expires_at < now():
|
||||||
|
revoked_at=now()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 app/user/api/routers/v1/auth.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
user.last_login_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# After
|
||||||
|
from app.utils.timezone import now
|
||||||
|
|
||||||
|
user.last_login_at = now()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4 app/social/services.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
buffer_time = datetime.now() + timedelta(minutes=10)
|
||||||
|
account.token_expires_at = datetime.now() + timedelta(...)
|
||||||
|
account.connected_at = datetime.now()
|
||||||
|
|
||||||
|
# After
|
||||||
|
from datetime import timedelta
|
||||||
|
from app.utils.timezone import now
|
||||||
|
|
||||||
|
buffer_time = now() + timedelta(minutes=10)
|
||||||
|
account.token_expires_at = now() + timedelta(...)
|
||||||
|
account.connected_at = now()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.5 app/social/worker/upload_task.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
upload.uploaded_at = datetime.now()
|
||||||
|
|
||||||
|
# After
|
||||||
|
from app.utils.timezone import now
|
||||||
|
|
||||||
|
upload.uploaded_at = now()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.6 app/utils/logger.py
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Before
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
today = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# After
|
||||||
|
from app.utils.timezone import today_str
|
||||||
|
|
||||||
|
today = today_str()
|
||||||
|
```
|
||||||
|
|
||||||
|
> **참고**: logger.py에서는 **날짜만 사용하는 것이 맞습니다.**
|
||||||
|
> 로그 파일명이 `{날짜}_app.log`, `{날짜}_error.log` 형식이므로 일별 로그 파일 관리에 적합합니다.
|
||||||
|
> 시간까지 포함하면 매 시간/분마다 새 파일이 생성되어 로그 관리가 어려워집니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 작업 체크리스트
|
||||||
|
|
||||||
|
| 순서 | 작업 | 파일 | 상태 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 1 | 타임존 유틸리티 생성 | `app/utils/timezone.py` | ✅ 완료 |
|
||||||
|
| 2 | JWT 서비스 수정 | `app/user/services/jwt.py` | ✅ 완료 |
|
||||||
|
| 3 | Auth 서비스 수정 | `app/user/services/auth.py` | ✅ 완료 |
|
||||||
|
| 4 | Auth 라우터 수정 | `app/user/api/routers/v1/auth.py` | ✅ 완료 |
|
||||||
|
| 5 | Social 서비스 수정 | `app/social/services.py` | ✅ 완료 |
|
||||||
|
| 6 | Upload Task 수정 | `app/social/worker/upload_task.py` | ✅ 완료 |
|
||||||
|
| 7 | Logger 유틸리티 수정 | `app/utils/logger.py` | ✅ 완료 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 예상 작업 범위
|
||||||
|
|
||||||
|
- **신규 파일**: 1개 (`app/utils/timezone.py`)
|
||||||
|
- **수정 파일**: 6개
|
||||||
|
- **수정 위치**: 약 15곳
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고 사항
|
||||||
|
|
||||||
|
### naive datetime vs aware datetime
|
||||||
|
- **naive datetime**: 타임존 정보가 없는 datetime (예: `datetime.now()`)
|
||||||
|
- **aware datetime**: 타임존 정보가 있는 datetime (예: `datetime.now(TIMEZONE)`)
|
||||||
|
|
||||||
|
### 왜 서울 타임존을 사용하는가?
|
||||||
|
1. 데이터베이스가 Asia/Seoul로 설정됨
|
||||||
|
2. 서비스 대상 지역이 한국
|
||||||
|
3. Python 코드와 DB 간 시간 일관성 확보
|
||||||
|
|
||||||
|
### 주의사항
|
||||||
|
- 기존 DB에 저장된 시간 데이터는 이미 서울 시간이므로 마이그레이션 불필요
|
||||||
|
- JWT 토큰 만료 시간 비교 시 타임존 일관성 필수
|
||||||
|
- 로그 파일명에 사용되는 날짜도 서울 기준으로 통일
|
||||||
Loading…
Reference in New Issue