import axios from 'axios' // 단일 jar 운영 시 same-origin, dev 시 proxy(/api → 8011). baseURL 비워 same-origin 사용. const api = axios.create({ baseURL: '' }) // 관리자 영역(/admin)은 관리자 토큰(mall_admin_token)을, 고객 영역은 고객 토큰(mall_token)을 사용. // 두 세션이 같은 키를 공유하면 한쪽 로그인이 다른 쪽 세션을 덮어쓰는 충돌이 발생하므로 분리한다. const isAdminArea = () => location.pathname.startsWith('/admin') const tokenKey = () => (isAdminArea() ? 'mall_admin_token' : 'mall_token') api.interceptors.request.use(config => { const token = localStorage.getItem(tokenKey()) if (token) config.headers.Authorization = `Bearer ${token}` return config }) api.interceptors.response.use(res => res, err => { if (err.response?.status === 401) { // 현재 영역의 토큰만 제거 (반대편 세션 보존) localStorage.removeItem(tokenKey()) // 관리자 영역에서만 강제 로그인 리다이렉트 if (isAdminArea() && location.pathname !== '/admin/login') { location.href = '/admin/login' } } return Promise.reject(err) }) export default api // ApiResponse = {success,message,data} → data 언랩 const u = (p: Promise): Promise => p.then(r => r.data?.data) /* ───────────── 0. 인증 ───────────── */ export const login = (username: string, password: string) => api.post('/api/mall/auth/login', { username, password }) export const register = (username: string, password: string, displayName: string) => api.post('/api/mall/auth/register', { username, password, displayName }) export const getMe = () => u(api.get('/api/mall/auth/me')) /* ───────────── 1. 매장 Store ───────────── */ export const getStores = (activeOnly = true) => u(api.get(`/api/mall/store?activeOnly=${activeOnly}`)) export const getStore = (id: number) => u(api.get(`/api/mall/store/${id}`)) export const createStore = (d: object) => u(api.post('/api/mall/store', d)) export const updateStore = (id: number, d: object) => u(api.put(`/api/mall/store/${id}`, d)) export const setStoreActive = (id: number, active: boolean) => u(api.put(`/api/mall/store/${id}/active`, { active })) /* ───────────── 2. 배송권역 Zone ───────────── */ export const zoneLookup = (zip: string) => u(api.get(`/api/mall/zone/lookup?zip=${encodeURIComponent(zip)}`)) export const getZones = (storeId: number) => u(api.get(`/api/mall/zone?storeId=${storeId}`)) export const createZone = (d: object) => u(api.post('/api/mall/zone', d)) export const deleteZone = (id: number) => api.delete(`/api/mall/zone/${id}`) /* ───────────── 3. 카탈로그/상품 ───────────── */ export interface ProductQuery { categoryId?: number; keyword?: string; occasion?: string; flowerType?: string maxPrice?: number; status?: string; sort?: string; page?: number; size?: number } export const getProducts = (q: ProductQuery = {}) => { const p = new URLSearchParams() Object.entries(q).forEach(([k, v]) => { if (v !== undefined && v !== '' && v !== null) p.set(k, String(v)) }) return u(api.get(`/api/mall/product?${p}`)) } export const getProduct = (id: number) => u(api.get(`/api/mall/product/${id}`)) export const createProduct = (d: object) => u(api.post('/api/mall/product', d)) export const updateProduct = (id: number, d: object) => u(api.put(`/api/mall/product/${id}`, d)) export const deleteProduct = (id: number) => api.delete(`/api/mall/product/${id}`) export const setProductStatus = (id: number, status: string) => u(api.put(`/api/mall/product/${id}/status`, { status })) export const getCategories = () => u(api.get('/api/mall/category')) export const getCategoryTree = () => u(api.get('/api/mall/category/tree')) /* ───────────── 4a. 매장별 재고 ON/OFF ───────────── */ export const getStoreInventory = (storeId: number) => u(api.get(`/api/mall/store-inventory/store/${storeId}`)) export const toggleStoreInventory = (storeId: number, productId: number, available: boolean) => u(api.put(`/api/mall/store-inventory/store/${storeId}/product/${productId}/toggle`, { available })) export const adjustStoreInventory = (storeId: number, productId: number, delta: number) => u(api.put(`/api/mall/store-inventory/store/${storeId}/product/${productId}/adjust`, { delta })) /* 4b. ERP 동기 단일재고 */ export const getInventory = () => u(api.get('/api/mall/inventory')) export const adjustInventory = (id: number, delta: number) => u(api.put(`/api/mall/inventory/${id}/adjust`, { delta })) export const erpSyncInventory = (id: number) => u(api.post(`/api/mall/inventory/${id}/erp-sync`, {})) /* ───────────── 5. 스케줄 ───────────── */ export const getAvailability = (storeId: number, date: string) => u(api.get(`/api/mall/schedule/availability?storeId=${storeId}&date=${date}`)) export const getHolidays = () => u(api.get('/api/mall/schedule/holidays')) export const createSlot = (d: object) => u(api.post('/api/mall/schedule/slot', d)) export const createHoliday = (d: object) => u(api.post('/api/mall/schedule/holiday', d)) /* ───────────── 6. 장바구니 ───────────── */ export const getCart = () => u(api.get('/api/mall/cart')) export const addToCart = (d: object) => u(api.post('/api/mall/cart', d)) export const updateCartItem = (id: number, quantity: number) => u(api.put(`/api/mall/cart/${id}`, { quantity })) export const removeCartItem = (id: number) => api.delete(`/api/mall/cart/${id}`) export const clearCart = () => api.delete('/api/mall/cart') /* ───────────── 7. 주문 ───────────── */ export const getMyOrders = (status = '') => u(api.get(`/api/mall/order?status=${status}`)) export const getOrder = (id: number) => u(api.get(`/api/mall/order/${id}`)) export const checkout = (d: object) => u(api.post('/api/mall/order/checkout', d)) export const setOrderStatus = (id: number, status: string) => u(api.put(`/api/mall/order/${id}/status`, { status })) export const getAdminOrders = (status = '', limit = 100) => u(api.get(`/api/mall/order/admin?status=${status}&limit=${limit}`)) export const getStoreOrders = (storeId: number) => u(api.get(`/api/mall/order/store/${storeId}`)) export const proofPhoto = (id: number, photoUrl: string) => u(api.post(`/api/mall/order/${id}/proof-photo`, { photoUrl })) export const podPhoto = (id: number, photoUrl: string) => u(api.post(`/api/mall/order/${id}/pod-photo`, { photoUrl })) /* ───────────── 8. 결제 ───────────── */ export const createPayment = (d: object) => u(api.post('/api/mall/payment', d)) export const vbankConfirm = (id: number) => u(api.post(`/api/mall/payment/${id}/vbank-confirm`, {})) export const getPayment = (orderId: number) => u(api.get(`/api/mall/payment/order/${orderId}`)) /* ───────────── 9. 구독 ───────────── */ export const getSubscriptions = () => u(api.get('/api/mall/subscription')) export const createSubscription = (d: object) => u(api.post('/api/mall/subscription', d)) export const setSubscriptionStatus = (id: number, status: string) => u(api.put(`/api/mall/subscription/${id}/status`, { status })) export const getAdminSubscriptions = () => u(api.get('/api/mall/subscription/admin')) // 관리자(MANAGER+) 구독 상태 변경 — 소유자 제한 없음 export const setAdminSubscriptionStatus = (id: number, status: string) => u(api.put(`/api/mall/subscription/admin/${id}/status`, { status })) /* ───────────── 10. 매장간 재고 이양 ───────────── */ export const getTransfers = (status = '', storeId = '') => { const p = new URLSearchParams(); if (status) p.set('status', status); if (storeId) p.set('storeId', storeId) return u(api.get(`/api/mall/transfer?${p}`)) } export const getTransfer = (id: number) => u(api.get(`/api/mall/transfer/${id}`)) export const createTransfer = (d: object) => u(api.post('/api/mall/transfer', d)) export const approveTransfer = (id: number) => u(api.put(`/api/mall/transfer/${id}/approve`, {})) export const rejectTransfer = (id: number) => u(api.put(`/api/mall/transfer/${id}/reject`, {})) /* ───────────── 11. 배송 ───────────── */ export const getDeliveryByOrder = (orderId: number) => u(api.get(`/api/mall/delivery/order/${orderId}`)) export const getDeliveries = () => u(api.get('/api/mall/delivery')) export const setDeliveryStatus = (id: number, status: string) => u(api.put(`/api/mall/delivery/${id}/status`, { status })) /* ───────────── 12. 리뷰 ───────────── */ export const getReviews = (productId: number) => u(api.get(`/api/mall/review/product/${productId}`)) export const getReviewStats = (productId: number) => u(api.get(`/api/mall/review/product/${productId}/stats`)) export const createReview = (d: object) => u(api.post('/api/mall/review', d)) export const reviewHelpful = (id: number) => u(api.put(`/api/mall/review/${id}/helpful`, {})) export const deleteReview = (id: number) => api.delete(`/api/mall/review/${id}`) /* ───────────── 13. 회원 ───────────── */ export const getMember = () => u(api.get('/api/mall/member/me')) export const updateMember = (d: object) => u(api.put('/api/mall/member/me', d)) export const getMemberInsight = () => u(api.get('/api/mall/member/me/insight')) // 관리자 회원 목록/검색 (PII 마스킹 응답). items/total 래핑. export const getAdminMembers = (keyword = '', tier = '', limit = 100) => { const p = new URLSearchParams(); if (keyword) p.set('keyword', keyword); if (tier) p.set('tier', tier); p.set('limit', String(limit)) return u(api.get(`/api/mall/member/admin?${p}`)) } /* ───────────── 14. CS 문의 ───────────── */ export const getMyCs = () => u(api.get('/api/mall/cs')) export const createCs = (d: object) => u(api.post('/api/mall/cs', d)) export const getAdminCs = () => u(api.get('/api/mall/cs/admin')) export const setCsStatus = (id: number, status: string) => u(api.put(`/api/mall/cs/${id}/status`, { status })) /* ───────────── 15. 찜 ───────────── */ export const getWishlist = () => u(api.get('/api/mall/wishlist')) export const addWishlist = (productId: number) => u(api.post(`/api/mall/wishlist/${productId}`, {})) export const removeWishlist = (productId: number) => api.delete(`/api/mall/wishlist/${productId}`) /* ───────────── 16. 분석 ───────────── */ export const getAnalyticsDashboard = (days = 1, bigOrderThreshold = 500) => u(api.get(`/api/mall/analytics/dashboard?days=${days}&bigOrderThreshold=${bigOrderThreshold}`)) export const getStoreSales = (days = 7) => u(api.get(`/api/mall/analytics/store-sales?days=${days}`)) export const getSalesTrend = (days = 14) => u(api.get(`/api/mall/analytics/trend?days=${days}`)) export const getTopProducts = (limit = 10) => u(api.get(`/api/mall/analytics/top-products?limit=${limit}`)) /* ───────────── 17. 게이트웨이 어댑터 ───────────── */ export const taxQuote = (amount: number, zip: string, state: string) => u(api.post('/api/mall/gateway/tax/quote', { amount, zip, state })) export const addressVerify = (address: string, zip: string) => u(api.post('/api/mall/gateway/address/verify', { address, zip })) export const addressRoute = (addresses: string[], origin: string) => u(api.post('/api/mall/gateway/address/route', { addresses, origin })) export const getProviders = () => u(api.get('/api/mall/gateway/providers')) /* ───────────── 18. GUARDiA AI ───────────── */ export const aiRecommend = (occasion = '', keyword = '', limit = 6) => { const p = new URLSearchParams(); if (occasion) p.set('occasion', occasion); if (keyword) p.set('keyword', keyword); p.set('limit', String(limit)) return u(api.get(`/api/mall/ai/recommend?${p}`)) } export const aiReviewSummary = (productId: number) => u(api.get(`/api/mall/ai/review-summary/${productId}`)) export const aiNlSearch = (query: string) => u(api.post('/api/mall/ai/nl-search', { query })) export const aiCardMessage = (occasion: string, tone: string, recipient: string) => u(api.post('/api/mall/ai/card-message', { occasion, tone, recipient })) export const aiCsReply = (subject: string, content: string) => u(api.post('/api/mall/ai/cs-reply', { subject, content })) export const aiDailyClearance = (storeId: number) => u(api.get(`/api/mall/ai/daily-clearance/${storeId}`)) export const aiDemandForecast = (season = 'valentine', days = 14) => u(api.get(`/api/mall/ai/demand-forecast?season=${season}&days=${days}`)) export const aiTransferRecommend = (storeIds: number[]) => u(api.post('/api/mall/ai/transfer-recommend', { storeIds })) /* ───────────── 19. 관리자 (RBAC) ───────────── */ export const getUsers = () => u(api.get('/api/admin/users')) export const createUser = (d: { username: string; password: string; role?: string }) => u(api.post('/api/admin/users', d)) export const updateUserRole = (id: number, role: string) => u(api.put(`/api/admin/users/${id}/role`, { role })) export const updateUserActive = (id: number, active: boolean) => u(api.put(`/api/admin/users/${id}/active`, { active })) export const resetPassword = (id: number, password: string) => u(api.put(`/api/admin/users/${id}/password`, { password })) export const deleteUser = (id: number) => api.delete(`/api/admin/users/${id}`) export const getAuditLogs = (action = '', actor = '', limit = 100) => { const p = new URLSearchParams(); if (action) p.set('action', action); if (actor) p.set('actor', actor); p.set('limit', String(limit)) return u(api.get(`/api/admin/audit?${p}`)) } export const getSettings = () => u(api.get('/api/admin/settings')) export const updateSetting = (key: string, value: string) => u(api.put(`/api/admin/settings/${encodeURIComponent(key)}`, { value })) /* ───────────── 20. 통합 (sanitize) ───────────── */ export const getItsmSr = (srId: string) => u(api.get(`/api/mall/integration/itsm/sr/${srId}`)) export const getErpStock = (sku: string) => u(api.get(`/api/mall/integration/erp/stock/${sku}`)) export const getCrmInsight = (key: string) => u(api.get(`/api/mall/integration/crm/customer/${key}/insight`)) /* ───────────── 21. 등급별 고객관리 Loyalty ───────────── */ export const getTiers = (activeOnly = true) => u(api.get(`/api/mall/loyalty/tiers?activeOnly=${activeOnly}`)) export const getTier = (code: string) => u(api.get(`/api/mall/loyalty/tiers/${code}`)) export const createTier = (d: object) => u(api.post('/api/mall/loyalty/tiers', d)) export const updateTier = (id: number, d: object) => u(api.put(`/api/mall/loyalty/tiers/${id}`, d)) export const deleteTier = (id: number) => api.delete(`/api/mall/loyalty/tiers/${id}`) export const getLoyaltyMe = () => u(api.get('/api/mall/loyalty/me')) export const getPointBalance = () => u(api.get('/api/mall/loyalty/points/balance')) export const getPointHistory = (limit = 100) => u(api.get(`/api/mall/loyalty/points/history?limit=${limit}`)) export const usePoints = (points: number, orderNo: string) => u(api.post('/api/mall/loyalty/points/use', { points, orderNo })) export const adjustPoints = (owner: string, points: number, reason: string) => u(api.post('/api/mall/loyalty/points/adjust', { owner, points, reason })) export const recalcLoyalty = (owner = '') => u(api.post(`/api/mall/loyalty/recalc?owner=${owner}`, {})) export const recalcAllLoyalty = () => u(api.post('/api/mall/loyalty/recalc-all', {})) export const getLoyaltyByTier = (days = 30) => u(api.get(`/api/mall/loyalty/analytics/by-tier?days=${days}`)) /* ───────────── 22. 이벤트 / 캠페인 Event ───────────── */ export const getOngoingEvents = (tier = '') => u(api.get(`/api/mall/event/ongoing${tier ? `?tier=${tier}` : ''}`)) export const getEventBanners = () => u(api.get('/api/mall/event/banners')) export const getEvent = (id: number) => u(api.get(`/api/mall/event/${id}`)) export const joinEvent = (id: number) => u(api.post(`/api/mall/event/${id}/join`, {})) export const getAdminEvents = (status = '', type = '', activeOnly = false) => { const p = new URLSearchParams(); if (status) p.set('status', status); if (type) p.set('type', type); p.set('activeOnly', String(activeOnly)) return u(api.get(`/api/mall/event?${p}`)) } export const createEvent = (d: object) => u(api.post('/api/mall/event', d)) export const updateEvent = (id: number, d: object) => u(api.put(`/api/mall/event/${id}`, d)) export const publishEvent = (id: number) => u(api.post(`/api/mall/event/${id}/publish`, {})) export const endEvent = (id: number) => u(api.post(`/api/mall/event/${id}/end`, {})) export const deleteEvent = (id: number) => api.delete(`/api/mall/event/${id}`) export const getEventPerformance = (id: number) => u(api.get(`/api/mall/event/${id}/performance`)) export const aiEventCopy = (eventType: string, theme: string, tone: string) => u(api.post('/api/mall/event/ai/copy', { eventType, theme, tone }))