utils files add
This commit is contained in:
parent
43066ca623
commit
bafdb86d42
214
src/utils/constants.js
Normal file
214
src/utils/constants.js
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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 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_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 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
|
||||
}
|
||||
|
||||
// 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 NOTIFICATION_TYPES = {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
WARNING: 'warning',
|
||||
INFO: 'info'
|
||||
}
|
||||
|
||||
// 로딩 상태 메시지
|
||||
export const LOADING_MESSAGES = {
|
||||
GENERATING_CONTENT: 'AI가 콘텐츠를 생성하고 있습니다...',
|
||||
UPLOADING_FILE: '파일을 업로드하고 있습니다...',
|
||||
SAVING_DATA: '데이터를 저장하고 있습니다...',
|
||||
LOADING_DATA: '데이터를 불러오고 있습니다...',
|
||||
ANALYZING_DATA: '데이터를 분석하고 있습니다...'
|
||||
}
|
||||
|
||||
// 에러 메시지
|
||||
export const ERROR_MESSAGES = {
|
||||
NETWORK_ERROR: '네트워크 연결을 확인해주세요.',
|
||||
SERVER_ERROR: '서버에서 오류가 발생했습니다.',
|
||||
UNAUTHORIZED: '로그인이 필요합니다.',
|
||||
FORBIDDEN: '접근 권한이 없습니다.',
|
||||
NOT_FOUND: '요청한 데이터를 찾을 수 없습니다.',
|
||||
VALIDATION_ERROR: '입력 정보를 확인해주세요.',
|
||||
FILE_SIZE_ERROR: '파일 크기가 너무 큽니다.',
|
||||
FILE_TYPE_ERROR: '지원하지 않는 파일 형식입니다.'
|
||||
}
|
||||
|
||||
// 성공 메시지
|
||||
export const SUCCESS_MESSAGES = {
|
||||
SAVE_SUCCESS: '성공적으로 저장되었습니다.',
|
||||
UPDATE_SUCCESS: '성공적으로 수정되었습니다.',
|
||||
DELETE_SUCCESS: '성공적으로 삭제되었습니다.',
|
||||
UPLOAD_SUCCESS: '파일이 성공적으로 업로드되었습니다.',
|
||||
CONTENT_GENERATED: 'AI 콘텐츠가 성공적으로 생성되었습니다.',
|
||||
LOGIN_SUCCESS: '로그인되었습니다.',
|
||||
LOGOUT_SUCCESS: '로그아웃되었습니다.'
|
||||
}
|
||||
345
src/utils/formatters.js
Normal file
345
src/utils/formatters.js
Normal file
@ -0,0 +1,345 @@
|
||||
/**
|
||||
* 데이터 포맷팅 유틸리티
|
||||
*
|
||||
* @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', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
})
|
||||
|
||||
if (showSymbol) {
|
||||
return formatter.format(amount)
|
||||
} else {
|
||||
return amount.toLocaleString('ko-KR')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 큰 숫자를 축약된 형태로 포맷팅 (예: 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* 퍼센트를 포맷팅
|
||||
* @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)}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜를 한국어 형식으로 포맷팅
|
||||
* @param {Date|string} date - 포맷팅할 날짜
|
||||
* @param {string} format - 포맷 형식 ('full', 'date', 'time', 'datetime', 'relative')
|
||||
* @returns {string} 포맷팅된 날짜 문자열
|
||||
*/
|
||||
export const formatDate = (date, format = '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))
|
||||
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 크기를 사람이 읽기 쉬운 형태로 포맷팅
|
||||
* @param {number} bytes - 파일 크기 (바이트)
|
||||
* @param {number} decimals - 소수점 자릿수 (기본값: 2)
|
||||
* @returns {string} 포맷팅된 파일 크기 문자열
|
||||
*/
|
||||
export const formatFileSize = (bytes, decimals = 2) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
if (!bytes) return ''
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 전화번호를 포맷팅
|
||||
* @param {string} phoneNumber - 전화번호 문자열
|
||||
* @returns {string} 포맷팅된 전화번호
|
||||
*/
|
||||
export const formatPhoneNumber = (phoneNumber) => {
|
||||
if (!phoneNumber) return ''
|
||||
|
||||
// 숫자만 추출
|
||||
const numbers = phoneNumber.replace(/\D/g, '')
|
||||
|
||||
// 휴대폰 번호 (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 (numbers.length === 11 && !numbers.startsWith('010')) {
|
||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`
|
||||
}
|
||||
|
||||
return phoneNumber
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업자등록번호를 포맷팅
|
||||
* @param {string} businessNumber - 사업자등록번호
|
||||
* @returns {string} 포맷팅된 사업자등록번호
|
||||
*/
|
||||
export const formatBusinessNumber = (businessNumber) => {
|
||||
if (!businessNumber) return ''
|
||||
|
||||
const numbers = businessNumber.replace(/\D/g, '')
|
||||
|
||||
if (numbers.length === 10) {
|
||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 5)}-${numbers.slice(5)}`
|
||||
}
|
||||
|
||||
return businessNumber
|
||||
}
|
||||
|
||||
/**
|
||||
* 텍스트를 지정된 길이로 자르고 말줄임표 추가
|
||||
* @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
|
||||
}
|
||||
|
||||
return text.slice(0, maxLength - suffix.length) + suffix
|
||||
}
|
||||
|
||||
/**
|
||||
* 해시태그 배열을 문자열로 포맷팅
|
||||
* @param {Array} hashtags - 해시태그 배열
|
||||
* @param {string} separator - 구분자 (기본값: ' ')
|
||||
* @returns {string} 포맷팅된 해시태그 문자열
|
||||
*/
|
||||
export const formatHashtags = (hashtags, separator = ' ') => {
|
||||
if (!Array.isArray(hashtags) || hashtags.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return hashtags
|
||||
.filter(tag => tag && tag.trim())
|
||||
.map(tag => tag.startsWith('#') ? tag : `#${tag}`)
|
||||
.join(separator)
|
||||
}
|
||||
|
||||
/**
|
||||
* 주소를 간단한 형태로 포맷팅
|
||||
* @param {string} fullAddress - 전체 주소
|
||||
* @param {boolean} showDetail - 상세 주소 표시 여부 (기본값: false)
|
||||
* @returns {string} 포맷팅된 주소
|
||||
*/
|
||||
export const formatAddress = (fullAddress, showDetail = false) => {
|
||||
if (!fullAddress) return ''
|
||||
|
||||
const parts = fullAddress.split(' ')
|
||||
|
||||
if (showDetail) {
|
||||
return fullAddress
|
||||
}
|
||||
|
||||
// 시/도, 구/군만 표시
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0]} ${parts[1]}`
|
||||
}
|
||||
|
||||
return fullAddress
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열을 문자열로 포맷팅
|
||||
* @param {Array} array - 포맷팅할 배열
|
||||
* @param {string} separator - 구분자 (기본값: ', ')
|
||||
* @param {number} maxItems - 최대 표시 항목 수
|
||||
* @returns {string} 포맷팅된 문자열
|
||||
*/
|
||||
export const formatArrayToString = (array, separator = ', ', maxItems = null) => {
|
||||
if (!Array.isArray(array) || array.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const items = maxItems ? array.slice(0, maxItems) : array
|
||||
const result = items.join(separator)
|
||||
|
||||
if (maxItems && array.length > maxItems) {
|
||||
const remaining = array.length - maxItems
|
||||
return `${result} 외 ${remaining}개`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
517
src/utils/validators.js
Normal file
517
src/utils/validators.js
Normal file
@ -0,0 +1,517 @@
|
||||
/**
|
||||
* 유효성 검증 유틸리티
|
||||
*
|
||||
* @description 다양한 입력값의 유효성을 검증하는 함수들을 제공합니다.
|
||||
* @author AI Marketing Team
|
||||
* @version 1.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* 필수 입력값 검증
|
||||
* @param {any} value - 검증할 값
|
||||
* @param {string} fieldName - 필드명 (에러 메시지용)
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateRequired = (value, fieldName = '필드') => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return `${fieldName}은(는) 필수 입력항목입니다.`
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
return `${fieldName}을(를) 입력해주세요.`
|
||||
}
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return `${fieldName}을(를) 선택해주세요.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 이메일 형식 검증
|
||||
* @param {string} email - 검증할 이메일
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateEmail = (email) => {
|
||||
if (!email) return '이메일을 입력해주세요.'
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
return '올바른 이메일 형식이 아닙니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 강도 검증
|
||||
* @param {string} password - 검증할 비밀번호
|
||||
* @param {Object} options - 검증 옵션
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validatePassword = (password, options = {}) => {
|
||||
const {
|
||||
minLength = 8,
|
||||
maxLength = 50,
|
||||
requireUppercase = true,
|
||||
requireLowercase = true,
|
||||
requireNumbers = true,
|
||||
requireSpecialChars = true
|
||||
} = options
|
||||
|
||||
if (!password) {
|
||||
return '비밀번호를 입력해주세요.'
|
||||
}
|
||||
|
||||
if (password.length < minLength) {
|
||||
return `비밀번호는 최소 ${minLength}자 이상이어야 합니다.`
|
||||
}
|
||||
|
||||
if (password.length > maxLength) {
|
||||
return `비밀번호는 최대 ${maxLength}자까지 가능합니다.`
|
||||
}
|
||||
|
||||
if (requireUppercase && !/[A-Z]/.test(password)) {
|
||||
return '비밀번호에 대문자가 포함되어야 합니다.'
|
||||
}
|
||||
|
||||
if (requireLowercase && !/[a-z]/.test(password)) {
|
||||
return '비밀번호에 소문자가 포함되어야 합니다.'
|
||||
}
|
||||
|
||||
if (requireNumbers && !/\d/.test(password)) {
|
||||
return '비밀번호에 숫자가 포함되어야 합니다.'
|
||||
}
|
||||
|
||||
if (requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
return '비밀번호에 특수문자가 포함되어야 합니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 확인 검증
|
||||
* @param {string} password - 원본 비밀번호
|
||||
* @param {string} confirmPassword - 확인 비밀번호
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validatePasswordConfirm = (password, confirmPassword) => {
|
||||
if (!confirmPassword) {
|
||||
return '비밀번호 확인을 입력해주세요.'
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return '비밀번호가 일치하지 않습니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 전화번호 형식 검증
|
||||
* @param {string} phoneNumber - 검증할 전화번호
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validatePhoneNumber = (phoneNumber) => {
|
||||
if (!phoneNumber) {
|
||||
return '전화번호를 입력해주세요.'
|
||||
}
|
||||
|
||||
// 숫자만 추출
|
||||
const numbers = phoneNumber.replace(/\D/g, '')
|
||||
|
||||
// 휴대폰 번호 (010-xxxx-xxxx)
|
||||
const mobileRegex = /^010\d{8}$/
|
||||
// 일반 전화번호 (02-xxx-xxxx, 03x-xxx-xxxx 등)
|
||||
const landlineRegex = /^(02|0[3-9]\d)\d{7,8}$/
|
||||
|
||||
if (!mobileRegex.test(numbers) && !landlineRegex.test(numbers)) {
|
||||
return '올바른 전화번호 형식이 아닙니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 사업자등록번호 검증
|
||||
* @param {string} businessNumber - 검증할 사업자등록번호
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateBusinessNumber = (businessNumber) => {
|
||||
if (!businessNumber) {
|
||||
return '사업자등록번호를 입력해주세요.'
|
||||
}
|
||||
|
||||
const numbers = businessNumber.replace(/\D/g, '')
|
||||
|
||||
if (numbers.length !== 10) {
|
||||
return '사업자등록번호는 10자리 숫자여야 합니다.'
|
||||
}
|
||||
|
||||
// 사업자등록번호 체크섬 검증
|
||||
const checkSum = [1, 3, 7, 1, 3, 7, 1, 3, 5]
|
||||
let sum = 0
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
sum += parseInt(numbers[i]) * checkSum[i]
|
||||
}
|
||||
|
||||
sum += parseInt((parseInt(numbers[8]) * 5) / 10)
|
||||
const remainder = (10 - (sum % 10)) % 10
|
||||
|
||||
if (remainder !== parseInt(numbers[9])) {
|
||||
return '올바르지 않은 사업자등록번호입니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자열 길이 검증
|
||||
* @param {string} value - 검증할 문자열
|
||||
* @param {number} minLength - 최소 길이
|
||||
* @param {number} maxLength - 최대 길이
|
||||
* @param {string} fieldName - 필드명
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateLength = (value, minLength, maxLength, fieldName = '입력값') => {
|
||||
if (!value) {
|
||||
return `${fieldName}을(를) 입력해주세요.`
|
||||
}
|
||||
|
||||
if (value.length < minLength) {
|
||||
return `${fieldName}은(는) 최소 ${minLength}자 이상이어야 합니다.`
|
||||
}
|
||||
|
||||
if (value.length > maxLength) {
|
||||
return `${fieldName}은(는) 최대 ${maxLength}자까지 가능합니다.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 숫자 범위 검증
|
||||
* @param {number} value - 검증할 숫자
|
||||
* @param {number} min - 최소값
|
||||
* @param {number} max - 최대값
|
||||
* @param {string} fieldName - 필드명
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateNumberRange = (value, min, max, fieldName = '숫자') => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return `${fieldName}을(를) 입력해주세요.`
|
||||
}
|
||||
|
||||
const numValue = Number(value)
|
||||
|
||||
if (isNaN(numValue)) {
|
||||
return `${fieldName}은(는) 숫자여야 합니다.`
|
||||
}
|
||||
|
||||
if (numValue < min) {
|
||||
return `${fieldName}은(는) ${min} 이상이어야 합니다.`
|
||||
}
|
||||
|
||||
if (numValue > max) {
|
||||
return `${fieldName}은(는) ${max} 이하여야 합니다.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 양의 정수 검증
|
||||
* @param {any} value - 검증할 값
|
||||
* @param {string} fieldName - 필드명
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validatePositiveInteger = (value, fieldName = '숫자') => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return `${fieldName}을(를) 입력해주세요.`
|
||||
}
|
||||
|
||||
const numValue = Number(value)
|
||||
|
||||
if (isNaN(numValue) || !Number.isInteger(numValue) || numValue <= 0) {
|
||||
return `${fieldName}은(는) 양의 정수여야 합니다.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 크기 검증
|
||||
* @param {File} file - 검증할 파일
|
||||
* @param {number} maxSize - 최대 크기 (바이트)
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateFileSize = (file, maxSize) => {
|
||||
if (!file) {
|
||||
return '파일을 선택해주세요.'
|
||||
}
|
||||
|
||||
if (file.size > maxSize) {
|
||||
const maxSizeMB = Math.round(maxSize / (1024 * 1024))
|
||||
return `파일 크기는 ${maxSizeMB}MB 이하여야 합니다.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 형식 검증
|
||||
* @param {File} file - 검증할 파일
|
||||
* @param {Array} allowedTypes - 허용된 MIME 타입 배열
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateFileType = (file, allowedTypes) => {
|
||||
if (!file) {
|
||||
return '파일을 선택해주세요.'
|
||||
}
|
||||
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
const allowedExtensions = allowedTypes
|
||||
.map(type => type.split('/')[1])
|
||||
.join(', ')
|
||||
return `허용된 파일 형식: ${allowedExtensions}`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지 파일 검증
|
||||
* @param {File} file - 검증할 파일
|
||||
* @param {number} maxSize - 최대 크기 (기본값: 10MB)
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateImageFile = (file, maxSize = 10 * 1024 * 1024) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
||||
|
||||
const sizeValidation = validateFileSize(file, maxSize)
|
||||
if (sizeValidation !== true) return sizeValidation
|
||||
|
||||
const typeValidation = validateFileType(file, allowedTypes)
|
||||
if (typeValidation !== true) return typeValidation
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 형식 검증
|
||||
* @param {string} url - 검증할 URL
|
||||
* @param {boolean} requireProtocol - 프로토콜 필수 여부 (기본값: true)
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateUrl = (url, requireProtocol = true) => {
|
||||
if (!url) {
|
||||
return 'URL을 입력해주세요.'
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
|
||||
if (requireProtocol && !['http:', 'https:'].includes(urlObj.protocol)) {
|
||||
return 'http 또는 https 프로토콜을 사용해주세요.'
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
return '올바른 URL 형식이 아닙니다.'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 형식 검증
|
||||
* @param {string} dateString - 검증할 날짜 문자열
|
||||
* @param {string} format - 예상 형식 (기본값: 'YYYY-MM-DD')
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateDate = (dateString, format = 'YYYY-MM-DD') => {
|
||||
if (!dateString) {
|
||||
return '날짜를 입력해주세요.'
|
||||
}
|
||||
|
||||
const date = new Date(dateString)
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return '올바른 날짜 형식이 아닙니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜 범위 검증
|
||||
* @param {string} startDate - 시작 날짜
|
||||
* @param {string} endDate - 종료 날짜
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateDateRange = (startDate, endDate) => {
|
||||
if (!startDate || !endDate) {
|
||||
return '시작날짜와 종료날짜를 모두 입력해주세요.'
|
||||
}
|
||||
|
||||
const start = new Date(startDate)
|
||||
const end = new Date(endDate)
|
||||
|
||||
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||
return '올바른 날짜 형식이 아닙니다.'
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
return '시작날짜는 종료날짜보다 이전이어야 합니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 한국어만 허용하는 검증
|
||||
* @param {string} value - 검증할 문자열
|
||||
* @param {string} fieldName - 필드명
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateKoreanOnly = (value, fieldName = '입력값') => {
|
||||
if (!value) {
|
||||
return `${fieldName}을(를) 입력해주세요.`
|
||||
}
|
||||
|
||||
const koreanRegex = /^[가-힣\s]+$/
|
||||
|
||||
if (!koreanRegex.test(value)) {
|
||||
return `${fieldName}은(는) 한글만 입력 가능합니다.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 영문과 숫자만 허용하는 검증
|
||||
* @param {string} value - 검증할 문자열
|
||||
* @param {string} fieldName - 필드명
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateAlphanumeric = (value, fieldName = '입력값') => {
|
||||
if (!value) {
|
||||
return `${fieldName}을(를) 입력해주세요.`
|
||||
}
|
||||
|
||||
const alphanumericRegex = /^[a-zA-Z0-9]+$/
|
||||
|
||||
if (!alphanumericRegex.test(value)) {
|
||||
return `${fieldName}은(는) 영문과 숫자만 입력 가능합니다.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열의 최소/최대 항목 수 검증
|
||||
* @param {Array} array - 검증할 배열
|
||||
* @param {number} min - 최소 항목 수
|
||||
* @param {number} max - 최대 항목 수
|
||||
* @param {string} fieldName - 필드명
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateArrayLength = (array, min, max, fieldName = '항목') => {
|
||||
if (!Array.isArray(array)) {
|
||||
return `${fieldName}을(를) 선택해주세요.`
|
||||
}
|
||||
|
||||
if (array.length < min) {
|
||||
return `${fieldName}은(는) 최소 ${min}개 이상 선택해야 합니다.`
|
||||
}
|
||||
|
||||
if (array.length > max) {
|
||||
return `${fieldName}은(는) 최대 ${max}개까지 선택 가능합니다.`
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 해시태그 형식 검증
|
||||
* @param {string} hashtag - 검증할 해시태그
|
||||
* @returns {boolean|string} 유효하면 true, 유효하지 않으면 에러 메시지
|
||||
*/
|
||||
export const validateHashtag = (hashtag) => {
|
||||
if (!hashtag) {
|
||||
return '해시태그를 입력해주세요.'
|
||||
}
|
||||
|
||||
// #으로 시작하고 공백이 없어야 함
|
||||
const hashtagRegex = /^#[a-zA-Z가-힣0-9_]+$/
|
||||
|
||||
if (!hashtagRegex.test(hashtag)) {
|
||||
return '해시태그는 #으로 시작하고 공백 없이 작성해주세요.'
|
||||
}
|
||||
|
||||
if (hashtag.length > 50) {
|
||||
return '해시태그는 50자를 초과할 수 없습니다.'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 검증 규칙을 순차적으로 실행
|
||||
* @param {any} value - 검증할 값
|
||||
* @param {Array} validators - 검증 함수 배열
|
||||
* @returns {boolean|string} 모든 검증을 통과하면 true, 실패하면 첫 번째 에러 메시지
|
||||
*/
|
||||
export const validateMultiple = (value, validators) => {
|
||||
for (const validator of validators) {
|
||||
const result = validator(value)
|
||||
if (result !== true) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 객체의 여러 필드를 한번에 검증
|
||||
* @param {Object} data - 검증할 데이터 객체
|
||||
* @param {Object} rules - 검증 규칙 객체 { fieldName: [validators] }
|
||||
* @returns {Object} { isValid: boolean, errors: Object }
|
||||
*/
|
||||
export const validateForm = (data, rules) => {
|
||||
const errors = {}
|
||||
let isValid = true
|
||||
|
||||
for (const [fieldName, validators] of Object.entries(rules)) {
|
||||
const value = data[fieldName]
|
||||
const result = validateMultiple(value, validators)
|
||||
|
||||
if (result !== true) {
|
||||
errors[fieldName] = result
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid, errors }
|
||||
}
|
||||
|
||||
/**
|
||||
* 실시간 검증을 위한 디바운스된 검증 함수 생성
|
||||
* @param {Function} validator - 검증 함수
|
||||
* @param {number} delay - 지연 시간 (기본값: 300ms)
|
||||
* @returns {Function} 디바운스된 검증 함수
|
||||
*/
|
||||
export const createDebouncedValidator = (validator, delay = 300) => {
|
||||
let timeoutId
|
||||
|
||||
return (value, callback) => {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
const result = validator(value)
|
||||
callback(result)
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user