diff --git a/public/images/menu-placeholder.png b/public/images/menu-placeholder.png new file mode 100644 index 0000000..f097a4b Binary files /dev/null and b/public/images/menu-placeholder.png differ diff --git a/src/services/api.js b/src/services/api.js index 6fbd09d..6ac5977 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,4 +1,5 @@ -//* src/services/api.js - 수정된 API URL 설정 +//* src/services/api.js - 수정된 버전 (createImageApiInstance 함수 추가) + import axios from 'axios' // 런타임 환경 설정에서 API URL 가져오기 @@ -11,10 +12,9 @@ const getApiUrls = () => { STORE_URL: config.STORE_URL || 'http://localhost:8082/api/store', CONTENT_URL: config.CONTENT_URL || 'http://localhost:8083/api/content', MENU_URL: config.MENU_URL || 'http://localhost:8082/api/menu', - // ⚠️ 수정: 매출 API는 store 서비스 (포트 8082) SALES_URL: config.SALES_URL || 'http://localhost:8082/api/sales', - // ⚠️ 수정: 추천 API는 ai-recommend 서비스 (포트 8084) - RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendations' + RECOMMEND_URL: config.RECOMMEND_URL || 'http://localhost:8084/api/recommendations', + IMAGE_URL: config.IMAGE_URL || 'http://localhost:8082/api/images' } } @@ -37,7 +37,6 @@ const createApiInstance = (baseURL) => { config.headers.Authorization = `Bearer ${token}` } - // ⚠️ 추가: 요청 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.log(`🌐 [API_REQ] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) } @@ -52,14 +51,12 @@ const createApiInstance = (baseURL) => { // 응답 인터셉터 - 토큰 갱신 및 에러 처리 instance.interceptors.response.use( (response) => { - // ⚠️ 추가: 응답 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.log(`✅ [API_RES] ${response.status} ${response.config?.method?.toUpperCase()} ${response.config?.url}`) } return response }, async (error) => { - // ⚠️ 추가: 에러 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.error(`❌ [API_ERR] ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error.response?.data) } @@ -102,10 +99,155 @@ const createApiInstance = (baseURL) => { return instance } +// ✅ 이미지 업로드 전용 API 인스턴스 생성 함수 추가 +const createImageApiInstance = (baseURL) => { + const instance = axios.create({ + baseURL, + timeout: 60000, // 이미지 업로드는 시간이 더 걸릴 수 있음 + headers: { + Accept: 'application/json', + }, + }) + + // 요청 인터셉터 - JWT 토큰 자동 추가 + instance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('accessToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + if (import.meta.env.DEV) { + console.log(`🌐 [IMG_REQ] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) + console.log('FormData 포함:', config.data instanceof FormData) + } + + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + // 응답 인터셉터 + instance.interceptors.response.use( + (response) => { + if (import.meta.env.DEV) { + console.log(`✅ [IMG_RES] ${response.status} ${response.config?.method?.toUpperCase()} ${response.config?.url}`) + } + return response + }, + async (error) => { + if (import.meta.env.DEV) { + console.error(`❌ [IMG_ERR] ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error.response?.data) + } + + // 토큰 갱신 로직은 기존과 동일 + const originalRequest = error.config + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + try { + const refreshToken = localStorage.getItem('refreshToken') + if (refreshToken) { + const refreshResponse = await axios.post(`${getApiUrls().AUTH_URL}/refresh`, { + refreshToken, + }) + const { accessToken, refreshToken: newRefreshToken } = refreshResponse.data.data + localStorage.setItem('accessToken', accessToken) + localStorage.setItem('refreshToken', newRefreshToken) + originalRequest.headers.Authorization = `Bearer ${accessToken}` + return instance(originalRequest) + } + } catch (refreshError) { + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + localStorage.removeItem('userInfo') + window.location.href = '/login' + } + } + return Promise.reject(error) + }, + ) + + return instance +} + +// ✅ 메뉴 이미지 업로드 전용 API 인스턴스 생성 함수 추가 +const createMenuImageApiInstance = (baseURL) => { + const instance = axios.create({ + baseURL, + timeout: 60000, // 이미지 업로드는 시간이 더 걸릴 수 있음 + headers: { + Accept: 'application/json', + }, + }) + + // 요청 인터셉터 - JWT 토큰 자동 추가 + instance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('accessToken') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + if (import.meta.env.DEV) { + console.log(`🌐 [MENU_IMG_REQ] ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`) + console.log('FormData 포함:', config.data instanceof FormData) + } + + return config + }, + (error) => { + return Promise.reject(error) + }, + ) + + // 응답 인터셉터 + instance.interceptors.response.use( + (response) => { + if (import.meta.env.DEV) { + console.log(`✅ [MENU_IMG_RES] ${response.status} ${response.config?.method?.toUpperCase()} ${response.config?.url}`) + } + return response + }, + async (error) => { + if (import.meta.env.DEV) { + console.error(`❌ [MENU_IMG_ERR] ${error.response?.status || 'Network'} ${error.config?.method?.toUpperCase()} ${error.config?.url}`, error.response?.data) + } + + // 토큰 갱신 로직은 기존과 동일 + const originalRequest = error.config + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true + try { + const refreshToken = localStorage.getItem('refreshToken') + if (refreshToken) { + const refreshResponse = await axios.post(`${getApiUrls().AUTH_URL}/refresh`, { + refreshToken, + }) + const { accessToken, refreshToken: newRefreshToken } = refreshResponse.data.data + localStorage.setItem('accessToken', accessToken) + localStorage.setItem('refreshToken', newRefreshToken) + originalRequest.headers.Authorization = `Bearer ${accessToken}` + return instance(originalRequest) + } + } catch (refreshError) { + localStorage.removeItem('accessToken') + localStorage.removeItem('refreshToken') + localStorage.removeItem('userInfo') + window.location.href = '/login' + } + } + return Promise.reject(error) + }, + ) + + return instance +} + // API 인스턴스들 생성 const apiUrls = getApiUrls() -// ⚠️ 추가: API URL 확인 로깅 (개발 환경에서만) if (import.meta.env.DEV) { console.log('🔧 [API_CONFIG] API URLs 설정:', apiUrls) } @@ -115,8 +257,11 @@ export const authApi = createApiInstance(apiUrls.AUTH_URL) export const storeApi = createApiInstance(apiUrls.STORE_URL) export const contentApi = createApiInstance(apiUrls.CONTENT_URL) export const menuApi = createApiInstance(apiUrls.MENU_URL) +export const menuImageApi = createMenuImageApiInstance(apiUrls.MENU_URL) // ✅ 추가 export const salesApi = createApiInstance(apiUrls.SALES_URL) export const recommendApi = createApiInstance(apiUrls.RECOMMEND_URL) +export const imageApi = createApiInstance(apiUrls.IMAGE_URL) +export const apiWithImage = imageApi // 별칭 (기존 코드 호환성) // 기본 API 인스턴스 (Gateway URL 사용) export const api = createApiInstance(apiUrls.GATEWAY_URL) @@ -185,7 +330,7 @@ export const formatSuccessResponse = (data, message = '요청이 성공적으로 } } -// ⚠️ 추가: API 상태 확인 함수 +// API 상태 확인 함수 export const checkApiHealth = async () => { const results = {} @@ -214,11 +359,14 @@ export const checkApiHealth = async () => { return results } -// ⚠️ 추가: 개발 환경에서 전역 노출 +// 개발 환경에서 전역 노출 if (import.meta.env.DEV) { window.__api_debug__ = { urls: apiUrls, - instances: { memberApi, authApi, storeApi, contentApi, menuApi, salesApi, recommendApi }, + instances: { + memberApi, authApi, storeApi, contentApi, menuApi, menuImageApi, + salesApi, recommendApi, imageApi + }, checkHealth: checkApiHealth } console.log('🔧 [DEBUG] API 인스턴스가 window.__api_debug__에 노출됨') diff --git a/src/store/index.js b/src/store/index.js index 8ff224a..98da823 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -79,80 +79,88 @@ export const useStoreStore = defineStore('store', { } }, - /** - * 메뉴 목록 조회 - 실제 API 연동 (매장 ID 필요) - ✅ ID 필드 보장 - */ - async fetchMenus() { - console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===') + // src/store/index.js에서 fetchMenus 부분만 수정 + +/** + * 메뉴 목록 조회 - 실제 API 연동 (매장 ID 필요) - ✅ 이미지 필드 매핑 수정 + */ +async fetchMenus() { + console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===') + + try { + // 매장 정보에서 storeId 가져오기 + const storeId = this.storeInfo?.storeId + if (!storeId) { + console.warn('매장 ID가 없습니다. 매장 정보를 먼저 조회해주세요.') + return { success: false, message: '매장 정보가 필요합니다', data: [] } + } + + // 메뉴 서비스 임포트 + const { menuService } = await import('@/services/menu') + + console.log('메뉴 목록 API 호출, 매장 ID:', storeId) + const result = await menuService.getMenus(storeId) + + console.log('=== Store 스토어: 메뉴 API 응답 분석 ===') + console.log('Result:', result) + console.log('Result.success:', result.success) + console.log('Result.data:', result.data) + console.log('Result.message:', result.message) + + if (result.success && result.data) { + // ✅ 백엔드 MenuResponse의 필드명에 맞게 매핑 수정 + const menusWithId = (result.data || []).map(menu => { + // ID 필드가 확실히 있도록 보장 + const menuId = menu.menuId || menu.id + + if (!menuId) { + console.warn('⚠️ 메뉴 ID가 없는 항목 발견:', menu) + } + + console.log('메뉴 원본 데이터:', menu) // 디버깅용 + + return { + ...menu, + id: menuId, // ✅ id 필드 확실히 설정 + menuId: menuId, // ✅ menuId 필드도 설정 + // 기타 필드들 보장 + menuName: menu.menuName || menu.name || '이름 없음', + category: menu.category || '기타', + price: menu.price || 0, + description: menu.description || '', + available: menu.available !== undefined ? menu.available : true, + recommended: menu.recommended !== undefined ? menu.recommended : false, + // ✅ 이미지 필드 수정: 백엔드는 'image' 필드 사용 + imageUrl: menu.image || menu.imageUrl || '/images/menu-placeholder.png', + image: menu.image || menu.imageUrl, // 백엔드 호환성 + createdAt: menu.createdAt, + updatedAt: menu.updatedAt + } + }) - try { - // 매장 정보에서 storeId 가져오기 - const storeId = this.storeInfo?.storeId - if (!storeId) { - console.warn('매장 ID가 없습니다. 매장 정보를 먼저 조회해주세요.') - return { success: false, message: '매장 정보가 필요합니다', data: [] } - } - - // 메뉴 서비스 임포트 - const { menuService } = await import('@/services/menu') - - console.log('메뉴 목록 API 호출, 매장 ID:', storeId) - const result = await menuService.getMenus(storeId) - - console.log('=== Store 스토어: 메뉴 API 응답 분석 ===') - console.log('Result:', result) - console.log('Result.success:', result.success) - console.log('Result.data:', result.data) - console.log('Result.message:', result.message) - - if (result.success && result.data) { - // ✅ 메뉴 데이터 ID 필드 보장 처리 - const menusWithId = (result.data || []).map(menu => { - // ID 필드가 확실히 있도록 보장 - const menuId = menu.menuId || menu.id - - if (!menuId) { - console.warn('⚠️ 메뉴 ID가 없는 항목 발견:', menu) - } - - return { - ...menu, - id: menuId, // ✅ id 필드 확실히 설정 - menuId: menuId, // ✅ menuId 필드도 설정 - // 기타 필드들 보장 - menuName: menu.menuName || menu.name || '이름 없음', - category: menu.category || '기타', - price: menu.price || 0, - description: menu.description || '', - available: menu.available !== undefined ? menu.available : true, - recommended: menu.recommended !== undefined ? menu.recommended : false, - imageUrl: menu.imageUrl || '/images/menu-placeholder.png' - } - }) - - // 메뉴 목록이 있는 경우 - console.log('✅ 메뉴 목록 설정 (ID 보장됨):', menusWithId) - this.menus = menusWithId - return { success: true, data: menusWithId } - } else { - // 메뉴가 없거나 조회 실패한 경우 - console.log('⚠️ 메뉴 목록 없음 또는 조회 실패') - this.menus = [] - - if (result.message === '등록된 메뉴가 없습니다') { - return { success: false, message: '등록된 메뉴가 없습니다', data: [] } - } else { - return { success: false, message: result.message || '메뉴 목록 조회에 실패했습니다', data: [] } - } - } - } catch (error) { - console.error('=== Store 스토어: 메뉴 목록 조회 실패 ===') - console.error('Error:', error) - - this.menus = [] - return { success: false, message: error.message || '메뉴 목록을 불러오는데 실패했습니다', data: [] } + // 메뉴 목록이 있는 경우 + console.log('✅ 메뉴 목록 설정 (이미지 필드 매핑 완료):', menusWithId) + this.menus = menusWithId + return { success: true, data: menusWithId } + } else { + // 메뉴가 없거나 조회 실패한 경우 + console.log('⚠️ 메뉴 목록 없음 또는 조회 실패') + this.menus = [] + + if (result.message === '등록된 메뉴가 없습니다') { + return { success: false, message: '등록된 메뉴가 없습니다', data: [] } + } else { + return { success: false, message: result.message || '메뉴 목록 조회에 실패했습니다', data: [] } } - }, + } + } catch (error) { + console.error('=== Store 스토어: 메뉴 목록 조회 실패 ===') + console.error('Error:', error) + + this.menus = [] + return { success: false, message: error.message || '메뉴 목록을 불러오는데 실패했습니다', data: [] } + } + }, /** * 매장 등록 diff --git a/src/views/ContentManagementView.vue b/src/views/ContentManagementView.vue index 26006c9..c927e13 100644 --- a/src/views/ContentManagementView.vue +++ b/src/views/ContentManagementView.vue @@ -1,351 +1,359 @@ //* src/views/ContentManagementView.vue