feat(시간 표시): 화면 시각 = 보는 사람 PC 시간대 · 서버 응답 UTC Z · 서버가 만드는 파일 글 = 한국 시각 (PLAN 593-1 · 요구 456)
- 공용 `common_util_time.ts`(날짜 · 시각 · 오늘/어제/n 일 전 · Z 없는 옛 글 = UTC) · `common_util_time.py`(UTC Z 글 · 한국 시각) - 화면 바꿈: 대시보드(프로젝트 · 휴지통 · 시스템 로그 · 보관함 · 홈 카드) · B09 굳힘 · E03 · E05 · E07 · M02 호표 이력 · 목록 고친 날 · 지난 판 - 서버: 응답 datetime = UTC + Z(main.py) · 로컬 시각 저장 → UTC · 대시보드 경과일 = UTC 기준 · 엑셀 날짜 · 파일 이름 = 한국 시각 - 시험: test_593_time(서울 · 뉴욕 · Z 없는 옛 글 · 서버 글) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
import B01_Dashboard.B01_Dashboard_Router_Home as home
|
||||
import B01_Dashboard.B01_Dashboard_Summary as summary
|
||||
from common_util.common_util_time import utc_now_naive
|
||||
|
||||
ROUTE = "B05_Profile/route/r.geojson"
|
||||
|
||||
@@ -65,7 +66,7 @@ class _Pool:
|
||||
|
||||
|
||||
def test_summaries_in_batch(monkeypatch, tmp_path):
|
||||
now = datetime.now()
|
||||
now = utc_now_naive() # DB 값 = UTC
|
||||
route_dir = tmp_path / "B05_Profile" / "route"
|
||||
route_dir.mkdir(parents=True)
|
||||
coords = [[0, 0, 1], [10, 0, 1], [10, 5, 1]]
|
||||
|
||||
@@ -87,6 +87,7 @@ def test_로그_거르기_글():
|
||||
] # fmt: skip
|
||||
out = _node(
|
||||
"type AuditLog = any;\n"
|
||||
+ "const dateText = (v: string | null | undefined, e: string) => (v ? v.slice(0, 10) : e);\n"
|
||||
+ src
|
||||
+ f"const l = {json.dumps(logs, ensure_ascii=False)};"
|
||||
+ "const f = (o: object) => filterLogs(l, { action: '', from: '', to: '', q: '', ...o }).length;"
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"""593-1 시간 표시 — UTC 글 → 보는 PC 시간대(TS) · 서버가 만드는 글 = UTC Z / 한국 시각(Python)."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from common_util import common_util_time as srv
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
SRC = ROOT / "common_util" / "common_util_time.ts"
|
||||
|
||||
_RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import * as m from "./common_util_time.js";
|
||||
const now = Date.parse("2026-10-30T20:00:00Z");
|
||||
const out = {
|
||||
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
date: ["2026-10-30 01:02:03", "2026-10-30T01:02:03", "2026-10-30T01:02:03Z", "2026-10-30T10:02:03+09:00", null, "", "x"].map((v) => m.dateTimeText(v)),
|
||||
day: [m.dateText("2026-10-30T20:30:00Z"), m.dateText("2026-10-30"), m.dateText(null), m.dateText("")],
|
||||
time: [m.timeText("2026-10-30 20:30:00"), m.timeText(null)],
|
||||
ago: ["2026-10-30T20:00:00Z", "2026-10-30T10:00:00Z", "2026-10-29T10:00:00Z", "2026-10-26T10:00:00Z", "2026-10-01T10:00:00Z", null].map((v) => m.agoText(v, "-", now)),
|
||||
kst: m.kstText("2026-10-30 01:02:03"),
|
||||
};
|
||||
writeFileSync(process.argv[2], JSON.stringify(out));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def runner(tmp_path_factory):
|
||||
if not TSC.is_file():
|
||||
pytest.skip("프론트엔드 의존성 미설치")
|
||||
tmp = tmp_path_factory.mktemp("time")
|
||||
out = tmp / "js"
|
||||
args = ["--outDir", str(out), "--module", "esnext", "--target", "es2022", "--ignoreConfig"]
|
||||
subprocess.run(
|
||||
["node", str(TSC), str(SRC), *args], cwd=str(ROOT), check=True, capture_output=True
|
||||
) # noqa: S603, S607
|
||||
for emitted in out.glob("*.js"):
|
||||
text = emitted.read_text(encoding="utf-8")
|
||||
emitted.write_text(re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', text), encoding="utf-8")
|
||||
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
|
||||
|
||||
def run(tz: str) -> dict:
|
||||
dst = tmp / f"out_{tz.replace('/', '_')}.json"
|
||||
env = {**os.environ, "TZ": tz}
|
||||
subprocess.run(
|
||||
["node", str(out / "runner.mjs"), str(dst)], check=True, capture_output=True, env=env
|
||||
) # noqa: S603, S607
|
||||
return json.loads(dst.read_text(encoding="utf-8"))
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def test_seoul(runner):
|
||||
r = runner("Asia/Seoul")
|
||||
assert r["tz"] == "Asia/Seoul"
|
||||
# Z 없는 옛 글 · ISO · Z · +09:00 모두 같은 한국 시각
|
||||
assert r["date"][:4] == ["2026-10-30 10:02"] * 4
|
||||
assert r["date"][4:] == ["—", "—", "—"]
|
||||
assert r["day"] == ["2026-10-31", "2026-10-30", "-", "-"] # 20:30Z = 한국 다음 날 05:30
|
||||
assert r["time"] == ["05:30", ""]
|
||||
assert r["kst"] == "2026-10-30 10:02"
|
||||
# 지금 = 한국 10-31 05:00 → 한국 달력 날짜로 센다
|
||||
assert r["ago"] == ["오늘", "어제", "2 일 전", "5 일 전", "2026-10-01", "-"]
|
||||
|
||||
|
||||
def test_other_zone_follows_viewer(runner):
|
||||
r = runner("America/New_York") # 10-30 = 서머타임(UTC-4)
|
||||
assert r["date"][:4] == ["2026-10-29 21:02"] * 4
|
||||
assert r["day"][0] == "2026-10-30"
|
||||
assert r["day"][1] == "2026-10-30" # 날짜만 있는 값은 안 바뀜
|
||||
|
||||
|
||||
def test_server_utc_text():
|
||||
assert srv.utc_text(datetime(2026, 10, 30, 1, 2, 3, 999)) == "2026-10-30T01:02:03Z"
|
||||
assert srv.utc_text(datetime(2026, 10, 30, 10, 2, 3, tzinfo=srv.KST)) == "2026-10-30T01:02:03Z"
|
||||
assert srv.utc_text(None) is None
|
||||
assert srv.utc_now_text().endswith("Z")
|
||||
assert srv.utc_from_timestamp(0) == "1970-01-01T00:00:00Z"
|
||||
|
||||
|
||||
def test_server_kst():
|
||||
assert srv.kst_from_timestamp(0).strftime("%Y-%m-%d %H:%M") == "1970-01-01 09:00"
|
||||
assert srv.kst_now().utcoffset().total_seconds() == 9 * 3600
|
||||
assert srv.kst_today() == srv.kst_now().date()
|
||||
|
||||
|
||||
def test_응답_시각_끝_Z():
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
srv.install_json_utc()
|
||||
body = jsonable_encoder({"at": datetime(2026, 10, 30, 1, 2, 3), "n": 1})
|
||||
assert body == {"at": "2026-10-30T01:02:03Z", "n": 1}
|
||||
Reference in New Issue
Block a user