store menu source edit

This commit is contained in:
unknown 2025-06-16 16:33:19 +09:00
parent 76a69a6453
commit 87871709f2
4 changed files with 1013 additions and 253 deletions

210
src/services/menu.js Normal file
View File

@ -0,0 +1,210 @@
// src/services/menu.js - 메뉴 관련 API 서비스
import { menuApi, handleApiError, formatSuccessResponse } from './api.js'
/**
* 메뉴 관련 API 서비스
* 백엔드 Menu Controller와 연동 (포트 8082)
*/
class MenuService {
/**
* 메뉴 목록 조회
* @param {number} storeId - 매장 ID
* @returns {Promise<Object>} 메뉴 목록
*/
async getMenus(storeId) {
try {
console.log('=== 메뉴 목록 조회 API 호출 ===')
console.log('매장 ID:', storeId)
if (!storeId) {
throw new Error('매장 ID가 필요합니다')
}
// GET /api/menu?storeId={storeId}
const response = await menuApi.get('', {
params: { storeId }
})
console.log('메뉴 목록 조회 API 응답:', response.data)
if (response.data && response.data.status === 200) {
return formatSuccessResponse(response.data.data, '메뉴 목록을 조회했습니다.')
} else {
throw new Error(response.data.message || '메뉴 목록을 찾을 수 없습니다.')
}
} catch (error) {
console.error('메뉴 목록 조회 실패:', error)
// 404 오류 또는 네트워크 오류 시 빈 배열 반환 (개발 중)
if (error.response?.status === 404 ||
error.code === 'ECONNREFUSED' ||
error.message.includes('Network Error')) {
console.warn('백엔드 미구현 또는 네트워크 오류 - 빈 메뉴 목록 반환')
return formatSuccessResponse([], '메뉴 목록이 비어있습니다.')
}
return handleApiError(error)
}
}
/**
* 메뉴 상세 조회
* @param {number} menuId - 메뉴 ID
* @returns {Promise<Object>} 메뉴 상세 정보
*/
async getMenuDetail(menuId) {
try {
console.log('=== 메뉴 상세 조회 API 호출 ===')
console.log('메뉴 ID:', menuId)
if (!menuId || menuId === 'undefined') {
throw new Error('올바른 메뉴 ID가 필요합니다')
}
const numericMenuId = parseInt(menuId)
if (isNaN(numericMenuId)) {
throw new Error('메뉴 ID는 숫자여야 합니다')
}
// GET /api/menu/{menuId}
const response = await menuApi.get(`/${numericMenuId}`)
console.log('메뉴 상세 조회 API 응답:', response.data)
if (response.data && response.data.status === 200) {
return formatSuccessResponse(response.data.data, '메뉴 상세 정보를 조회했습니다.')
} else {
throw new Error(response.data.message || '메뉴를 찾을 수 없습니다.')
}
} catch (error) {
console.error('메뉴 상세 조회 실패:', error)
return handleApiError(error)
}
}
/**
* 메뉴 등록
* @param {Object} menuData - 메뉴 정보
* @returns {Promise<Object>} 등록 결과
*/
async createMenu(menuData) {
try {
console.log('=== 메뉴 등록 API 호출 ===')
console.log('요청 데이터:', menuData)
const requestData = {
storeId: menuData.storeId,
menuName: menuData.menuName,
category: menuData.category,
price: parseInt(menuData.price) || 0,
description: menuData.description || ''
}
console.log('백엔드 전송 데이터:', requestData)
// POST /api/menu/register
const response = await menuApi.post('/register', requestData)
console.log('메뉴 등록 API 응답:', response.data)
if (response.data && response.data.status === 200) {
return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 등록되었습니다.')
} else {
throw new Error(response.data.message || '메뉴 등록에 실패했습니다.')
}
} catch (error) {
console.error('메뉴 등록 실패:', error)
return handleApiError(error)
}
}
/**
* 메뉴 수정
* @param {number} menuId - 메뉴 ID
* @param {Object} menuData - 수정할 메뉴 정보
* @returns {Promise<Object>} 수정 결과
*/
async updateMenu(menuId, menuData) {
try {
console.log('=== 메뉴 수정 API 호출 ===')
console.log('메뉴 ID:', menuId)
console.log('수정 데이터:', menuData)
if (!menuId || menuId === 'undefined') {
throw new Error('올바른 메뉴 ID가 필요합니다')
}
const numericMenuId = parseInt(menuId)
if (isNaN(numericMenuId)) {
throw new Error('메뉴 ID는 숫자여야 합니다')
}
const requestData = {
menuName: menuData.menuName,
category: menuData.category,
price: parseInt(menuData.price) || 0,
description: menuData.description || ''
}
console.log('백엔드 전송 데이터:', requestData)
// PUT /api/menu/{menuId}
const response = await menuApi.put(`/${numericMenuId}`, requestData)
console.log('메뉴 수정 API 응답:', response.data)
if (response.data && response.data.status === 200) {
return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 수정되었습니다.')
} else {
throw new Error(response.data.message || '메뉴 수정에 실패했습니다.')
}
} catch (error) {
console.error('메뉴 수정 실패:', error)
return handleApiError(error)
}
}
/**
* 메뉴 삭제
* @param {number} menuId - 메뉴 ID
* @returns {Promise<Object>} 삭제 결과
*/
async deleteMenu(menuId) {
try {
console.log('=== 메뉴 삭제 API 호출 ===')
console.log('메뉴 ID:', menuId)
if (!menuId || menuId === 'undefined') {
throw new Error('올바른 메뉴 ID가 필요합니다')
}
const numericMenuId = parseInt(menuId)
if (isNaN(numericMenuId)) {
throw new Error('메뉴 ID는 숫자여야 합니다')
}
// DELETE /api/menu/{menuId}
const response = await menuApi.delete(`/${numericMenuId}`)
console.log('메뉴 삭제 API 응답:', response.data)
if (response.data && response.data.status === 200) {
return formatSuccessResponse(response.data.data, '메뉴가 성공적으로 삭제되었습니다.')
} else {
throw new Error(response.data.message || '메뉴 삭제에 실패했습니다.')
}
} catch (error) {
console.error('메뉴 삭제 실패:', error)
return handleApiError(error)
}
}
}
// 싱글톤 인스턴스 생성 및 export
export const menuService = new MenuService()
export default menuService
// 디버깅을 위한 전역 노출 (개발 환경에서만)
if (process.env.NODE_ENV === 'development') {
window.menuService = menuService
}

