Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from "next/link";
import { formatKST } from "../../../../../../lib/datetime";
import { notFound } from "next/navigation";

import { AutoRefresh } from "../../../../../../components/AutoRefresh";
Expand Down Expand Up @@ -35,7 +36,7 @@ export default async function SessionPage({ params }: Params) {
</p>
<h1 className="text-[26px] font-semibold tracking-tight text-text-primary">세션</h1>
<p className="text-text-secondary tabular-nums">
상태: {sess.status} · 시작 {new Date(sess.createdAt).toLocaleString("ko-KR")}
상태: {sess.status} · 시작 {formatKST(sess.createdAt)}
</p>
</div>
{report && report.status === "ready" ? (
Expand Down
3 changes: 1 addition & 2 deletions apps/web/components/DashboardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ export function DashboardHeader({ user }: { user: SessionUser | null }) {
<span className="h-[5px] w-[5px] rounded-full bg-ink" />
</span>
<span className="text-[15px] font-semibold tracking-tight text-text-primary">
Neuro-Sync{" "}
<span className="font-normal text-text-secondary">의료진</span>
의료진용 대시보드
</span>
</a>
<div className="flex items-center gap-4 text-[13px]">
Expand Down
3 changes: 2 additions & 1 deletion apps/web/components/HandoffReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -202,7 +203,7 @@ export function HandoffReportView({ report }: { report: HandoffReport }) {
</div>
{report.status === "ready" && report.generatedAt ? (
<time className="text-xs text-text-secondary tabular-nums">
생성 {new Date(report.generatedAt).toLocaleString("ko-KR")}
생성 {formatKST(report.generatedAt)}
</time>
) : null}
</header>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/components/MessageRow.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { MessageOut } from "../lib/api";
import { formatKST } from "../lib/datetime";

const roleLabel: Record<MessageOut["role"], string> = {
user: "환자",
Expand All @@ -24,7 +25,7 @@ export function MessageRow({ msg }: { msg: MessageOut }) {
{msg.content}
</p>
<p className="text-xs text-faint mt-1 tabular-nums">
{new Date(msg.createdAt).toLocaleString("ko-KR")}
{formatKST(msg.createdAt)}
{msg.inputModality === "voice" ? " · 🎤 음성" : ""}
</p>
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/components/ReportActions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useState } from "react";
import { formatKST } from "../lib/datetime";

import type { HandoffReport } from "../lib/api";

Expand All @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/components/RiskEventCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { RiskEventOut } from "../lib/api";
import { formatKST } from "../lib/datetime";
import { riskColor } from "../lib/tokens";
import { RiskBadge } from "./RiskBadge";

Expand Down Expand Up @@ -131,7 +132,7 @@ export function RiskEventCard({ event }: { event: RiskEventOut }) {
<header className="flex items-center justify-between gap-2">
<RiskBadge level={event.level} />
<time className="text-xs text-text-secondary tabular-nums">
{new Date(event.detectedAt).toLocaleString("ko-KR")}
{formatKST(event.detectedAt)}
</time>
</header>
<AiCategoryBadges evidence={aiCategoryEvidence} />
Expand Down
5 changes: 3 additions & 2 deletions apps/web/components/SessionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from "next/link";
import { formatKST } from "../lib/datetime";

import type { SessionSummary } from "../lib/api";
import { RiskBadge } from "./RiskBadge";
Expand Down Expand Up @@ -28,11 +29,11 @@ export function SessionCard({
{statusLabel[session.status] ?? session.status}
</p>
<p className="text-[13px] text-text-secondary tabular-nums">
시작 {new Date(session.createdAt).toLocaleString("ko-KR")}
시작 {formatKST(session.createdAt)}
</p>
{session.submittedAt ? (
<p className="text-[13px] text-text-secondary tabular-nums">
제출 {new Date(session.submittedAt).toLocaleString("ko-KR")}
제출 {formatKST(session.submittedAt)}
</p>
) : null}
</div>
Expand Down
18 changes: 18 additions & 0 deletions apps/web/lib/datetime.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
29 changes: 19 additions & 10 deletions apps/web/lib/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
},
};
5 changes: 4 additions & 1 deletion apps/web/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading