guardia-mall/frontend/src/api/client.ts

244 lines
17 KiB
TypeScript

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<T> = {success,message,data} → data 언랩
const u = <T = any>(p: Promise<any>): Promise<T> => 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 }))