View File

@ -80,51 +80,53 @@ export const useStoreStore = defineStore('store', {
}, },
/** /**
* 메뉴 목록 조회 - 추가된 메서드 * 메뉴 목록 조회 - 실제 API 연동 (매장 ID 필요)
*/ */
async fetchMenus() { async fetchMenus() {
console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===') console.log('=== Store 스토어: 메뉴 목록 조회 시작 ===')
try { try {
// 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) // 매장 정보에서 storeId 가져오기
const mockMenus = [ const storeId = this.storeInfo?.storeId
{ if (!storeId) {
id: 1, console.warn('매장 ID가 없습니다. 매장 정보를 먼저 조회해주세요.')
name: '아메리카노', return { success: false, message: '매장 정보가 필요합니다', data: [] }
price: 4000,
category: '커피',
description: '진한 풍미의 아메리카노',
imageUrl: '/images/americano.jpg',
isAvailable: true
},
{
id: 2,
name: '카페라떼',
price: 4500,
category: '커피',
description: '부드러운 우유가 들어간 라떼',
imageUrl: '/images/latte.jpg',
isAvailable: true
},
{
id: 3,
name: '치즈케이크',
price: 6000,
category: '디저트',
description: '진한 크림치즈로 만든 케이크',
imageUrl: '/images/cheesecake.jpg',
isAvailable: false
} }
]
this.menus = mockMenus // 메뉴 서비스 임포트
console.log('✅ 메뉴 목록 설정 완료:', mockMenus) const { menuService } = await import('@/services/menu')
return { success: true, data: mockMenus } console.log('메뉴 목록 API 호출, 매장 ID:', storeId)
} catch (error) { const result = await menuService.getMenus(storeId)
console.error('메뉴 목록 조회 실패:', error)
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) {
// 메뉴 목록이 있는 경우
console.log('✅ 메뉴 목록 설정:', result.data)
this.menus = result.data
return { success: true, data: result.data }
} else {
// 메뉴가 없거나 조회 실패한 경우
console.log('⚠️ 메뉴 목록 없음 또는 조회 실패')
this.menus = [] this.menus = []
return { success: false, message: '메뉴 목록을 불러오는데 실패했습니다' }
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: [] }
} }
}, },

View File

