| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- 'use client';
- import { clsx, type ClassValue } from 'clsx';
- import { twMerge } from 'tailwind-merge';
- import { TokenData, ResultDto, ApiError } from '@/types/response/common';
- import { CLAIM_NAME_IDENTIFIER, CLAIM_EMAIL, CLAIM_NAME } from '@/constants/common';
- export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
- }
- // 클라이언트용 fetch 유틸 (Route Handler 호출용)
- interface FetchApiOptions extends Omit<RequestInit, 'body'> {
- body?: unknown;
- }
- export async function fetchApi<T>(url: string, options?: FetchApiOptions): Promise<ResultDto<T>> {
- const isFormData = options?.body instanceof FormData;
- const hasBody = options?.body !== undefined;
- const res = (await fetch(url, {
- ...options,
- headers: {
- ...(!isFormData && hasBody ? { 'Content-Type': 'application/json' } : {}),
- ...options?.headers
- },
- body: isFormData ? options.body as BodyInit : (options?.body ? JSON.stringify(options.body) : undefined)
- }));
- return res.json();
- }
- // JWT 토큰에서 사용자 정보 추출
- export function decodeAccessToken(token: string): TokenData|null {
- try {
- const base64URL = token.split('.')[1]; // JWT의 Payload 부분
- const base64 = base64URL.replace(/-/g, '+').replace(/_/g, '/'); // Base64 형식 변환
- const jsonPayload = decodeURIComponent(
- atob(base64).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')
- );
- const payload = JSON.parse(jsonPayload);
- return {
- id: payload[CLAIM_NAME_IDENTIFIER] || null,
- email: payload[CLAIM_EMAIL] || null,
- name: payload[CLAIM_NAME] || null
- };
- } catch {
- return null;
- }
- }
- // 날짜 형식 변환 표시
- export function formatDate(dateString: string|null): string {
- if (!dateString) {
- return '-';
- }
- const date = new Date(dateString);
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- const diffSec = Math.floor(diffMs / 1000);
- const diffMin = Math.floor(diffSec / 60);
- const diffHour = Math.floor(diffMin / 60);
- const diffDay = Math.floor(diffHour / 24);
- const diffWeek = Math.floor(diffDay / 7);
- const diffMonth = Math.floor(diffDay / 30);
- const diffYear = Math.floor(diffDay / 365);
- if (diffSec < 60) return '방금 전';
- if (diffMin < 60) return `${diffMin}분 전`;
- if (diffHour < 24) return `${diffHour}시간 전`;
- if (diffDay < 7) return `${diffDay}일 전`;
- if (diffWeek < 5) return `${diffWeek}주 전`;
- if (diffMonth < 12) return `${diffMonth}개월 전`;
- return `${diffYear}년 전`;
- }
- // 2025.01.01. 12:00:00 으로 조회
- export function getDateTime(dateString: string|null) {
- if (!dateString) {
- return '-';
- }
- const date = new Date(dateString);
- return date.toLocaleString('ko-KR', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- hour12: false
- }).replace(/\. /g, '.').replace(/\.(\d{2}:)/, ' $1');
- }
- // 파일 이름 생성
- export function getDateFilename(prefix: string): string {
- if (!prefix) {
- return '';
- }
- const now = new Date();
- const yyyy = now.getFullYear();
- const MM = String(now.getMonth() + 1).padStart(2, '0');
- const dd = String(now.getDate()).padStart(2, '0');
- const hh = String(now.getHours()).padStart(2, '0');
- const mm = String(now.getMinutes()).padStart(2, '0');
- const ss = String(now.getSeconds()).padStart(2, '0');
- return `${prefix}-${yyyy}${MM}${dd}${hh}${mm}${ss}.png`;
- }
- // HTML 태그 제거
- export function stripHtmlTags(str?: string|null): string {
- if (!str) {
- return '';
- }
- const div = document.createElement('div');
- div.innerHTML = str;
- return div.textContent || div.innerText || '';
- }
- // 비밀번호 정책 안내 문구 생성성
- export function getPasswordPolicyMessage(policy: {
- passwordUppercaseLength: number;
- passwordNumbersLength: number;
- passwordSpecialcharsLength: number;
- }) {
- const parts: string[] = [];
- if (policy.passwordUppercaseLength > 0) {
- parts.push(`영문 대문자 ${policy.passwordUppercaseLength}자 이상`);
- }
- if (policy.passwordNumbersLength > 0) {
- parts.push(`숫자 ${policy.passwordNumbersLength}자 이상`);
- }
- if (policy.passwordSpecialcharsLength > 0) {
- parts.push(`특수문자 ${policy.passwordSpecialcharsLength}자 이상`);
- }
- if (parts.length > 0) {
- return `비밀번호는 ${parts.join(', ')}를 포함해야 합니다.`;
- }
- return '';
- }
- // 특수문자 제거
- export function filterSpecialCharacters(str: string) {
- return str.replace(/[^\p{L}\p{N}\s]/gu, '');
- }
- // ResultDto에서 첫 번째 오류 조회
- export function getError(errors: ApiError[]|null|undefined, index = 0): string|undefined {
- if (!errors || errors.length === 0) {
- return undefined;
- }
- return errors[index]?.description;
- }
- // 서버 응답 메시지 분석
- export function throwError<T>(res: ResultDto<T>): void {
- if (res.success) {
- return;
- }
- let message: string|null|undefined = getError(res.errors);
- if (!message) {
- message = res.message;
- }
- if (!message) {
- switch (res.status) {
- case 400: message = '잘못된 요청입니다.'; break;
- case 401:
- message = '로그인 후 이용해 주세요.';
- // 인증 만료 이벤트 발행
- window.dispatchEvent(new CustomEvent('auth:unauthorized'));;
- break;
- case 403: message = '권한이 없습니다.'; break;
- case 404: message = '요청하신 페이지를 찾을 수 없습니다.'; break;
- case 500: message = '서버 오류가 발생했습니다. 다시 시도해 주세요.'; break;
- }
- }
- if (message) {
- throw new Error(message);
- }
- }
- // 1000회 이상 조회된 게시글인지 확인
- export function isHotPost(showHotIcon: boolean, post: { views: number; createdAt: string }): boolean {
- return (showHotIcon && post.views >= 1000 && Date.now() - new Date(post.createdAt).getTime() < 7 * 24 * 60 * 60 * 1000);
- }
- // 24시간 이내에 작성된 게시글인지 확인
- export function isNewPost(showNewIcon: boolean, post: { createdAt: string }): boolean {
- return (showNewIcon && Date.now() - new Date(post.createdAt).getTime() < 24 * 60 * 60 * 1000);
- }
- // a 태그에 target="_blank" 추가
- export function forceTargetBlank(html: string) {
- return html.replace(/<a\s+(?![^>]*target=)([^>]*href=["'][^"']+["'])/gi, '<a target="_blank" rel="noopener noreferrer" $1');
- };
- // 날짜가 현재 날짜보다 지났는지 확인
- export function isDateOverdue(dateString: string|null, days = 0): boolean {
- if (!dateString) {
- return false;
- }
- const limitDate = new Date(new Date(dateString));
- limitDate.setDate(limitDate.getDate() + days);
- const now = new Date();
- return now > limitDate;
- }
|