feat(time): 프로젝트 전체 시간 KST(UTC+9) 통일
- app/timezone.py 신설: KST 고정오프셋(+9, DST 없음) + now_kst/today_kst/to_kst_iso - DB 직렬화 created_at/updated_at/decided_at/uploaded_at 표시를 UTC→KST 로 변환 (expense/cupang/vacation db.py) - _now_iso(JSON 저장) KST 기준 - 라우터 date.today()/datetime.now() → today_kst()/now_kst() (달력 '오늘'/엑셀 파일명) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ from typing import Any
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg_pool import ConnectionPool
|
||||
|
||||
from app.timezone import KST
|
||||
|
||||
from .store import CATEGORIES, METHODS, STATUSES
|
||||
|
||||
|
||||
@@ -364,7 +366,7 @@ class ExpenseDBStore:
|
||||
out = dict(row)
|
||||
v = out.get("uploaded_at")
|
||||
if isinstance(v, datetime):
|
||||
out["uploaded_at"] = v.astimezone(timezone.utc).isoformat(timespec="seconds")
|
||||
out["uploaded_at"] = v.astimezone(KST).isoformat(timespec="seconds")
|
||||
out["size_bytes"] = int(out.get("size_bytes", 0))
|
||||
return out
|
||||
|
||||
@@ -468,7 +470,7 @@ class ExpenseDBStore:
|
||||
for k in ("created_at", "updated_at", "decided_at"):
|
||||
v = out.get(k)
|
||||
if isinstance(v, datetime):
|
||||
out[k] = v.astimezone(timezone.utc).isoformat(timespec="seconds")
|
||||
out[k] = v.astimezone(KST).isoformat(timespec="seconds")
|
||||
out["amount"] = int(out.get("amount", 0))
|
||||
return out
|
||||
|
||||
|
||||
@@ -13,10 +13,11 @@ import mimetypes
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.timezone import now_kst
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
@@ -231,11 +232,11 @@ async def reports_page(request: Request) -> HTMLResponse:
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
store = _store(request)
|
||||
_require_db_store(store)
|
||||
year_str = request.query_params.get("year") or str(datetime.now().year)
|
||||
year_str = request.query_params.get("year") or str(now_kst().year)
|
||||
try:
|
||||
year = int(year_str)
|
||||
except ValueError:
|
||||
year = datetime.now().year
|
||||
year = now_kst().year
|
||||
rows = store.monthly_summary(email=user["email"], year=year)
|
||||
|
||||
# 피벗: month → {category → total}
|
||||
@@ -633,7 +634,7 @@ async def export_xlsx(
|
||||
buf.seek(0)
|
||||
|
||||
fname_scope = "all" if target_email is None else "mine"
|
||||
fname = f"expense_{fname_scope}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
fname = f"expense_{fname_scope}_{now_kst().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
||||
return StreamingResponse(
|
||||
buf,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
|
||||
@@ -13,17 +13,18 @@ import os
|
||||
import tempfile
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.timezone import now_kst_iso
|
||||
|
||||
CATEGORIES: tuple[str, ...] = ("식대", "교통", "숙박", "비품", "접대", "통신", "기타")
|
||||
METHODS: tuple[str, ...] = ("법인카드", "개인지출", "현금")
|
||||
STATUSES: tuple[str, ...] = ("작성중", "제출", "승인", "반려", "정산완료")
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
return now_kst_iso()
|
||||
|
||||
|
||||
class ExpenseStore:
|
||||
|
||||
Reference in New Issue
Block a user