@ -1,233 +1,164 @@
//* src/services/store.js - 매장 서비스 완전 수정 // src/store/store.js - StoreService 클래스의 getMenus 메서드 올바른 문법으로 수정
import { storeApi, handleApiError, formatSuccessResponse } from './api.js'
/** /**
* 매장 관련 API 서비스 * 메뉴 목록 조회 (수정된 버전 - storeId 파라미터 추가)
* 백엔드 Store Controller와 연동 (포트 8082) * @param {number} storeId - 매장 ID (옵션, 없으면 목업 데이터 반환)
*/
class StoreService {
/**
* 매장 등록 (STR-015: 매장 등록)
* @param {Object} storeData - 매장 정보
* @returns {Promise<Object>} 매장 등록 결과
*/
async registerStore(storeData) {
try {
console.log('=== 매장 등록 API 호출 ===')
console.log('요청 데이터:', storeData)
// 백엔드 StoreCreateRequest에 맞는 형태로 변환
const requestData = {
storeName: storeData.storeName,
businessType: storeData.businessType,
address: storeData.address,
phoneNumber: storeData.phoneNumber,
businessHours: storeData.businessHours,
closedDays: storeData.closedDays,
seatCount: parseInt(storeData.seatCount) || 0,
instaAccounts: storeData.instaAccounts || '',
blogAccounts: storeData.blogAccounts || '',
description: storeData.description || ''
}
console.log('=== 각 필드 상세 검증 ===')
console.log('storeName:', requestData.storeName, '(타입:', typeof requestData.storeName, ')')
console.log('businessType:', requestData.businessType, '(타입:', typeof requestData.businessType, ')')
console.log('address:', requestData.address, '(타입:', typeof requestData.address, ')')
console.log('seatCount:', requestData.seatCount, '(타입:', typeof requestData.seatCount, ')')
console.log('백엔드 전송 데이터:', requestData)
const response = await storeApi.post('/register', requestData)
console.log('매장 등록 API 응답:', response.data)
// 백엔드 응답 구조에 맞게 처리
if (response.data && (response.data.status === 200 || response.data.success !== false)) {
return {
success: true,
message: response.data.message || '매장이 등록되었습니다.',
data: response.data.data
}
} else {
throw new Error(response.data.message || '매장 등록에 실패했습니다.')
}
} catch (error) {
console.error('매장 등록 실패:', error)
if (error.response) {
console.error('응답 상태:', error.response.status)
console.error('응답 데이터:', error.response.data)
}
return handleApiError(error)
}
}
/**
* 매장 정보 조회 (STR-005: 매장 정보 관리)
* @returns {Promise<Object>} 매장 정보
*/
async getStore() {
try {
console.log('=== 매장 정보 조회 API 호출 ===')
const response = await storeApi.get('/')
console.log('매장 정보 조회 API 응답:', response.data)
// 백엔드 응답 구조에 맞게 처리
if (response.data && response.data.data) {
return {
success: true,
message: '매장 정보를 조회했습니다.',
data: response.data.data
}
} else if (response.data && response.data.data === null) {
// 매장이 없는 경우
return {
success: false,
message: '등록된 매장이 없습니다',
data: null
}
} else {
throw new Error(response.data.message || '매장 정보를 찾을 수 없습니다.')
}
} catch (error) {
console.error('매장 정보 조회 실패:', error)
// 404 오류 처리 (매장이 없음)
if (error.response?.status === 404) {
return {
success: false,
message: '등록된 매장이 없습니다',
data: null
}
}
// 500 오류 처리 (서버 내부 오류)
if (error.response?.status === 500) {
console.error('서버 내부 오류 - 백엔드 로그 확인 필요:', error.response?.data)
return {
success: false,
message: '서버 오류가 발생했습니다. 관리자에게 문의하세요.',
data: null
}
}
return handleApiError(error)
}
}
/**
* 매장 정보 수정 (STR-010: 매장 수정)
* @param {number} storeId - 매장 ID (현재는 사용하지 않음 - JWT에서 사용자 확인)
* @param {Object} storeData - 수정할 매장 정보
* @returns {Promise<Object>} 매장 수정 결과
*/
async updateStore(storeId, storeData) {
try {
console.log('=== 매장 정보 수정 API 호출 ===')
console.log('요청 데이터:', storeData)
// 백엔드 StoreUpdateRequest에 맞는 형태로 변환
const requestData = {
storeName: storeData.storeName,
businessType: storeData.businessType,
address: storeData.address,
phoneNumber: storeData.phoneNumber,
businessHours: storeData.businessHours,
closedDays: storeData.closedDays,
seatCount: parseInt(storeData.seatCount) || 0,
instaAccounts: storeData.instaAccounts || '',
blogAccounts: storeData.blogAccounts || '',
description: storeData.description || ''
}
console.log('백엔드 전송 데이터:', requestData)
// PUT 요청 (storeId는 JWT에서 추출하므로 URL에 포함하지 않음)
const response = await storeApi.put('/', requestData)
console.log('매장 정보 수정 API 응답:', response.data)
if (response.data && (response.data.status === 200 || response.data.success !== false)) {
return {
success: true,
message: response.data.message || '매장 정보가 수정되었습니다.',
data: response.data.data
}
} else {
throw new Error(response.data.message || '매장 정보 수정에 실패했습니다.')
}
} catch (error) {
console.error('매장 정보 수정 실패:', error)
return handleApiError(error)
}
}
/**
* 매출 정보 조회 (STR-020: 대시보드)
* @param {string} period - 조회 기간 (today, week, month, year)
* @returns {Promise<Object>} 매출 정보
*/
async getSales(period = 'today') {
try {
// 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정)
const mockSalesData = {
todaySales: 150000,
yesterdaySales: 120000,
changeRate: 25.0,
monthlyTarget: 3000000,
achievementRate: 45.2
}
return formatSuccessResponse(mockSalesData, '매출 정보를 조회했습니다.')
} catch (error) {
return handleApiError(error)
}
}
/**
* 메뉴 목록 조회 (개발 예정)
* @returns {Promise<Object>} 메뉴 목록 * @returns {Promise<Object>} 메뉴 목록
*/ */
async getMenus() { async getMenus(storeId) {
try { try {
// 현재는 목업 데이터 반환 (추후 실제 API 연동 시 수정) console.log('=== 메뉴 목록 조회 API 호출 ===')
console.log('매장 ID:', storeId)
// storeId가 없으면 목업 데이터 반환 (개발 중)
if (!storeId) {
console.warn('매장 ID가 없어서 목업 데이터 반환')
const mockMenus = [ const mockMenus = [
{ {
id: 1, menuId: 1, // id 대신 menuId 사용
id: 1, // 호환성을 위해
name: '아메리카노', name: '아메리카노',
menuName: '아메리카노', // 백엔드 형식
price: 4000, price: 4000,
category: '커피', category: '커피',
description: '진한 풍미의 아메리카노', description: '진한 풍미의 아메리카노',
imageUrl: '/images/americano.jpg', imageUrl: '/images/americano.jpg',
isAvailable: true isAvailable: true,
available: true // 백엔드 형식
}, },
{ {
menuId: 2,
id: 2, id: 2,
name: '카페라떼', name: '카페라떼',
menuName: '카페라떼',
price: 4500, price: 4500,
category: '커피', category: '커피',
description: '부드러운 우유가 들어간 라떼', description: '부드러운 우유가 들어간 라떼',
imageUrl: '/images/latte.jpg', imageUrl: '/images/latte.jpg',
isAvailable: true isAvailable: true,
available: true
} }
] ]
return formatSuccessResponse(mockMenus, '메뉴 목록을 조회했습니다.') return formatSuccessResponse(mockMenus, '목업 메뉴 목록을 조회했습니다.')
}
// 실제 백엔드 API 호출
try {
// 메뉴 API import
const { menuApi } = await import('./api.js')
// GET /api/menu?storeId={storeId}
const response = await menuApi.get('', {
params: { storeId }
})
console.log('메뉴 목록 조회 API 응답:', response.data)
if (response.data && response.data.status === 200) {
// 백엔드에서 받은 메뉴 데이터를 프론트엔드 형식으로 변환
const menus = response.data.data.map(menu => ({
menuId: menu.menuId,
id: menu.menuId, // 호환성을 위해
storeId: menu.storeId,
menuName: menu.menuName,
name: menu.menuName, // 호환성을 위해
category: menu.category,
price: menu.price,
description: menu.description,
available: menu.available !== undefined ? menu.available : true,
isAvailable: menu.available !== undefined ? menu.available : true, // 호환성
imageUrl: menu.imageUrl || '/images/menu-placeholder.png',
createdAt: menu.createdAt,
updatedAt: menu.updatedAt
}))
return formatSuccessResponse(menus, '메뉴 목록을 조회했습니다.')
} else {
throw new Error(response.data.message || '메뉴 목록을 찾을 수 없습니다.')
}
} catch (apiError) {
console.error('백엔드 API 호출 실패:', apiError)
// 백엔드 미구현이나 네트워크 오류 시 목업 데이터 반환
if (apiError.response?.status === 404 ||
apiError.code === 'ECONNREFUSED' ||
apiError.message.includes('Network Error')) {
console.warn('백엔드 미구현 - 목업 데이터 반환')
const mockMenus = [
{
menuId: 1,
id: 1,
storeId: storeId,
name: '아메리카노',
menuName: '아메리카노',
price: 4000,
category: '커피',
description: '진한 풍미의 아메리카노',
imageUrl: '/images/americano.jpg',
isAvailable: true,
available: true
},
{
menuId: 2,
id: 2,
storeId: storeId,
name: '카페라떼',
menuName: '카페라떼',
price: 4500,
category: '커피',
description: '부드러운 우유가 들어간 라떼',
imageUrl: '/images/latte.jpg',
isAvailable: true,
available: true
}
]
return formatSuccessResponse(mockMenus, '목업 메뉴 목록을 조회했습니다. (백엔드 미구현)')
}
throw apiError
}
} catch (error) { } catch (error) {
console.error('메뉴 목록 조회 실패:', error)
return handleApiError(error) return handleApiError(error)
} }
}
// 만약 fetchMenus 메서드가 따로 필요하다면 다음과 같이 추가:
/**
* 메뉴 목록 조회 (fetchMenus 별칭)
* @param {number} storeId - 매장 ID
* @returns {Promise<Object>} 메뉴 목록
*/
async fetchMenus(storeId) {
return await this.getMenus(storeId)
}
// StoreService 클래스 전체 구조 예시:
class StoreService {
// ... 기존 메서드들 (registerStore, getStore, updateStore 등) ...
/**
* 메뉴 목록 조회 (위의 getMenus 메서드)
*/
async getMenus(storeId) {
// 위의 구현 내용
}
/**
* 메뉴 목록 조회 별칭
*/
async fetchMenus(storeId) {
return await this.getMenus(storeId)
} }
} }
// 싱글톤 인스턴스 생성 및 export // 올바른 JavaScript 클래스 메서드 문법:
export const storeService = new StoreService() // ❌ 잘못된 문법:
export default storeService // async function getMenus(storeId) { ... }
// function async getMenus(storeId) { ... }
// 디버깅을 위한 전역 노출 (개발 환경에서만) // ✅ 올바른 문법:
if (process.env.NODE_ENV === 'development') { // async getMenus(storeId) { ... }
window.storeService = storeService
}

