From cba7e4b522db2281e2712d279cde7188a4dac29c Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 21 Jan 2026 17:11:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Phase=203=20-=20Zustand=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useAuthStore: 인증 상태 관리 (localStorage persist) - useScheduleStore: 스케줄 페이지 상태 (검색, 필터, 날짜, 뷰) - useUIStore: UI 상태 (토스트, 모달, 라이트박스, 확인 다이얼로그) - stores/index.js: 통합 export Co-Authored-By: Claude Opus 4.5 --- frontend-temp/src/App.jsx | 88 +++++++++++--- frontend-temp/src/stores/.gitkeep | 0 frontend-temp/src/stores/index.js | 6 + frontend-temp/src/stores/useAuthStore.js | 54 +++++++++ frontend-temp/src/stores/useScheduleStore.js | 107 ++++++++++++++++ frontend-temp/src/stores/useUIStore.js | 121 +++++++++++++++++++ 6 files changed, 359 insertions(+), 17 deletions(-) delete mode 100644 frontend-temp/src/stores/.gitkeep create mode 100644 frontend-temp/src/stores/index.js create mode 100644 frontend-temp/src/stores/useAuthStore.js create mode 100644 frontend-temp/src/stores/useScheduleStore.js create mode 100644 frontend-temp/src/stores/useUIStore.js diff --git a/frontend-temp/src/App.jsx b/frontend-temp/src/App.jsx index c95df24..daf2c96 100644 --- a/frontend-temp/src/App.jsx +++ b/frontend-temp/src/App.jsx @@ -1,20 +1,31 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import { isMobile } from "react-device-detect"; -import { cn, getTodayKST, formatFullDate, formatXDateTime } from "@/utils"; -import { CATEGORY_NAMES, SOCIAL_LINKS } from "@/constants"; +import { cn, getTodayKST, formatFullDate } from "@/utils"; +import { useAuthStore, useScheduleStore, useUIStore } from "@/stores"; /** * 프로미스나인 팬사이트 메인 앱 * - * Phase 2: 유틸리티 및 상수 완료 - * - constants/index.js: 상수 정의 (카테고리, SNS 링크 등) - * - utils/cn.js: className 유틸리티 (clsx 기반) - * - utils/date.js: 날짜 관련 유틸리티 - * - utils/format.js: 포맷팅 유틸리티 - * - utils/index.js: 통합 export + * Phase 3: 스토어 완료 + * - useAuthStore: 인증 상태 (localStorage 지속) + * - useScheduleStore: 스케줄 페이지 상태 + * - useUIStore: UI 상태 (모달, 토스트, 라이트박스 등) */ function App() { const today = getTodayKST(); + const { isAuthenticated, login, logout } = useAuthStore(); + const { viewMode, setViewMode, selectedCategories, toggleCategory } = useScheduleStore(); + const { showSuccess, showError, toasts } = useUIStore(); + + const handleTestLogin = () => { + login("test-token", { name: "테스트 유저" }); + showSuccess("로그인 성공!"); + }; + + const handleTestLogout = () => { + logout(); + showError("로그아웃됨"); + }; return ( @@ -27,21 +38,64 @@ function App() {

fromis_9 Frontend Refactoring

-

- Phase 2 완료 - 유틸리티 및 상수 -

+

Phase 3 완료 - 스토어

디바이스: {isMobile ? "모바일" : "PC"}

-
-

오늘 날짜: {today}

-

포맷된 날짜: {formatFullDate(today)}

-

X 스타일: {formatXDateTime(today, "19:00")}

-

카테고리: {Object.values(CATEGORY_NAMES).join(", ")}

-

SNS 개수: {Object.keys(SOCIAL_LINKS).length}개

+
+

오늘: {formatFullDate(today)}

+ +
+

useAuthStore 테스트

+

인증 상태: {isAuthenticated ? "✅ 로그인됨" : "❌ 로그아웃"}

+
+ + +
+
+ +
+

useScheduleStore 테스트

+

뷰 모드: {viewMode}

+
+ + +
+

선택된 카테고리: {selectedCategories.length > 0 ? selectedCategories.join(", ") : "없음"}

+
+ {[1, 2, 3].map((id) => ( + + ))} +
+
+ +
+

useUIStore 토스트

+

활성 토스트: {toasts.length}개

+
+ + {/* 토스트 표시 */} +
+ {toasts.map((toast) => ( +
+ {toast.message} +
+ ))} +
} /> diff --git a/frontend-temp/src/stores/.gitkeep b/frontend-temp/src/stores/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/frontend-temp/src/stores/index.js b/frontend-temp/src/stores/index.js new file mode 100644 index 0000000..26fc97a --- /dev/null +++ b/frontend-temp/src/stores/index.js @@ -0,0 +1,6 @@ +/** + * 스토어 통합 export + */ +export { default as useAuthStore } from './useAuthStore'; +export { default as useScheduleStore } from './useScheduleStore'; +export { default as useUIStore } from './useUIStore'; diff --git a/frontend-temp/src/stores/useAuthStore.js b/frontend-temp/src/stores/useAuthStore.js new file mode 100644 index 0000000..75147a5 --- /dev/null +++ b/frontend-temp/src/stores/useAuthStore.js @@ -0,0 +1,54 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +/** + * 인증 상태 스토어 + * localStorage에 지속되어 새로고침 후에도 유지 + */ +const useAuthStore = create( + persist( + (set, get) => ({ + // 상태 + token: null, + user: null, + isAuthenticated: false, + + // 로그인 + login: (token, user) => { + set({ + token, + user, + isAuthenticated: true, + }); + }, + + // 로그아웃 + logout: () => { + set({ + token: null, + user: null, + isAuthenticated: false, + }); + }, + + // 토큰 가져오기 + getToken: () => get().token, + + // 인증 여부 확인 + checkAuth: () => { + const { token } = get(); + return !!token; + }, + }), + { + name: 'auth-storage', + partialize: (state) => ({ + token: state.token, + user: state.user, + isAuthenticated: state.isAuthenticated, + }), + } + ) +); + +export default useAuthStore; diff --git a/frontend-temp/src/stores/useScheduleStore.js b/frontend-temp/src/stores/useScheduleStore.js new file mode 100644 index 0000000..076c060 --- /dev/null +++ b/frontend-temp/src/stores/useScheduleStore.js @@ -0,0 +1,107 @@ +import { create } from 'zustand'; + +/** + * 스케줄 페이지 상태 스토어 + * 메모리 기반 - SPA 내 페이지 이동 시 유지, 새로고침 시 초기화 + */ +const useScheduleStore = create((set, get) => ({ + // ===== 검색 관련 ===== + searchInput: '', + searchTerm: '', + isSearchMode: false, + + // ===== 필터 관련 ===== + selectedCategories: [], + selectedMembers: [], + + // ===== 날짜 관련 ===== + selectedDate: undefined, // undefined: 오늘, null: 전체, Date: 특정 날짜 + currentDate: new Date(), + + // ===== 뷰 관련 ===== + viewMode: 'list', // 'list' | 'calendar' + scrollPosition: 0, + + // ===== 검색 액션 ===== + setSearchInput: (value) => set({ searchInput: value }), + setSearchTerm: (value) => set({ searchTerm: value }), + setIsSearchMode: (value) => set({ isSearchMode: value }), + + startSearch: (term) => { + set({ + searchTerm: term, + isSearchMode: true, + selectedDate: null, // 검색 시 날짜 필터 해제 + }); + }, + + clearSearch: () => { + set({ + searchInput: '', + searchTerm: '', + isSearchMode: false, + }); + }, + + // ===== 필터 액션 ===== + setSelectedCategories: (value) => set({ selectedCategories: value }), + setSelectedMembers: (value) => set({ selectedMembers: value }), + + toggleCategory: (categoryId) => { + const { selectedCategories } = get(); + const isSelected = selectedCategories.includes(categoryId); + set({ + selectedCategories: isSelected + ? selectedCategories.filter((id) => id !== categoryId) + : [...selectedCategories, categoryId], + }); + }, + + toggleMember: (memberId) => { + const { selectedMembers } = get(); + const isSelected = selectedMembers.includes(memberId); + set({ + selectedMembers: isSelected + ? selectedMembers.filter((id) => id !== memberId) + : [...selectedMembers, memberId], + }); + }, + + clearFilters: () => { + set({ + selectedCategories: [], + selectedMembers: [], + }); + }, + + // ===== 날짜 액션 ===== + setSelectedDate: (value) => set({ selectedDate: value }), + setCurrentDate: (value) => set({ currentDate: value }), + + goToToday: () => { + set({ + selectedDate: undefined, + currentDate: new Date(), + }); + }, + + // ===== 뷰 액션 ===== + setViewMode: (mode) => set({ viewMode: mode }), + setScrollPosition: (value) => set({ scrollPosition: value }), + + // ===== 전체 초기화 ===== + reset: () => + set({ + searchInput: '', + searchTerm: '', + isSearchMode: false, + selectedCategories: [], + selectedMembers: [], + selectedDate: undefined, + currentDate: new Date(), + viewMode: 'list', + scrollPosition: 0, + }), +})); + +export default useScheduleStore; diff --git a/frontend-temp/src/stores/useUIStore.js b/frontend-temp/src/stores/useUIStore.js new file mode 100644 index 0000000..877f923 --- /dev/null +++ b/frontend-temp/src/stores/useUIStore.js @@ -0,0 +1,121 @@ +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 }), + + // ===== 확인 다이얼로그 ===== + 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;