refactor: 미사용 코드 제거
삭제된 함수: - format.js: formatNumber, formatViewCount, formatFileSize, formatDuration, truncateText - date.js: nowKST, parseDateKST, isPast, isFuture 삭제된 상수: - constants: DEFAULT_PAGE_SIZE, API_BASE_URL 삭제된 hooks (8개, 모두 미사용): - useAlbumData, useCalendar, useLightbox, useMediaQuery - useMemberData, useScheduleData, useScheduleFiltering, useScheduleSearch 삭제된 stores: - useUIStore (미사용) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b314b70014
commit
21dde0fd35
15 changed files with 0 additions and 797 deletions
|
|
@ -22,15 +22,9 @@ export const TIMEZONE = 'Asia/Seoul';
|
|||
/** 요일 이름 */
|
||||
export const WEEKDAYS = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
|
||||
/** 기본 페이지 크기 */
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
/** 검색 결과 페이지 크기 */
|
||||
export const SEARCH_LIMIT = 20;
|
||||
|
||||
/** API 기본 URL */
|
||||
export const API_BASE_URL = '/api';
|
||||
|
||||
/** 캘린더 최소 년도 */
|
||||
export const MIN_YEAR = 2017;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,2 @@
|
|||
// 미디어 쿼리
|
||||
export { useMediaQuery, useIsMobile, useIsDesktop, useIsTablet } from './useMediaQuery';
|
||||
|
||||
// 멤버 데이터
|
||||
export { useMembers, useMemberDetail } from './useMemberData';
|
||||
|
||||
// 앨범 데이터
|
||||
export { useAlbums, useAlbumDetail, useAlbumGallery } from './useAlbumData';
|
||||
|
||||
// 스케줄 데이터
|
||||
export {
|
||||
useScheduleData,
|
||||
useScheduleDetail,
|
||||
useUpcomingSchedules,
|
||||
useCategories,
|
||||
} from './useScheduleData';
|
||||
|
||||
// 스케줄 검색
|
||||
export { useScheduleSearch } from './useScheduleSearch';
|
||||
|
||||
// 스케줄 필터링
|
||||
export { useScheduleFiltering, useCategoryCounts } from './useScheduleFiltering';
|
||||
|
||||
// 캘린더
|
||||
export { useCalendar } from './useCalendar';
|
||||
|
||||
// 라이트박스
|
||||
export { useLightbox } from './useLightbox';
|
||||
|
||||
// 토스트
|
||||
export { default as useToast } from './useToast';
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { albumApi } from '@/api';
|
||||
|
||||
/**
|
||||
* 앨범 목록 조회 훅
|
||||
*/
|
||||
export function useAlbums() {
|
||||
return useQuery({
|
||||
queryKey: ['albums'],
|
||||
queryFn: albumApi.getAlbums,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 앨범 상세 조회 훅
|
||||
* @param {string} title - 앨범 타이틀 또는 폴더명
|
||||
*/
|
||||
export function useAlbumDetail(title) {
|
||||
return useQuery({
|
||||
queryKey: ['album', title],
|
||||
queryFn: () => albumApi.getAlbumByTitle(title),
|
||||
enabled: !!title,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 앨범 갤러리 조회 훅
|
||||
* @param {string} title - 앨범 타이틀 또는 폴더명
|
||||
*/
|
||||
export function useAlbumGallery(title) {
|
||||
return useQuery({
|
||||
queryKey: ['album-gallery', title],
|
||||
queryFn: () => albumApi.getAlbumGallery(title),
|
||||
enabled: !!title,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { MIN_YEAR, WEEKDAYS, MONTH_NAMES } from '@/constants';
|
||||
import { getTodayKST } from '@/utils';
|
||||
|
||||
/**
|
||||
* 캘린더 훅
|
||||
* 날짜 선택, 월 이동 등 캘린더 로직 제공
|
||||
* @param {Date} initialDate - 초기 날짜
|
||||
*/
|
||||
export function useCalendar(initialDate = new Date()) {
|
||||
const [currentDate, setCurrentDate] = useState(initialDate);
|
||||
const [selectedDate, setSelectedDate] = useState(getTodayKST());
|
||||
|
||||
const year = currentDate.getFullYear();
|
||||
const month = currentDate.getMonth();
|
||||
|
||||
// 캘린더 데이터 계산
|
||||
const calendarData = useMemo(() => {
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const prevMonthDays = new Date(year, month, 0).getDate();
|
||||
|
||||
return {
|
||||
year,
|
||||
month,
|
||||
monthName: MONTH_NAMES[month],
|
||||
firstDay,
|
||||
daysInMonth,
|
||||
prevMonthDays,
|
||||
weekdays: WEEKDAYS,
|
||||
};
|
||||
}, [year, month]);
|
||||
|
||||
// 이전 월로 이동 가능 여부
|
||||
const canGoPrevMonth = !(year === MIN_YEAR && month === 0);
|
||||
|
||||
// 선택 날짜 업데이트 헬퍼
|
||||
const updateSelectedDate = useCallback((newDate) => {
|
||||
const today = new Date();
|
||||
if (
|
||||
newDate.getFullYear() === today.getFullYear() &&
|
||||
newDate.getMonth() === today.getMonth()
|
||||
) {
|
||||
setSelectedDate(getTodayKST());
|
||||
} else {
|
||||
const firstDay = `${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}-01`;
|
||||
setSelectedDate(firstDay);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 이전 월로 이동
|
||||
const goToPrevMonth = useCallback(() => {
|
||||
if (!canGoPrevMonth) return;
|
||||
const newDate = new Date(year, month - 1, 1);
|
||||
setCurrentDate(newDate);
|
||||
updateSelectedDate(newDate);
|
||||
}, [year, month, canGoPrevMonth, updateSelectedDate]);
|
||||
|
||||
// 다음 월로 이동
|
||||
const goToNextMonth = useCallback(() => {
|
||||
const newDate = new Date(year, month + 1, 1);
|
||||
setCurrentDate(newDate);
|
||||
updateSelectedDate(newDate);
|
||||
}, [year, month, updateSelectedDate]);
|
||||
|
||||
// 특정 월로 이동
|
||||
const goToMonth = useCallback(
|
||||
(newYear, newMonth) => {
|
||||
const newDate = new Date(newYear, newMonth, 1);
|
||||
setCurrentDate(newDate);
|
||||
updateSelectedDate(newDate);
|
||||
},
|
||||
[updateSelectedDate]
|
||||
);
|
||||
|
||||
// 오늘로 이동
|
||||
const goToToday = useCallback(() => {
|
||||
const today = new Date();
|
||||
setCurrentDate(today);
|
||||
setSelectedDate(getTodayKST());
|
||||
}, []);
|
||||
|
||||
// 날짜 선택
|
||||
const selectDate = useCallback(
|
||||
(day) => {
|
||||
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
setSelectedDate(dateStr);
|
||||
},
|
||||
[year, month]
|
||||
);
|
||||
|
||||
// 반환 객체 메모이제이션
|
||||
return useMemo(
|
||||
() => ({
|
||||
...calendarData,
|
||||
currentDate,
|
||||
selectedDate,
|
||||
canGoPrevMonth,
|
||||
goToPrevMonth,
|
||||
goToNextMonth,
|
||||
goToMonth,
|
||||
goToToday,
|
||||
selectDate,
|
||||
setSelectedDate,
|
||||
}),
|
||||
[
|
||||
calendarData,
|
||||
currentDate,
|
||||
selectedDate,
|
||||
canGoPrevMonth,
|
||||
goToPrevMonth,
|
||||
goToNextMonth,
|
||||
goToMonth,
|
||||
goToToday,
|
||||
selectDate,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* 라이트박스 상태 및 동작 관리 훅
|
||||
* @param {Object} options
|
||||
* @param {Array<{url: string, thumb_url?: string}|string>} options.images - 이미지 배열
|
||||
* @param {Function} options.onClose - 닫기 콜백 (optional)
|
||||
* @returns {Object} 라이트박스 상태 및 메서드
|
||||
*/
|
||||
export function useLightbox({ images = [], onClose } = {}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
// 라이트박스 열기
|
||||
const open = useCallback((index = 0) => {
|
||||
setCurrentIndex(index);
|
||||
setIsOpen(true);
|
||||
window.history.pushState({ lightbox: true }, '');
|
||||
}, []);
|
||||
|
||||
// 라이트박스 닫기
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
onClose?.();
|
||||
}, [onClose]);
|
||||
|
||||
// 이전 이미지
|
||||
const goToPrev = useCallback(() => {
|
||||
if (images.length <= 1) return;
|
||||
setCurrentIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1));
|
||||
}, [images.length]);
|
||||
|
||||
// 다음 이미지
|
||||
const goToNext = useCallback(() => {
|
||||
if (images.length <= 1) return;
|
||||
setCurrentIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0));
|
||||
}, [images.length]);
|
||||
|
||||
// 특정 인덱스로 이동
|
||||
const goToIndex = useCallback(
|
||||
(index) => {
|
||||
if (index >= 0 && index < images.length) {
|
||||
setCurrentIndex(index);
|
||||
}
|
||||
},
|
||||
[images.length]
|
||||
);
|
||||
|
||||
// 뒤로가기 처리
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handlePopState = () => {
|
||||
close();
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, [isOpen, close]);
|
||||
|
||||
// body 스크롤 방지 (Lightbox 컴포넌트에서 처리하므로 여기선 생략)
|
||||
|
||||
// 이미지 프리로딩
|
||||
useEffect(() => {
|
||||
if (!isOpen || images.length === 0) return;
|
||||
|
||||
// 현재, 이전, 다음 이미지 프리로드
|
||||
const indicesToPreload = [
|
||||
currentIndex,
|
||||
(currentIndex + 1) % images.length,
|
||||
(currentIndex - 1 + images.length) % images.length,
|
||||
];
|
||||
|
||||
indicesToPreload.forEach((index) => {
|
||||
const img = new Image();
|
||||
const imageItem = images[index];
|
||||
img.src = typeof imageItem === 'string' ? imageItem : imageItem?.url || imageItem;
|
||||
});
|
||||
}, [isOpen, currentIndex, images]);
|
||||
|
||||
// 이미지 다운로드
|
||||
const downloadImage = useCallback(async () => {
|
||||
const imageItem = images[currentIndex];
|
||||
const imageUrl = typeof imageItem === 'string' ? imageItem : imageItem?.url || imageItem;
|
||||
if (!imageUrl) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `image_${currentIndex + 1}.jpg`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('이미지 다운로드 실패:', error);
|
||||
}
|
||||
}, [images, currentIndex]);
|
||||
|
||||
// 현재 이미지 URL 가져오기
|
||||
const getCurrentImageUrl = useCallback(() => {
|
||||
const imageItem = images[currentIndex];
|
||||
return typeof imageItem === 'string' ? imageItem : imageItem?.url || imageItem;
|
||||
}, [images, currentIndex]);
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
currentIndex,
|
||||
currentImage: images[currentIndex],
|
||||
currentImageUrl: getCurrentImageUrl(),
|
||||
totalCount: images.length,
|
||||
open,
|
||||
close,
|
||||
goToPrev,
|
||||
goToNext,
|
||||
goToIndex,
|
||||
downloadImage,
|
||||
setCurrentIndex,
|
||||
};
|
||||
}
|
||||
|
||||
export default useLightbox;
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* 미디어 쿼리 훅
|
||||
* @param {string} query - CSS 미디어 쿼리 문자열
|
||||
* @returns {boolean} 쿼리 매칭 여부
|
||||
*/
|
||||
export function useMediaQuery(query) {
|
||||
const [matches, setMatches] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.matchMedia(query).matches;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 핸들러 메모이제이션
|
||||
const handler = useCallback((e) => {
|
||||
setMatches(e.matches);
|
||||
}, []);
|
||||
|
||||
// DOM 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia(query);
|
||||
setMatches(mediaQuery.matches);
|
||||
|
||||
mediaQuery.addEventListener('change', handler);
|
||||
return () => mediaQuery.removeEventListener('change', handler);
|
||||
}, [query, handler]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모바일 여부 확인 훅
|
||||
*/
|
||||
export const useIsMobile = () => useMediaQuery('(max-width: 768px)');
|
||||
|
||||
/**
|
||||
* 데스크탑 여부 확인 훅
|
||||
*/
|
||||
export const useIsDesktop = () => useMediaQuery('(min-width: 769px)');
|
||||
|
||||
/**
|
||||
* 태블릿 여부 확인 훅
|
||||
*/
|
||||
export const useIsTablet = () => useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { memberApi } from '@/api';
|
||||
|
||||
/**
|
||||
* 멤버 목록 조회 훅
|
||||
*/
|
||||
export function useMembers() {
|
||||
return useQuery({
|
||||
queryKey: ['members'],
|
||||
queryFn: memberApi.getMembers,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 멤버 상세 조회 훅
|
||||
* @param {number} id - 멤버 ID
|
||||
*/
|
||||
export function useMemberDetail(id) {
|
||||
return useQuery({
|
||||
queryKey: ['member', id],
|
||||
queryFn: () => memberApi.getMemberById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { scheduleApi } from '@/api';
|
||||
|
||||
/**
|
||||
* 스케줄 목록 조회 훅 (월별)
|
||||
* @param {number} year - 년도
|
||||
* @param {number} month - 월 (1-12)
|
||||
*/
|
||||
export function useScheduleData(year, month) {
|
||||
return useQuery({
|
||||
queryKey: ['schedules', year, month],
|
||||
queryFn: () => scheduleApi.getSchedules(year, month),
|
||||
enabled: !!year && !!month,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄 상세 조회 훅
|
||||
* @param {number} id - 스케줄 ID
|
||||
*/
|
||||
export function useScheduleDetail(id) {
|
||||
return useQuery({
|
||||
queryKey: ['schedule', id],
|
||||
queryFn: () => scheduleApi.getSchedule(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 다가오는 스케줄 조회 훅
|
||||
* @param {number} limit - 조회 개수
|
||||
*/
|
||||
export function useUpcomingSchedules(limit = 3) {
|
||||
return useQuery({
|
||||
queryKey: ['schedules', 'upcoming', limit],
|
||||
queryFn: () => scheduleApi.getUpcomingSchedules(limit),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리 목록 조회 훅
|
||||
*/
|
||||
export function useCategories() {
|
||||
return useQuery({
|
||||
queryKey: ['categories'],
|
||||
queryFn: scheduleApi.getCategories,
|
||||
staleTime: 1000 * 60 * 10, // 10분 캐시
|
||||
});
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { formatDate } from '@/utils';
|
||||
import { getCategoryId, getScheduleDate, isBirthdaySchedule } from '@/utils/schedule';
|
||||
|
||||
/**
|
||||
* 스케줄 필터링 훅
|
||||
* @param {Array} schedules - 스케줄 배열
|
||||
* @param {object} options - 필터 옵션
|
||||
* @param {number[]} options.categoryIds - 선택된 카테고리 ID 배열
|
||||
* @param {string} options.selectedDate - 선택된 날짜 (YYYY-MM-DD)
|
||||
* @param {boolean} options.birthdayFirst - 생일 우선 정렬 여부
|
||||
* @returns {Array} 필터링된 스케줄 배열
|
||||
*/
|
||||
export function useScheduleFiltering(schedules, options = {}) {
|
||||
const {
|
||||
categoryIds = [],
|
||||
selectedDate = null,
|
||||
birthdayFirst = true,
|
||||
} = options;
|
||||
|
||||
return useMemo(() => {
|
||||
if (!schedules || schedules.length === 0) return [];
|
||||
|
||||
let result = schedules.filter((schedule) => {
|
||||
// 카테고리 필터
|
||||
if (categoryIds.length > 0) {
|
||||
const catId = getCategoryId(schedule);
|
||||
if (!categoryIds.includes(catId)) return false;
|
||||
}
|
||||
|
||||
// 날짜 필터
|
||||
if (selectedDate) {
|
||||
const scheduleDate = getScheduleDate(schedule);
|
||||
if (scheduleDate !== selectedDate) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 생일 우선 정렬
|
||||
if (birthdayFirst) {
|
||||
result = [...result].sort((a, b) => {
|
||||
const aIsBirthday = isBirthdaySchedule(a);
|
||||
const bIsBirthday = isBirthdaySchedule(b);
|
||||
if (aIsBirthday && !bIsBirthday) return -1;
|
||||
if (!aIsBirthday && bIsBirthday) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [schedules, categoryIds, selectedDate, birthdayFirst]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리별 개수 계산 훅
|
||||
* @param {Array} schedules - 스케줄 배열
|
||||
* @returns {object} 카테고리 ID별 개수 객체
|
||||
*/
|
||||
export function useCategoryCounts(schedules) {
|
||||
return useMemo(() => {
|
||||
if (!schedules || schedules.length === 0) return {};
|
||||
|
||||
const counts = {};
|
||||
for (const schedule of schedules) {
|
||||
const categoryId = getCategoryId(schedule);
|
||||
counts[categoryId] = (counts[categoryId] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}, [schedules]);
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
import { fetchApi } from '@/api';
|
||||
import { SEARCH_LIMIT } from '@/constants';
|
||||
|
||||
/**
|
||||
* 스케줄 검색 훅
|
||||
* 무한 스크롤, 검색어 추천 지원
|
||||
*/
|
||||
export function useScheduleSearch() {
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [isSearchMode, setIsSearchMode] = useState(false);
|
||||
|
||||
// 검색 결과 (무한 스크롤)
|
||||
const searchQuery = useInfiniteQuery({
|
||||
queryKey: ['scheduleSearch', searchTerm],
|
||||
queryFn: async ({ pageParam = 0, signal }) => {
|
||||
const response = await fetch(
|
||||
`/api/schedules?search=${encodeURIComponent(searchTerm)}&offset=${pageParam}&limit=${SEARCH_LIMIT}`,
|
||||
{ signal }
|
||||
);
|
||||
if (!response.ok) throw new Error('Search failed');
|
||||
return response.json();
|
||||
},
|
||||
getNextPageParam: (lastPage) => {
|
||||
return lastPage.hasMore
|
||||
? lastPage.offset + lastPage.schedules.length
|
||||
: undefined;
|
||||
},
|
||||
enabled: !!searchTerm && isSearchMode,
|
||||
});
|
||||
|
||||
// 검색 결과 평탄화
|
||||
const searchResults = useMemo(() => {
|
||||
return searchQuery.data?.pages?.flatMap((page) => page.schedules) || [];
|
||||
}, [searchQuery.data]);
|
||||
|
||||
const searchTotal = searchQuery.data?.pages?.[0]?.total || 0;
|
||||
|
||||
// 검색 실행
|
||||
const executeSearch = () => {
|
||||
if (searchInput.trim()) {
|
||||
setSearchTerm(searchInput.trim());
|
||||
setIsSearchMode(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 검색 초기화
|
||||
const clearSearch = () => {
|
||||
setSearchInput('');
|
||||
setSearchTerm('');
|
||||
setIsSearchMode(false);
|
||||
};
|
||||
|
||||
return {
|
||||
// 상태
|
||||
searchInput,
|
||||
searchTerm,
|
||||
isSearchMode,
|
||||
searchResults,
|
||||
searchTotal,
|
||||
isLoading: searchQuery.isLoading,
|
||||
isFetching: searchQuery.isFetching,
|
||||
hasNextPage: searchQuery.hasNextPage,
|
||||
isFetchingNextPage: searchQuery.isFetchingNextPage,
|
||||
|
||||
// 액션
|
||||
setSearchInput,
|
||||
setIsSearchMode,
|
||||
executeSearch,
|
||||
clearSearch,
|
||||
fetchNextPage: searchQuery.fetchNextPage,
|
||||
};
|
||||
}
|
||||
|
|
@ -3,4 +3,3 @@
|
|||
*/
|
||||
export { default as useAuthStore } from './useAuthStore';
|
||||
export { default as useScheduleStore } from './useScheduleStore';
|
||||
export { default as useUIStore } from './useUIStore';
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* UI 상태 스토어
|
||||
* 모달, 토스트, 사이드바 등 전역 UI 상태 관리
|
||||
*/
|
||||
const useUIStore = create((set, get) => ({
|
||||
// ===== 모달 =====
|
||||
modalOpen: false,
|
||||
modalContent: null,
|
||||
modalProps: {},
|
||||
|
||||
openModal: (content, props = {}) => {
|
||||
set({
|
||||
modalOpen: true,
|
||||
modalContent: content,
|
||||
modalProps: props,
|
||||
});
|
||||
},
|
||||
|
||||
closeModal: () => {
|
||||
set({
|
||||
modalOpen: false,
|
||||
modalContent: null,
|
||||
modalProps: {},
|
||||
});
|
||||
},
|
||||
|
||||
// ===== 토스트 =====
|
||||
toasts: [],
|
||||
|
||||
addToast: (message, type = 'info', duration = 3000) => {
|
||||
const id = Date.now();
|
||||
const toast = { id, message, type, duration };
|
||||
|
||||
set((state) => ({
|
||||
toasts: [...state.toasts, toast],
|
||||
}));
|
||||
|
||||
// 자동 제거
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
get().removeToast(id);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
removeToast: (id) => {
|
||||
set((state) => ({
|
||||
toasts: state.toasts.filter((t) => t.id !== id),
|
||||
}));
|
||||
},
|
||||
|
||||
clearToasts: () => set({ toasts: [] }),
|
||||
|
||||
// 편의 메서드
|
||||
showSuccess: (message, duration) => get().addToast(message, 'success', duration),
|
||||
showError: (message, duration) => get().addToast(message, 'error', duration),
|
||||
showWarning: (message, duration) => get().addToast(message, 'warning', duration),
|
||||
showInfo: (message, duration) => get().addToast(message, 'info', duration),
|
||||
|
||||
// ===== 사이드바 (모바일) =====
|
||||
sidebarOpen: false,
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
openSidebar: () => set({ sidebarOpen: true }),
|
||||
closeSidebar: () => set({ sidebarOpen: false }),
|
||||
|
||||
// ===== 라이트박스 =====
|
||||
lightboxOpen: false,
|
||||
lightboxImages: [],
|
||||
lightboxIndex: 0,
|
||||
|
||||
openLightbox: (images, index = 0) => {
|
||||
set({
|
||||
lightboxOpen: true,
|
||||
lightboxImages: Array.isArray(images) ? images : [images],
|
||||
lightboxIndex: index,
|
||||
});
|
||||
},
|
||||
|
||||
closeLightbox: () => {
|
||||
set({
|
||||
lightboxOpen: false,
|
||||
lightboxImages: [],
|
||||
lightboxIndex: 0,
|
||||
});
|
||||
},
|
||||
|
||||
setLightboxIndex: (index) => set({ lightboxIndex: index }),
|
||||
|
||||
// ===== 로딩 =====
|
||||
globalLoading: false,
|
||||
setGlobalLoading: (value) => set({ globalLoading: value }),
|
||||
}));
|
||||
|
||||
export default useUIStore;
|
||||
|
|
@ -19,14 +19,6 @@ export const getTodayKST = () => {
|
|||
return dayjs().tz(TIMEZONE).format('YYYY-MM-DD');
|
||||
};
|
||||
|
||||
/**
|
||||
* KST 기준 현재 시각
|
||||
* @returns {dayjs.Dayjs} dayjs 객체
|
||||
*/
|
||||
export const nowKST = () => {
|
||||
return dayjs().tz(TIMEZONE);
|
||||
};
|
||||
|
||||
/**
|
||||
* 날짜 문자열 포맷팅
|
||||
* @param {string|Date} date - 날짜
|
||||
|
|
@ -38,21 +30,6 @@ export const formatDate = (date, format = 'YYYY-MM-DD') => {
|
|||
return dayjs(date).tz(TIMEZONE).format(format);
|
||||
};
|
||||
|
||||
/**
|
||||
* 날짜에서 년, 월, 일, 요일 추출
|
||||
* @param {string|Date} date - 날짜
|
||||
* @returns {{ year: number, month: number, day: number, weekday: string }}
|
||||
*/
|
||||
export const parseDateKST = (date) => {
|
||||
const d = dayjs(date).tz(TIMEZONE);
|
||||
return {
|
||||
year: d.year(),
|
||||
month: d.month() + 1,
|
||||
day: d.date(),
|
||||
weekday: WEEKDAYS[d.day()],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 두 날짜 비교 (같은 날인지)
|
||||
* @param {string|Date} date1
|
||||
|
|
@ -75,24 +52,6 @@ export const isToday = (date) => {
|
|||
return isSameDay(date, dayjs());
|
||||
};
|
||||
|
||||
/**
|
||||
* 날짜가 과거인지 확인
|
||||
* @param {string|Date} date
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isPast = (date) => {
|
||||
return dayjs(date).tz(TIMEZONE).isBefore(dayjs().tz(TIMEZONE), 'day');
|
||||
};
|
||||
|
||||
/**
|
||||
* 날짜가 미래인지 확인
|
||||
* @param {string|Date} date
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isFuture = (date) => {
|
||||
return dayjs(date).tz(TIMEZONE).isAfter(dayjs().tz(TIMEZONE), 'day');
|
||||
};
|
||||
|
||||
/**
|
||||
* 전체 날짜 포맷 (YYYY. M. D. (요일))
|
||||
* @param {string|Date} date - 날짜
|
||||
|
|
|
|||
|
|
@ -39,75 +39,6 @@ export const formatTime = (time) => {
|
|||
return time.slice(0, 5);
|
||||
};
|
||||
|
||||
/**
|
||||
* 숫자에 천 단위 콤마 추가
|
||||
* @param {number} num - 숫자
|
||||
* @returns {string} 콤마가 추가된 문자열
|
||||
*/
|
||||
export const formatNumber = (num) => {
|
||||
if (num === null || num === undefined) return '0';
|
||||
return num.toLocaleString('ko-KR');
|
||||
};
|
||||
|
||||
/**
|
||||
* 조회수 포맷팅 (1만 이상이면 '만' 단위로)
|
||||
* @param {number} count - 조회수
|
||||
* @returns {string} 포맷된 조회수
|
||||
*/
|
||||
export const formatViewCount = (count) => {
|
||||
if (!count) return '0';
|
||||
if (count >= 10000) {
|
||||
return `${(count / 10000).toFixed(1)}만`;
|
||||
}
|
||||
return formatNumber(count);
|
||||
};
|
||||
|
||||
/**
|
||||
* 파일 크기 포맷팅
|
||||
* @param {number} bytes - 바이트 단위 크기
|
||||
* @returns {string} 포맷된 크기 (예: "1.5 MB")
|
||||
*/
|
||||
export const formatFileSize = (bytes) => {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 재생 시간 포맷팅 (초 -> MM:SS 또는 HH:MM:SS)
|
||||
* @param {number} seconds - 초 단위 시간
|
||||
* @returns {string} 포맷된 시간
|
||||
*/
|
||||
export const formatDuration = (seconds) => {
|
||||
if (!seconds) return '0:00';
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
return `${minutes}:${String(secs).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 텍스트 말줄임 처리
|
||||
* @param {string} text - 원본 텍스트
|
||||
* @param {number} maxLength - 최대 길이
|
||||
* @returns {string} 말줄임 처리된 텍스트
|
||||
*/
|
||||
export const truncateText = (text, maxLength) => {
|
||||
if (!text) return '';
|
||||
if (text.length <= maxLength) return text;
|
||||
return `${text.slice(0, maxLength)}...`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 크레딧 텍스트를 배열로 분리
|
||||
* @param {string} text - 쉼표로 구분된 크레딧 텍스트
|
||||
|
|
|
|||
|
|
@ -8,13 +8,9 @@ export { cn } from './cn';
|
|||
// 날짜 관련
|
||||
export {
|
||||
getTodayKST,
|
||||
nowKST,
|
||||
formatDate,
|
||||
parseDateKST,
|
||||
isSameDay,
|
||||
isToday,
|
||||
isPast,
|
||||
isFuture,
|
||||
formatFullDate,
|
||||
formatXDateTime,
|
||||
extractDate,
|
||||
|
|
@ -26,11 +22,6 @@ export {
|
|||
export {
|
||||
decodeHtmlEntities,
|
||||
formatTime,
|
||||
formatNumber,
|
||||
formatViewCount,
|
||||
formatFileSize,
|
||||
formatDuration,
|
||||
truncateText,
|
||||
parseCredits,
|
||||
calculateTotalDuration,
|
||||
} from './format';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue