spin-frontend/src/lib/format.ts
theorose49 581fd7a19f
All checks were successful
build-and-push / build (push) Successful in 30s
feat(project): 계약범위 글/그림 입력칸 + 인턴/직책/초과근무/로고/로그아웃 개선분
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 11:23:38 +09:00

142 lines
4.5 KiB
TypeScript

import type { FixStatus, LeaveType, ReqStatus, TxnKind } from "@/types";
export function classNames(...xs: (string | false | null | undefined)[]) {
return xs.filter(Boolean).join(" ");
}
export function formatDate(value?: string | null): string {
if (!value) return "—";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}.${m}.${day}`;
}
export function formatDateTime(value?: string | null): string {
if (!value) return "—";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
return `${formatDate(value)} ${String(d.getHours()).padStart(2, "0")}:${String(
d.getMinutes()
).padStart(2, "0")}`;
}
export function formatTime(value?: string | null): string {
if (!value) return "—";
const d = new Date(value);
if (Number.isNaN(d.getTime())) return "—";
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}
// Korean-style money: 1,2300,0000 → "1억 2,300만". Compact for dashboards.
export function formatKRW(n?: number): string {
if (n == null) return "—";
const neg = n < 0;
let v = Math.abs(Math.round(n));
if (v === 0) return "₩0";
const eok = Math.floor(v / 100_000_000);
v = v % 100_000_000;
const man = Math.floor(v / 10_000);
const rest = v % 10_000;
const parts: string[] = [];
if (eok) parts.push(`${eok}`);
if (man) parts.push(`${man.toLocaleString()}`);
if (rest && !eok) parts.push(`${rest.toLocaleString()}`);
const s = parts.join(" ") || "0";
return `${neg ? "-" : ""}${s}`;
}
// Full numeric KRW with grouping (for ledgers / inputs).
export function formatWon(n?: number): string {
if (n == null) return "—";
return `${Math.round(n).toLocaleString()}`;
}
export function formatPoints(n?: number): string {
if (n == null) return "—";
return `${(Math.round(n * 10) / 10).toLocaleString()}P`;
}
export function minutesToHM(min?: number): string {
if (!min || min <= 0) return "0시간";
const h = Math.floor(min / 60);
const m = min % 60;
return m ? `${h}시간 ${m}` : `${h}시간`;
}
export function formatSize(bytes?: number): string {
if (!bytes || bytes <= 0) return "—";
const units = ["B", "KB", "MB", "GB"];
let n = bytes;
let i = 0;
while (n >= 1024 && i < units.length - 1) {
n /= 1024;
i++;
}
return `${n.toFixed(n < 10 && i > 0 ? 1 : 0)} ${units[i]}`;
}
/* ---- status metadata ---- */
export const REQ_STATUS_META: Record<ReqStatus, { label: string; fg: string; bg: string }> = {
pending: { label: "대기", fg: "#B54708", bg: "#FEF0C7" },
approved: { label: "승인", fg: "#067647", bg: "#DCFAE6" },
rejected: { label: "반려", fg: "#B42318", bg: "#FEE4E2" },
canceled: { label: "취소", fg: "#475467", bg: "#F2F4F7" },
};
export const FIX_STATUS_META: Record<FixStatus, { label: string; fg: string; bg: string }> = {
planned: { label: "예정", fg: "#475467", bg: "#F2F4F7" },
applying: { label: "반영중", fg: "#175CD3", bg: "#D1E9FF" },
applied: { label: "반영완료", fg: "#5925DC", bg: "#EBE9FE" },
paid: { label: "지급완료", fg: "#067647", bg: "#DCFAE6" },
};
export const FIX_ORDER: FixStatus[] = ["planned", "applying", "applied", "paid"];
export const LEAVE_LABELS: Record<LeaveType, string> = {
annual: "연차",
half_am: "오전 반차",
half_pm: "오후 반차",
public: "공가",
sick: "병가",
family: "경조사",
unpaid: "무급",
};
export const TXN_LABELS: Record<TxnKind, string> = {
income: "수입",
expense: "비용",
tax: "세금",
payroll: "급여",
incentive: "인센티브",
};
export const STAGE_KIND_LABELS: Record<string, string> = {
deposit: "계약금",
middle: "중도금",
final: "잔금",
};
// 인센티브 BE/non-BE 라벨 (프로젝트 계약범위와 무관 — 그쪽은 자유 텍스트)
export const SCOPE_LABELS: Record<string, string> = {
be: "BE",
non_be: "non-BE",
};
export const PROJECT_STATUS_META: Record<string, { label: string; fg: string; bg: string }> = {
planned: { label: "예정", fg: "#475467", bg: "#F2F4F7" },
active: { label: "진행중", fg: "#175CD3", bg: "#D1E9FF" },
hold: { label: "보류", fg: "#B54708", bg: "#FEF0C7" },
done: { label: "완료", fg: "#067647", bg: "#DCFAE6" },
dropped: { label: "중단", fg: "#B42318", bg: "#FEE4E2" },
};
export const LANE_LABELS: Record<string, string> = {
todo: "할 일",
doing: "진행중",
review: "검토",
done: "완료",
};