This commit is contained in:
SeoJHeasdw
2025-06-11 16:35:16 +09:00
parent ea04212298
commit df016023be
12 changed files with 3242 additions and 3894 deletions
+192 -193
View File
@@ -1,214 +1,213 @@
//* src/utils/constants.js
/**
* AI 마케팅 서비스 상수 정의
*
* @description 애플리케이션 전반에서 사용되는 상수들을 정의합니다.
* @author AI Marketing Team
* @version 1.0
* 애플리케이션 전역 상수 정의
*/
// API 엔드포인트 상수
export const API_ENDPOINTS = {
AUTH: '/auth',
MEMBER: '/member',
STORE: '/store',
MENU: '/menu',
CONTENT: '/content',
RECOMMEND: '/recommend',
ANALYSIS: '/analysis'
// 콘텐츠 타입
export const CONTENT_TYPES = {
SNS: 'sns',
POSTER: 'poster',
VIDEO: 'video',
BLOG: 'blog',
}
// 인증 관련 상수
export const AUTH_CONSTANTS = {
ACCESS_TOKEN_KEY: 'accessToken',
REFRESH_TOKEN_KEY: 'refreshToken',
USER_INFO_KEY: 'userInfo',
TOKEN_EXPIRY_BUFFER: 5 * 60 * 1000, // 5분 (밀리초)
MAX_LOGIN_ATTEMPTS: 5,
LOCKOUT_DURATION: 15 * 60 * 1000 // 15분 (밀리초)
export const CONTENT_TYPE_LABELS = {
[CONTENT_TYPES.SNS]: 'SNS 게시물',
[CONTENT_TYPES.POSTER]: '홍보 포스터',
[CONTENT_TYPES.VIDEO]: '비디오',
[CONTENT_TYPES.BLOG]: '블로그 포스트',
}
// 콘텐츠 타입 상수
export const CONTENT_TYPES = [
{ text: 'SNS 포스트', value: 'SNS_POST', icon: 'mdi-instagram' },
{ text: '포스터', value: 'POSTER', icon: 'mdi-image' },
{ text: '블로그 글', value: 'BLOG', icon: 'mdi-post' },
{ text: '광고 문구', value: 'AD_COPY', icon: 'mdi-bullhorn' }
]
// SNS 플랫폼 상수
export const PLATFORMS = [
{ text: '인스타그램', value: 'INSTAGRAM', icon: 'mdi-instagram', color: '#E4405F' },
{ text: '페이스북', value: 'FACEBOOK', icon: 'mdi-facebook', color: '#1877F2' },
{ text: '네이버 블로그', value: 'NAVER_BLOG', icon: 'mdi-post', color: '#03C75A' },
{ text: '카카오톡', value: 'KAKAO_TALK', icon: 'mdi-chat', color: '#FEE500' },
{ text: '트위터', value: 'TWITTER', icon: 'mdi-twitter', color: '#1DA1F2' },
{ text: '유튜브', value: 'YOUTUBE', icon: 'mdi-youtube', color: '#FF0000' }
]
// 콘텐츠 상태 상수
export const CONTENT_STATUS = [
{ text: '초안', value: 'DRAFT', color: 'grey', icon: 'mdi-file-document-outline' },
{ text: '검토중', value: 'REVIEW', color: 'orange', icon: 'mdi-eye' },
{ text: '승인됨', value: 'APPROVED', color: 'green', icon: 'mdi-check-circle' },
{ text: '게시됨', value: 'PUBLISHED', color: 'blue', icon: 'mdi-publish' },
{ text: '보관됨', value: 'ARCHIVED', color: 'grey-darken-2', icon: 'mdi-archive' }
]
// 톤앤매너 옵션
export const TONE_OPTIONS = [
{ text: '친근함', value: '친근함', description: '고객과 가까운 느낌의 편안한 톤' },
{ text: '전문적', value: '전문적', description: '신뢰할 수 있는 전문가 느낌' },
{ text: '유머러스', value: '유머러스', description: '재미있고 유쾌한 분위기' },
{ text: '고급스러움', value: '고급스러움', description: '품격 있고 세련된 느낌' },
{ text: '트렌디', value: '트렌디', description: '최신 트렌드를 반영한 스타일' }
]
// 감정 강도 옵션
export const EMOTION_INTENSITY = [
{ text: '차분함', value: '차분함', description: '차분하고 안정적인 톤' },
{ text: '보통', value: '보통', description: '적당한 감정 표현' },
{ text: '활발함', value: '활발함', description: '에너지 넘치는 표현' },
{ text: '열정적', value: '열정적', description: '강렬하고 역동적인 표현' }
]
// 프로모션 옵션
export const PROMOTION_OPTIONS = [
{ text: '없음', value: '없음' },
{ text: '할인 이벤트', value: '할인 이벤트' },
{ text: '신메뉴 출시', value: '신메뉴 출시' },
{ text: '시즌 특가', value: '시즌 특가' },
{ text: '회원 혜택', value: '회원 혜택' },
{ text: '기념일 이벤트', value: '기념일 이벤트' }
]
// 업종 카테고리
export const BUSINESS_CATEGORIES = [
{ text: '음식점', value: 'RESTAURANT', icon: 'mdi-food' },
{ text: '카페', value: 'CAFE', icon: 'mdi-coffee' },
{ text: '베이커리', value: 'BAKERY', icon: 'mdi-cake' },
{ text: '치킨/피자', value: 'CHICKEN_PIZZA', icon: 'mdi-pizza' },
{ text: '분식', value: 'SNACK_BAR', icon: 'mdi-food-hot-dog' },
{ text: '술집', value: 'BAR', icon: 'mdi-glass-mug' },
{ text: '기타', value: 'OTHER', icon: 'mdi-store' }
]
// 메뉴 카테고리
export const MENU_CATEGORIES = [
{ text: '주메뉴', value: 'MAIN', color: 'primary' },
{ text: '사이드', value: 'SIDE', color: 'secondary' },
{ text: '음료', value: 'BEVERAGE', color: 'info' },
{ text: '디저트', value: 'DESSERT', color: 'warning' },
{ text: '세트메뉴', value: 'SET', color: 'success' }
]
// 매출 분석 기간 옵션
export const ANALYSIS_PERIODS = [
{ text: '오늘', value: 'today' },
{ text: '어제', value: 'yesterday' },
{ text: '이번 주', value: 'this_week' },
{ text: '지난 주', value: 'last_week' },
{ text: '이번 달', value: 'this_month' },
{ text: '지난 달', value: 'last_month' },
{ text: '지난 3개월', value: 'last_3_months' },
{ text: '직접 선택', value: 'custom' }
]
// 차트 타입 옵션
export const CHART_TYPES = [
{ text: '일별', value: 'daily', icon: 'mdi-calendar-today' },
{ text: '주별', value: 'weekly', icon: 'mdi-calendar-week' },
{ text: '월별', value: 'monthly', icon: 'mdi-calendar-month' }
]
// AI 추천 타입
export const AI_RECOMMENDATION_TYPES = [
{ text: '마케팅 팁', value: 'MARKETING_TIP', icon: 'mdi-lightbulb' },
{ text: '메뉴 제안', value: 'MENU_SUGGESTION', icon: 'mdi-food' },
{ text: '프로모션 아이디어', value: 'PROMOTION_IDEA', icon: 'mdi-sale' },
{ text: '콘텐츠 아이디어', value: 'CONTENT_IDEA', icon: 'mdi-lightbulb-variant' }
]
// 날씨 조건 상수
export const WEATHER_CONDITIONS = [
{ text: '맑음', value: 'SUNNY', icon: 'mdi-weather-sunny' },
{ text: '흐림', value: 'CLOUDY', icon: 'mdi-weather-cloudy' },
{ text: '비', value: 'RAINY', icon: 'mdi-weather-rainy' },
{ text: '눈', value: 'SNOWY', icon: 'mdi-weather-snowy' },
{ text: '바람', value: 'WINDY', icon: 'mdi-weather-windy' }
]
// 시간대 분류
export const TIME_SLOTS = [
{ text: '아침 (06:00-11:00)', value: 'MORNING', start: 6, end: 11 },
{ text: '점심 (11:00-14:00)', value: 'LUNCH', start: 11, end: 14 },
{ text: '오후 (14:00-17:00)', value: 'AFTERNOON', start: 14, end: 17 },
{ text: '저녁 (17:00-21:00)', value: 'DINNER', start: 17, end: 21 },
{ text: '밤 (21:00-24:00)', value: 'NIGHT', start: 21, end: 24 },
{ text: '심야 (00:00-06:00)', value: 'LATE_NIGHT', start: 0, end: 6 }
]
// 페이지네이션 상수
export const PAGINATION = {
DEFAULT_PAGE_SIZE: 10,
PAGE_SIZE_OPTIONS: [5, 10, 20, 50],
MAX_VISIBLE_PAGES: 5
// 플랫폼
export const PLATFORMS = {
INSTAGRAM: 'instagram',
NAVER_BLOG: 'naver_blog',
FACEBOOK: 'facebook',
TWITTER: 'twitter',
YOUTUBE: 'youtube',
KAKAO: 'kakao',
}
// 파일 업로드 상수
export const FILE_UPLOAD = {
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
ALLOWED_VIDEO_TYPES: ['video/mp4', 'video/avi', 'video/mov'],
MAX_IMAGES_COUNT: 10,
MAX_VIDEOS_COUNT: 3
export const PLATFORM_LABELS = {
[PLATFORMS.INSTAGRAM]: '인스타그램',
[PLATFORMS.NAVER_BLOG]: '네이버 블로그',
[PLATFORMS.FACEBOOK]: '페이스북',
[PLATFORMS.TWITTER]: '트위터',
[PLATFORMS.YOUTUBE]: '유튜브',
[PLATFORMS.KAKAO]: '카카오',
}
// UI 관련 상수
export const UI_CONSTANTS = {
MOBILE_BREAKPOINT: 768,
TABLET_BREAKPOINT: 1024,
DESKTOP_BREAKPOINT: 1200,
HEADER_HEIGHT: 64,
FOOTER_HEIGHT: 80,
SIDEBAR_WIDTH: 280
export const PLATFORM_COLORS = {
[PLATFORMS.INSTAGRAM]: 'purple',
[PLATFORMS.NAVER_BLOG]: 'green',
[PLATFORMS.FACEBOOK]: 'blue',
[PLATFORMS.TWITTER]: 'light-blue',
[PLATFORMS.YOUTUBE]: 'red',
[PLATFORMS.KAKAO]: 'yellow',
}
// 알림 타입
export const NOTIFICATION_TYPES = {
// 콘텐츠 상태
export const CONTENT_STATUS = {
DRAFT: 'draft',
PUBLISHED: 'published',
SCHEDULED: 'scheduled',
ARCHIVED: 'archived',
FAILED: 'failed',
}
export const CONTENT_STATUS_LABELS = {
[CONTENT_STATUS.DRAFT]: '임시저장',
[CONTENT_STATUS.PUBLISHED]: '발행됨',
[CONTENT_STATUS.SCHEDULED]: '예약됨',
[CONTENT_STATUS.ARCHIVED]: '보관됨',
[CONTENT_STATUS.FAILED]: '실패',
}
export const CONTENT_STATUS_COLORS = {
[CONTENT_STATUS.DRAFT]: 'orange',
[CONTENT_STATUS.PUBLISHED]: 'success',
[CONTENT_STATUS.SCHEDULED]: 'info',
[CONTENT_STATUS.ARCHIVED]: 'grey',
[CONTENT_STATUS.FAILED]: 'error',
}
// 매장 업종
export const BUSINESS_TYPES = {
RESTAURANT: 'restaurant',
CAFE: 'cafe',
SNACK_BAR: 'snack_bar',
FAST_FOOD: 'fast_food',
BAKERY: 'bakery',
DESSERT: 'dessert',
CONVENIENCE: 'convenience',
OTHER: 'other',
}
export const BUSINESS_TYPE_LABELS = {
[BUSINESS_TYPES.RESTAURANT]: '일반음식점',
[BUSINESS_TYPES.CAFE]: '카페',
[BUSINESS_TYPES.SNACK_BAR]: '분식점',
[BUSINESS_TYPES.FAST_FOOD]: '패스트푸드',
[BUSINESS_TYPES.BAKERY]: '제과점',
[BUSINESS_TYPES.DESSERT]: '디저트카페',
[BUSINESS_TYPES.CONVENIENCE]: '편의점',
[BUSINESS_TYPES.OTHER]: '기타',
}
// 톤앤매너
export const TONE_AND_MANNER = {
FRIENDLY: 'friendly',
PROFESSIONAL: 'professional',
HUMOROUS: 'humorous',
ELEGANT: 'elegant',
CASUAL: 'casual',
TRENDY: 'trendy',
}
export const TONE_AND_MANNER_LABELS = {
[TONE_AND_MANNER.FRIENDLY]: '친근함',
[TONE_AND_MANNER.PROFESSIONAL]: '전문적',
[TONE_AND_MANNER.HUMOROUS]: '유머러스',
[TONE_AND_MANNER.ELEGANT]: '고급스러운',
[TONE_AND_MANNER.CASUAL]: '캐주얼',
[TONE_AND_MANNER.TRENDY]: '트렌디',
}
// 감정 강도
export const EMOTION_INTENSITY = {
CALM: 'calm',
NORMAL: 'normal',
ENTHUSIASTIC: 'enthusiastic',
EXCITING: 'exciting',
}
export const EMOTION_INTENSITY_LABELS = {
[EMOTION_INTENSITY.CALM]: '차분함',
[EMOTION_INTENSITY.NORMAL]: '보통',
[EMOTION_INTENSITY.ENTHUSIASTIC]: '열정적',
[EMOTION_INTENSITY.EXCITING]: '과장된',
}
// 프로모션 타입
export const PROMOTION_TYPES = {
DISCOUNT: 'discount',
EVENT: 'event',
NEW_MENU: 'new_menu',
NONE: 'none',
}
export const PROMOTION_TYPE_LABELS = {
[PROMOTION_TYPES.DISCOUNT]: '할인 정보',
[PROMOTION_TYPES.EVENT]: '이벤트 정보',
[PROMOTION_TYPES.NEW_MENU]: '신메뉴 알림',
[PROMOTION_TYPES.NONE]: '없음',
}
// 이미지 스타일
export const PHOTO_STYLES = {
MODERN: 'modern',
CLASSIC: 'classic',
EMOTIONAL: 'emotional',
MINIMALIST: 'minimalist',
}
export const PHOTO_STYLE_LABELS = {
[PHOTO_STYLES.MODERN]: '모던',
[PHOTO_STYLES.CLASSIC]: '클래식',
[PHOTO_STYLES.EMOTIONAL]: '감성적',
[PHOTO_STYLES.MINIMALIST]: '미니멀',
}
// 파일 업로드 제한
export const FILE_LIMITS = {
MAX_SIZE: 10 * 1024 * 1024, // 10MB
ALLOWED_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
}
// API 응답 상태
export const API_STATUS = {
SUCCESS: 'success',
ERROR: 'error',
WARNING: 'warning',
INFO: 'info'
LOADING: 'loading',
IDLE: 'idle',
}
// 로딩 상태 메시지
export const LOADING_MESSAGES = {
GENERATING_CONTENT: 'AI가 콘텐츠를 생성하고 있습니다...',
UPLOADING_FILE: '파일을 업로드하고 있습니다...',
SAVING_DATA: '데이터를 저장하고 있습니다...',
LOADING_DATA: '데이터를 불러오고 있습니다...',
ANALYZING_DATA: '데이터를 분석하고 있습니다...'
// 페이지네이션
export const PAGINATION = {
DEFAULT_PAGE_SIZE: 20,
PAGE_SIZE_OPTIONS: [10, 20, 50, 100],
}
// 에러 메시지
export const ERROR_MESSAGES = {
NETWORK_ERROR: '네트워크 연결을 확인해주세요.',
SERVER_ERROR: '서버에서 오류가 발생했습니다.',
UNAUTHORIZED: '로그인이 필요합니다.',
FORBIDDEN: '접근 권한이 없습니다.',
NOT_FOUND: '요청한 데이터를 찾을 수 없습니다.',
VALIDATION_ERROR: '입력 정보를 확인해주세요.',
FILE_SIZE_ERROR: '파일 크기가 너무 큽니다.',
FILE_TYPE_ERROR: '지원하지 않는 파일 형식입니다.'
// 로컬 스토리지 키
export const STORAGE_KEYS = {
AUTH_TOKEN: 'auth_token',
USER_INFO: 'user_info',
APP_SETTINGS: 'app_settings',
CONTENT_FILTERS: 'content_filters',
}
// 성공 메시지
export const SUCCESS_MESSAGES = {
SAVE_SUCCESS: '성공적으로 저장되었습니다.',
UPDATE_SUCCESS: '성공적으로 수정되었습니다.',
DELETE_SUCCESS: '성공적으로 삭제되었습니다.',
UPLOAD_SUCCESS: '파일이 성공적으로 업로드되었습니다.',
CONTENT_GENERATED: 'AI 콘텐츠가 성공적으로 생성되었습니다.',
LOGIN_SUCCESS: '로그인되었습니다.',
LOGOUT_SUCCESS: '로그아웃되었습니다.'
}
// 시간 관련 상수
export const TIME_FORMATS = {
DATE: 'YYYY-MM-DD',
DATETIME: 'YYYY-MM-DD HH:mm:ss',
TIME: 'HH:mm',
}
export const DATE_RANGES = {
TODAY: 'today',
WEEK: 'week',
MONTH: 'month',
QUARTER: 'quarter',
YEAR: 'year',
ALL: 'all',
}
export const DATE_RANGE_LABELS = {
[DATE_RANGES.TODAY]: '오늘',
[DATE_RANGES.WEEK]: '최근 1주일',
[DATE_RANGES.MONTH]: '최근 1개월',
[DATE_RANGES.QUARTER]: '최근 3개월',
[DATE_RANGES.YEAR]: '최근 1년',
[DATE_RANGES.ALL]: '전체',
}
+209 -273
View File
@@ -1,345 +1,281 @@
//* src/utils/formatters.js
/**
* 데이터 포맷팅 유틸리티
*
* @description 다양한 데이터 타입을 사용자 친화적인 형태로 포맷팅하는 함수들을 제공합니다.
* @author AI Marketing Team
* @version 1.0
* 포맷팅 유틸리티 함수들
*/
/**
* 숫자를 통화 형식으로 포맷팅
* @param {number} amount - 포맷팅할 금액
* @param {string} currency - 통화 단위 (기본값: 'KRW')
* @param {boolean} showSymbol - 통화 기호 표시 여부 (기본값: true)
* @returns {string} 포맷팅된 통화 문자열
* 통화 포맷 (한국 원화)
*/
export const formatCurrency = (amount, currency = 'KRW', showSymbol = true) => {
if (amount === null || amount === undefined || isNaN(amount)) {
return '0원'
}
const formatter = new Intl.NumberFormat('ko-KR', {
export const formatCurrency = (amount) => {
if (!amount && amount !== 0) return '₩0'
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: currency,
currency: 'KRW',
minimumFractionDigits: 0,
maximumFractionDigits: 0
})
if (showSymbol) {
return formatter.format(amount)
} else {
return amount.toLocaleString('ko-KR')
}
maximumFractionDigits: 0,
}).format(amount)
}
/**
* 큰 숫자를 축약된 형태로 포맷 (예: 1000 -> 1K)
* @param {number} num - 포맷팅할 숫자
* @param {number} digits - 소수점 자릿수 (기본값: 1)
* @returns {string} 축약된 숫자 문자열
* 숫자 포맷 (천 단위 콤마)
*/
export const formatAbbreviatedNumber = (num, digits = 1) => {
if (num === null || num === undefined || isNaN(num)) {
return '0'
}
const units = [
{ value: 1e9, symbol: 'B' },
{ value: 1e8, symbol: '억' },
{ value: 1e6, symbol: 'M' },
{ value: 1e4, symbol: '만' },
{ value: 1e3, symbol: 'K' }
]
for (let unit of units) {
if (Math.abs(num) >= unit.value) {
return (num / unit.value).toFixed(digits) + unit.symbol
}
}
return num.toString()
export const formatNumber = (number) => {
if (!number && number !== 0) return '0'
return new Intl.NumberFormat('ko-KR').format(number)
}
/**
* 퍼센트 포맷
* @param {number} value - 퍼센트 값
* @param {number} decimals - 소수점 자릿수 (기본값: 1)
* @param {boolean} showSign - 부호 표시 여부 (기본값: false)
* @returns {string} 포맷팅된 퍼센트 문자열
* 퍼센트 포맷
*/
export const formatPercentage = (value, decimals = 1, showSign = false) => {
if (value === null || value === undefined || isNaN(value)) {
return '0%'
}
const sign = showSign && value > 0 ? '+' : ''
return `${sign}${value.toFixed(decimals)}%`
export const formatPercent = (value, decimals = 1) => {
if (!value && value !== 0) return '0%'
return `${parseFloat(value).toFixed(decimals)}%`
}
/**
* 날짜를 한국어 형식으로 포맷팅
* @param {Date|string} date - 포맷팅할 날짜
* @param {string} format - 포맷 형식 ('full', 'date', 'time', 'datetime', 'relative')
* @returns {string} 포맷팅된 날짜 문자열
* 상대적 시간 포맷 (예: "30분 전", "2시간 전")
*/
export const formatDate = (date, format = 'date') => {
export const formatRelativeTime = (date) => {
if (!date) return ''
const dateObj = typeof date === 'string' ? new Date(date) : date
if (isNaN(dateObj.getTime())) {
return '잘못된 날짜'
}
const options = {
full: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
},
date: {
year: 'numeric',
month: '2-digit',
day: '2-digit'
},
time: {
hour: '2-digit',
minute: '2-digit',
hour12: false
},
datetime: {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}
}
if (format === 'relative') {
return formatRelativeTime(dateObj)
}
const formatOptions = options[format] || options.date
return new Intl.DateTimeFormat('ko-KR', formatOptions).format(dateObj)
}
/**
* 상대적 시간 포맷팅 (예: 2시간 전, 3일 후)
* @param {Date|string} date - 기준 날짜
* @param {Date} baseDate - 비교 기준 날짜 (기본값: 현재 시간)
* @returns {string} 상대적 시간 문자열
*/
export const formatRelativeTime = (date, baseDate = new Date()) => {
if (!date) return ''
const dateObj = typeof date === 'string' ? new Date(date) : date
const baseDateObj = typeof baseDate === 'string' ? new Date(baseDate) : baseDate
if (isNaN(dateObj.getTime()) || isNaN(baseDateObj.getTime())) {
return '잘못된 날짜'
}
const diffInSeconds = Math.floor((baseDateObj - dateObj) / 1000)
const absDiff = Math.abs(diffInSeconds)
const units = [
{ name: '년', seconds: 31536000 },
{ name: '개월', seconds: 2592000 },
{ name: '일', seconds: 86400 },
{ name: '시간', seconds: 3600 },
{ name: '분', seconds: 60 }
]
for (let unit of units) {
const interval = Math.floor(absDiff / unit.seconds)
if (interval >= 1) {
return diffInSeconds > 0
? `${interval}${unit.name}`
: `${interval}${unit.name}`
}
}
return diffInSeconds > 0 ? '방금 전' : '곧'
}
/**
* 날짜와 시간을 사용자 친화적 형식으로 포맷팅
* @param {Date|string} dateTime - 포맷팅할 날짜시간
* @returns {string} 포맷팅된 날짜시간 문자열
*/
export const formatDateTime = (dateTime) => {
if (!dateTime) return ''
const dateObj = typeof dateTime === 'string' ? new Date(dateTime) : dateTime
if (isNaN(dateObj.getTime())) {
return '잘못된 날짜'
}
const now = new Date()
const diffInDays = Math.floor((now - dateObj) / (1000 * 60 * 60 * 24))
const targetDate = new Date(date)
const diffInMs = now - targetDate
const diffInMinutes = Math.floor(diffInMs / (1000 * 60))
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60))
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24))
if (diffInDays === 0) {
// 오늘인 경우 시간만 표시
return formatDate(dateObj, 'time')
} else if (diffInDays === 1) {
// 어제인 경우
return `어제 ${formatDate(dateObj, 'time')}`
} else if (diffInDays < 7) {
// 일주일 이내인 경우
const weekday = dateObj.toLocaleDateString('ko-KR', { weekday: 'short' })
return `${weekday} ${formatDate(dateObj, 'time')}`
} else {
// 일주일 이상인 경우 전체 날짜 표시
return formatDate(dateObj, 'datetime')
}
if (diffInMinutes < 1) return '방금 전'
if (diffInMinutes < 60) return `${diffInMinutes}분 전`
if (diffInHours < 24) return `${diffInHours}시간 전`
if (diffInDays < 7) return `${diffInDays}일 전`
return targetDate.toLocaleDateString('ko-KR')
}
/**
* 파일 크기를 사람이 읽기 쉬운 형태로 포맷팅
* @param {number} bytes - 파일 크기 (바이트)
* @param {number} decimals - 소수점 자릿수 (기본값: 2)
* @returns {string} 포맷팅된 파일 크기 문자열
* 날짜 포맷 (YYYY-MM-DD 형식)
*/
export const formatFileSize = (bytes, decimals = 2) => {
if (bytes === 0) return '0 B'
if (!bytes) return ''
export const formatDate = (date, options = {}) => {
if (!date) return ''
const targetDate = new Date(date)
const defaultOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}
return targetDate.toLocaleDateString('ko-KR', { ...defaultOptions, ...options })
}
/**
* 날짜시간 포맷 (YYYY-MM-DD HH:mm 형식)
*/
export const formatDateTime = (date, options = {}) => {
if (!date) return ''
const targetDate = new Date(date)
const defaultOptions = {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}
return targetDate.toLocaleString('ko-KR', { ...defaultOptions, ...options })
}
/**
* 시간 포맷 (HH:mm 형식)
*/
export const formatTime = (date) => {
if (!date) return ''
const targetDate = new Date(date)
return targetDate.toLocaleTimeString('ko-KR', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
}
/**
* 파일 크기 포맷
*/
export const formatFileSize = (bytes) => {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
/**
* 전화번호 포맷
* @param {string} phoneNumber - 전화번호 문자열
* @returns {string} 포맷팅된 전화번호
* 전화번호 포맷
*/
export const formatPhoneNumber = (phoneNumber) => {
if (!phoneNumber) return ''
export const formatPhoneNumber = (phone) => {
if (!phone) return ''
// 숫자만 추출
const numbers = phoneNumber.replace(/\D/g, '')
const cleaned = phone.replace(/\D/g, '')
const match = cleaned.match(/^(\d{3})(\d{4})(\d{4})$/)
// 휴대폰 번호 (010-xxxx-xxxx)
if (numbers.length === 11 && numbers.startsWith('010')) {
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`
}
// 일반 전화번호 (02-xxx-xxxx 또는 03x-xxx-xxxx)
if (numbers.length === 10) {
if (numbers.startsWith('02')) {
return `${numbers.slice(0, 2)}-${numbers.slice(2, 6)}-${numbers.slice(6)}`
} else {
return `${numbers.slice(0, 3)}-${numbers.slice(3, 6)}-${numbers.slice(6)}`
}
if (match) {
return `${match[1]}-${match[2]}-${match[3]}`
}
if (numbers.length === 11 && !numbers.startsWith('010')) {
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`
}
return phoneNumber
return phone
}
/**
* 사업자등록번호 포맷
* @param {string} businessNumber - 사업자등록번호
* @returns {string} 포맷팅된 사업자등록번호
* 사업자등록번호 포맷
*/
export const formatBusinessNumber = (businessNumber) => {
if (!businessNumber) return ''
export const formatBusinessNumber = (number) => {
if (!number) return ''
const numbers = businessNumber.replace(/\D/g, '')
if (numbers.length === 10) {
return `${numbers.slice(0, 3)}-${numbers.slice(3, 5)}-${numbers.slice(5)}`
const cleaned = number.replace(/\D/g, '')
const match = cleaned.match(/^(\d{3})(\d{2})(\d{5})$/)
if (match) {
return `${match[1]}-${match[2]}-${match[3]}`
}
return businessNumber
return number
}
/**
* 텍스트를 지정된 길이로 자르고 말줄임표 추가
* @param {string} text - 자를 텍스트
* @param {number} maxLength - 최대 길이
* @param {string} suffix - 말줄임표 (기본값: '...')
* @returns {string} 잘린 텍스트
* 해시태그 포맷 (#포함하여 문자열로 변환)
*/
export const truncateText = (text, maxLength, suffix = '...') => {
if (!text) return ''
if (text.length <= maxLength) {
return text
}
export const formatHashtags = (hashtags) => {
if (!hashtags || !Array.isArray(hashtags)) return ''
return text.slice(0, maxLength - suffix.length) + suffix
return hashtags.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`)).join(' ')
}
/**
* 해시태그 배열을 문자열로 포맷팅
* @param {Array} hashtags - 해시태그 배열
* @param {string} separator - 구분자 (기본값: ' ')
* @returns {string} 포맷팅된 해시태그 문자열
* 콘텐츠 미리보기 포맷 (길이 제한)
*/
export const formatHashtags = (hashtags, separator = ' ') => {
if (!Array.isArray(hashtags) || hashtags.length === 0) {
return ''
}
export const formatContentPreview = (content, maxLength = 100) => {
if (!content) return ''
return hashtags
.filter(tag => tag && tag.trim())
.map(tag => tag.startsWith('#') ? tag : `#${tag}`)
.join(separator)
if (content.length <= maxLength) return content
return content.substring(0, maxLength) + '...'
}
/**
* 주소를 간단한 형태로 포맷
* @param {string} fullAddress - 전체 주소
* @param {boolean} showDetail - 상세 주소 표시 여부 (기본값: false)
* @returns {string} 포맷팅된 주소
* 소셜미디어 URL 포맷
*/
export const formatAddress = (fullAddress, showDetail = false) => {
if (!fullAddress) return ''
export const formatSocialUrl = (username, platform) => {
if (!username) return ''
const parts = fullAddress.split(' ')
if (showDetail) {
return fullAddress
const urls = {
instagram: `https://instagram.com/${username}`,
facebook: `https://facebook.com/${username}`,
twitter: `https://twitter.com/${username}`,
youtube: `https://youtube.com/@${username}`,
naver_blog: `https://blog.naver.com/${username}`,
}
// 시/도, 구/군만 표시
if (parts.length >= 2) {
return `${parts[0]} ${parts[1]}`
}
return fullAddress
return urls[platform] || username
}
/**
* 배열을 문자열로 포맷팅
* @param {Array} array - 포맷팅할 배열
* @param {string} separator - 구분자 (기본값: ', ')
* @param {number} maxItems - 최대 표시 항목 수
* @returns {string} 포맷팅된 문자열
* 성과 지표 포맷 (K, M 단위)
*/
export const formatArrayToString = (array, separator = ', ', maxItems = null) => {
if (!Array.isArray(array) || array.length === 0) {
return ''
export const formatMetric = (number) => {
if (!number && number !== 0) return '0'
if (number >= 1000000) {
return `${(number / 1000000).toFixed(1)}M`
}
if (number >= 1000) {
return `${(number / 1000).toFixed(1)}K`
}
const items = maxItems ? array.slice(0, maxItems) : array
const result = items.join(separator)
return formatNumber(number)
}
if (maxItems && array.length > maxItems) {
const remaining = array.length - maxItems
return `${result}${remaining}`
/**
* 진행률 포맷
*/
export const formatProgress = (current, total) => {
if (!total || total === 0) return '0%'
const percentage = (current / total) * 100
return `${Math.round(percentage)}%`
}
/**
* 이메일 마스킹 포맷
*/
export const formatEmailMask = (email) => {
if (!email) return ''
const [username, domain] = email.split('@')
if (!domain) return email
const maskedUsername =
username.length > 2 ? username.substring(0, 2) + '*'.repeat(username.length - 2) : username
return `${maskedUsername}@${domain}`
}
/**
* 주소 간단 포맷
*/
export const formatAddressShort = (address) => {
if (!address) return ''
// 상세주소 제거하고 주요 정보만 표시
const parts = address.split(' ')
if (parts.length > 3) {
return parts.slice(0, 3).join(' ') + '...'
}
return result
}
return address
}
/**
* 평점 포맷 (별점)
*/
export const formatRating = (rating, maxRating = 5) => {
if (!rating && rating !== 0) return '☆'.repeat(maxRating)
const fullStars = Math.floor(rating)
const hasHalfStar = rating % 1 >= 0.5
const emptyStars = maxRating - fullStars - (hasHalfStar ? 1 : 0)
return '★'.repeat(fullStars) + (hasHalfStar ? '☆' : '') + '☆'.repeat(emptyStars)
}
/**
* 운영시간 포맷
*/
export const formatBusinessHours = (openTime, closeTime) => {
if (!openTime || !closeTime) return '운영시간 미정'
return `${formatTime(openTime)} - ${formatTime(closeTime)}`
}
/**
* 나이 계산 및 포맷
*/
export const formatAge = (birthDate) => {
if (!birthDate) return ''
const today = new Date()
const birth = new Date(birthDate)
let age = today.getFullYear() - birth.getFullYear()
const monthDiff = today.getMonth() - birth.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--
}
return `${age}`
}