refactor: Critical 코드 품질 개선

- useAdminAuth: useRef로 logout 함수 안정화하여 무한 루프 방지
- useAdminAuth/useRedirectIfAuthenticated: queryKey 충돌 해결
- useAuthStore: 미사용 getToken, checkAuth 메서드 제거
- useUIStore: 미사용 confirmDialog 관련 코드 제거
- 카드 컴포넌트 React.memo 적용 (PC/Mobile ScheduleCard, BirthdayCard 등)
- 접근성(a11y) 개선: aria-label, role 속성 추가
  - Toast: role="alert", aria-live="polite"
  - Lightbox: role="dialog", aria-modal, aria-label
  - Calendar: 버튼 aria-label, aria-pressed, aria-expanded
  - LightboxIndicator: aria-label, aria-current

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-22 12:33:26 +09:00
parent 72b3800ce7
commit d9b8e67b9a
14 changed files with 83 additions and 70 deletions

View file

@ -84,6 +84,9 @@ function Lightbox({ images, currentIndex, isOpen, onClose, onIndexChange }) {
<AnimatePresence> <AnimatePresence>
{isOpen && images.length > 0 && ( {isOpen && images.length > 0 && (
<motion.div <motion.div
role="dialog"
aria-modal="true"
aria-label="이미지 뷰어"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
@ -96,22 +99,24 @@ function Lightbox({ images, currentIndex, isOpen, onClose, onIndexChange }) {
<div className="min-w-[800px] min-h-[600px] w-full h-full relative flex items-center justify-center"> <div className="min-w-[800px] min-h-[600px] w-full h-full relative flex items-center justify-center">
{/* 닫기 버튼 */} {/* 닫기 버튼 */}
<button <button
aria-label="닫기"
className="absolute top-6 right-6 text-white/70 hover:text-white transition-colors z-10" className="absolute top-6 right-6 text-white/70 hover:text-white transition-colors z-10"
onClick={onClose} onClick={onClose}
> >
<X size={32} /> <X size={32} aria-hidden="true" />
</button> </button>
{/* 이전 버튼 */} {/* 이전 버튼 */}
{images.length > 1 && ( {images.length > 1 && (
<button <button
aria-label="이전 이미지"
className="absolute left-6 p-2 text-white/70 hover:text-white transition-colors z-10" className="absolute left-6 p-2 text-white/70 hover:text-white transition-colors z-10"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
goToPrev(); goToPrev();
}} }}
> >
<ChevronLeft size={48} /> <ChevronLeft size={48} aria-hidden="true" />
</button> </button>
)} )}
@ -142,13 +147,14 @@ function Lightbox({ images, currentIndex, isOpen, onClose, onIndexChange }) {
{/* 다음 버튼 */} {/* 다음 버튼 */}
{images.length > 1 && ( {images.length > 1 && (
<button <button
aria-label="다음 이미지"
className="absolute right-6 p-2 text-white/70 hover:text-white transition-colors z-10" className="absolute right-6 p-2 text-white/70 hover:text-white transition-colors z-10"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
goToNext(); goToNext();
}} }}
> >
<ChevronRight size={48} /> <ChevronRight size={48} aria-hidden="true" />
</button> </button>
)} )}

View file