View File

@ -518,6 +518,267 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- 메뉴 상세 다이얼로그 -->
<v-dialog
v-model="showMenuDetailDialog"
max-width="600"
persistent
>
<v-card v-if="selectedMenu">
<v-card-title class="pa-4">
<div class="d-flex align-center justify-space-between w-100">
<div class="d-flex align-center">
<v-icon class="mr-2" color="primary">mdi-food</v-icon>
메뉴 상세 정보
</div>
<div class="d-flex align-center">
<!-- 상태 배지 -->
<v-chip
:color="selectedMenu.available ? 'success' : 'error'"
size="small"
variant="elevated"
class="mr-2"
>
{{ selectedMenu.available ? '판매중' : '품절' }}
</v-chip>
<!-- 추천 메뉴 배지 -->
<v-chip
v-if="selectedMenu.recommended"
color="warning"
size="small"
variant="elevated"
>
<v-icon size="small" class="mr-1">mdi-star</v-icon>
추천
</v-chip>
</div>
</div>
</v-card-title>
<v-divider />
<v-card-text class="pa-0">
<!-- 메뉴 이미지 -->
<v-img
:src="selectedMenu.imageUrl || selectedMenu.image || '/images/menu-placeholder.png'"
:alt="selectedMenu.menuName"
height="300"
cover
class="mb-4"
>
<template v-slot:placeholder>
<div class="d-flex align-center justify-center fill-height">
<v-icon size="64" color="grey-lighten-2">mdi-food</v-icon>
</div>
</template>
</v-img>
<div class="pa-4">
<!-- 메뉴 기본 정보 -->
<div class="mb-4">
<div class="d-flex align-center justify-space-between mb-2">
<h2 class="text-h5 font-weight-bold">{{ selectedMenu.menuName }}</h2>
<span class="text-h4 font-weight-bold text-primary">
{{ formatCurrency(selectedMenu.price) }}
</span>
</div>
<v-chip
:color="getCategoryColor(selectedMenu.category)"
size="small"
variant="tonal"
class="mb-3"
>
{{ selectedMenu.category }}
</v-chip>
<p class="text-body-1 text-grey-darken-1 mb-0">
{{ selectedMenu.description || '메뉴 설명이 없습니다.' }}
</p>
</div>
<!-- 메뉴 상세 정보 -->
<v-divider class="my-4" />
<div class="mb-4">
<h4 class="text-h6 font-weight-bold mb-3 d-flex align-center">
<v-icon class="mr-2" color="primary">mdi-information</v-icon>
상세 정보
</h4>
<v-row>
<v-col cols="6">
<div class="info-item">
<v-icon class="mr-2" color="grey">mdi-tag</v-icon>
<div>
<div class="text-caption text-grey">카테고리</div>
<div class="text-body-1">{{ selectedMenu.category }}</div>
</div>
</div>
</v-col>
<v-col cols="6">
<div class="info-item">
<v-icon class="mr-2" color="grey">mdi-currency-krw</v-icon>
<div>
<div class="text-caption text-grey">가격</div>
<div class="text-body-1">{{ formatCurrency(selectedMenu.price) }}</div>
</div>
</div>
</v-col>
</v-row>
</div>
</div>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn
color="grey"
variant="outlined"
@click="closeMenuDetail"
>
닫기
</v-btn>
<v-btn
color="primary"
variant="outlined"
prepend-icon="mdi-pencil"
@click="editMenuFromDetail"
>
수정
</v-btn>
<v-btn
color="error"
variant="outlined"
prepend-icon="mdi-delete"
@click="deleteMenuFromDetail"
>
삭제
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 메뉴 등록/수정 다이얼로그 -->
<v-dialog
v-model="showMenuDialog"
max-width="600"
persistent
>
<v-card>
<v-card-title class="pa-4">
<div class="d-flex align-center">
<v-icon class="mr-2" color="primary">mdi-food-plus</v-icon>
{{ menuEditMode ? '메뉴 수정' : '메뉴 등록' }}
</div>
</v-card-title>
<v-divider />
<v-card-text class="pa-4">
<v-form ref="menuFormRef" v-model="formValid">
<v-row>
<!-- 메뉴명 -->
<v-col cols="12">
<v-text-field
v-model="menuFormData.menuName"
label="메뉴명 *"
:rules="[v => !!v || '메뉴명을 입력해주세요']"
prepend-inner-icon="mdi-food"
variant="outlined"
/>
</v-col>
<!-- 카테고리 -->
<v-col cols="12" sm="6">
<v-select
v-model="menuFormData.category"
label="카테고리 *"
:items="menuCategories"
:rules="[v => !!v || '카테고리를 선택해주세요']"
prepend-inner-icon="mdi-tag"
variant="outlined"
/>
</v-col>
<!-- 가격 -->
<v-col cols="12" sm="6">
<v-text-field
v-model.number="menuFormData.price"
label="가격 *"
type="number"
:rules="[v => !!v || '가격을 입력해주세요', v => v > 0 || '가격은 0원보다 커야 합니다']"
prepend-inner-icon="mdi-currency-krw"
variant="outlined"
:min="0"
/>
</v-col>
<!-- 메뉴 설명 -->
<v-col cols="12">
<v-textarea
v-model="menuFormData.description"
label="메뉴 설명"
prepend-inner-icon="mdi-text"
variant="outlined"
rows="3"
:rules="[v => !v || v.length <= 500 || '설명은 500자 이하로 입력해주세요']"
/>
</v-col>
<!-- 메뉴 옵션 -->
<v-col cols="12">
<v-row>
<v-col cols="6">
<v-switch
v-model="menuFormData.available"
label="판매 중"
color="success"
hide-details
/>
</v-col>
<v-col cols="6">
<v-switch
v-model="menuFormData.recommended"
label="추천 메뉴"
color="warning"
hide-details
/>
</v-col>
</v-row>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-divider />
<v-card-actions class="pa-4">
<v-spacer />
<v-btn
color="grey"
variant="outlined"
@click="cancelMenuForm"
:disabled="saving"
>
취소
</v-btn>
<v-btn
color="primary"
@click="saveMenu"
:loading="saving"
:disabled="!formValid"
>
{{ menuEditMode ? '수정' : '등록' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 스낵바 --> <!-- 스낵바 -->
<v-snackbar <v-snackbar
v-model="snackbar.show" v-model="snackbar.show"
@ -532,7 +793,7 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useStoreStore } from '@/store/index' import { useStoreStore } from '@/store/index' //
const storeStore = useStoreStore() const storeStore = useStoreStore()
@ -544,6 +805,28 @@ const formValid = ref(false)
const saving = ref(false) const saving = ref(false)
const storeFormRef = ref(null) // const storeFormRef = ref(null) //
//
const menus = ref([])
const menuSearch = ref('')
const menuCategoryFilter = ref('전체')
const menuCategories = ref(['커피', '음료', '디저트', '베이커리', '샐러드', '샌드위치'])
const showMenuDialog = ref(false)
const menuEditMode = ref(false)
const menuFormRef = ref(null)
const menuFormData = ref({
menuName: '',
category: '',
price: 0,
description: '',
available: true,
recommended: false,
imageUrl: ''
})
//
const showMenuDetailDialog = ref(false)
const selectedMenu = ref(null)
const snackbar = ref({ const snackbar = ref({
show: false, show: false,
message: '', message: '',
@ -583,6 +866,27 @@ const weekDays = [
// //
const storeInfo = computed(() => storeStore.storeInfo || {}) const storeInfo = computed(() => storeStore.storeInfo || {})
//
const filteredMenus = computed(() => {
let filtered = menus.value
//
if (menuSearch.value) {
const search = menuSearch.value.toLowerCase()
filtered = filtered.filter(menu =>
menu.menuName.toLowerCase().includes(search) ||
menu.description.toLowerCase().includes(search)
)
}
//
if (menuCategoryFilter.value && menuCategoryFilter.value !== '전체') {
filtered = filtered.filter(menu => menu.category === menuCategoryFilter.value)
}
return filtered
})
// //
const formatClosedDays = (closedDays) => { const formatClosedDays = (closedDays) => {
if (!closedDays) return '미설정' if (!closedDays) return '미설정'
@ -650,6 +954,161 @@ const addMenu = () => {
showSnackbar('메뉴 관리 기능은 곧 업데이트 예정입니다', 'info') showSnackbar('메뉴 관리 기능은 곧 업데이트 예정입니다', 'info')
} }
//
const openCreateMenuDialog = () => {
console.log('메뉴 등록 다이얼로그 열기')
menuEditMode.value = false
resetMenuForm()
showMenuDialog.value = true
}
const editMenu = (menu) => {
console.log('메뉴 수정:', menu)
menuEditMode.value = true
//
menuFormData.value = {
id: menu.id || menu.menuId, // ID
menuName: menu.menuName || '',
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 || menu.image || ''
}
showMenuDialog.value = true
}
const viewMenuDetail = async (menu) => {
console.log('메뉴 상세 보기:', menu)
try {
//
const { menuService } = await import('@/services/menu')
//
const result = await menuService.getMenuDetail(menu.id)
if (result.success) {
selectedMenu.value = result.data
showMenuDetailDialog.value = true
console.log('✅ 메뉴 상세 정보 로드:', result.data)
} else {
// API
selectedMenu.value = menu
showMenuDetailDialog.value = true
console.log('⚠️ 상세 정보 로드 실패, 기본 정보 사용:', menu)
}
} catch (error) {
//
console.error('메뉴 상세 정보 로드 실패:', error)
selectedMenu.value = menu
showMenuDetailDialog.value = true
}
}
const closeMenuDetail = () => {
showMenuDetailDialog.value = false
selectedMenu.value = null
}
const confirmDeleteMenu = (menu) => {
console.log('메뉴 삭제 확인:', menu)
if (confirm(`'${menu.menuName}' 메뉴를 삭제하시겠습니까?`)) {
deleteMenu(menu.id)
}
}
const clearFilters = () => {
menuSearch.value = ''
menuCategoryFilter.value = '전체'
}
const cancelMenuForm = () => {
console.log('메뉴 폼 취소')
showMenuDialog.value = false
menuEditMode.value = false
resetMenuForm()
}
const resetMenuForm = () => {
menuFormData.value = {
id: null, // ID
menuName: '',
category: '',
price: 0,
description: '',
available: true,
recommended: false,
imageUrl: ''
}
// validation
if (menuFormRef.value) {
menuFormRef.value.resetValidation()
}
}
const getCategoryColor = (category) => {
const colors = {
'커피': 'brown',
'음료': 'blue',
'디저트': 'pink',
'베이커리': 'orange',
'샐러드': 'green',
'샌드위치': 'purple'
}
return colors[category] || 'grey'
}
const formatCurrency = (amount) => {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(amount)
}
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return '-'
try {
const date = new Date(dateTimeString)
return new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date)
} catch (error) {
return dateTimeString
}
}
//
const editMenuFromDetail = () => {
if (selectedMenu.value) {
//
closeMenuDetail()
//
editMenu(selectedMenu.value)
}
}
//
const deleteMenuFromDetail = () => {
if (selectedMenu.value) {
//
closeMenuDetail()
//
confirmDeleteMenu(selectedMenu.value)
}
}
const cancelForm = () => { const cancelForm = () => {
console.log('폼 취소') console.log('폼 취소')
showCreateDialog.value = false showCreateDialog.value = false
@ -765,13 +1224,9 @@ onMounted(async () => {
if (result.success) { if (result.success) {
console.log('✅ 매장 정보 로드 완료:', result.data) console.log('✅ 매장 정보 로드 완료:', result.data)
// //
try { await loadMenus()
await storeStore.fetchMenus()
console.log('메뉴 정보 로드 완료')
} catch (menuError) {
console.log('메뉴 정보 로드 실패 (개발 중이므로 무시):', menuError)
}
} else { } else {
if (result.message === '등록된 매장이 없습니다') { if (result.message === '등록된 매장이 없습니다') {
console.log('⚠️ 등록된 매장이 없음 - 등록 화면 표시') console.log('⚠️ 등록된 매장이 없음 - 등록 화면 표시')
@ -786,6 +1241,120 @@ onMounted(async () => {
showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error') showSnackbar('매장 정보를 불러오는 중 오류가 발생했습니다', 'error')
} }
}) })
// - API
const loadMenus = async () => {
try {
console.log('메뉴 데이터 로드 시작')
const result = await storeStore.fetchMenus()
if (result.success) {
menus.value = result.data
console.log('✅ 메뉴 데이터 로드 완료:', result.data)
} else {
console.log('메뉴 데이터 없음 또는 로드 실패:', result.message)
menus.value = [] // ( UI )
}
} catch (error) {
console.error('메뉴 데이터 로드 실패:', error)
menus.value = [] //
}
}
// /
const saveMenu = async () => {
if (!menuFormRef.value) {
showSnackbar('폼 오류가 발생했습니다', 'error')
return
}
const { valid } = await menuFormRef.value.validate()
if (!valid) {
showSnackbar('필수 정보를 모두 입력해주세요', 'error')
return
}
saving.value = true
try {
//
const { menuService } = await import('@/services/menu')
let result
if (menuEditMode.value) {
// - PUT /api/menu/{menuId}
const menuId = menuFormData.value.id
if (!menuId) {
showSnackbar('메뉴 ID가 없습니다', 'error')
return
}
console.log('메뉴 수정 API 호출, 메뉴 ID:', menuId)
result = await menuService.updateMenu(menuId, menuFormData.value)
} else {
// - POST /api/menu/register
const storeId = storeInfo.value.storeId
if (!storeId) {
showSnackbar('매장 정보를 찾을 수 없습니다', 'error')
return
}
// ( ID )
const menuData = {
...menuFormData.value,
storeId: storeId
}
console.log('메뉴 등록 API 호출, 매장 ID:', storeId)
result = await menuService.registerMenu(menuData)
}
console.log('메뉴 저장 결과:', result)
if (result.success) {
showSnackbar(
menuEditMode.value ? '메뉴가 수정되었습니다' : '메뉴가 등록되었습니다',
'success'
)
showMenuDialog.value = false
menuEditMode.value = false
resetMenuForm()
//
await loadMenus()
} else {
showSnackbar(result.message || '저장에 실패했습니다', 'error')
}
} catch (error) {
console.error('메뉴 저장 중 오류:', error)
showSnackbar('저장 중 오류가 발생했습니다', 'error')
} finally {
saving.value = false
}
}
//
const deleteMenu = async (menuId) => {
try {
//
const { menuService } = await import('@/services/menu')
console.log('메뉴 삭제:', menuId)
const result = await menuService.deleteMenu(menuId)
if (result.success) {
showSnackbar('메뉴가 삭제되었습니다', 'success')
//
await loadMenus()
} else {
showSnackbar(result.message || '메뉴 삭제에 실패했습니다', 'error')
}
} catch (error) {
console.error('메뉴 삭제 실패:', error)
showSnackbar('메뉴 삭제 중 오류가 발생했습니다', 'error')
}
}
</script> </script>
<style scoped> <style scoped>
@ -811,9 +1380,57 @@ onMounted(async () => {
overflow: hidden; overflow: hidden;
} }
/* 메뉴 카드 스타일 */
.menu-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
cursor: pointer;
}
.menu-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
}
.position-relative {
position: relative;
}
.position-absolute {
position: absolute;
}
.top-0 {
top: 0;
}
.right-0 {
right: 0;
}
.left-0 {
left: 0;
}
.h-100 {
height: 100%;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.info-item { .info-item {
margin-bottom: 12px; margin-bottom: 12px;
} }
.menu-card {
margin-bottom: 16px;
}
.empty-state {
padding: 2rem 1rem;
}
} }
</style> </style>