From 078a50e1f98e9b65a7343d805332ec499cec994c Mon Sep 17 00:00:00 2001 From: Seohyunjho Date: Wed, 29 Jul 2026 12:05:02 +0900 Subject: [PATCH] =?UTF-8?q?fix(web):=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=8B=9C=EA=B0=81=20KST=20=ED=91=9C=EA=B8=B0=20+?= =?UTF-8?q?=20CTRS=20=EC=83=89=20=EC=A4=91=EC=A6=9D=EB=8F=84=20=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EB=8D=B0=EC=9D=B4=EC=85=98=C2=B7=EA=B0=80=EC=8B=9C?= =?UTF-8?q?=EC=84=B1=20+=20=ED=97=A4=EB=8D=94=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3건 통합(대시보드 폴리시): 1) 시각 타임존 — API는 UTC로 내려주는데 DGX 웹 컨테이너 Node TZ가 UTC라 시각이 9시간 어긋나 보였다. lib/datetime.ts `formatKST()`(timeZone:"Asia/Seoul" 명시, ICU 변환)로 세션 시작/제출·리포트 생성·위험감지·메시지·세션상세 7곳 교체 → 컨테이너 TZ 무관하게 KST, SSR/CSR 동일(하이드레이션 안전). 2) CTRS 색 — tailwind content에 lib/ 추가(tokens.ts의 배지 색 클래스가 스캔 안 돼 CSS 미생성 → CTRS 1이 흰 배경+흰 글씨로 안 보이던 근본 원인). CTRS_META를 중증도 그라데이션으로 재설계: 1 고위험=solid red+흰 글씨(대비 확보) → 2 danger-soft → 3 warn → 4 중립 → 5 옅은 중립. 3) 헤더 — "Neuro-Sync 의료진" → "의료진용 대시보드". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[id]/sessions/[sessionId]/page.tsx | 3 +- apps/web/components/DashboardHeader.tsx | 3 +- apps/web/components/HandoffReportView.tsx | 3 +- apps/web/components/MessageRow.tsx | 3 +- apps/web/components/ReportActions.tsx | 3 +- apps/web/components/RiskEventCard.tsx | 3 +- apps/web/components/SessionCard.tsx | 5 ++-- apps/web/lib/datetime.ts | 18 ++++++++++++ apps/web/lib/tokens.ts | 29 ++++++++++++------- apps/web/tailwind.config.ts | 5 +++- 10 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 apps/web/lib/datetime.ts diff --git a/apps/web/app/dashboard/patients/[id]/sessions/[sessionId]/page.tsx b/apps/web/app/dashboard/patients/[id]/sessions/[sessionId]/page.tsx index 1b785c9..7d16cc2 100644 --- a/apps/web/app/dashboard/patients/[id]/sessions/[sessionId]/page.tsx +++ b/apps/web/app/dashboard/patients/[id]/sessions/[sessionId]/page.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { formatKST } from "../../../../../../lib/datetime"; import { notFound } from "next/navigation"; import { AutoRefresh } from "../../../../../../components/AutoRefresh"; @@ -35,7 +36,7 @@ export default async function SessionPage({ params }: Params) {

세션

- 상태: {sess.status} · 시작 {new Date(sess.createdAt).toLocaleString("ko-KR")} + 상태: {sess.status} · 시작 {formatKST(sess.createdAt)}

{report && report.status === "ready" ? ( diff --git a/apps/web/components/DashboardHeader.tsx b/apps/web/components/DashboardHeader.tsx index 57baa61..0f79c3a 100644 --- a/apps/web/components/DashboardHeader.tsx +++ b/apps/web/components/DashboardHeader.tsx @@ -27,8 +27,7 @@ export function DashboardHeader({ user }: { user: SessionUser | null }) { - Neuro-Sync{" "} - 의료진 + 의료진용 대시보드
diff --git a/apps/web/components/HandoffReportView.tsx b/apps/web/components/HandoffReportView.tsx index 9832f76..5ab49d9 100644 --- a/apps/web/components/HandoffReportView.tsx +++ b/apps/web/components/HandoffReportView.tsx @@ -5,6 +5,7 @@ import type { QuestionnaireScore, ReportRiskSignal, } from "../lib/api"; +import { formatKST } from "../lib/datetime"; import { riskColor } from "../lib/tokens"; import { RiskBadge } from "./RiskBadge"; @@ -202,7 +203,7 @@ export function HandoffReportView({ report }: { report: HandoffReport }) {
{report.status === "ready" && report.generatedAt ? ( ) : null} diff --git a/apps/web/components/MessageRow.tsx b/apps/web/components/MessageRow.tsx index 3de9480..36854f8 100644 --- a/apps/web/components/MessageRow.tsx +++ b/apps/web/components/MessageRow.tsx @@ -1,4 +1,5 @@ import type { MessageOut } from "../lib/api"; +import { formatKST } from "../lib/datetime"; const roleLabel: Record = { user: "환자", @@ -24,7 +25,7 @@ export function MessageRow({ msg }: { msg: MessageOut }) { {msg.content}

- {new Date(msg.createdAt).toLocaleString("ko-KR")} + {formatKST(msg.createdAt)} {msg.inputModality === "voice" ? " · 🎤 음성" : ""}

diff --git a/apps/web/components/ReportActions.tsx b/apps/web/components/ReportActions.tsx index fdd17e0..aee48df 100644 --- a/apps/web/components/ReportActions.tsx +++ b/apps/web/components/ReportActions.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { formatKST } from "../lib/datetime"; import type { HandoffReport } from "../lib/api"; @@ -24,7 +25,7 @@ function toEmrText(report: HandoffReport, patientName: string): string { const L: string[] = []; L.push("[Neuro-Sync 사전 문진 리포트]"); L.push(`환자: ${patientName}`); - if (report.generatedAt) L.push(`생성: ${new Date(report.generatedAt).toLocaleString("ko-KR")}`); + if (report.generatedAt) L.push(`생성: ${formatKST(report.generatedAt)}`); L.push(""); if (report.questionnaires.length) { diff --git a/apps/web/components/RiskEventCard.tsx b/apps/web/components/RiskEventCard.tsx index 5a91611..f8b98c1 100644 --- a/apps/web/components/RiskEventCard.tsx +++ b/apps/web/components/RiskEventCard.tsx @@ -1,4 +1,5 @@ import type { RiskEventOut } from "../lib/api"; +import { formatKST } from "../lib/datetime"; import { riskColor } from "../lib/tokens"; import { RiskBadge } from "./RiskBadge"; @@ -131,7 +132,7 @@ export function RiskEventCard({ event }: { event: RiskEventOut }) {
diff --git a/apps/web/components/SessionCard.tsx b/apps/web/components/SessionCard.tsx index 2f05781..bfd6541 100644 --- a/apps/web/components/SessionCard.tsx +++ b/apps/web/components/SessionCard.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { formatKST } from "../lib/datetime"; import type { SessionSummary } from "../lib/api"; import { RiskBadge } from "./RiskBadge"; @@ -28,11 +29,11 @@ export function SessionCard({ {statusLabel[session.status] ?? session.status}

- 시작 {new Date(session.createdAt).toLocaleString("ko-KR")} + 시작 {formatKST(session.createdAt)}

{session.submittedAt ? (

- 제출 {new Date(session.submittedAt).toLocaleString("ko-KR")} + 제출 {formatKST(session.submittedAt)}

) : null} diff --git a/apps/web/lib/datetime.ts b/apps/web/lib/datetime.ts new file mode 100644 index 0000000..8620266 --- /dev/null +++ b/apps/web/lib/datetime.ts @@ -0,0 +1,18 @@ +/** + * ISO 시각 문자열을 항상 **한국 시간(KST, Asia/Seoul)**으로 포맷한다. + * + * API는 타임스탬프를 UTC(`...Z`)로 내려주는데, `toLocaleString("ko-KR")`은 실행 + * 런타임의 기본 타임존을 쓴다 — DGX 배포 컨테이너의 Node는 TZ 미설정이라 UTC로 + * 동작해 시각이 9시간 어긋나 보였다(BUG). `timeZone: "Asia/Seoul"`을 명시하면 + * Node/브라우저 내장 ICU가 컨테이너 TZ와 무관하게 KST로 변환하며, 서버(SSR)와 + * 클라이언트가 동일 문자열을 내므로 하이드레이션에도 안전하다. + */ +export function formatKST( + iso: string | null | undefined, + opts?: Intl.DateTimeFormatOptions, +): string { + if (!iso) return "—"; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return String(iso); + return d.toLocaleString("ko-KR", { timeZone: "Asia/Seoul", ...opts }); +} diff --git a/apps/web/lib/tokens.ts b/apps/web/lib/tokens.ts index 59074e5..b8660db 100644 --- a/apps/web/lib/tokens.ts +++ b/apps/web/lib/tokens.ts @@ -67,21 +67,30 @@ export function ctrsFromLevel(level: RiskLevel | null | undefined): CtrsStage { } } +// 중증도 그라데이션: 1(고위험)로 갈수록 강한 위험색, 5(안정)로 갈수록 옅은 중립. +// 1은 solid red + 흰 글씨로 대비 확보(기존엔 배경 CSS 미생성으로 흰 배경에 안 보였음). export const CTRS_META: Record< CtrsStage, { label: string; badge: { bg: string; border: string; text: string; dot: string } } > = { - 1: { label: "고위험", badge: riskBadge.critical }, - 2: { label: "위험", badge: riskBadge.high }, - 3: { label: "주의", badge: riskBadge.medium }, - 4: { label: "관찰", badge: riskBadge.low }, + 1: { + label: "고위험", + badge: { bg: "bg-state-danger", border: "border-state-danger", text: "text-white", dot: "bg-white" }, + }, + 2: { + label: "위험", + badge: { bg: "bg-danger-soft", border: "border-state-danger", text: "text-danger-ink", dot: "bg-state-danger" }, + }, + 3: { + label: "주의", + badge: { bg: "bg-warn-soft", border: "border-warn-line", text: "text-warn", dot: "bg-warn" }, + }, + 4: { + label: "관찰", + badge: { bg: "bg-surface", border: "border-border-strong", text: "text-ink2", dot: "bg-text-secondary" }, + }, 5: { label: "안정", - badge: { - bg: "bg-surface-elevated", - border: "border-border", - text: "text-text-secondary", - dot: "bg-faint", - }, + badge: { bg: "bg-surface-elevated", border: "border-border", text: "text-text-secondary", dot: "bg-faint" }, }, }; diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 827ae52..cdc0717 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -5,7 +5,10 @@ import type { Config } from "tailwindcss"; * 흰 바탕 · ink 텍스트 · 색은 위험(danger)과 경고(warn)에만. 나머지는 무채색. */ const config: Config = { - content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"], + // lib/ 포함 필수 — tokens.ts의 배지 색 클래스(riskBadge/CTRS_META의 bg-state-danger + // 등)는 여기서만 문자열로 존재한다. 스캔하지 않으면 해당 utility CSS가 생성되지 않아 + // 배경/색이 통째로 빠진다(CTRS 1 흰 배경+흰 글씨로 안 보이던 원인). + content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./lib/**/*.{ts,tsx}"], theme: { extend: { colors: {