@ -39,6 +39,8 @@ const LightboxIndicator = memo(function LightboxIndicator({
{Array.from({ length: count }).map((_, i) => ( {Array.from({ length: count }).map((_, i) => (
<button <button
key={i} key={i}
aria-label={`이미지 ${i + 1}/${count}`}
aria-current={i === currentIndex ? 'true' : undefined}
className={`rounded-full flex-shrink-0 transition-all duration-300 ${ className={`rounded-full flex-shrink-0 transition-all duration-300 ${
i === currentIndex i === currentIndex
? 'w-3 h-3 bg-white' ? 'w-3 h-3 bg-white'

View file

@ -10,6 +10,8 @@ function Toast({ toast, onClose }) {
<AnimatePresence> <AnimatePresence>
{toast && ( {toast && (
<motion.div <motion.div
role="alert"
aria-live="polite"
initial={{ opacity: 0, y: 50 }} initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 50 }} exit={{ opacity: 0, y: 50 }}

View file

@ -1,3 +1,4 @@
import { memo } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { dayjs, decodeHtmlEntities } from '@/utils'; import { dayjs, decodeHtmlEntities } from '@/utils';
@ -8,7 +9,7 @@ import { dayjs, decodeHtmlEntities } from '@/utils';
* @param {number} delay - 애니메이션 딜레이 () * @param {number} delay - 애니메이션 딜레이 ()
* @param {function} onClick - 클릭 핸들러 * @param {function} onClick - 클릭 핸들러
*/ */
function BirthdayCard({ schedule, showYear = false, delay = 0, onClick }) { const BirthdayCard = memo(function BirthdayCard({ schedule, showYear = false, delay = 0, onClick }) {
const scheduleDate = dayjs(schedule.date); const scheduleDate = dayjs(schedule.date);
const formatted = { const formatted = {
year: scheduleDate.year(), year: scheduleDate.year(),
@ -79,6 +80,6 @@ function BirthdayCard({ schedule, showYear = false, delay = 0, onClick }) {
{CardContent} {CardContent}
</div> </div>
); );
} });
export default BirthdayCard; export default BirthdayCard;

View file

@ -204,7 +204,13 @@ function Calendar({
const daySchedules = item.isCurrentMonth ? getDaySchedules(item.date) : []; const daySchedules = item.isCurrentMonth ? getDaySchedules(item.date) : [];
return ( return (
<button key={index} onClick={() => onSelectDate(item.date)} className="flex flex-col items-center py-2"> <button
key={index}
aria-label={`${item.date.getMonth() + 1}${item.day}${isToday(item.date) ? ' (오늘)' : ''}${daySchedules.length > 0 ? `, 일정 ${daySchedules.length}` : ''}`}
aria-pressed={isSelected(item.date)}
onClick={() => onSelectDate(item.date)}
className="flex flex-col items-center py-2"
>
<span <span
className={`w-9 h-9 flex items-center justify-center text-sm font-medium rounded-full transition-all ${ className={`w-9 h-9 flex items-center justify-center text-sm font-medium rounded-full transition-all ${
!item.isCurrentMonth !item.isCurrentMonth
@ -259,19 +265,20 @@ function Calendar({
{/* 년도 범위 헤더 */} {/* 년도 범위 헤더 */}
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<button <button
aria-label="이전 연도 범위"
onClick={() => onClick={() =>
canGoPrevYearRange && setYearRangeStart(Math.max(MIN_YEAR, yearRangeStart - 12)) canGoPrevYearRange && setYearRangeStart(Math.max(MIN_YEAR, yearRangeStart - 12))
} }
disabled={!canGoPrevYearRange} disabled={!canGoPrevYearRange}
className={`p-1 ${canGoPrevYearRange ? '' : 'opacity-30'}`} className={`p-1 ${canGoPrevYearRange ? '' : 'opacity-30'}`}
> >
<ChevronLeft size={18} /> <ChevronLeft size={18} aria-hidden="true" />
</button> </button>
<span className="font-semibold text-sm"> <span className="font-semibold text-sm">
{yearRangeStart} - {yearRangeStart + 11} {yearRangeStart} - {yearRangeStart + 11}
</span> </span>
<button onClick={() => setYearRangeStart(yearRangeStart + 12)} className="p-1"> <button aria-label="다음 연도 범위" onClick={() => setYearRangeStart(yearRangeStart + 12)} className="p-1">
<ChevronRight size={18} /> <ChevronRight size={18} aria-hidden="true" />
</button> </button>
</div> </div>
@ -283,6 +290,8 @@ function Calendar({
return ( return (
<button <button
key={y} key={y}
aria-label={`${y}년 선택`}
aria-pressed={y === year}
onClick={() => { onClick={() => {
const newDate = new Date(viewDate); const newDate = new Date(viewDate);
newDate.setFullYear(y); newDate.setFullYear(y);
@ -311,6 +320,8 @@ function Calendar({
return ( return (
<button <button
key={m} key={m}
aria-label={`${m}월 선택`}
aria-pressed={m === month + 1}
onClick={() => { onClick={() => {
const newDate = new Date(year, m - 1, 1); const newDate = new Date(year, m - 1, 1);
setViewDate(newDate); setViewDate(newDate);
@ -342,21 +353,24 @@ function Calendar({
{!hideHeader && ( {!hideHeader && (
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<button <button
aria-label="이전 달"
onClick={() => changeMonth(-1)} onClick={() => changeMonth(-1)}
disabled={!canGoPrevMonth} disabled={!canGoPrevMonth}
className={`p-1 ${!canGoPrevMonth ? 'opacity-30' : ''}`} className={`p-1 ${!canGoPrevMonth ? 'opacity-30' : ''}`}
> >
<ChevronLeft size={18} /> <ChevronLeft size={18} aria-hidden="true" />
</button> </button>
<button <button
aria-label="년/월 선택"
aria-expanded={showYearMonth}
onClick={() => setShowYearMonth(true)} onClick={() => setShowYearMonth(true)}
className="flex items-center gap-1 font-semibold text-sm hover:text-primary transition-colors" className="flex items-center gap-1 font-semibold text-sm hover:text-primary transition-colors"
> >
{year} {month + 1} {year} {month + 1}
<ChevronDown size={16} /> <ChevronDown size={16} aria-hidden="true" />
</button> </button>
<button onClick={() => changeMonth(1)} className="p-1"> <button aria-label="다음 달" onClick={() => changeMonth(1)} className="p-1">
<ChevronRight size={18} /> <ChevronRight size={18} aria-hidden="true" />
</button> </button>
</div> </div>
)} )}
@ -367,6 +381,7 @@ function Calendar({
{/* 오늘 버튼 */} {/* 오늘 버튼 */}
<div className="mt-3 flex justify-center"> <div className="mt-3 flex justify-center">
<button <button
aria-label="오늘 날짜로 이동"
onClick={() => onSelectDate(new Date())} onClick={() => onSelectDate(new Date())}
className="text-xs text-primary font-medium px-4 py-1.5 bg-primary/10 rounded-full" className="text-xs text-primary font-medium px-4 py-1.5 bg-primary/10 rounded-full"
> >

View file

@ -1,3 +1,4 @@
import { memo } from 'react';
import { Clock, Tag, Link2 } from 'lucide-react'; import { Clock, Tag, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
@ -6,7 +7,7 @@ import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime
* 페이지의 다가오는 일정 섹션에서 사용 * 페이지의 다가오는 일정 섹션에서 사용
* 간결한 레이아웃 * 간결한 레이아웃
*/ */
function ScheduleCard({ schedule, onClick, className = '' }) { const ScheduleCard = memo(function ScheduleCard({ schedule, onClick, className = '' }) {
const scheduleDate = new Date(schedule.date); const scheduleDate = new Date(schedule.date);
const today = new Date(); const today = new Date();
const currentYear = today.getFullYear(); const currentYear = today.getFullYear();
@ -94,6 +95,6 @@ function ScheduleCard({ schedule, onClick, className = '' }) {
</div> </div>
</div> </div>
); );
} });
export default ScheduleCard; export default ScheduleCard;

View file

@ -1,3 +1,4 @@
import { memo } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Clock, Link2 } from 'lucide-react'; import { Clock, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
@ -7,7 +8,7 @@ import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime
* 스케줄 페이지에서 날짜별 일정 목록에 사용 * 스케줄 페이지에서 날짜별 일정 목록에 사용
* 날짜가 이미 헤더에 표시되므로 날짜 없이 표시 * 날짜가 이미 헤더에 표시되므로 날짜 없이 표시
*/ */
function ScheduleListCard({ const ScheduleListCard = memo(function ScheduleListCard({
schedule, schedule,
onClick, onClick,
delay = 0, delay = 0,
@ -81,6 +82,6 @@ function ScheduleListCard({
</div> </div>
</motion.div> </motion.div>
); );
} });
export default ScheduleListCard; export default ScheduleListCard;

View file

@ -1,3 +1,4 @@
import { memo } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Clock, Link2 } from 'lucide-react'; import { Clock, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
@ -7,7 +8,7 @@ import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime
* 스케줄 페이지의 검색 결과에서 사용 * 스케줄 페이지의 검색 결과에서 사용
* 날짜를 왼쪽에 표시하는 레이아웃 * 날짜를 왼쪽에 표시하는 레이아웃
*/ */
function ScheduleSearchCard({ const ScheduleSearchCard = memo(function ScheduleSearchCard({
schedule, schedule,
onClick, onClick,
delay = 0, delay = 0,
@ -110,6 +111,6 @@ function ScheduleSearchCard({
</div> </div>
</motion.div> </motion.div>
); );
} });
export default ScheduleSearchCard; export default ScheduleSearchCard;

View file

@ -1,9 +1,10 @@
import { memo } from 'react';
import { dayjs } from '@/utils'; import { dayjs } from '@/utils';
/** /**
* PC용 생일 카드 컴포넌트 * PC용 생일 카드 컴포넌트
*/ */
function BirthdayCard({ schedule, showYear = false, onClick }) { const BirthdayCard = memo(function BirthdayCard({ schedule, showYear = false, onClick }) {
const scheduleDate = dayjs(schedule.date); const scheduleDate = dayjs(schedule.date);
const formatted = { const formatted = {
year: scheduleDate.year(), year: scheduleDate.year(),
@ -55,6 +56,6 @@ function BirthdayCard({ schedule, showYear = false, onClick }) {
</div> </div>
</div> </div>
); );
} });
export default BirthdayCard; export default BirthdayCard;

View file

@ -137,21 +137,24 @@ function Calendar({
{/* 헤더 */} {/* 헤더 */}
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<button <button
aria-label="이전 달"
onClick={prevMonth} onClick={prevMonth}
disabled={!canGoPrevMonth} disabled={!canGoPrevMonth}
className={`p-2 rounded-full transition-colors ${canGoPrevMonth ? 'hover:bg-gray-100' : 'opacity-30'}`} className={`p-2 rounded-full transition-colors ${canGoPrevMonth ? 'hover:bg-gray-100' : 'opacity-30'}`}
> >
<ChevronLeft size={24} /> <ChevronLeft size={24} aria-hidden="true" />
</button> </button>
<button <button
aria-label="년/월 선택"
aria-expanded={showYearMonthPicker}
onClick={() => setShowYearMonthPicker(!showYearMonthPicker)} onClick={() => setShowYearMonthPicker(!showYearMonthPicker)}
className="flex items-center gap-1 text-xl font-bold hover:text-primary transition-colors" className="flex items-center gap-1 text-xl font-bold hover:text-primary transition-colors"
> >
<span>{year} {month + 1}</span> <span>{year} {month + 1}</span>
<ChevronDown size={20} className={`transition-transform ${showYearMonthPicker ? 'rotate-180' : ''}`} /> <ChevronDown size={20} aria-hidden="true" className={`transition-transform ${showYearMonthPicker ? 'rotate-180' : ''}`} />
</button> </button>
<button onClick={nextMonth} className="p-2 hover:bg-gray-100 rounded-full transition-colors"> <button aria-label="다음 달" onClick={nextMonth} className="p-2 hover:bg-gray-100 rounded-full transition-colors">
<ChevronRight size={24} /> <ChevronRight size={24} aria-hidden="true" />
</button> </button>
</div> </div>
@ -166,17 +169,18 @@ function Calendar({
> >
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<button <button
aria-label="이전 연도 범위"
onClick={prevYearRange} onClick={prevYearRange}
disabled={!canGoPrevYearRange} disabled={!canGoPrevYearRange}
className={`p-1.5 rounded-lg transition-colors ${canGoPrevYearRange ? 'hover:bg-gray-100' : 'opacity-30'}`} className={`p-1.5 rounded-lg transition-colors ${canGoPrevYearRange ? 'hover:bg-gray-100' : 'opacity-30'}`}
> >
<ChevronLeft size={20} className="text-gray-600" /> <ChevronLeft size={20} aria-hidden="true" className="text-gray-600" />
</button> </button>
<span className="font-medium text-gray-900"> <span className="font-medium text-gray-900">
{yearRange[0]} - {yearRange[yearRange.length - 1]} {yearRange[0]} - {yearRange[yearRange.length - 1]}
</span> </span>
<button onClick={nextYearRange} className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"> <button aria-label="다음 연도 범위" onClick={nextYearRange} className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors">
<ChevronRight size={20} className="text-gray-600" /> <ChevronRight size={20} aria-hidden="true" className="text-gray-600" />
</button> </button>
</div> </div>
@ -186,6 +190,8 @@ function Calendar({
{yearRange.map((y) => ( {yearRange.map((y) => (
<button <button
key={y} key={y}
aria-label={`${y}년 선택`}
aria-pressed={year === y}
onClick={() => selectYear(y)} onClick={() => selectYear(y)}
className={`py-2 text-sm rounded-lg transition-colors ${ className={`py-2 text-sm rounded-lg transition-colors ${
year === y year === y
@ -206,6 +212,8 @@ function Calendar({
{MONTHS.map((m, i) => ( {MONTHS.map((m, i) => (
<button <button
key={m} key={m}
aria-label={`${m} 선택`}
aria-pressed={month === i}
onClick={() => selectMonth(i)} onClick={() => selectMonth(i)}
className={`py-2 text-sm rounded-lg transition-colors ${ className={`py-2 text-sm rounded-lg transition-colors ${
month === i month === i
@ -269,6 +277,8 @@ function Calendar({
return ( return (
<button <button
key={day} key={day}
aria-label={`${month + 1}${day}${isToday ? ' (오늘)' : ''}${daySchedules.length > 0 ? `, 일정 ${daySchedules.length}` : ''}`}
aria-pressed={isSelected}
onClick={() => selectDate(day)} onClick={() => selectDate(day)}
className={`aspect-square flex flex-col items-center justify-center rounded-full text-base font-medium transition-all relative hover:bg-gray-100 className={`aspect-square flex flex-col items-center justify-center rounded-full text-base font-medium transition-all relative hover:bg-gray-100
${isSelected ? 'bg-primary text-white shadow-lg hover:bg-primary' : ''} ${isSelected ? 'bg-primary text-white shadow-lg hover:bg-primary' : ''}

View file

@ -1,3 +1,4 @@
import { memo } from 'react';
import { Clock, Tag, Link2 } from 'lucide-react'; import { Clock, Tag, Link2 } from 'lucide-react';
import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils'; import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime } from '@/utils';
@ -5,7 +6,7 @@ import { decodeHtmlEntities, getDisplayMembers, getCategoryInfo, getScheduleTime
* PC용 일정 카드 컴포넌트 * PC용 일정 카드 컴포넌트
* , 스케줄 페이지에서 공통으로 사용 * , 스케줄 페이지에서 공통으로 사용
*/ */
function ScheduleCard({ schedule, onClick, className = '' }) { const ScheduleCard = memo(function ScheduleCard({ schedule, onClick, className = '' }) {
const scheduleDate = new Date(schedule.date); const scheduleDate = new Date(schedule.date);
const today = new Date(); const today = new Date();
const currentYear = today.getFullYear(); const currentYear = today.getFullYear();
@ -89,6 +90,6 @@ function ScheduleCard({ schedule, onClick, className = '' }) {
</div> </div>
</div> </div>
); );
} });
export default ScheduleCard; export default ScheduleCard;

View file

@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/stores'; import { useAuthStore } from '@/stores';
@ -16,9 +16,13 @@ export function useAdminAuth(options = {}) {
const navigate = useNavigate(); const navigate = useNavigate();
const { token, user, logout, isAuthenticated } = useAuthStore(); const { token, user, logout, isAuthenticated } = useAuthStore();
// 토큰 검증 쿼리 // logout 함수를 ref로 안정화하여 무한 루프 방지
const logoutRef = useRef(logout);
logoutRef.current = logout;
// 토큰 검증 쿼리 - 고유 queryKey 사용
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ['admin', 'auth'], queryKey: ['admin', 'auth', 'verify'],
queryFn: authApi.verifyToken, queryFn: authApi.verifyToken,
enabled: !!token, enabled: !!token,
retry: false, retry: false,
@ -29,10 +33,10 @@ export function useAdminAuth(options = {}) {
// 리다이렉트는 DOM 조작이므로 useEffect 사용 허용 // 리다이렉트는 DOM 조작이므로 useEffect 사용 허용
useEffect(() => { useEffect(() => {
if (required && (!token || isError)) { if (required && (!token || isError)) {
logout(); logoutRef.current();
navigate(redirectTo); navigate(redirectTo);
} }
}, [token, isError, required, logout, navigate, redirectTo]); }, [token, isError, required, navigate, redirectTo]);
return { return {
user: data?.user || user, user: data?.user || user,
@ -49,11 +53,11 @@ export function useAdminAuth(options = {}) {
*/ */
export function useRedirectIfAuthenticated(redirectTo = '/admin/dashboard') { export function useRedirectIfAuthenticated(redirectTo = '/admin/dashboard') {
const navigate = useNavigate(); const navigate = useNavigate();
const { isAuthenticated, token } = useAuthStore(); const { token } = useAuthStore();
// 토큰 검증 // 토큰 검증 - 고유 queryKey 사용
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ['admin', 'auth'], queryKey: ['admin', 'auth', 'redirect-check'],
queryFn: authApi.verifyToken, queryFn: authApi.verifyToken,
enabled: !!token, enabled: !!token,
retry: false, retry: false,

View file

@ -30,15 +30,6 @@ const useAuthStore = create(
isAuthenticated: false, isAuthenticated: false,
}); });
}, },
// 토큰 가져오기
getToken: () => get().token,
// 인증 여부 확인
checkAuth: () => {
const { token } = get();
return !!token;
},
}), }),
{ {
name: 'auth-storage', name: 'auth-storage',

View file

@ -93,29 +93,6 @@ const useUIStore = create((set, get) => ({
// ===== 로딩 ===== // ===== 로딩 =====
globalLoading: false, globalLoading: false,
setGlobalLoading: (value) => set({ globalLoading: value }), setGlobalLoading: (value) => set({ globalLoading: value }),
// ===== 확인 다이얼로그 =====
confirmDialog: null,
showConfirm: (options) => {
return new Promise((resolve) => {
set({
confirmDialog: {
...options,
onConfirm: () => {
set({ confirmDialog: null });
resolve(true);
},
onCancel: () => {
set({ confirmDialog: null });
resolve(false);
},
},
});
});
},
closeConfirm: () => set({ confirmDialog: null }),
})); }));
export default useUIStore; export default useUIStore;