Phase 3. 인증 및 공통 레이아웃 작업

This commit is contained in:
2026-06-09 23:37:33 +09:00
parent a93a6fa8e1
commit dbf9b3e180
17 changed files with 1092 additions and 71 deletions
+81
View File
@@ -0,0 +1,81 @@
export const ACCOUNT_TYPES = {
BASIC_20: 1,
STANDARD_50: 2,
PRO_100: 3,
SCHOOL_500: 4,
SCHOOL_1000: 5,
STUDENT_TRIAL: 100,
MAESTRO_TRIAL: 101,
} as const;
export const ACCOUNT_TYPE_LABELS = {
[ACCOUNT_TYPES.BASIC_20]: "20명 (1만원)",
[ACCOUNT_TYPES.STANDARD_50]: "50명 (2만원)",
[ACCOUNT_TYPES.PRO_100]: "100명 (3만원)",
[ACCOUNT_TYPES.SCHOOL_500]: "500명 (4만원)",
[ACCOUNT_TYPES.SCHOOL_1000]: "1,000명 (5만원)",
[ACCOUNT_TYPES.STUDENT_TRIAL]: "학생체험",
[ACCOUNT_TYPES.MAESTRO_TRIAL]: "마에체험",
} as const;
export const TRIAL_ACCOUNT_TYPE_LABELS = {
[ACCOUNT_TYPES.BASIC_20]: "20명 (0원)",
[ACCOUNT_TYPES.STANDARD_50]: "50명 (0원)",
[ACCOUNT_TYPES.PRO_100]: "100명 (0원)",
[ACCOUNT_TYPES.SCHOOL_500]: "500명 (0원)",
[ACCOUNT_TYPES.SCHOOL_1000]: "1,000명 (0원)",
} as const;
export const ACCOUNT_TYPE_PLAYER_COUNTS = {
[ACCOUNT_TYPES.BASIC_20]: 20,
[ACCOUNT_TYPES.STANDARD_50]: 50,
[ACCOUNT_TYPES.PRO_100]: 100,
[ACCOUNT_TYPES.SCHOOL_500]: 500,
[ACCOUNT_TYPES.SCHOOL_1000]: 1000,
} as const;
export const ACCOUNT_TYPE_PRICES = {
[ACCOUNT_TYPES.BASIC_20]: 10000,
[ACCOUNT_TYPES.STANDARD_50]: 20000,
[ACCOUNT_TYPES.PRO_100]: 30000,
[ACCOUNT_TYPES.SCHOOL_500]: 40000,
[ACCOUNT_TYPES.SCHOOL_1000]: 50000,
} as const;
export const ACTIVATE_STATUSES = {
PENDING: 0,
TRIAL: 1,
ACTIVE: 2,
CANCELED: 100,
} as const;
export const ACTIVATE_STATUS_LABELS = {
[ACTIVATE_STATUSES.PENDING]: "미승인",
[ACTIVATE_STATUSES.TRIAL]: "체험",
[ACTIVATE_STATUSES.ACTIVE]: "활성화",
[ACTIVATE_STATUSES.CANCELED]: "등록 취소",
} as const;
export const REQUEST_STATUSES = {
REQUESTED: 1,
CLOSED: 2,
APPLIED: 3,
} as const;
export const REQUEST_STATUS_LABELS = {
[REQUEST_STATUSES.REQUESTED]: "요청",
[REQUEST_STATUSES.CLOSED]: "취소",
[REQUEST_STATUSES.APPLIED]: "적용 완료",
} as const;
export type AccountType = (typeof ACCOUNT_TYPES)[keyof typeof ACCOUNT_TYPES];
export type PaidAccountType =
| typeof ACCOUNT_TYPES.BASIC_20
| typeof ACCOUNT_TYPES.STANDARD_50
| typeof ACCOUNT_TYPES.PRO_100
| typeof ACCOUNT_TYPES.SCHOOL_500
| typeof ACCOUNT_TYPES.SCHOOL_1000;
export type ActivateStatus =
(typeof ACTIVATE_STATUSES)[keyof typeof ACTIVATE_STATUSES];
export type RequestStatus =
(typeof REQUEST_STATUSES)[keyof typeof REQUEST_STATUSES];
+25
View File
@@ -0,0 +1,25 @@
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { PrismaClient } from "@/lib/generated/prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
function createPrismaClient() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is required to initialize Prisma Client.");
}
const adapter = new PrismaMariaDb(databaseUrl);
return new PrismaClient({ adapter });
}
export const db = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = db;
}
+113
View File
@@ -1,6 +1,119 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import {
ACCOUNT_TYPE_LABELS,
ACCOUNT_TYPE_PLAYER_COUNTS,
ACCOUNT_TYPE_PRICES,
ACTIVATE_STATUS_LABELS,
REQUEST_STATUS_LABELS,
TRIAL_ACCOUNT_TYPE_LABELS,
type AccountType,
type ActivateStatus,
type PaidAccountType,
type RequestStatus,
} from "@/lib/constants";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function getAccountTypeLabel(accountType: number): string {
return (
ACCOUNT_TYPE_LABELS[accountType as AccountType] ?? `${accountType}? (N/A)`
);
}
export function getTrialAccountTypeLabel(accountType: number): string {
return (
TRIAL_ACCOUNT_TYPE_LABELS[accountType as PaidAccountType] ??
`${accountType}? (N/A)`
);
}
export function getAccountTypePlayerCount(accountType: number): number {
return ACCOUNT_TYPE_PLAYER_COUNTS[accountType as PaidAccountType] ?? 0;
}
export function getAccountTypePrice(accountType: number): number {
return ACCOUNT_TYPE_PRICES[accountType as PaidAccountType] ?? 0;
}
export function getActivateStatusLabel(activateStatus: number): string {
return (
ACTIVATE_STATUS_LABELS[activateStatus as ActivateStatus] ??
`${activateStatus}? (N/A)`
);
}
export function getRequestStatusLabel(status: number): string {
return REQUEST_STATUS_LABELS[status as RequestStatus] ?? `${status}? (N/A)`;
}
export function calculateUpgradePrice(
requestedAccountType: number,
registeredAccountType: number
): number {
return (
getAccountTypePrice(requestedAccountType) -
getAccountTypePrice(registeredAccountType)
);
}
export function formatDate(value: Date | string | null | undefined): string {
const date = toDate(value);
if (!date) {
return "";
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
export function formatDateTime(
value: Date | string | null | undefined
): string {
const date = toDate(value);
if (!date) {
return "";
}
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
return `${formatDate(date)} ${hours}:${minutes}:${seconds}`;
}
export function calculateExtendedAvailableDate(
availableDate: Date,
now = new Date()
): Date {
const baseDate =
availableDate.getTime() < now.getTime() ? now : availableDate;
const extendedDate = new Date(baseDate);
extendedDate.setFullYear(extendedDate.getFullYear() + 1);
extendedDate.setHours(0, 0, 0, 0);
return extendedDate;
}
function toDate(value: Date | string | null | undefined): Date | null {
if (!value) {
return null;
}
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date;
}