All checks were successful
build-and-push / build (push) Successful in 30s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
4.5 KiB
TypeScript
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: "완료",
|
|
};
|