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 WEEKDAYS = ['일', '월', '화', '수', '목', '금', '토'];
|
||||||
|
|
||||||
/** 기본 페이지 크기 */
|
|
||||||
export const DEFAULT_PAGE_SIZE = 20;
|
|
||||||
|
|
||||||
/** 검색 결과 페이지 크기 */
|
/** 검색 결과 페이지 크기 */
|
||||||
export const SEARCH_LIMIT = 20;
|
export const SEARCH_LIMIT = 20;
|
||||||
|
|
||||||
/** API 기본 URL */
|
|
||||||
export const API_BASE_URL = '/api';
|
|
||||||
|
|
||||||
/** 캘린더 최소 년도 */
|
/** 캘린더 최소 년도 */
|
||||||
export const MIN_YEAR = 2017;
|
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';
|
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 useAuthStore } from './useAuthStore';
|
||||||
export { default as useScheduleStore } from './useScheduleStore';
|
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');
|
return dayjs().tz(TIMEZONE).format('YYYY-MM-DD');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* KST 기준 현재 시각
|
|
||||||
* @returns {dayjs.Dayjs} dayjs 객체
|
|
||||||
*/
|
|
||||||
export const nowKST = () => {
|
|
||||||
return dayjs().tz(TIMEZONE);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 날짜 문자열 포맷팅
|
* 날짜 문자열 포맷팅
|
||||||
* @param {string|Date} date - 날짜
|
* @param {string|Date} date - 날짜
|
||||||
|
|
@ -38,21 +30,6 @@ export const formatDate = (date, format = 'YYYY-MM-DD') => {
|
||||||
return dayjs(date).tz(TIMEZONE).format(format);
|
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
|
* @param {string|Date} date1
|
||||||
|
|
@ -75,24 +52,6 @@ export const isToday = (date) => {
|
||||||
return isSameDay(date, dayjs());
|
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. (요일))
|
* 전체 날짜 포맷 (YYYY. M. D. (요일))
|
||||||
* @param {string|Date} date - 날짜
|
* @param {string|Date} date - 날짜
|
||||||
|
|
|
||||||
|
|
@ -39,75 +39,6 @@ export const formatTime = (time) => {
|
||||||
return time.slice(0, 5);
|
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 - 쉼표로 구분된 크레딧 텍스트
|
* @param {string} text - 쉼표로 구분된 크레딧 텍스트
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,9 @@ export { cn } from './cn';
|
||||||
// 날짜 관련
|
// 날짜 관련
|
||||||
export {
|
export {
|
||||||
getTodayKST,
|
getTodayKST,
|
||||||
nowKST,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
parseDateKST,
|
|
||||||
isSameDay,
|
isSameDay,
|
||||||
isToday,
|
isToday,
|
||||||
isPast,
|
|
||||||
isFuture,
|
|
||||||
formatFullDate,
|
formatFullDate,
|
||||||
formatXDateTime,
|
formatXDateTime,
|
||||||
extractDate,
|
extractDate,
|
||||||
|
|
@ -26,11 +22,6 @@ export {
|
||||||
export {
|
export {
|
||||||
decodeHtmlEntities,
|
decodeHtmlEntities,
|
||||||
formatTime,
|
formatTime,
|
||||||
formatNumber,
|
|
||||||
formatViewCount,
|
|
||||||
formatFileSize,
|
|
||||||
formatDuration,
|
|
||||||
truncateText,
|
|
||||||
parseCredits,
|
parseCredits,
|
||||||
calculateTotalDuration,
|
calculateTotalDuration,
|
||||||
} from './format';
|
} from './format';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue