'use client'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import type { ChannelSearchRow, ProductType } from '@/types/store'; const STORAGE_KEY = 'antooza.cart.v1'; const MAX_QUANTITY_PER_LINE = 999; export interface CartItem { productID: number; name: string; thumbnail: string|null; price: number; type: ProductType; gameName: string; stock: number; // 스냅샷 — 결제 시 서버가 최종 검증. -1 = 무제한 requireDonationChannel: boolean; // 구매 시 후원 채널 필수 여부 (서버가 최종 강제) quantity: number; } type AddItemArgs = Omit & { quantity: number }; export interface CartState { items: CartItem[]; channel: ChannelSearchRow|null; } export interface CartContextValue extends CartState { addItem: (args: AddItemArgs) => void; updateQuantity: (productID: number, quantity: number) => void; removeItem: (productID: number) => void; setChannel: (channel: ChannelSearchRow|null) => void; clear: () => void; totalCount: number; totalPrice: number; } const CartContext = createContext(null); function clampQty(value: number, stock: number): number { const hardMax = stock > 0 ? Math.min(stock, MAX_QUANTITY_PER_LINE) : MAX_QUANTITY_PER_LINE; return Math.max(1, Math.min(value, hardMax)); } function loadFromStorage(): CartState { if (typeof window === 'undefined') { return { items: [], channel: null }; } try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) { return { items: [], channel: null }; } const parsed = JSON.parse(raw) as CartState; if (!parsed || !Array.isArray(parsed.items)) { return { items: [], channel: null }; } return { items: parsed.items .filter(i => i && typeof i.productID === 'number' && typeof i.quantity === 'number') .map(i => ({ ...i, requireDonationChannel: Boolean(i.requireDonationChannel) })), channel: parsed.channel ?? null }; } catch { return { items: [], channel: null }; } } export function CartProvider({ children }: { children: React.ReactNode }) { const [state, setState] = useState({ items: [], channel: null }); const [hydrated, setHydrated] = useState(false); // SSR 안전 hydration useEffect(() => { setState(loadFromStorage()); setHydrated(true); }, []); // state 변경 시 localStorage 영구화 (hydration 후만) useEffect(() => { if (!hydrated) { return; } try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch { // quota 초과 등 — 무시 } }, [state, hydrated]); const addItem = useCallback((args: AddItemArgs) => { setState(prev => { const existing = prev.items.find(i => i.productID === args.productID); if (existing) { const merged = clampQty(existing.quantity + args.quantity, args.stock); return { ...prev, items: prev.items.map(i => i.productID === args.productID ? { ...i, quantity: merged, price: args.price, stock: args.stock, thumbnail: args.thumbnail, name: args.name, type: args.type, gameName: args.gameName, requireDonationChannel: args.requireDonationChannel } : i ) }; } return { ...prev, items: [...prev.items, { ...args, quantity: clampQty(args.quantity, args.stock) }] }; }); }, []); const updateQuantity = useCallback((productID: number, quantity: number) => { setState(prev => ({ ...prev, items: prev.items.map(i => i.productID === productID ? { ...i, quantity: clampQty(quantity, i.stock) } : i ) })); }, []); const removeItem = useCallback((productID: number) => { setState(prev => ({ ...prev, items: prev.items.filter(i => i.productID !== productID) })); }, []); const setChannel = useCallback((channel: ChannelSearchRow|null) => { setState(prev => ({ ...prev, channel })); }, []); const clear = useCallback(() => { setState({ items: [], channel: null }); }, []); const totalCount = useMemo( () => state.items.reduce((sum, i) => sum + i.quantity, 0), [state.items] ); const totalPrice = useMemo( () => state.items.reduce((sum, i) => sum + i.price * i.quantity, 0), [state.items] ); const value: CartContextValue = useMemo(() => ({ items: state.items, channel: state.channel, addItem, updateQuantity, removeItem, setChannel, clear, totalCount, totalPrice }), [state.items, state.channel, addItem, updateQuantity, removeItem, setChannel, clear, totalCount, totalPrice]); return ( {children} ); } export function useCartContext(): CartContextValue { const ctx = useContext(CartContext); if (ctx === null) { throw new Error('useCartContext는 CartProvider 내부에서만 사용 가능합니다.'); } return ctx; }