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:
2026-10-30 10:03:35 +09:00
co-authored by Claude Sonnet 5.5
parent 1c0c44f9e2
commit 59610fecf1
62 changed files with 364 additions and 140 deletions
+2 -2
View File
@@ -27,11 +27,11 @@
from __future__ import annotations
import json
from datetime import datetime
from decimal import Decimal
from pathlib import Path
from typing import Any
from common_util.common_util_time import utc_now_text
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
@@ -217,7 +217,7 @@ class Builder:
}
def build(self, checked: dict[int, dict[str, Any]], whole: list[dict[str, Any]]) -> dict:
stamp = datetime.now().isoformat(timespec="seconds")
stamp = utc_now_text()
hopyo, file = self.hopyo, self.doc.get("파일") or ""
lines, mats, missing, used, mat_lines = [], {}, [], set(), {}
nation: dict[tuple, dict[str, Any]] = {}
+2 -2
View File
@@ -14,11 +14,11 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from pathlib import Path
from typing import Any
from common_util.common_util_time import utc_now_text
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
@@ -245,7 +245,7 @@ def write(
if not entries:
return 0
have = read(folder)
stamp = datetime.now().isoformat(timespec="seconds")
stamp = utc_now_text()
version = _version()
out: dict[str, dict[str, Any] | None] = dict(have) # 옛 꼴도 새 꼴로 다시 씀
for full, row in entries.items():
+2 -2
View File
@@ -11,11 +11,11 @@ from __future__ import annotations
import json
import re
import threading
from datetime import datetime
from pathlib import Path
from typing import Any
from common_util.common_util_json import atomic_write_json
from common_util.common_util_time import utc_now_text
from M02_MasterTemplete import M02_MasterTemplete_Store as store
DIRNAME = "실무자료"
@@ -91,7 +91,7 @@ def save(
doc = {
"키": key,
"파일": name,
"올린때": datetime.now().isoformat(timespec="seconds"),
"올린때": utc_now_text(),
"올린이": who,
**parsed,
}
+3 -3
View File
@@ -22,7 +22,6 @@ from __future__ import annotations
import logging
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any
@@ -31,6 +30,7 @@ from B05_Profile.B05_Profile_Structures_Schema import STRUCTURE_ID_RULE
from common_util.common_util_drainage_pipes import pipe_points_path_in, read_pipe_points_file
from common_util.common_util_json import atomic_write_json
from common_util.common_util_summary_item import GROUP_JOIN, snapshot_folder_name
from common_util.common_util_time import kst_now, utc_now_text
from M02_MasterTemplete import M02_Template_Layers as layers
from M02_MasterTemplete.M02_Item_Source import (
LABOR_FILE,
@@ -82,7 +82,7 @@ def safe_folder(base: Path, name: str) -> Path | None:
def now_text() -> str:
return datetime.now().isoformat(timespec="seconds")
return utc_now_text()
def same_item(manifest: Any, item: dict[str, Any]) -> bool:
@@ -181,7 +181,7 @@ def retire(folder: Path) -> str | None:
if isinstance(manifest, dict) and manifest.get("고침"):
past = folder.parent / PAST_DIRNAME
past.mkdir(parents=True, exist_ok=True)
target = past / f"{folder.name}_{datetime.now():%Y%m%d%H%M%S}"
target = past / f"{folder.name}_{kst_now():%Y%m%d%H%M%S}"
shutil.move(str(folder), str(target))
return target.name
shutil.rmtree(folder)
@@ -14,11 +14,11 @@ from __future__ import annotations
import json
import re
from datetime import date
from pathlib import Path
from typing import Any
from common_util.common_util_json import atomic_write_json
from common_util.common_util_time import kst_today
from M01_MasterData import M01_MasterData_Store as m01
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M02_MasterTemplete import M02_MasterTemplete_Store as store
@@ -109,7 +109,7 @@ def _load() -> tuple[dict[str, Any], str]:
return _m01(combo_store._read)
path = store.root() / FILE
if not path.is_file():
head = {"그룹": mcb.GROUP, "판": str(date.today().year), "폴더": [], "다음": 1}
head = {"그룹": mcb.GROUP, "판": str(kst_today().year), "폴더": [], "다음": 1}
return {**head, "줄": []}, ""
raw = path.read_bytes()
return json.loads(raw), store.version_of(raw)
@@ -20,6 +20,7 @@ import {
type LedgerRow,
} from "./M02_MasterTemplete_Hopyo_Api";
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
import { dateTimeText } from "../common_util/common_util_time";
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
import "./M02_MasterTemplete_Hopyo.css";
@@ -262,9 +263,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
},
}),
);
const when = [r.누가?.이름, r.때?.replace("T", " ").slice(0, 16), r.처음파일]
.filter(Boolean)
.join(" · ");
const when = [r.누가?.이름, dateTimeText(r.때, ""), r.처음파일].filter(Boolean).join(" · ");
return el("tr", {
className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`,
attrs: { "data-ledger": r.열쇠 },
@@ -9,6 +9,7 @@
import { createInputField, el, showToast } from "@ui/ui_template_elements";
import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells";
import { dateText } from "../common_util/common_util_time";
import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head";
import "../M01_MasterData/M01_MasterData_UI_Style.css";
import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch";
@@ -50,7 +51,7 @@ const TEXT = {
const SIZE = 30;
const DASH = "—";
const day = (iso?: string): string => (iso ? iso.slice(0, 10) : "");
const day = (iso?: string): string => dateText(iso, "");
const money = (value: number | null | undefined): string =>
typeof value === "number" ? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 }) : DASH;
/** 가장 늦게 고친 날 — 항목 통째 */
@@ -9,11 +9,11 @@
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from typing import Any
from common_util.common_util_json import atomic_write_json
from common_util.common_util_time import utc_from_timestamp
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
from M02_MasterTemplete import M02_MasterTemplete_Store as store
@@ -103,9 +103,7 @@ def _list_extras(folder: Path, col: str) -> dict[str, Any]:
for kind in ("structure", "area", "labor"):
path = folder / kind / f"{col}.json"
if path.is_file():
stamps[kind] = datetime.fromtimestamp(path.stat().st_mtime).isoformat(
timespec="seconds"
)
stamps[kind] = utc_from_timestamp(path.stat().st_mtime)
labor = None
path = folder / "labor" / f"{col}.json"
if path.is_file():
@@ -13,12 +13,12 @@ import threading
from collections.abc import Iterator
from contextlib import contextmanager
from contextvars import ContextVar
from datetime import datetime
from pathlib import Path
from typing import Any
from common_util import common_util_spreadsheet as spreadsheet
from common_util.common_util_json import atomic_write_json
from common_util.common_util_time import utc_from_timestamp
from M02_MasterTemplete import M02_MasterTemplete_Tree as tree
from M02_MasterTemplete import M02_Template_Layers as layers
@@ -106,7 +106,7 @@ def _path(kind: str, name: str) -> Path:
def _info(kind: str, path: Path) -> dict[str, Any]:
raw = path.read_bytes()
stamp = datetime.fromtimestamp(path.stat().st_mtime).isoformat(timespec="seconds")
stamp = utc_from_timestamp(path.stat().st_mtime)
row = {"종류": kind, "이름": path.stem, "판": version_of(raw), "수정일": stamp}
if (kind, path.stem) in IN_USE:
row["쓰는곳"] = IN_USE[(kind, path.stem)] # 화면이 지우기 확인 창 전에 막음
+3 -3
View File
@@ -18,11 +18,11 @@ from __future__ import annotations
import hashlib
import json
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any
from common_util.common_util_json import atomic_write_json
from common_util.common_util_time import utc_from_timestamp, utc_now_text
from config import config_system
LAYERS = ("system", "company", "personal", "project")
@@ -128,7 +128,7 @@ def version_of(path: str | Path) -> str | None:
def _modified(path: Path) -> str:
return datetime.fromtimestamp(path.stat().st_mtime).isoformat(timespec="seconds")
return utc_from_timestamp(path.stat().st_mtime)
def list_templates(layer_dir: str | Path) -> list[dict[str, Any]]:
@@ -314,7 +314,7 @@ def _stamp(layer: str, name: str, version: str | None, **extra: Any) -> dict[str
"층": layer,
"이름": name,
"판": version,
"적용일": datetime.now().isoformat(timespec="seconds"),
"적용일": utc_now_text(),
}
entry.update({key: value for key, value in extra.items() if value is not None})
return entry
+3 -2
View File
@@ -16,10 +16,11 @@ import asyncio
import copy
import os
import sys
from datetime import datetime
from pathlib import Path, PurePosixPath
from typing import Any
from common_util.common_util_time import kst_now
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from config import config_system # noqa: E402
@@ -133,7 +134,7 @@ def render(report: list[dict[str, Any]], system: list[dict[str, Any]]) -> str:
lines = [
"# 기존 프로젝트 양식 넣기",
"",
f"- 돌린 때: {datetime.now().isoformat(timespec='seconds')}",
f"- 돌린 때: {kst_now().isoformat(timespec='seconds')}",
f"- 시스템 양식: {names}",
"- 방법: 작업본 · `_initial/` 에 없는 것만 더함 · 있는 양식 · 다른 파일은 그대로",
f"- 프로젝트 {len(report)}개 · 넣음 {sum(r['상태'] == '넣음' for r in report)}개",
@@ -41,6 +41,7 @@ import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
import { createTabs } from "@ui/ui_template_tabs";
import { dateTimeText } from "../../common_util/common_util_time";
/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */
const OLD_EDITOR = false;
@@ -362,7 +363,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
...(versions.length
? versions.map((v) => {
const b = createButton({
label: to("PastItem", `${v.판} · ${v.저장일}`),
label: to("PastItem", `${v.판} · ${dateTimeText(v.저장일)}`),
variant: past?.판 === v.판 ? "filled" : "ghost",
size: "sm",
});
@@ -9,7 +9,6 @@ from __future__ import annotations
import json
from collections.abc import Callable
from datetime import datetime
from pathlib import Path
from typing import Any
@@ -17,6 +16,7 @@ from pydantic import ValidationError
from B05_Profile import B05_Profile_Structures_Schema as schema
from common_util.common_util_json import atomic_write_json
from common_util.common_util_time import utc_from_timestamp
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.StructOptions.M02_SO_Check import catalog_errors
@@ -43,7 +43,7 @@ def history() -> list[dict[str, Any]]:
for path in folder.glob("판_*.json") if folder.is_dir() else []:
number = path.stem.removeprefix("판_")
if number.isdigit():
when = datetime.fromtimestamp(path.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
when = utc_from_timestamp(path.stat().st_mtime)
rows.append({"판": int(number), "저장일": when})
return sorted(rows, key=lambda r: r["판"], reverse=True)