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:
@@ -1,12 +1,13 @@
|
|||||||
"""B01 대시보드 홈 API (PLAN 547) — 프로젝트 카드 요약 · 역할별 홈."""
|
"""B01 대시보드 홈 API (PLAN 547) — 프로젝트 카드 요약 · 역할별 홈."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiomysql
|
import aiomysql
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|
||||||
from common_util.common_util_auth import is_forest_email, verify_session
|
from common_util.common_util_auth import is_forest_email, verify_session
|
||||||
|
from common_util.common_util_time import utc_now_naive, utc_text
|
||||||
from config.config_db import get_db_pool
|
from config.config_db import get_db_pool
|
||||||
|
|
||||||
from .B01_Dashboard_Repository import get_system_resources
|
from .B01_Dashboard_Repository import get_system_resources
|
||||||
@@ -111,13 +112,13 @@ async def _todos(cursor: Any, session: dict[str, Any], role: str) -> dict[str, A
|
|||||||
(int(session["user_id"]),),
|
(int(session["user_id"]),),
|
||||||
)
|
)
|
||||||
batches = list(await cursor.fetchall())
|
batches = list(await cursor.fetchall())
|
||||||
soon = datetime.now() + timedelta(days=EXPIRE_SOON_DAYS)
|
soon = utc_now_naive() + timedelta(days=EXPIRE_SOON_DAYS)
|
||||||
todos["expiring_temp"] = [
|
todos["expiring_temp"] = [
|
||||||
{
|
{
|
||||||
"batch_id": str(b["id"]),
|
"batch_id": str(b["id"]),
|
||||||
"name": b["name"],
|
"name": b["name"],
|
||||||
"expires_at": b["expires_at"].isoformat(),
|
"expires_at": utc_text(b["expires_at"]),
|
||||||
"days_left": max(0, (b["expires_at"] - datetime.now()).days),
|
"days_left": max(0, (b["expires_at"] - utc_now_naive()).days),
|
||||||
}
|
}
|
||||||
for b in batches
|
for b in batches
|
||||||
if b["expires_at"] and b["expires_at"] <= soon
|
if b["expires_at"] and b["expires_at"] <= soon
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import aiomysql
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
from common_util.common_util_auth import verify_session
|
from common_util.common_util_auth import verify_session
|
||||||
|
from common_util.common_util_time import utc_now_naive
|
||||||
from config.config_db import get_db_pool
|
from config.config_db import get_db_pool
|
||||||
|
|
||||||
from .B01_Dashboard_Repository import _project_rows
|
from .B01_Dashboard_Repository import _project_rows
|
||||||
@@ -64,7 +65,7 @@ def idle_days(updated_at: datetime | None, now: datetime | None = None) -> int |
|
|||||||
"""마지막 수정 뒤 지난 날 수."""
|
"""마지막 수정 뒤 지난 날 수."""
|
||||||
if updated_at is None:
|
if updated_at is None:
|
||||||
return None
|
return None
|
||||||
return ((now or datetime.now()) - updated_at).days
|
return ((now or utc_now_naive()) - updated_at).days
|
||||||
|
|
||||||
|
|
||||||
def is_mine(row: dict[str, Any], user_id: int) -> bool:
|
def is_mine(row: dict[str, Any], user_id: int) -> bool:
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_now_naive, utc_text
|
||||||
from config.config_db import get_db_pool
|
from config.config_db import get_db_pool
|
||||||
from M02_MasterTemplete.M02_Template_Migrate import project_root
|
from M02_MasterTemplete.M02_Template_Migrate import project_root
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ async def project_summaries(project_ids: list[str]) -> list[dict[str, Any]]:
|
|||||||
)
|
)
|
||||||
stations = {int(r["route_id"]): int(r["n"]) for r in await cursor.fetchall()}
|
stations = {int(r["route_id"]): int(r["n"]) for r in await cursor.fetchall()}
|
||||||
|
|
||||||
now = datetime.now()
|
now = utc_now_naive()
|
||||||
result = []
|
result = []
|
||||||
for pid in ids:
|
for pid in ids:
|
||||||
row = rows.get(pid)
|
row = rows.get(pid)
|
||||||
@@ -159,7 +159,7 @@ async def project_summaries(project_ids: list[str]) -> list[dict[str, Any]]:
|
|||||||
"stage_total": len(names),
|
"stage_total": len(names),
|
||||||
"stage_name": names[index],
|
"stage_name": names[index],
|
||||||
"done": bool(stages) and all(s["state"] == "COMPLETE" for s in stages),
|
"done": bool(stages) and all(s["state"] == "COMPLETE" for s in stages),
|
||||||
"updated_at": updated.isoformat() if updated else None,
|
"updated_at": utc_text(updated),
|
||||||
"stale_days": (now - updated).days if updated else None,
|
"stale_days": (now - updated).days if updated else None,
|
||||||
"owner_user_id": row.get("user_id"),
|
"owner_user_id": row.get("user_id"),
|
||||||
"owner_names": owners,
|
"owner_names": owners,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
} from "@ui/ui_template_elements";
|
} from "@ui/ui_template_elements";
|
||||||
import type { DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
import type { DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
||||||
|
import { dateText, timeText } from "../common_util/common_util_time";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 목록형 컨테이너가 한 번에 보여 주는 행 수. 넘치는 만큼은 컨테이너 안에서 스크롤한다 —
|
* 목록형 컨테이너가 한 번에 보여 주는 행 수. 넘치는 만큼은 컨테이너 안에서 스크롤한다 —
|
||||||
@@ -36,16 +37,9 @@ export async function runRequest(action: () => Promise<unknown>): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(value?: string | null): string {
|
/** 날짜 · 시:분 — 서버 UTC 를 보는 PC 시간대로(`common_util_time`). 시:분은 표에서 날짜 아래 줄에 붙인다. */
|
||||||
return value ? value.slice(0, 10) : "-";
|
export const formatDate = (value?: string | null): string => dateText(value);
|
||||||
}
|
export const formatTime = (value?: string | null): string => timeText(value);
|
||||||
|
|
||||||
/** 시:분 — 표에서 날짜 아래 줄에 붙인다 (2026-09-06 사용자 지시). */
|
|
||||||
export function formatTime(value?: string | null): string {
|
|
||||||
if (!value) return "";
|
|
||||||
const time = value.includes("T") ? value.split("T")[1] : value.slice(11);
|
|
||||||
return time ? time.slice(0, 5) : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 위·아래 두 줄짜리 표 칸. 아랫줄은 작은 글씨라 **행 높이는 한 줄일 때와 같다**.
|
* 위·아래 두 줄짜리 표 칸. 아랫줄은 작은 글씨라 **행 높이는 한 줄일 때와 같다**.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { createButton, el } from "@ui/ui_template_elements";
|
|||||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflow_nav";
|
||||||
import { groupFlowOf } from "../A00_Common/group_workflow";
|
import { groupFlowOf } from "../A00_Common/group_workflow";
|
||||||
import type { ProjectSummary } from "./B01_Dashboard_Api_Home";
|
import type { ProjectSummary } from "./B01_Dashboard_Api_Home";
|
||||||
|
import { calendarDaysAgo } from "../common_util/common_util_time";
|
||||||
import { L } from "./B01_Dashboard_UI_Common";
|
import { L } from "./B01_Dashboard_UI_Common";
|
||||||
import { isStale } from "./logic/B01_Dashboard_Home_Layout";
|
import { isStale } from "./logic/B01_Dashboard_Home_Layout";
|
||||||
|
|
||||||
@@ -59,10 +60,10 @@ export const stageText = (card: ProjectSummary): string =>
|
|||||||
|
|
||||||
/** 「n 일째 그대로」 · 오늘 · n 일 전 */
|
/** 「n 일째 그대로」 · 오늘 · n 일 전 */
|
||||||
export function whenText(card: ProjectSummary): string {
|
export function whenText(card: ProjectSummary): string {
|
||||||
const days = card.stale_days ?? null;
|
const days = calendarDaysAgo(card.updated_at) ?? card.stale_days ?? null;
|
||||||
if (days === null) return "";
|
if (days === null) return "";
|
||||||
if (isStale(card)) return `${days} 일째 그대로`;
|
if (isStale(card)) return `${days} 일째 그대로`;
|
||||||
return days === 0 ? "오늘" : `${days} 일 전`;
|
return days <= 0 ? "오늘" : `${days} 일 전`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function continueStage(card: ProjectSummary): void {
|
export function continueStage(card: ProjectSummary): void {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
stageProgress,
|
stageProgress,
|
||||||
} from "./B01_Dashboard_UI_ProjectList_Parts";
|
} from "./B01_Dashboard_UI_ProjectList_Parts";
|
||||||
import { openMenuAt, rowMenuItems } from "./B01_Dashboard_UI_ProjectList_Views";
|
import { openMenuAt, rowMenuItems } from "./B01_Dashboard_UI_ProjectList_Views";
|
||||||
|
import { dateText, dateTimeText } from "../common_util/common_util_time";
|
||||||
|
|
||||||
/** 인쇄 · 내려받기 화면(B09 초안 길) — 도면 PDF · 수량 · 내역 xlsx 를 받는 곳 */
|
/** 인쇄 · 내려받기 화면(B09 초안 길) — 도면 PDF · 수량 · 내역 xlsx 를 받는 곳 */
|
||||||
const PRINT_ROUTE = "b09-print" as RoutePath;
|
const PRINT_ROUTE = "b09-print" as RoutePath;
|
||||||
@@ -117,7 +118,7 @@ function infoPane(detail: ProjectDetail): HTMLElement {
|
|||||||
["시행청", p.client_org],
|
["시행청", p.client_org],
|
||||||
["사업 번호", p.project_number],
|
["사업 번호", p.project_number],
|
||||||
["담당", people],
|
["담당", people],
|
||||||
["만든 날", (p.created_at ?? "").slice(0, 10)],
|
["만든 날", dateText(p.created_at, "")],
|
||||||
];
|
];
|
||||||
const kv = el("div", { className: "b01-pd-kv" });
|
const kv = el("div", { className: "b01-pd-kv" });
|
||||||
for (const [label, value] of rows) {
|
for (const [label, value] of rows) {
|
||||||
@@ -178,7 +179,7 @@ function changeText(change: ProjectChange, detail: ProjectDetail): string {
|
|||||||
: change.action === "PROJECT_CREATE"
|
: change.action === "PROJECT_CREATE"
|
||||||
? "만듦"
|
? "만듦"
|
||||||
: "정보 고침";
|
: "정보 고침";
|
||||||
return [change.at.slice(0, 16), change.who, what].filter(Boolean).join(" · ");
|
return [dateTimeText(change.at, ""), change.who, what].filter(Boolean).join(" · ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePane(detail: ProjectDetail): HTMLElement | null {
|
function changePane(detail: ProjectDetail): HTMLElement | null {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../A00_Common/b_workflo
|
|||||||
import { groupFlowOf } from "../A00_Common/group_workflow";
|
import { groupFlowOf } from "../A00_Common/group_workflow";
|
||||||
import type { RoutePath } from "@config/config_frontend";
|
import type { RoutePath } from "@config/config_frontend";
|
||||||
import type { BoardColumn, OverviewProject } from "./B01_Dashboard_Api_Projects";
|
import type { BoardColumn, OverviewProject } from "./B01_Dashboard_Api_Projects";
|
||||||
|
import { agoText } from "../common_util/common_util_time";
|
||||||
import { L } from "./B01_Dashboard_UI_Common";
|
import { L } from "./B01_Dashboard_UI_Common";
|
||||||
|
|
||||||
/** 종류 — 칩 · 배지 차례 · 사방설계 = 준비 중 */
|
/** 종류 — 칩 · 배지 차례 · 사방설계 = 준비 중 */
|
||||||
@@ -121,10 +122,5 @@ export function thumbSvg(points: number[][] | null | undefined): SVGElement {
|
|||||||
|
|
||||||
/** 「오늘」 · 「어제」 · 「n 일 전」 · 그 밖 날짜 */
|
/** 「오늘」 · 「어제」 · 「n 일 전」 · 그 밖 날짜 */
|
||||||
export function ago(project: OverviewProject): string {
|
export function ago(project: OverviewProject): string {
|
||||||
const days = project.idle_days;
|
return agoText(project.updated_at);
|
||||||
if (days === null || days === undefined) return "-";
|
|
||||||
if (days <= 0) return "오늘";
|
|
||||||
if (days === 1) return "어제";
|
|
||||||
if (days < 7) return `${days} 일 전`;
|
|
||||||
return (project.updated_at ?? "").slice(0, 10);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { el } from "@ui/ui_template_elements_base";
|
import { el } from "@ui/ui_template_elements_base";
|
||||||
import type { DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
import type { DashboardUser } from "./B01_Dashboard_Api_Fetch";
|
||||||
import type { OverviewProject } from "./B01_Dashboard_Api_Projects";
|
import type { OverviewProject } from "./B01_Dashboard_Api_Projects";
|
||||||
|
import { dateText } from "../common_util/common_util_time";
|
||||||
import { canDeleteProject, canEditProject } from "./B01_Dashboard_UI_Helper";
|
import { canDeleteProject, canEditProject } from "./B01_Dashboard_UI_Helper";
|
||||||
import { openDeleteProjectModal, openEditProjectModal } from "./B01_Dashboard_UI_Modals";
|
import { openDeleteProjectModal, openEditProjectModal } from "./B01_Dashboard_UI_Modals";
|
||||||
import {
|
import {
|
||||||
@@ -148,7 +149,7 @@ export function projectRows(
|
|||||||
}),
|
}),
|
||||||
el("td", { text: boardLabel(project.board) }),
|
el("td", { text: boardLabel(project.board) }),
|
||||||
el("td", { text: project.manager_name ?? "-" }),
|
el("td", { text: project.manager_name ?? "-" }),
|
||||||
el("td", { text: (project.updated_at ?? "").slice(0, 10) || "-" }),
|
el("td", { text: dateText(project.updated_at) }),
|
||||||
el("td", { children: [more] }),
|
el("td", { children: [more] }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { icon, type IconName } from "@ui/ui_template_icons";
|
|||||||
import { currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
|
import { currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
|
||||||
import { readTouchMode, setTouchMode, type TouchMode } from "../A00_Common/device_context";
|
import { readTouchMode, setTouchMode, type TouchMode } from "../A00_Common/device_context";
|
||||||
import { currentQuery, navigateTo, type MountedPage } from "../A00_Common/router";
|
import { currentQuery, navigateTo, type MountedPage } from "../A00_Common/router";
|
||||||
|
import { parseUtcMs } from "../common_util/common_util_time";
|
||||||
import {
|
import {
|
||||||
fetchAllCompanies,
|
fetchAllCompanies,
|
||||||
fetchCompanyJoinRequests,
|
fetchCompanyJoinRequests,
|
||||||
@@ -216,7 +217,7 @@ async function refreshBadges(ctx: ViewCtx, nav: DashboardNav, bell: HTMLElement)
|
|||||||
const newPlans = plans.filter((p) => !p.linked_project_id || p.update_available).length;
|
const newPlans = plans.filter((p) => !p.linked_project_id || p.update_available).length;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const expiring = temp.filter(
|
const expiring = temp.filter(
|
||||||
(b) => b.expires_at && Date.parse(b.expires_at) - now < EXPIRE_SOON_MS,
|
(b) => b.expires_at && (parseUtcMs(b.expires_at) ?? Infinity) - now < EXPIRE_SOON_MS,
|
||||||
).length;
|
).length;
|
||||||
nav.setBadge("people", joins.length, true);
|
nav.setBadge("people", joins.length, true);
|
||||||
nav.setBadge("assigned", newPlans, true);
|
nav.setBadge("assigned", newPlans, true);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
uploadTempChunk,
|
uploadTempChunk,
|
||||||
type TempBatchItem,
|
type TempBatchItem,
|
||||||
} from "./B01_Dashboard_Api_Temp";
|
} from "./B01_Dashboard_Api_Temp";
|
||||||
|
import { dateText } from "../common_util/common_util_time";
|
||||||
import { L } from "./B01_Dashboard_UI_Common";
|
import { L } from "./B01_Dashboard_UI_Common";
|
||||||
import {
|
import {
|
||||||
badge,
|
badge,
|
||||||
@@ -73,12 +74,6 @@ function taskKey(file: File): string {
|
|||||||
return `${file.name}::${file.size}`;
|
return `${file.name}::${file.size}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: string | null): string {
|
|
||||||
if (!value) return "-";
|
|
||||||
const date = new Date(value);
|
|
||||||
return Number.isNaN(date.getTime()) ? "-" : date.toLocaleDateString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS: Column[] = [
|
||||||
{ label: "보관 이름" },
|
{ label: "보관 이름" },
|
||||||
{ label: "상태", width: "120px" },
|
{ label: "상태", width: "120px" },
|
||||||
@@ -263,7 +258,7 @@ export function buildTempBox(onGuide: () => void): TempBox {
|
|||||||
nameWithCaret(batch, opened),
|
nameWithCaret(batch, opened),
|
||||||
status,
|
status,
|
||||||
leftCell(batch),
|
leftCell(batch),
|
||||||
formatDate(batch.created_at),
|
dateText(batch.created_at),
|
||||||
rowMenu([
|
rowMenu([
|
||||||
{ label: L("B01_Temp_Btn_Add"), onClick: () => openAddModal(batch) },
|
{ label: L("B01_Temp_Btn_Add"), onClick: () => openAddModal(batch) },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { fetchAuditLogPage, type AuditLog } from "./B01_Dashboard_Api_Fetch";
|
import { fetchAuditLogPage, type AuditLog } from "./B01_Dashboard_Api_Fetch";
|
||||||
import { formatDate, formatTime } from "./B01_Dashboard_UI_Common";
|
import { formatDate, formatTime } from "./B01_Dashboard_UI_Common";
|
||||||
|
import { dateText } from "../common_util/common_util_time";
|
||||||
import { createButton } from "@ui/ui_template_elements";
|
import { createButton } from "@ui/ui_template_elements";
|
||||||
import { dataTable, emptyState, pageFrame } from "./B01_Dashboard_UI_PageKit";
|
import { dataTable, emptyState, pageFrame } from "./B01_Dashboard_UI_PageKit";
|
||||||
import { el as h, loadPage, searchBox, selectBox, toolbar } from "./B01_Dashboard_UI_View_Parts";
|
import { el as h, loadPage, searchBox, selectBox, toolbar } from "./B01_Dashboard_UI_View_Parts";
|
||||||
@@ -54,7 +55,7 @@ export interface LogFilter {
|
|||||||
/** 동작 · 기간(날짜 YYYY-MM-DD 양끝 포함) · 찾기(이메일 · 대상 · 접속 주소) */
|
/** 동작 · 기간(날짜 YYYY-MM-DD 양끝 포함) · 찾기(이메일 · 대상 · 접속 주소) */
|
||||||
export function filterLogs(logs: AuditLog[], f: LogFilter): AuditLog[] {
|
export function filterLogs(logs: AuditLog[], f: LogFilter): AuditLog[] {
|
||||||
return logs.filter((l) => {
|
return logs.filter((l) => {
|
||||||
const day = (l.timestamp ?? "").slice(0, 10);
|
const day = dateText(l.timestamp, "");
|
||||||
if (f.action && l.action !== f.action) return false;
|
if (f.action && l.action !== f.action) return false;
|
||||||
if (f.from && day < f.from) return false;
|
if (f.from && day < f.from) return false;
|
||||||
if (f.to && day > f.to) return false;
|
if (f.to && day > f.to) return false;
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ export function daysLeft(
|
|||||||
now: number = Date.now(),
|
now: number = Date.now(),
|
||||||
): number | null {
|
): number | null {
|
||||||
if (!expiresAt) return null;
|
if (!expiresAt) return null;
|
||||||
const at = new Date(expiresAt).getTime();
|
// 시간대 표시 없는 서버 글 = UTC(짝: common_util_time.parseUtcMs — 이 파일은 단독으로 돎)
|
||||||
|
const iso = expiresAt.trim().replace(" ", "T");
|
||||||
|
const at = Date.parse(/(Z|[+-]\d\d:?\d\d)$/.test(iso) ? iso : `${iso}Z`);
|
||||||
if (Number.isNaN(at)) return null;
|
if (Number.isNaN(at)) return null;
|
||||||
return Math.max(0, Math.ceil((at - now) / DAY_MS));
|
return Math.max(0, Math.ceil((at - now) / DAY_MS));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import aiomysql
|
|||||||
from common_util.common_util_audit import record_audit
|
from common_util.common_util_audit import record_audit
|
||||||
from common_util.common_util_json import atomic_write_json
|
from common_util.common_util_json import atomic_write_json
|
||||||
from common_util.common_util_storage import PROJECT_STORAGE_LAYOUT_V2
|
from common_util.common_util_storage import PROJECT_STORAGE_LAYOUT_V2
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from common_util.common_util_workflow import load_project_workflow
|
from common_util.common_util_workflow import load_project_workflow
|
||||||
from common_util.common_util_workflow_state import initialize_project_stages
|
from common_util.common_util_workflow_state import initialize_project_stages
|
||||||
from config.config_db import get_db_pool
|
from config.config_db import get_db_pool
|
||||||
@@ -40,7 +41,7 @@ def _initialize_project_storage(project_root: Path, project_id: str) -> None:
|
|||||||
{
|
{
|
||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"storage_version": 2,
|
"storage_version": 2,
|
||||||
"created_at": datetime.utcnow().isoformat(timespec="seconds"),
|
"created_at": utc_now_text(),
|
||||||
"stages": [f"{stage}/{subdir}" for stage, subdir in PROJECT_STORAGE_LAYOUT_V2],
|
"stages": [f"{stage}/{subdir}" for stage, subdir in PROJECT_STORAGE_LAYOUT_V2],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
라우터가 700줄을 넘어 떼어냈다(2026-09-04). 본체와 청크 모듈이 함께 쓰는 것만 둔다.
|
라우터가 700줄을 넘어 떼어냈다(2026-09-04). 본체와 청크 모듈이 함께 쓰는 것만 둔다.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ from B03_FileInput.B03_FileInput_Repository_Temp import (
|
|||||||
from common_util.common_util_storage import (
|
from common_util.common_util_storage import (
|
||||||
resolve_temp_batch_path,
|
resolve_temp_batch_path,
|
||||||
)
|
)
|
||||||
|
from common_util.common_util_time import utc_text
|
||||||
|
|
||||||
_POINT_CLOUD_FILE_TYPES = frozenset({"las", "laz"})
|
_POINT_CLOUD_FILE_TYPES = frozenset({"las", "laz"})
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ def _total_chunks(size_bytes: int, chunk_size_bytes: int) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _iso(value: Any) -> str | None:
|
def _iso(value: Any) -> str | None:
|
||||||
return value.isoformat() if value is not None and hasattr(value, "isoformat") else None
|
return utc_text(value) if isinstance(value, datetime) else None
|
||||||
|
|
||||||
|
|
||||||
async def _batch_root(connection: Any, *, batch_id: str, user_id: int) -> Path:
|
async def _batch_root(connection: Any, *, batch_id: str, user_id: int) -> Path:
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
|
||||||
import http.cookiejar
|
import http.cookiejar
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -27,6 +26,7 @@ import urllib.request
|
|||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from common_util.common_util_time import kst_today
|
||||||
from config.config_system import (
|
from config.config_system import (
|
||||||
MAP_SHEETS_DIRNAME,
|
MAP_SHEETS_DIRNAME,
|
||||||
MAP_SHEETS_INDEX_FILENAME,
|
MAP_SHEETS_INDEX_FILENAME,
|
||||||
@@ -305,7 +305,7 @@ def ingest_zip(store_dir: str | Path, src_path: str | Path, sheet_no: str | None
|
|||||||
"original_name": src.name,
|
"original_name": src.name,
|
||||||
"crs": crs,
|
"crs": crs,
|
||||||
"bounds_wgs84": [round(v, 8) for v in bounds],
|
"bounds_wgs84": [round(v, 8) for v in bounds],
|
||||||
"acquired": datetime.date.today().isoformat(),
|
"acquired": kst_today().isoformat(),
|
||||||
"source": "수동 다운로드 인제스트",
|
"source": "수동 다운로드 인제스트",
|
||||||
}
|
}
|
||||||
_save_index(store, index)
|
_save_index(store, index)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
@@ -23,6 +22,7 @@ import numpy as np
|
|||||||
from shapely.geometry.base import BaseGeometry
|
from shapely.geometry.base import BaseGeometry
|
||||||
|
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from config.config_system import DRAINAGE_CACHE_DIRNAME
|
from config.config_system import DRAINAGE_CACHE_DIRNAME
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -181,7 +181,7 @@ def _update_manifest(
|
|||||||
logger.warning("배수유역: manifest를 읽지 못해 새로 만듭니다 (%s).", manifest_path)
|
logger.warning("배수유역: manifest를 읽지 못해 새로 만듭니다 (%s).", manifest_path)
|
||||||
manifest[stage] = {
|
manifest[stage] = {
|
||||||
"file": filename,
|
"file": filename,
|
||||||
"saved_at": datetime.now().isoformat(timespec="seconds"),
|
"saved_at": utc_now_text(),
|
||||||
"properties": properties,
|
"properties": properties,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from uuid import UUID
|
|||||||
|
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_text
|
||||||
|
|
||||||
_STAGE_ROOT = "B04_PreProcess"
|
_STAGE_ROOT = "B04_PreProcess"
|
||||||
|
|
||||||
|
|
||||||
@@ -224,7 +226,7 @@ async def list_project_point_cloud_inputs(
|
|||||||
"file_size_mb": float(row[4]) if row[4] is not None else None,
|
"file_size_mb": float(row[4]) if row[4] is not None else None,
|
||||||
"crs_epsg": row[5],
|
"crs_epsg": row[5],
|
||||||
"status": row[6],
|
"status": row[6],
|
||||||
"created_at": row[7].isoformat() if row[7] else None,
|
"created_at": utc_text(row[7]),
|
||||||
}
|
}
|
||||||
for row in rows
|
for row in rows
|
||||||
]
|
]
|
||||||
@@ -279,7 +281,7 @@ async def list_surface_models(
|
|||||||
"resolution_m": row[3],
|
"resolution_m": row[3],
|
||||||
"model_file_path": row[4],
|
"model_file_path": row[4],
|
||||||
"generation_params": generation_params,
|
"generation_params": generation_params,
|
||||||
"created_at": row[6].isoformat() if row[6] else None,
|
"created_at": utc_text(row[6]),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return models
|
return models
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, AsyncIterator, Callable
|
from typing import Any, AsyncIterator, Callable
|
||||||
|
|
||||||
@@ -37,6 +36,7 @@ from B05_Profile.B05_Profile_Engine_RouteInitial import (
|
|||||||
resolve_weights,
|
resolve_weights,
|
||||||
)
|
)
|
||||||
from common_util.common_util_json import atomic_write_json
|
from common_util.common_util_json import atomic_write_json
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
|
|
||||||
# 3 = 곡선 MC 일반측점(351-2) · 4 = 다투는 곡선 끔(364-5) · 5 = 후보마다 맞춤(383-1) ·
|
# 3 = 곡선 MC 일반측점(351-2) · 4 = 다투는 곡선 끔(364-5) · 5 = 후보마다 맞춤(383-1) ·
|
||||||
# 6 = 같은 쪽 곡선 합치기 · 거리 한도 대신 직접 잼(406) · 7 = 측점 밖이 늘면 옛 차례 맞춤(406-4) ·
|
# 6 = 같은 쪽 곡선 합치기 · 거리 한도 대신 직접 잼(406) · 7 = 측점 밖이 늘면 옛 차례 맞춤(406-4) ·
|
||||||
@@ -202,7 +202,7 @@ def write_stored(
|
|||||||
"key": key,
|
"key": key,
|
||||||
"pool": pool,
|
"pool": pool,
|
||||||
"default": default,
|
"default": default,
|
||||||
"created_at": datetime.now().isoformat(timespec="seconds"),
|
"created_at": utc_now_text(),
|
||||||
"request": request,
|
"request": request,
|
||||||
"result": result,
|
"result": result,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from uuid import UUID
|
|||||||
|
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_text
|
||||||
|
|
||||||
_STAGE_ROOT = "B05_Profile"
|
_STAGE_ROOT = "B05_Profile"
|
||||||
|
|
||||||
|
|
||||||
@@ -172,7 +174,7 @@ async def get_latest_route(
|
|||||||
return None
|
return None
|
||||||
result = dict(row)
|
result = dict(row)
|
||||||
result["id"] = int(result["id"])
|
result["id"] = int(result["id"])
|
||||||
result["computed_at"] = result["computed_at"].isoformat() if result["computed_at"] else None
|
result["computed_at"] = utc_text(result["computed_at"])
|
||||||
for key in ("constraints", "algorithm_params"):
|
for key in ("constraints", "algorithm_params"):
|
||||||
if isinstance(result.get(key), str):
|
if isinstance(result.get(key), str):
|
||||||
result[key] = json.loads(result[key])
|
result[key] = json.loads(result[key])
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from uuid import UUID
|
|||||||
|
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_text
|
||||||
|
|
||||||
_STAGE_ROOT = "B06_Section"
|
_STAGE_ROOT = "B06_Section"
|
||||||
|
|
||||||
|
|
||||||
@@ -335,7 +337,7 @@ async def get_longitudinal_section(
|
|||||||
"data": data if isinstance(data, dict) else None,
|
"data": data if isinstance(data, dict) else None,
|
||||||
"longitudinal_file_path": row[2],
|
"longitudinal_file_path": row[2],
|
||||||
"status": row[3],
|
"status": row[3],
|
||||||
"computed_at": row[4].isoformat() if row[4] else None,
|
"computed_at": utc_text(row[4]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from B09_Estimation.B09_Estimation_Calc_Base import num, rounddown, roundup, total
|
from B09_Estimation.B09_Estimation_Calc_Base import num, rounddown, roundup, total
|
||||||
|
from common_util.common_util_time import kst_today
|
||||||
|
|
||||||
#: 제비율 줄 (키, 이름, M01 요율 후보 키, 산식 글) — 엑셀 총괄 49~58 차례 · 나.소계에 듦
|
#: 제비율 줄 (키, 이름, M01 요율 후보 키, 산식 글) — 엑셀 총괄 49~58 차례 · 나.소계에 듦
|
||||||
RATE_LINES: tuple[tuple[str, str, str, str], ...] = (
|
RATE_LINES: tuple[tuple[str, str, str, str], ...] = (
|
||||||
@@ -155,7 +155,7 @@ def compute_total(
|
|||||||
"target": target,
|
"target": target,
|
||||||
"price": num(est.get("estimated_price")),
|
"price": num(est.get("estimated_price")),
|
||||||
"days": num(est.get("duration_days")),
|
"days": num(est.get("duration_days")),
|
||||||
"year": year or date.today().year,
|
"year": year or kst_today().year,
|
||||||
"env": est.get("environment_kind"),
|
"env": est.get("environment_kind"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from B08_Quantity.B08_Quantity_Material_Calc import DEDUCT_KEY
|
from B08_Quantity.B08_Quantity_Material_Calc import DEDUCT_KEY
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from M01_MasterData import M01_MasterData_Store as m01
|
from M01_MasterData import M01_MasterData_Store as m01
|
||||||
|
|
||||||
#: 운전사 시간 노임 계수 표(건설품셈 공통 8-1-3 · 건협기술-2230)
|
#: 운전사 시간 노임 계수 표(건설품셈 공통 8-1-3 · 건협기술-2230)
|
||||||
@@ -194,7 +194,7 @@ def refresh_book(keys: list[str], book: dict[str, Any] | None = None) -> dict[st
|
|||||||
old = (book or {}).get("rows") or {}
|
old = (book or {}).get("rows") or {}
|
||||||
rows.update({key: old[key] for key in want["missing"] if key in old})
|
rows.update({key: old[key] for key in want["missing"] if key in old})
|
||||||
return {
|
return {
|
||||||
"made": datetime.now().isoformat(timespec="seconds"),
|
"made": utc_now_text(),
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
"driver_factor": driver_factor(master),
|
"driver_factor": driver_factor(master),
|
||||||
"fx": fx_inputs(master),
|
"fx": fx_inputs(master),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@@ -23,6 +22,7 @@ from fastapi.responses import FileResponse
|
|||||||
from B03_FileInput.B03_FileInput_Engine import save_upload_stream
|
from B03_FileInput.B03_FileInput_Engine import save_upload_stream
|
||||||
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||||||
from B09_Estimation import B09_Estimation_Store as store
|
from B09_Estimation import B09_Estimation_Store as store
|
||||||
|
from common_util.common_util_time import utc_from_timestamp
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
PATH = "/{project_id}/estimation/quotes"
|
PATH = "/{project_id}/estimation/quotes"
|
||||||
@@ -33,7 +33,7 @@ def _listing(folder: Path) -> list[dict[str, Any]]:
|
|||||||
{
|
{
|
||||||
"name": f.name,
|
"name": f.name,
|
||||||
"size": f.stat().st_size,
|
"size": f.stat().st_size,
|
||||||
"at": datetime.fromtimestamp(f.stat().st_mtime).isoformat(timespec="seconds"),
|
"at": utc_from_timestamp(f.stat().st_mtime),
|
||||||
}
|
}
|
||||||
for f in store.quote_files(folder)
|
for f in store.quote_files(folder)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
|||||||
import { createBadge } from "@ui/ui_template_badge";
|
import { createBadge } from "@ui/ui_template_badge";
|
||||||
import { createText } from "@ui/ui_template_text";
|
import { createText } from "@ui/ui_template_text";
|
||||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||||
|
import { dateTimeText } from "../common_util/common_util_time";
|
||||||
import { tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
|
import { tabName, type Link, type TabId } from "./B09_Estimation_UI_Tabs";
|
||||||
|
|
||||||
export interface CheckItem {
|
export interface CheckItem {
|
||||||
@@ -38,7 +39,7 @@ export function checkItems(sheets: Row, payload: Row): { items: CheckItem[]; not
|
|||||||
/** [업데이트] 호버 글 — 굳힌 시각 + 낡은 까닭 */
|
/** [업데이트] 호버 글 — 굳힌 시각 + 낡은 까닭 */
|
||||||
export function refreshTip(payload: Row | null): string {
|
export function refreshTip(payload: Row | null): string {
|
||||||
const made = payload?.book?.made;
|
const made = payload?.book?.made;
|
||||||
const when = made ? `단가 굳힘 ${String(made).replace("T", " ")}` : "단가 아직 안 굳힘";
|
const when = made ? `단가 굳힘 ${dateTimeText(String(made))}` : "단가 아직 안 굳힘";
|
||||||
const stale = payload?.stale;
|
const stale = payload?.stale;
|
||||||
return `${when} · 누르면 지금 M01 값으로 새로 굳힘${stale ? ` · 업데이트 필요 — ${stale}` : ""}`;
|
return `${when} · 누르면 지금 M01 값으로 새로 굳힘${stale ? ` · 업데이트 필요 — ${stale}` : ""}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||||
import type { DataRow } from "@ui/ui_template_data_table";
|
import type { DataRow } from "@ui/ui_template_data_table";
|
||||||
import { SOURCES, type Row } from "./B09_Estimation_Calc_Base";
|
import { SOURCES, type Row } from "./B09_Estimation_Calc_Base";
|
||||||
|
import { dateText } from "../common_util/common_util_time";
|
||||||
import { openQuotes } from "./B09_Estimation_UI_Tabs_Base_Quote";
|
import { openQuotes } from "./B09_Estimation_UI_Tabs_Base_Quote";
|
||||||
import {
|
import {
|
||||||
blockLevel,
|
blockLevel,
|
||||||
@@ -64,7 +65,7 @@ export const buildExpense: TabBuild = ({ sheets, payload }) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
function madeText(payload: Row): string {
|
function madeText(payload: Row): string {
|
||||||
return payload.book?.made ? `${String(payload.book.made).slice(0, 10)} 굳힘` : "아직 안 굳힘";
|
return payload.book?.made ? `${dateText(String(payload.book.made))} 굳힘` : "아직 안 굳힘";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildBasis: TabBuild = ({ sheets }): TabTable => {
|
export const buildBasis: TabBuild = ({ sheets }): TabTable => {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from typing import Any
|
|||||||
from B09_Estimation.B09_Estimation_Calc_Base import num
|
from B09_Estimation.B09_Estimation_Calc_Base import num
|
||||||
from B09_Estimation.B09_Estimation_Summary import expand, hopyo_lines
|
from B09_Estimation.B09_Estimation_Summary import expand, hopyo_lines
|
||||||
from B09_Estimation.draft.B09_Draft_Schedule_Calc import HOLIDAY_KINDS, compute_schedule
|
from B09_Estimation.draft.B09_Draft_Schedule_Calc import HOLIDAY_KINDS, compute_schedule
|
||||||
|
from common_util.common_util_time import kst_today
|
||||||
|
|
||||||
HOLIDAYS = Path(__file__).with_name("B09_Draft_Schedule_Holidays.json")
|
HOLIDAYS = Path(__file__).with_name("B09_Draft_Schedule_Holidays.json")
|
||||||
CLIMATE = {
|
CLIMATE = {
|
||||||
@@ -116,7 +117,7 @@ def holiday_table() -> dict[str, list[list[str]]]:
|
|||||||
|
|
||||||
|
|
||||||
def schedule_inputs(sheets: dict[str, Any], today: datetime.date | None = None) -> dict[str, Any]:
|
def schedule_inputs(sheets: dict[str, Any], today: datetime.date | None = None) -> dict[str, Any]:
|
||||||
today = today or datetime.date.today()
|
today = today or kst_today()
|
||||||
return {
|
return {
|
||||||
"units": unit_resources(sheets),
|
"units": unit_resources(sheets),
|
||||||
"holidays": holiday_table(),
|
"holidays": holiday_table(),
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import math
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -30,6 +29,7 @@ from common_util.common_util_derived_store import (
|
|||||||
write_derived,
|
write_derived,
|
||||||
)
|
)
|
||||||
from common_util.common_util_json import atomic_write_json
|
from common_util.common_util_json import atomic_write_json
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
|
|
||||||
Box = tuple[float, float, float, float]
|
Box = tuple[float, float, float, float]
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ def save_roads(
|
|||||||
{
|
{
|
||||||
"files": [Path(name).name for name, _ in files],
|
"files": [Path(name).name for name, _ in files],
|
||||||
"bytes": len(data),
|
"bytes": len(data),
|
||||||
"uploaded_at": datetime.now().isoformat(timespec="seconds"),
|
"uploaded_at": utc_now_text(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return road_layer(project_root, box, epsg)
|
return road_layer(project_root, box, epsg)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -48,6 +47,7 @@ from common_util.common_util_derived_store import (
|
|||||||
from common_util.common_util_json import atomic_write_json
|
from common_util.common_util_json import atomic_write_json
|
||||||
from common_util.common_util_station_key import chainage_key
|
from common_util.common_util_station_key import chainage_key
|
||||||
from common_util.common_util_surface_sampler import DtmGridSampler
|
from common_util.common_util_surface_sampler import DtmGridSampler
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from E01_StabRegister import E01_StabRegister_Source as src
|
from E01_StabRegister import E01_StabRegister_Source as src
|
||||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||||
from E02_StabGround import E02_StabGround_Router as e02
|
from E02_StabGround import E02_StabGround_Router as e02
|
||||||
@@ -307,7 +307,7 @@ def run_all(root: Path, plan: dict[str, Any], report: Any) -> int:
|
|||||||
rows.append(view.station_row(value["meta"], value["out"], plan["factors"], value["error"]))
|
rows.append(view.station_row(value["meta"], value["out"], plan["factors"], value["error"]))
|
||||||
summary = {
|
summary = {
|
||||||
"fingerprint": plan["fp"],
|
"fingerprint": plan["fp"],
|
||||||
"computed_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
"computed_at": utc_now_text(),
|
||||||
"conditions": shared["conditions"],
|
"conditions": shared["conditions"],
|
||||||
"method": view.PRIMARY,
|
"method": view.PRIMARY,
|
||||||
"factors": plan["factors"],
|
"factors": plan["factors"],
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
@@ -37,6 +36,7 @@ from common_util.common_util_derived_store import (
|
|||||||
read_derived,
|
read_derived,
|
||||||
write_derived,
|
write_derived,
|
||||||
)
|
)
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from E02_StabGround import E02_StabGround_Router as e02
|
from E02_StabGround import E02_StabGround_Router as e02
|
||||||
from E03_StabSection import E03_StabSection_Engine_Prob as prob
|
from E03_StabSection import E03_StabSection_Engine_Prob as prob
|
||||||
from E03_StabSection import E03_StabSection_Router as e03
|
from E03_StabSection import E03_StabSection_Router as e03
|
||||||
@@ -286,7 +286,7 @@ def run_prob(root: Path, station_key: str, req: ProbRequest, report: Any) -> dic
|
|||||||
"surface_kind": slot["surface"]["kind"],
|
"surface_kind": slot["surface"]["kind"],
|
||||||
"request": req.model_dump(),
|
"request": req.model_dump(),
|
||||||
"result": result,
|
"result": result,
|
||||||
"computed_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
"computed_at": utc_now_text(),
|
||||||
}
|
}
|
||||||
write_derived(root, KIND, key, record, keep=KEEP)
|
write_derived(root, KIND, key, record, keep=KEEP)
|
||||||
return record
|
return record
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import hashlib
|
|||||||
import inspect
|
import inspect
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
from functools import lru_cache, partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from functools import lru_cache, partial
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
@@ -34,13 +34,14 @@ from common_util.common_util_derived_store import (
|
|||||||
read_derived,
|
read_derived,
|
||||||
write_derived,
|
write_derived,
|
||||||
)
|
)
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from E01_StabRegister import E01_StabRegister_Source as src
|
from E01_StabRegister import E01_StabRegister_Source as src
|
||||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||||
from E02_StabGround import E02_StabGround_Router as e02
|
from E02_StabGround import E02_StabGround_Router as e02
|
||||||
from E03_StabSection import E03_StabSection_Router as e03
|
from E03_StabSection import E03_StabSection_Router as e03
|
||||||
from E03_StabSection import E03_StabSection_View as view
|
from E03_StabSection import E03_StabSection_View as view
|
||||||
from E03_StabSection import E03_StabSection_Walls as walls_mod
|
|
||||||
from E03_StabSection import E03_StabSection_WallCheck as wallcheck
|
from E03_StabSection import E03_StabSection_WallCheck as wallcheck
|
||||||
|
from E03_StabSection import E03_StabSection_Walls as walls_mod
|
||||||
from E03_StabSection import E03_StabSection_WhatIf as whatif
|
from E03_StabSection import E03_StabSection_WhatIf as whatif
|
||||||
from E03_StabSection.E03_StabSection_Engine_Model import WALL_MATERIAL, section_model_from_b06
|
from E03_StabSection.E03_StabSection_Engine_Model import WALL_MATERIAL, section_model_from_b06
|
||||||
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station
|
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station
|
||||||
@@ -301,7 +302,7 @@ def run_whatif(root: Path, station_key: str, req: WhatIfRequest) -> dict[str, An
|
|||||||
# 엔진 고리 전 = 네일 · 앵커 힘 없음(결과 = 보강 없는 재해석 · 화면은 그림만)
|
# 엔진 고리 전 = 네일 · 앵커 힘 없음(결과 = 보강 없는 재해석 · 화면은 그림만)
|
||||||
"support_forces": _forces(out, req.side) if supports else None,
|
"support_forces": _forces(out, req.side) if supports else None,
|
||||||
"supports_pending": bool(supports) and not engine,
|
"supports_pending": bool(supports) and not engine,
|
||||||
"computed_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
"computed_at": utc_now_text(),
|
||||||
"elapsed_s": round(time.perf_counter() - t0, 2),
|
"elapsed_s": round(time.perf_counter() - t0, 2),
|
||||||
}
|
}
|
||||||
shown = _section(ctx, variant, after, out)
|
shown = _section(ctx, variant, after, out)
|
||||||
@@ -396,7 +397,7 @@ def run_wallcheck(root: Path, station_key: str, req: WallRequest) -> dict[str, A
|
|||||||
"base_fp": row["fp"],
|
"base_fp": row["fp"],
|
||||||
"params": params,
|
"params": params,
|
||||||
"walls": walls,
|
"walls": walls,
|
||||||
"computed_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
"computed_at": utc_now_text(),
|
||||||
}
|
}
|
||||||
write_derived(root, KIND, key, record, keep=KEEP)
|
write_derived(root, KIND, key, record, keep=KEEP)
|
||||||
return record
|
return record
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||||
import { createSegment } from "@ui/ui_template_segment";
|
import { createSegment } from "@ui/ui_template_segment";
|
||||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||||
|
import { dateTimeText } from "../common_util/common_util_time";
|
||||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||||
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
|
import { buildMissingBanner } from "../E01_StabRegister/E01_StabRegister_UI_Missing";
|
||||||
import { buildList } from "./E03_StabSection_UI_List";
|
import { buildList } from "./E03_StabSection_UI_List";
|
||||||
@@ -279,7 +280,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
|||||||
if (!result) lines.push(t("E03_Loading"));
|
if (!result) lines.push(t("E03_Loading"));
|
||||||
else if (result.status === "none") lines.push(t("E03_NotYet"));
|
else if (result.status === "none") lines.push(t("E03_NotYet"));
|
||||||
else {
|
else {
|
||||||
lines.push(`${t("E03_Computed")} ${result.computed_at ?? ""}`);
|
lines.push(`${t("E03_Computed")} ${dateTimeText(result.computed_at, "")}`);
|
||||||
if (!result.fresh) lines.push(t("E03_Stale"));
|
if (!result.fresh) lines.push(t("E03_Stale"));
|
||||||
}
|
}
|
||||||
if (result && !result.editable) lines.push(t("E03_ReadOnly"));
|
if (result && !result.editable) lines.push(t("E03_ReadOnly"));
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import json
|
|||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -30,6 +29,7 @@ from typing import Any
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.spatial import cKDTree
|
from scipy.spatial import cKDTree
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from E01_StabRegister import E01_StabRegister_Source as source
|
from E01_StabRegister import E01_StabRegister_Source as source
|
||||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||||
from E03_StabSection.E03_StabSection_Types import DEFAULT_ROAD_SURCHARGE_KPA
|
from E03_StabSection.E03_StabSection_Types import DEFAULT_ROAD_SURCHARGE_KPA
|
||||||
@@ -335,7 +335,7 @@ def pack(
|
|||||||
"fingerprint": finger,
|
"fingerprint": finger,
|
||||||
"engine": engine_hash(),
|
"engine": engine_hash(),
|
||||||
"settings": settings,
|
"settings": settings,
|
||||||
"computed_at": datetime.now().isoformat(timespec="seconds"),
|
"computed_at": utc_now_text(),
|
||||||
"elapsed_s": round(elapsed_s, 2),
|
"elapsed_s": round(elapsed_s, 2),
|
||||||
"grid": grid,
|
"grid": grid,
|
||||||
"z": _flat(np.asarray(raw["z"], dtype=float), 2),
|
"z": _flat(np.asarray(raw["z"], dtype=float), 2),
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ from __future__ import annotations
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import date
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from common_util.common_util_derived_store import read_derived
|
from common_util.common_util_derived_store import read_derived
|
||||||
|
from common_util.common_util_time import kst_today
|
||||||
from E01_StabRegister import E01_StabRegister_Source as source
|
from E01_StabRegister import E01_StabRegister_Source as source
|
||||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||||
from E03_StabSection import E03_StabSection_View as view2d
|
from E03_StabSection import E03_StabSection_View as view2d
|
||||||
@@ -263,7 +263,7 @@ def fill_cover(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
|||||||
"C9",
|
"C9",
|
||||||
" · ".join(str(v) for v in (project.get("region"), project.get("road_type")) if v),
|
" · ".join(str(v) for v in (project.get("region"), project.get("road_type")) if v),
|
||||||
)
|
)
|
||||||
_set(sheet, "C11", date.today().isoformat())
|
_set(sheet, "C11", kst_today().isoformat())
|
||||||
|
|
||||||
|
|
||||||
# ── 프로그램 몫 ──────────────────────────────────────────────────────────────
|
# ── 프로그램 몫 ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -22,6 +21,7 @@ from common_util.common_util_json import atomic_write_json
|
|||||||
from common_util.common_util_print_pdf import PrintError
|
from common_util.common_util_print_pdf import PrintError
|
||||||
from common_util.common_util_spreadsheet import with_server_values
|
from common_util.common_util_spreadsheet import with_server_values
|
||||||
from common_util.common_util_svg_png import svgs_to_pngs
|
from common_util.common_util_svg_png import svgs_to_pngs
|
||||||
|
from common_util.common_util_time import kst_now, utc_now_text
|
||||||
from E05_StabReport import E05_StabReport_Print as printer
|
from E05_StabReport import E05_StabReport_Print as printer
|
||||||
from E05_StabReport.E05_StabReport_Build import (
|
from E05_StabReport.E05_StabReport_Build import (
|
||||||
META,
|
META,
|
||||||
@@ -78,7 +78,7 @@ def _numbered(book: dict[str, Any]) -> dict[str, Any]:
|
|||||||
def _meta(book: dict[str, Any], inputs: dict[str, Any], figures: list[str]) -> dict[str, Any]:
|
def _meta(book: dict[str, Any], inputs: dict[str, Any], figures: list[str]) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"fingerprint": inputs["fingerprint"],
|
"fingerprint": inputs["fingerprint"],
|
||||||
"built_at": datetime.now().isoformat(timespec="seconds"),
|
"built_at": utc_now_text(),
|
||||||
"signs": {s["id"]: sheet_sign(s) for s in book["시트"] if s["id"] in PROGRAM_SHEETS},
|
"signs": {s["id"]: sheet_sign(s) for s in book["시트"] if s["id"] in PROGRAM_SHEETS},
|
||||||
"figures": figures,
|
"figures": figures,
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ def refresh(project_root: Path) -> dict[str, Any]:
|
|||||||
if overwritten:
|
if overwritten:
|
||||||
old = folder / "_지난"
|
old = folder / "_지난"
|
||||||
old.mkdir(parents=True, exist_ok=True)
|
old.mkdir(parents=True, exist_ok=True)
|
||||||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
stamp = kst_now().strftime("%Y%m%d_%H%M%S")
|
||||||
shutil.copy2(folder / REPORT, old / f"{stamp}.json")
|
shutil.copy2(folder / REPORT, old / f"{stamp}.json")
|
||||||
have = {s["id"] for s in book["시트"]}
|
have = {s["id"] for s in book["시트"]}
|
||||||
# 새 판에 없는 프로그램 몫(보강검토 — 저장본 없음)은 뺌
|
# 새 판에 없는 프로그램 몫(보강검토 — 저장본 없음)은 뺌
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { openModal } from "@ui/ui_template_modal";
|
|||||||
import { createText } from "@ui/ui_template_text";
|
import { createText } from "@ui/ui_template_text";
|
||||||
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
|
||||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||||
|
import { dateTimeText } from "../common_util/common_util_time";
|
||||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||||
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||||
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types";
|
import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet_types";
|
||||||
@@ -162,7 +163,7 @@ export function renderE05StabReport(root: HTMLElement): void {
|
|||||||
if (busy) lines.push(t("E05_Working"));
|
if (busy) lines.push(t("E05_Working"));
|
||||||
if (dirty) lines.push(t("E05_Unsaved"));
|
if (dirty) lines.push(t("E05_Unsaved"));
|
||||||
if (state?.meta?.built_at)
|
if (state?.meta?.built_at)
|
||||||
lines.push(`${t("E05_BuiltAt")} ${state.meta.built_at.replace("T", " ")}`);
|
lines.push(`${t("E05_BuiltAt")} ${dateTimeText(state.meta.built_at)}`);
|
||||||
status.replaceChildren(...(lines.length ? [note(lines.join("\n"), "e05-status__text")] : []));
|
status.replaceChildren(...(lines.length ? [note(lines.join("\n"), "e05-status__text")] : []));
|
||||||
const ready = Boolean(state?.book) && !busy;
|
const ready = Boolean(state?.book) && !busy;
|
||||||
saveButton.disabled = !ready || !editable || !dirty;
|
saveButton.disabled = !ready || !editable || !dirty;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
@@ -39,6 +38,7 @@ from pydantic import BaseModel, Field, model_validator
|
|||||||
from common_util.common_util_auth import require_company
|
from common_util.common_util_auth import require_company
|
||||||
from common_util.common_util_json import atomic_write_json
|
from common_util.common_util_json import atomic_write_json
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from config.config_db import get_db_pool
|
from config.config_db import get_db_pool
|
||||||
from E01_StabRegister import E01_StabRegister_Source as source
|
from E01_StabRegister import E01_StabRegister_Source as source
|
||||||
from E02_StabGround import E02_StabGround_Model as ground_model
|
from E02_StabGround import E02_StabGround_Model as ground_model
|
||||||
@@ -245,7 +245,7 @@ def run(
|
|||||||
"fingerprint": finger,
|
"fingerprint": finger,
|
||||||
"engine": engine_hash(),
|
"engine": engine_hash(),
|
||||||
"settings": settings,
|
"settings": settings,
|
||||||
"computed_at": datetime.now().isoformat(timespec="seconds"),
|
"computed_at": utc_now_text(),
|
||||||
"elapsed_s": round(time.perf_counter() - started, 2),
|
"elapsed_s": round(time.perf_counter() - started, 2),
|
||||||
"grid": {
|
"grid": {
|
||||||
"x0": x0 + int(ix.min()) * cell,
|
"x0": x0 + int(ix.min()) * cell,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { createSidePanels } from "@ui/ui_template_side_panels";
|
|||||||
import { createText } from "@ui/ui_template_text";
|
import { createText } from "@ui/ui_template_text";
|
||||||
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
|
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
|
||||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||||
|
import { dateTimeText } from "../common_util/common_util_time";
|
||||||
import { currentProjectId } from "../A00_Common/b_project_switch";
|
import { currentProjectId } from "../A00_Common/b_project_switch";
|
||||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||||
import { createAreaMap, type MapLayer, type StationPoint } from "./E07_StabArea_UI_Map";
|
import { createAreaMap, type MapLayer, type StationPoint } from "./E07_StabArea_UI_Map";
|
||||||
@@ -337,10 +338,7 @@ export function renderE07StabArea(root: HTMLElement): void {
|
|||||||
"E07_Sum_Covered",
|
"E07_Sum_Covered",
|
||||||
`${Math.round(s.covered_area_m2).toLocaleString()} / ${Math.round(s.band_area_m2).toLocaleString()} m²`,
|
`${Math.round(s.covered_area_m2).toLocaleString()} / ${Math.round(s.band_area_m2).toLocaleString()} m²`,
|
||||||
],
|
],
|
||||||
[
|
["E07_Sum_Time", `${result.elapsed_s.toFixed(1)} s · ${dateTimeText(result.computed_at)}`],
|
||||||
"E07_Sum_Time",
|
|
||||||
`${result.elapsed_s.toFixed(1)} s · ${result.computed_at.replace("T", " ")}`,
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
const list = createKv(pairs.map(([key, value]) => ({ label: t(key), value })));
|
const list = createKv(pairs.map(([key, value]) => ({ label: t(key), value })));
|
||||||
list.classList.add("e07-summary");
|
list.classList.add("e07-summary");
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import date
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from common_util.common_util_time import kst_today
|
||||||
from config import config_system
|
from config import config_system
|
||||||
from M01_MasterData import M01_MasterData_Store as store
|
from M01_MasterData import M01_MasterData_Store as store
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ def _make_file() -> None:
|
|||||||
path = _path()
|
path = _path()
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
return
|
return
|
||||||
head = {"그룹": mcb.GROUP, "판": str(date.today().year), "줄": []}
|
head = {"그룹": mcb.GROUP, "판": str(kst_today().year), "줄": []}
|
||||||
path.write_text(store.dump(head), encoding="utf-8", newline="\n")
|
path.write_text(store.dump(head), encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import date
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from common_util.common_util_time import kst_today
|
||||||
from M01_MasterData import M01_MasterData_Store as store
|
from M01_MasterData import M01_MasterData_Store as store
|
||||||
|
|
||||||
cm, mk, mcp = store.cm, store.mk, store.mcp
|
cm, mk, mcp = store.cm, store.mk, store.mcp
|
||||||
@@ -274,7 +274,7 @@ def _make_file() -> None:
|
|||||||
path = _own_path()
|
path = _own_path()
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
return
|
return
|
||||||
head = {"그룹": "로직", "원문": "자체", "판": str(date.today().year), "줄": []}
|
head = {"그룹": "로직", "원문": "자체", "판": str(kst_today().year), "줄": []}
|
||||||
path.write_text(store.dump(head), encoding="utf-8", newline="\n")
|
path.write_text(store.dump(head), encoding="utf-8", newline="\n")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from M01_MasterData import M01_MasterData_Store as m01
|
from M01_MasterData import M01_MasterData_Store as m01
|
||||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
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:
|
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 ""
|
hopyo, file = self.hopyo, self.doc.get("파일") or ""
|
||||||
lines, mats, missing, used, mat_lines = [], {}, [], set(), {}
|
lines, mats, missing, used, mat_lines = [], {}, [], set(), {}
|
||||||
nation: dict[tuple, dict[str, Any]] = {}
|
nation: dict[tuple, dict[str, Any]] = {}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
from M01_MasterData import M01_MasterData_Store as m01
|
from M01_MasterData import M01_MasterData_Store as m01
|
||||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||||
@@ -245,7 +245,7 @@ def write(
|
|||||||
if not entries:
|
if not entries:
|
||||||
return 0
|
return 0
|
||||||
have = read(folder)
|
have = read(folder)
|
||||||
stamp = datetime.now().isoformat(timespec="seconds")
|
stamp = utc_now_text()
|
||||||
version = _version()
|
version = _version()
|
||||||
out: dict[str, dict[str, Any] | None] = dict(have) # 옛 꼴도 새 꼴로 다시 씀
|
out: dict[str, dict[str, Any] | None] = dict(have) # 옛 꼴도 새 꼴로 다시 씀
|
||||||
for full, row in entries.items():
|
for full, row in entries.items():
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from common_util.common_util_json import atomic_write_json
|
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
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||||
|
|
||||||
DIRNAME = "실무자료"
|
DIRNAME = "실무자료"
|
||||||
@@ -91,7 +91,7 @@ def save(
|
|||||||
doc = {
|
doc = {
|
||||||
"키": key,
|
"키": key,
|
||||||
"파일": name,
|
"파일": name,
|
||||||
"올린때": datetime.now().isoformat(timespec="seconds"),
|
"올린때": utc_now_text(),
|
||||||
"올린이": who,
|
"올린이": who,
|
||||||
**parsed,
|
**parsed,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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_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_json import atomic_write_json
|
||||||
from common_util.common_util_summary_item import GROUP_JOIN, snapshot_folder_name
|
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 import M02_Template_Layers as layers
|
||||||
from M02_MasterTemplete.M02_Item_Source import (
|
from M02_MasterTemplete.M02_Item_Source import (
|
||||||
LABOR_FILE,
|
LABOR_FILE,
|
||||||
@@ -82,7 +82,7 @@ def safe_folder(base: Path, name: str) -> Path | None:
|
|||||||
|
|
||||||
|
|
||||||
def now_text() -> str:
|
def now_text() -> str:
|
||||||
return datetime.now().isoformat(timespec="seconds")
|
return utc_now_text()
|
||||||
|
|
||||||
|
|
||||||
def same_item(manifest: Any, item: dict[str, Any]) -> bool:
|
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("고침"):
|
if isinstance(manifest, dict) and manifest.get("고침"):
|
||||||
past = folder.parent / PAST_DIRNAME
|
past = folder.parent / PAST_DIRNAME
|
||||||
past.mkdir(parents=True, exist_ok=True)
|
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))
|
shutil.move(str(folder), str(target))
|
||||||
return target.name
|
return target.name
|
||||||
shutil.rmtree(folder)
|
shutil.rmtree(folder)
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import date
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from common_util.common_util_json import atomic_write_json
|
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 as m01
|
||||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||||
from M02_MasterTemplete import M02_MasterTemplete_Store as 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)
|
return _m01(combo_store._read)
|
||||||
path = store.root() / FILE
|
path = store.root() / FILE
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
head = {"그룹": mcb.GROUP, "판": str(date.today().year), "폴더": [], "다음": 1}
|
head = {"그룹": mcb.GROUP, "판": str(kst_today().year), "폴더": [], "다음": 1}
|
||||||
return {**head, "줄": []}, ""
|
return {**head, "줄": []}, ""
|
||||||
raw = path.read_bytes()
|
raw = path.read_bytes()
|
||||||
return json.loads(raw), store.version_of(raw)
|
return json.loads(raw), store.version_of(raw)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
type LedgerRow,
|
type LedgerRow,
|
||||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||||
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
|
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
|
||||||
|
import { dateTimeText } from "../common_util/common_util_time";
|
||||||
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
|
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
|
||||||
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick";
|
||||||
import "./M02_MasterTemplete_Hopyo.css";
|
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.처음파일]
|
const when = [r.누가?.이름, dateTimeText(r.때, ""), r.처음파일].filter(Boolean).join(" · ");
|
||||||
.filter(Boolean)
|
|
||||||
.join(" · ");
|
|
||||||
return el("tr", {
|
return el("tr", {
|
||||||
className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`,
|
className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`,
|
||||||
attrs: { "data-ledger": r.열쇠 },
|
attrs: { "data-ledger": r.열쇠 },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { createInputField, el, showToast } from "@ui/ui_template_elements";
|
import { createInputField, el, showToast } from "@ui/ui_template_elements";
|
||||||
import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells";
|
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 { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head";
|
||||||
import "../M01_MasterData/M01_MasterData_UI_Style.css";
|
import "../M01_MasterData/M01_MasterData_UI_Style.css";
|
||||||
import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch";
|
import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch";
|
||||||
@@ -50,7 +51,7 @@ const TEXT = {
|
|||||||
|
|
||||||
const SIZE = 30;
|
const SIZE = 30;
|
||||||
const DASH = "—";
|
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 =>
|
const money = (value: number | null | undefined): string =>
|
||||||
typeof value === "number" ? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 }) : DASH;
|
typeof value === "number" ? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 }) : DASH;
|
||||||
/** 가장 늦게 고친 날 — 항목 통째 */
|
/** 가장 늦게 고친 날 — 항목 통째 */
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from common_util.common_util_json import atomic_write_json
|
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_Combo_Store as combos
|
||||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
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"):
|
for kind in ("structure", "area", "labor"):
|
||||||
path = folder / kind / f"{col}.json"
|
path = folder / kind / f"{col}.json"
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
stamps[kind] = datetime.fromtimestamp(path.stat().st_mtime).isoformat(
|
stamps[kind] = utc_from_timestamp(path.stat().st_mtime)
|
||||||
timespec="seconds"
|
|
||||||
)
|
|
||||||
labor = None
|
labor = None
|
||||||
path = folder / "labor" / f"{col}.json"
|
path = folder / "labor" / f"{col}.json"
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import threading
|
|||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from common_util import common_util_spreadsheet as spreadsheet
|
from common_util import common_util_spreadsheet as spreadsheet
|
||||||
from common_util.common_util_json import atomic_write_json
|
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_MasterTemplete_Tree as tree
|
||||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
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]:
|
def _info(kind: str, path: Path) -> dict[str, Any]:
|
||||||
raw = path.read_bytes()
|
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}
|
row = {"종류": kind, "이름": path.stem, "판": version_of(raw), "수정일": stamp}
|
||||||
if (kind, path.stem) in IN_USE:
|
if (kind, path.stem) in IN_USE:
|
||||||
row["쓰는곳"] = IN_USE[(kind, path.stem)] # 화면이 지우기 확인 창 전에 막음
|
row["쓰는곳"] = IN_USE[(kind, path.stem)] # 화면이 지우기 확인 창 전에 막음
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ from __future__ import annotations
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from common_util.common_util_json import atomic_write_json
|
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
|
from config import config_system
|
||||||
|
|
||||||
LAYERS = ("system", "company", "personal", "project")
|
LAYERS = ("system", "company", "personal", "project")
|
||||||
@@ -128,7 +128,7 @@ def version_of(path: str | Path) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def _modified(path: Path) -> str:
|
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]]:
|
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,
|
"층": layer,
|
||||||
"이름": name,
|
"이름": name,
|
||||||
"판": version,
|
"판": version,
|
||||||
"적용일": datetime.now().isoformat(timespec="seconds"),
|
"적용일": utc_now_text(),
|
||||||
}
|
}
|
||||||
entry.update({key: value for key, value in extra.items() if value is not None})
|
entry.update({key: value for key, value in extra.items() if value is not None})
|
||||||
return entry
|
return entry
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ import asyncio
|
|||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from common_util.common_util_time import kst_now
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
from config import config_system # noqa: E402
|
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 = [
|
lines = [
|
||||||
"# 기존 프로젝트 양식 넣기",
|
"# 기존 프로젝트 양식 넣기",
|
||||||
"",
|
"",
|
||||||
f"- 돌린 때: {datetime.now().isoformat(timespec='seconds')}",
|
f"- 돌린 때: {kst_now().isoformat(timespec='seconds')}",
|
||||||
f"- 시스템 양식: {names}",
|
f"- 시스템 양식: {names}",
|
||||||
"- 방법: 작업본 · `_initial/` 에 없는 것만 더함 · 있는 양식 · 다른 파일은 그대로",
|
"- 방법: 작업본 · `_initial/` 에 없는 것만 더함 · 있는 양식 · 다른 파일은 그대로",
|
||||||
f"- 프로젝트 {len(report)}개 · 넣음 {sum(r['상태'] == '넣음' for r in report)}개",
|
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 { createText } from "@ui/ui_template_text";
|
||||||
import { createEmpty } from "@ui/ui_template_empty";
|
import { createEmpty } from "@ui/ui_template_empty";
|
||||||
import { createTabs } from "@ui/ui_template_tabs";
|
import { createTabs } from "@ui/ui_template_tabs";
|
||||||
|
import { dateTimeText } from "../../common_util/common_util_time";
|
||||||
|
|
||||||
/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */
|
/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */
|
||||||
const OLD_EDITOR = false;
|
const OLD_EDITOR = false;
|
||||||
@@ -362,7 +363,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
|||||||
...(versions.length
|
...(versions.length
|
||||||
? versions.map((v) => {
|
? versions.map((v) => {
|
||||||
const b = createButton({
|
const b = createButton({
|
||||||
label: to("PastItem", `${v.판} · ${v.저장일}`),
|
label: to("PastItem", `${v.판} · ${dateTimeText(v.저장일)}`),
|
||||||
variant: past?.판 === v.판 ? "filled" : "ghost",
|
variant: past?.판 === v.판 ? "filled" : "ghost",
|
||||||
size: "sm",
|
size: "sm",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -17,6 +16,7 @@ from pydantic import ValidationError
|
|||||||
|
|
||||||
from B05_Profile import B05_Profile_Structures_Schema as schema
|
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_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 import M02_MasterTemplete_Store as store
|
||||||
from M02_MasterTemplete.StructOptions.M02_SO_Check import catalog_errors
|
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 []:
|
for path in folder.glob("판_*.json") if folder.is_dir() else []:
|
||||||
number = path.stem.removeprefix("판_")
|
number = path.stem.removeprefix("판_")
|
||||||
if number.isdigit():
|
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})
|
rows.append({"판": int(number), "저장일": when})
|
||||||
return sorted(rows, key=lambda r: r["판"], reverse=True)
|
return sorted(rows, key=lambda r: r["판"], reverse=True)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -32,6 +31,7 @@ import aiomysql
|
|||||||
from B05_Profile.B05_Profile_Engine_Sections_Names import find_cross_file
|
from B05_Profile.B05_Profile_Engine_Sections_Names import find_cross_file
|
||||||
from common_util.common_util_storage import resolve_stored_project_path
|
from common_util.common_util_storage import resolve_stored_project_path
|
||||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||||
|
from common_util.common_util_time import utc_now_text
|
||||||
|
|
||||||
Scope = Literal["full", "stability"]
|
Scope = Literal["full", "stability"]
|
||||||
KIND_LABELS = {"ROAD": "B", "PLAN": "D", "STABILITY": "E"}
|
KIND_LABELS = {"ROAD": "B", "PLAN": "D", "STABILITY": "E"}
|
||||||
@@ -390,7 +390,7 @@ async def clone_full(
|
|||||||
"kind": kind,
|
"kind": kind,
|
||||||
"source_project_id": source_id,
|
"source_project_id": source_id,
|
||||||
"project_id": new_id,
|
"project_id": new_id,
|
||||||
"copied_at": datetime.now().isoformat(timespec="seconds"),
|
"copied_at": utc_now_text(),
|
||||||
"tables": {name: len(items) for name, items in rows.items()},
|
"tables": {name: len(items) for name, items in rows.items()},
|
||||||
"skipped_tables": skipped,
|
"skipped_tables": skipped,
|
||||||
"files": _tree_files(new_root),
|
"files": _tree_files(new_root),
|
||||||
@@ -517,7 +517,7 @@ async def copy_stability(connection, source_id: str, new_id: str, project_root:
|
|||||||
"scope": "stability",
|
"scope": "stability",
|
||||||
"source_project_id": source_id,
|
"source_project_id": source_id,
|
||||||
"project_id": new_id,
|
"project_id": new_id,
|
||||||
"copied_at": datetime.now().isoformat(timespec="seconds"),
|
"copied_at": utc_now_text(),
|
||||||
"source_fingerprint": digest.hexdigest(),
|
"source_fingerprint": digest.hexdigest(),
|
||||||
"route_id": dump["route"]["id"] if dump["route"] else None,
|
"route_id": dump["route"]["id"] if dump["route"] else None,
|
||||||
"tables": {
|
"tables": {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""서버 시각 글 — 저장·전송은 UTC(끝에 Z) · 서버가 만드는 파일 글(엑셀 · PDF · 메일 · 파일 이름)은 한국 시각 고정.
|
||||||
|
|
||||||
|
화면 글은 `common_util_time.ts`(보는 PC 시간대) — 서버 PC 시간대에 기대지 않음(한국은 서머타임 없음 = 고정 +9).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date, datetime, timedelta, timezone
|
||||||
|
|
||||||
|
KST = timezone(timedelta(hours=9), "KST")
|
||||||
|
|
||||||
|
|
||||||
|
def utc_text(value: datetime | None) -> str | None:
|
||||||
|
"""시간대 없는 값 = UTC 로 보고 「2026-10-30T01:02:03Z」 · 시간대 있는 값은 UTC 로 바꿈 · 없음 = None."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if value.tzinfo is not None:
|
||||||
|
value = value.astimezone(timezone.utc)
|
||||||
|
return value.replace(tzinfo=None, microsecond=0).isoformat() + "Z"
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now_text() -> str:
|
||||||
|
"""지금 — UTC 글(저장용)."""
|
||||||
|
return utc_text(datetime.now(timezone.utc)) or ""
|
||||||
|
|
||||||
|
|
||||||
|
def utc_from_timestamp(seconds: float) -> str:
|
||||||
|
"""파일 수정 시각 등 epoch 초 → UTC 글."""
|
||||||
|
return utc_text(datetime.fromtimestamp(seconds, timezone.utc)) or ""
|
||||||
|
|
||||||
|
|
||||||
|
def kst_now() -> datetime:
|
||||||
|
"""지금 — 한국 시각(서버 PC 시간대와 무관)."""
|
||||||
|
return datetime.now(KST)
|
||||||
|
|
||||||
|
|
||||||
|
def kst_today() -> date:
|
||||||
|
return kst_now().date()
|
||||||
|
|
||||||
|
|
||||||
|
def kst_from_timestamp(seconds: float) -> datetime:
|
||||||
|
return datetime.fromtimestamp(seconds, KST)
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now_naive() -> datetime:
|
||||||
|
"""지금 — DB 칸(UTC, 시간대 표시 없음)과 견주는 값."""
|
||||||
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def install_json_utc() -> None:
|
||||||
|
"""응답의 datetime(DB 값 = UTC) 을 끝에 Z 붙은 글로 — 화면이 보는 사람 시간대로 바꿈."""
|
||||||
|
from fastapi.encoders import ENCODERS_BY_TYPE
|
||||||
|
|
||||||
|
ENCODERS_BY_TYPE[datetime] = lambda value: utc_text(value) or ""
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
* common_util_time.ts
|
||||||
|
* 화면 시간 표시 — 저장·전송은 UTC 하나, 보일 때만 보는 사람 PC 시간대로 바꿈.
|
||||||
|
* 서버 글(「2026-10-30 01:02:03」 · 「…T01:02:03」 = 시간대 표시 없음)은 UTC 로 읽음.
|
||||||
|
* 짝: `common_util/common_util_time.py`(서버가 만드는 파일 글 = 한국 시각 고정).
|
||||||
|
* ========================================================================== */
|
||||||
|
|
||||||
|
const DAY_MS = 24 * 3600 * 1000;
|
||||||
|
const HAS_ZONE = /(Z|[+-]\d\d:?\d\d)$/;
|
||||||
|
|
||||||
|
/** 서버 시각 글 → 밀리초(UTC 기준) · 못 읽으면 null */
|
||||||
|
export function parseUtcMs(value: string | number | null | undefined): number | null {
|
||||||
|
if (value === null || value === undefined || value === "") return null;
|
||||||
|
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
||||||
|
const iso = value.trim().replace(" ", "T");
|
||||||
|
if (!iso.includes("T")) return null; // 날짜만 = 시각이 아니라 바꾸지 않음
|
||||||
|
const ms = Date.parse(HAS_ZONE.test(iso) ? iso : `${iso}Z`);
|
||||||
|
return Number.isNaN(ms) ? null : ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
const two = (n: number): string => String(n).padStart(2, "0");
|
||||||
|
|
||||||
|
function dateOf(d: Date): string {
|
||||||
|
return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 날짜만 「YYYY-MM-DD」(보는 PC 시간대) · 빈 값 · 못 읽음 = empty */
|
||||||
|
export function dateText(value: string | null | undefined, empty = "-"): string {
|
||||||
|
const ms = parseUtcMs(value);
|
||||||
|
if (ms !== null) return dateOf(new Date(ms));
|
||||||
|
return /^\d{4}-\d\d-\d\d$/.test(String(value ?? "").trim()) ? String(value).trim() : empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 시:분 「HH:MM」(보는 PC 시간대) · 빈 값 · 못 읽음 = 빈 글 */
|
||||||
|
export function timeText(value: string | null | undefined): string {
|
||||||
|
const ms = parseUtcMs(value);
|
||||||
|
if (ms === null) return "";
|
||||||
|
const d = new Date(ms);
|
||||||
|
return `${two(d.getHours())}:${two(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 날짜 + 시각 「YYYY-MM-DD HH:MM」(보는 PC 시간대) · 빈 값 · 못 읽음 = empty */
|
||||||
|
export function dateTimeText(value: string | null | undefined, empty = "—"): string {
|
||||||
|
const ms = parseUtcMs(value);
|
||||||
|
if (ms === null) return empty;
|
||||||
|
const d = new Date(ms);
|
||||||
|
return `${dateOf(d)} ${two(d.getHours())}:${two(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 달력 날짜(보는 PC 시간대) 차이 — 오늘 0 · 어제 1 · 미래 음수 */
|
||||||
|
export function calendarDaysAgo(value: string | null | undefined, now = Date.now()): number | null {
|
||||||
|
const ms = parseUtcMs(value);
|
||||||
|
if (ms === null) return null;
|
||||||
|
const day = (t: number): number => {
|
||||||
|
const d = new Date(t);
|
||||||
|
return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
|
||||||
|
};
|
||||||
|
return Math.round((day(now) - day(ms)) / DAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「오늘」 · 「어제」 · 「n 일 전」(7 일 미만) · 그 밖 날짜 · 빈 값 = empty */
|
||||||
|
export function agoText(value: string | null | undefined, empty = "-", now = Date.now()): string {
|
||||||
|
const days = calendarDaysAgo(value, now);
|
||||||
|
if (days === null) return empty;
|
||||||
|
if (days <= 0) return "오늘";
|
||||||
|
if (days === 1) return "어제";
|
||||||
|
if (days < 7) return `${days} 일 전`;
|
||||||
|
return dateText(value, empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 옛 이름 — 확정 시각 등(`dateTimeText` 와 같음) */
|
||||||
|
export const kstText = (utc: string | null | undefined): string => dateTimeText(utc);
|
||||||
@@ -1,15 +1,2 @@
|
|||||||
/* =============================================================================
|
/* 옛 이름 — `common_util_time.ts` 로 옮김(보는 PC 시간대). 호출부가 바뀌면 이 파일을 지움. */
|
||||||
* common_util_time_kst.ts
|
export { kstText } from "./common_util_time";
|
||||||
* 서버 UTC 시각(DB 세션 time_zone +00:00) → 화면 KST(UTC+9) 글자 — 서버 값은 UTC 그대로.
|
|
||||||
* ========================================================================== */
|
|
||||||
|
|
||||||
const KST_OFFSET_MS = 9 * 3600 * 1000;
|
|
||||||
|
|
||||||
/** "YYYY-MM-DD HH:MM:SS" · ISO(UTC) → "YYYY-MM-DD HH:MM" KST · 빈 값 = "—" · 못 읽으면 원글 */
|
|
||||||
export function kstText(utc: string | null | undefined): string {
|
|
||||||
if (!utc) return "—";
|
|
||||||
const iso = utc.trim().replace(" ", "T");
|
|
||||||
const ms = Date.parse(/(Z|[+-]\d\d:?\d\d)$/.test(iso) ? iso : `${iso}Z`);
|
|
||||||
if (Number.isNaN(ms)) return utc;
|
|
||||||
return new Date(ms + KST_OFFSET_MS).toISOString().slice(0, 16).replace("T", " ");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
|
from common_util.common_util_time import utc_text
|
||||||
|
|
||||||
# `project_workflow_stages.message` 는 varchar(255) — 넘치면 UPDATE 자체가 실패한다.
|
# `project_workflow_stages.message` 는 varchar(255) — 넘치면 UPDATE 자체가 실패한다.
|
||||||
_STAGE_MESSAGE_MAX_LENGTH = 200
|
_STAGE_MESSAGE_MAX_LENGTH = 200
|
||||||
|
|
||||||
@@ -492,7 +494,7 @@ async def analysis_lock_owner(
|
|||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"name": name,
|
"name": name,
|
||||||
"email": email,
|
"email": email,
|
||||||
"started_at": started_at.isoformat() if started_at else None,
|
"started_at": utc_text(started_at),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ from common_util.common_util_auth import (
|
|||||||
# 개발환경 전용 — 「확정 없이 다음으로」. **문은 서버가 정본이다** — `ENVIRONMENT` 가
|
# 개발환경 전용 — 「확정 없이 다음으로」. **문은 서버가 정본이다** — `ENVIRONMENT` 가
|
||||||
# 개발이 아니면 세 입구 모두 403 으로 거절한다(화면 단추 숨김은 보조).
|
# 개발이 아니면 세 입구 모두 403 으로 거절한다(화면 단추 숨김은 보조).
|
||||||
from common_util.common_util_dev_unlock_router import router as dev_unlock_router
|
from common_util.common_util_dev_unlock_router import router as dev_unlock_router
|
||||||
|
from common_util.common_util_time import install_json_utc
|
||||||
from common_util.common_util_help_router import router as help_router
|
from common_util.common_util_help_router import router as help_router
|
||||||
from common_util.common_util_map_router import router as map_router
|
from common_util.common_util_map_router import router as map_router
|
||||||
from common_util.common_util_resource_monitor import sample_resources_loop
|
from common_util.common_util_resource_monitor import sample_resources_loop
|
||||||
@@ -298,6 +299,7 @@ app = FastAPI(
|
|||||||
debug=DEBUG,
|
debug=DEBUG,
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
install_json_utc() # 응답의 시각 = UTC 글 + Z(화면이 보는 사람 시간대로 바꿈)
|
||||||
app.add_exception_handler(ProjectNotFoundError, project_not_found_response) # 지워진 프로젝트 = 404
|
app.add_exception_handler(ProjectNotFoundError, project_not_found_response) # 지워진 프로젝트 = 404
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
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_Router_Home as home
|
||||||
import B01_Dashboard.B01_Dashboard_Summary as summary
|
import B01_Dashboard.B01_Dashboard_Summary as summary
|
||||||
|
from common_util.common_util_time import utc_now_naive
|
||||||
|
|
||||||
ROUTE = "B05_Profile/route/r.geojson"
|
ROUTE = "B05_Profile/route/r.geojson"
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class _Pool:
|
|||||||
|
|
||||||
|
|
||||||
def test_summaries_in_batch(monkeypatch, tmp_path):
|
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 = tmp_path / "B05_Profile" / "route"
|
||||||
route_dir.mkdir(parents=True)
|
route_dir.mkdir(parents=True)
|
||||||
coords = [[0, 0, 1], [10, 0, 1], [10, 5, 1]]
|
coords = [[0, 0, 1], [10, 0, 1], [10, 5, 1]]
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ def test_로그_거르기_글():
|
|||||||
] # fmt: skip
|
] # fmt: skip
|
||||||
out = _node(
|
out = _node(
|
||||||
"type AuditLog = any;\n"
|
"type AuditLog = any;\n"
|
||||||
|
+ "const dateText = (v: string | null | undefined, e: string) => (v ? v.slice(0, 10) : e);\n"
|
||||||
+ src
|
+ src
|
||||||
+ f"const l = {json.dumps(logs, ensure_ascii=False)};"
|
+ f"const l = {json.dumps(logs, ensure_ascii=False)};"
|
||||||
+ "const f = (o: object) => filterLogs(l, { action: '', from: '', to: '', q: '', ...o }).length;"
|
+ "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}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements";
|
import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements";
|
||||||
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
import { readTemplate } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||||
|
import { dateText } from "../common_util/common_util_time";
|
||||||
import { calcLabor, type LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
import { calcLabor, type LaborDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api";
|
||||||
import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
|
import type { EditKind, ItemCardsHandle } from "../M02_MasterTemplete/M02_MasterTemplete_ItemCards";
|
||||||
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
|
import type { ItemSource } from "../M02_MasterTemplete/M02_MasterTemplete_ItemSource";
|
||||||
@@ -117,7 +118,7 @@ const TEXT = {
|
|||||||
|
|
||||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||||
const fill = (text: string, value: string): string => text.replace("{value}", value);
|
const fill = (text: string, value: string): string => text.replace("{value}", value);
|
||||||
const dateOf = (iso?: string | null): string => (iso ? iso.slice(0, 10) : "");
|
const dateOf = (iso?: string | null): string => dateText(iso, "");
|
||||||
|
|
||||||
export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
export function createItemDetail(opts: ItemDetailOptions): ItemDetail {
|
||||||
const root = el("div", { className: "ui-item-picker__detail" });
|
const root = el("div", { className: "ui-item-picker__detail" });
|
||||||
|
|||||||
Reference in New Issue
Block a user