diff --git a/src/apis/discount/client.ts b/src/apis/discount/client.ts new file mode 100644 index 0000000..2cd9930 --- /dev/null +++ b/src/apis/discount/client.ts @@ -0,0 +1,81 @@ +import apiClient from '../ApiClient'; +import CustomError from '../CustomError'; +import { + CreateDiscountReservation, + DiscountReservation, + UpdateDiscountReservation, +} from './model'; + +/** + * GET /owner/products/discount/reservations/{marketId} + * 가게에 관련된 상품 할인 예약 조회 + */ +export const getDiscountReservations = async ( + marketId: number, +): Promise => { + try { + const res = await apiClient.get( + `/owner/products/discount/reservations/${marketId}`, + ); + return res; + } catch (error) { + console.error('할인 예약 조회 에러', error); + throw new CustomError(error); + } +}; + +/** + * POST /owner/products/discount/reservations + * 상품리스트 할인 예약 생성 + */ +export const createDiscountReservation = async ( + data: CreateDiscountReservation, +): Promise => { + try { + const res = await apiClient.post<{code: number; message: string}>( + '/owner/products/discount/reservations', + data, + ); + return !!res && res.code === 200; + } catch (error) { + console.error('할인 예약 생성 에러', error); + throw new CustomError(error); + } +}; + +/** + * PATCH /owner/products/discount/reservations + * 상품리스트 할인 예약 수정 + */ +export const updateDiscountReservation = async ( + data: UpdateDiscountReservation, +): Promise => { + try { + const res = await apiClient.patch<{code: number; message: string}>( + '/owner/products/discount/reservations', + data, + ); + return !!res && res.code === 200; + } catch (error) { + console.error('할인 예약 수정 에러', error); + throw new CustomError(error); + } +}; + +/** + * DELETE /owner/products/discount/reservations/{discountReservationId} + * 상품리스트 할인 예약 삭제 + */ +export const deleteDiscountReservation = async ( + discountReservationId: number, +): Promise => { + try { + const res = await apiClient.del<{code: number; message: string}>( + `/owner/products/discount/reservations/${discountReservationId}`, + ); + return !!res && res.code === 200; + } catch (error) { + console.error('할인 예약 삭제 에러', error); + throw new CustomError(error); + } +}; diff --git a/src/apis/discount/model.ts b/src/apis/discount/model.ts new file mode 100644 index 0000000..dc7faad --- /dev/null +++ b/src/apis/discount/model.ts @@ -0,0 +1,25 @@ +import {MenuType} from '@/types/ProductType'; + +export type DiscountReservation = { + discountReservationId: number; + products: MenuType[]; + discountRate: number; + startAt: string; + endAt: string; +}; + +export type CreateDiscountReservation = { + marketId: number; + productIds: number[]; + discountRate: number; + startAt: string; + endAt: string; +}; + +export type UpdateDiscountReservation = { + discountReservationId: number; + productIds: number[]; + discountRate: number; + startAt: string; + endAt: string; +}; diff --git a/src/apis/discount/query.ts b/src/apis/discount/query.ts new file mode 100644 index 0000000..776b503 --- /dev/null +++ b/src/apis/discount/query.ts @@ -0,0 +1,56 @@ +import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; + +import useProfile from '@/hooks/useProfile'; + +import { + createDiscountReservation, + deleteDiscountReservation, + getDiscountReservations, + updateDiscountReservation, +} from './client'; +import {CreateDiscountReservation, UpdateDiscountReservation} from './model'; + +const QUERY_KEY = ['discountReservations']; + +export const useDiscountReservations = () => { + const {profile} = useProfile(); + const queryClient = useQueryClient(); + + const {data: reservations = [], ...queryResult} = useQuery({ + queryKey: [...QUERY_KEY, profile?.marketId], + queryFn: () => getDiscountReservations(profile?.marketId!), + enabled: !!profile?.marketId, + }); + + const invalidateQueries = () => { + queryClient.invalidateQueries({ + queryKey: [...QUERY_KEY, profile?.marketId], + }); + }; + + const createMutation = useMutation({ + mutationFn: (data: Omit) => + createDiscountReservation({marketId: profile?.marketId!, ...data}), + onSuccess: invalidateQueries, + }); + + const updateMutation = useMutation({ + mutationFn: (data: UpdateDiscountReservation) => + updateDiscountReservation(data), + onSuccess: invalidateQueries, + }); + + const deleteMutation = useMutation({ + mutationFn: (discountReservationId: number) => + deleteDiscountReservation(discountReservationId), + onSuccess: invalidateQueries, + }); + + return { + reservations, + ...queryResult, + createReservation: createMutation.mutateAsync, + updateReservation: updateMutation.mutateAsync, + deleteReservation: deleteMutation.mutateAsync, + }; +}; diff --git a/src/components/common/TabBar.tsx b/src/components/common/TabBar.tsx index 20e0a52..15ccb2c 100644 --- a/src/components/common/TabBar.tsx +++ b/src/components/common/TabBar.tsx @@ -28,7 +28,7 @@ type TabBarComponentType = { }; }; const tabBarData: TabBarComponentType = { - MenuManage: { + Menu: { label: '메뉴 관리', icon: { family: 'Feather', diff --git a/src/components/discount/DiscountReservationModal.style.tsx b/src/components/discount/DiscountReservationModal.style.tsx new file mode 100644 index 0000000..904899d --- /dev/null +++ b/src/components/discount/DiscountReservationModal.style.tsx @@ -0,0 +1,142 @@ +import styled from '@emotion/native'; + +const S = { + ModalOverlay: styled.View` + flex: 1; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + `, + SafeArea: styled.SafeAreaView` + flex: 1; + width: 100%; + `, + Container: styled.KeyboardAvoidingView` + flex: 1; + justify-content: center; + align-items: center; + `, + + ModalView: styled.View` + display: flex; + width: 90%; + background-color: white; + border-radius: 12px; + padding: 20px; + align-items: center; + `, + ModalTitle: styled.Text` + ${({theme}) => theme.fonts.h6} + font-weight: 600; + margin-bottom: 20px; + `, + InputRow: styled.View` + width: 100%; + margin-bottom: 12px; + `, + InputLabel: styled.Text` + ${({theme}) => theme.fonts.subtitle1} + margin-bottom: 4px; + `, + ProductListContainer: styled.ScrollView` + width: 100%; + border: 1px solid ${({theme}) => theme.colors.tertiaryHover}; + border-radius: 8px; + `, + ProductItem: styled.TouchableOpacity<{ + isSelected: boolean; + disabled: boolean; + }>` + padding: 10px; + background-color: ${({isSelected, disabled, theme}) => + disabled + ? theme.colors.tertiaryHover + : isSelected + ? theme.colors.primaryLight + : 'white'}; + `, + ProductItemText: styled.Text<{disabled: boolean}>` + ${({theme}) => theme.fonts.body2} + color: ${({disabled, theme}) => + disabled ? theme.colors.disabled : theme.colors.dark}; + `, + ButtonContainer: styled.View` + flex-direction: row; + justify-content: center; + width: 100%; + margin-top: 20px; + `, + ModalButton: styled.TouchableOpacity<{status?: 'warning' | 'error'}>` + flex: 1; + padding: 10px; + border-radius: 8px; + align-items: center; + margin: 0 5px; + background-color: ${({status, theme}) => + status === 'error' + ? theme.colors.error + : status === 'warning' + ? theme.colors.tertiaryDisabled + : theme.colors.primary}; + `, + ModalButtonText: styled.Text` + ${({theme}) => theme.fonts.body2} + color: white; + font-weight: bold; + `, + TimeRangeContainer: styled.View` + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; + `, + TimeInputTouchable: styled.TouchableOpacity` + flex: 1; + border: 1px solid ${({theme}) => theme.colors.primary}; + border-radius: 8px; + padding: 12px 0; + background-color: white; + `, + TimeInputText: styled.Text` + ${({theme}) => theme.fonts.body1} + text-align: center; + `, + SelectedProductsContainer: styled.View` + flex-direction: row; + flex-wrap: wrap; + align-items: center; + width: 100%; + padding: 8px; + border: 1px solid #e0e0e0; + border-radius: 8px; + `, + + ProductTag: styled.View` + border: 1px solid ${({theme}) => theme.colors.primaryLight}; + border-radius: 16px; + padding: 6px 12px; + margin-right: 8px; + margin-bottom: 8px; + align-items: center; + justify-content: center; + `, + + ProductTagText: styled.Text` + ${({theme}) => theme.fonts.body1} + color: ${({theme}) => theme.colors.primaryLight}; + font-size: 14px; + `, + + PlaceholderText: styled.Text` + color: #999; + font-size: 14px; + line-height: 24px; + `, + + AlertText: styled.Text` + ${({theme}) => theme.fonts.body2} + color: ${({theme}) => theme.colors.error}; + `, +}; + +export default S; diff --git a/src/components/discount/DiscountReservationModal.tsx b/src/components/discount/DiscountReservationModal.tsx new file mode 100644 index 0000000..259b1a3 --- /dev/null +++ b/src/components/discount/DiscountReservationModal.tsx @@ -0,0 +1,260 @@ +import React, {useState, useEffect} from 'react'; +import {Modal, Alert, Platform} from 'react-native'; +import useProduct from '@/hooks/useProduct'; +import {useDiscountReservations} from '@/apis/discount/query'; +import {DiscountReservation} from '@/apis/discount/model'; +import CustomTextInput from '../common/CustomTextInput'; +import S from './DiscountReservationModal.style'; + +import DatePicker from 'react-native-date-picker'; +import {format} from '@/utils/date'; + +type Props = { + isVisible: boolean; + onClose: () => void; + initialData: DiscountReservation | null; +}; + +const DiscountReservationModal = ({isVisible, onClose, initialData}: Props) => { + const {products, fetch: fetchProducts} = useProduct(); + const {createReservation, updateReservation, deleteReservation} = + useDiscountReservations(); + + const [discountRate, setDiscountRate] = useState(''); + const [startAt, setStartAt] = useState(''); + const [endAt, setEndAt] = useState(''); + const [selectedProductIds, setSelectedProductIds] = useState([]); + + const [pickerVisibleFor, setPickerVisibleFor] = useState< + 'startAt' | 'endAt' | null + >(null); + + const selectedProducts = products.filter(p => + selectedProductIds.includes(p.id), + ); + + useEffect(() => { + if (isVisible) { + fetchProducts(); + if (initialData) { + setDiscountRate(initialData.discountRate.toString()); + setStartAt(initialData.startAt); + setEndAt(initialData.endAt); + setSelectedProductIds(initialData.products.map(p => p.id)); + } else { + setDiscountRate(''); + setStartAt(''); + setEndAt(''); + setSelectedProductIds([]); + } + } + }, [initialData, isVisible, fetchProducts]); + + const getPickerDefaultDate = () => { + const timeString = pickerVisibleFor === 'startAt' ? startAt : endAt; + const defaultDate = new Date(); + if (timeString) { + const [hours, minutes] = timeString.split(':').map(Number); + defaultDate.setHours(hours, minutes, 0, 0); + } + return defaultDate; + }; + + const handleToggleProduct = (productId: number, isDisabled: boolean) => { + if (isDisabled) return; + + setSelectedProductIds(prev => + prev.includes(productId) + ? prev.filter(id => id !== productId) + : [...prev, productId], + ); + }; + + const handleSave = async () => { + const rate = parseInt(discountRate, 10); + if ( + !rate || + rate <= 0 || + rate > 100 || + !startAt || + !endAt || + selectedProductIds.length === 0 + ) { + Alert.alert('입력 오류', '모든 값을 올바르게 입력해주세요.'); + return; + } + + try { + if (initialData) { + await updateReservation({ + discountReservationId: initialData.discountReservationId, + productIds: selectedProductIds, + discountRate: rate, + startAt, + endAt, + }); + Alert.alert('성공', '예약 할인이 수정되었습니다.'); + } else { + await createReservation({ + productIds: selectedProductIds, + discountRate: rate, + startAt, + endAt, + }); + Alert.alert('성공', '새로운 예약 할인이 생성되었습니다.'); + } + onClose(); + } catch (e) { + Alert.alert('오류', '저장에 실패했습니다.'); + } + }; + + const handleDelete = async () => { + if (!initialData) return; + + Alert.alert('삭제 확인', '정말로 이 예약을 삭제하시겠습니까?', [ + {text: '취소', style: 'cancel'}, + { + text: '삭제', + style: 'destructive', + onPress: async () => { + try { + await deleteReservation(initialData.discountReservationId); + Alert.alert('성공', '예약이 삭제되었습니다.'); + onClose(); + } catch (e) { + Alert.alert('오류', '삭제에 실패했습니다.'); + } + }, + }, + ]); + }; + + return ( + + + + + + + {initialData ? '예약 할인 수정' : '새 예약 할인'} + + + + 상품 선택 + + {products.map(product => { + const isReservedByAnother = + product.reservationStatus !== null; + const isPartOfCurrentReservation = + initialData?.products.some(p => p.id === product.id) ?? + false; + const isDisabled = + isReservedByAnother && !isPartOfCurrentReservation; + + return ( + + handleToggleProduct(product.id, isDisabled) + } + isSelected={selectedProductIds.includes(product.id)} + disabled={isDisabled}> + + {product.name} + + + ); + })} + + 예약 할인은 상품당 1개만 가능합니다. + + + + 선택된 반찬 + + {selectedProducts.length > 0 ? ( + selectedProducts.map(product => ( + + {product.name} + + )) + ) : ( + 상품을 선택해주세요. + )} + + + + + 할인율 (%) + + + + + 할인 시간 + + setPickerVisibleFor('startAt')}> + {startAt || '시작 시간'} + + ~ + setPickerVisibleFor('endAt')}> + {endAt || '종료 시간'} + + + + + + + 저장 + + + 취소 + + {initialData && ( + + 삭제 + + )} + + + + + { + const formattedTime = format(date.getTime(), 'HH:mm'); + if (pickerVisibleFor === 'startAt') { + setStartAt(formattedTime); + } else if (pickerVisibleFor === 'endAt') { + setEndAt(formattedTime); + } + setPickerVisibleFor(null); + }} + onCancel={() => { + setPickerVisibleFor(null); + }} + title={ + pickerVisibleFor === 'startAt' + ? '시작 시간 선택' + : '종료 시간 선택' + } + confirmText="확인" + cancelText="취소" + /> + + + + ); +}; + +export default DiscountReservationModal; diff --git a/src/components/menu/Menu.style.tsx b/src/components/menu/Menu.style.tsx index 8c9682c..1b961d4 100644 --- a/src/components/menu/Menu.style.tsx +++ b/src/components/menu/Menu.style.tsx @@ -1,35 +1,56 @@ import styled from '@emotion/native'; +import {MenuType} from '@/types/ProductType'; const S = { MenuWrapper: styled.View` background-color: white; display: flex; flex-direction: row; + border: 1.2px solid ${({theme}) => theme.colors.primary}; + border-radius: 20px; align-items: center; - margin: 4px 20px; + margin: 6px 20px; padding: 12px; + shadow-color: ${({theme}) => theme.colors.tertiary}; + shadow-opacity: 0.04; + shadow-radius: 8px; + shadow-offset: 0px 1px; + elevation: 2; + `, + + Col: styled.View` + display: flex; + flex; 1: + flex-direction: column; + `, + + Row: styled.View` + display: flex; + flex: 1; + flex-direction: row; `, MenuImage: styled.Image` - width: 60px; - height: 60px; - margin-right: 24px; + width: 88px; + height: 88px; + margin-right: 16px; border-radius: 16px; `, MenuInfoWrapper: styled.View` display: flex; flex-direction: column; flex: 1; + gap: 4px; `, MenuNameText: styled.Text` + ${props => props.theme.fonts.body1}; font-size: 16px; font-weight: bold; - margin-bottom: 4px; `, DicountInfoWrapper: styled.View` display: flex; flex-direction: row; + align-items: center; gap: 8px; - margin: 2px 0px; `, DiscountRateText: styled.Text` color: red; @@ -47,12 +68,42 @@ const S = { CurrentInfoWrapper: styled.View` display: flex; flex-direction: row; + align-items: center; gap: 8px; - margin: 2px 0px; + `, + + StatusBorder: styled.View<{status: MenuType['reservationStatus']}>` + position: absolute; + top: 10px; + right: 10px; + + flex-direction: row; + align-items: center; + border-radius: 10px; + padding: 2px 6px; + + ${({theme, status}) => + status === 'ACTIVE' + ? `border: 1px solid ${theme.colors.primary};` + : status === 'PENDING' + ? `border: 1px solid ${theme.colors.warning};` + : `border: none;`} `, CurrentStatusText: styled.Text` color: ${props => props.theme.colors.primary}; `, + + CurrentDiscountStatusText: styled.Text<{ + status: MenuType['reservationStatus']; + }>` + ${({theme, status}) => + status === 'ACTIVE' + ? `color: ${theme.colors.primary};` + : status === 'PENDING' + ? `color: ${theme.colors.warning};` + : `color: transparent;`} + ${({theme}) => theme.fonts.subtitle2} + `, MenuCounter: styled.View` display: flex; flex-direction: row; diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index 9df4a10..192d3b0 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -14,13 +14,35 @@ const statusMap: Record = { OUT_OF_STOCK: '품절', HIDDEN: '숨김', }; + +const discountStatusMap: Record< + NonNullable, + string +> = { + ACTIVE: '일괄 할인 중', + PENDING: '할인 예약', +}; + const Menu = ({menu, onEdit, onIncreaseStock, onDecreaseStock}: Props) => { const isOutOfStock = menu.stock === 0; + const discountStatus = menu.reservationStatus + ? discountStatusMap[menu.reservationStatus] + : ''; return ( + {menu.reservationStatus && ( + + + {discountStatus} + + + )} - {menu.name} + + {menu.name.length > 8 ? menu.name.slice(0, 8) + '...' : menu.name} + + {menu.discountRate} % diff --git a/src/components/menu/MenuModal.tsx b/src/components/menu/MenuModal.tsx index 0b7e764..2d5a659 100644 --- a/src/components/menu/MenuModal.tsx +++ b/src/components/menu/MenuModal.tsx @@ -59,6 +59,7 @@ const MenuModal = ({ discountPrice: 0, stock: 0, productStatus: 'HIDDEN', + reservationStatus: 'PENDING', tags: [], }); @@ -81,6 +82,7 @@ const MenuModal = ({ discountPrice: 0, stock: 0, productStatus: 'HIDDEN', + reservationStatus: 'PENDING', tags: [], }); } @@ -262,6 +264,7 @@ const MenuModal = ({ onChangeText={text => handleInputChange('originPrice', text) } + disabled={menuData.reservationStatus != null} /> @@ -271,6 +274,7 @@ const MenuModal = ({ onChangeText={text => handleInputChange('discountPrice', text) } + disabled={menuData.reservationStatus != null} /> diff --git a/src/navigation/HomeNavigator.tsx b/src/navigation/HomeNavigator.tsx index c6b5fb8..b7a0eb0 100644 --- a/src/navigation/HomeNavigator.tsx +++ b/src/navigation/HomeNavigator.tsx @@ -8,11 +8,11 @@ import HeaderTitle from '@/components/common/Appbar/HeaderTitle'; import {TabBar} from '@components/common'; import MarketInfoScreen from '@/screens/MarketInfoScreen'; -import MenuManageScreen from '@/screens/MenuManageScreen'; import MyPageScreen from '@/screens/MyPageScreen'; import {HomeStackParamList} from '@/types/StackNavigationType'; +import MenuNavigator from './MenuNavigator'; import OrderNavigator from './OrderNavigator'; import ReviewNavigator from './ReviewNavigator'; import theme from '@/context/theme'; @@ -35,8 +35,7 @@ const marketInfoScreenOptions: BottomTabNavigationOptions = { }; const menuManageScreenOptions: BottomTabNavigationOptions = { - ...defaultScreenOptions, - headerTitle: () => , + headerShown: false, }; const reviewScreenOptions: BottomTabNavigationOptions = { @@ -62,8 +61,8 @@ const HomeNavigator = () => { options={marketInfoScreenOptions} /> (); + +const menuManageScreenOptions: StackNavigationOptions = { + ...defaultOptions, + headerTitle: () => , +}; + +const menuDiscountManageScreenOptions: StackNavigationOptions = { + ...defaultOptions, + headerTitle: () => , +}; + +const MenuNavigator = () => { + return ( + + + + + ); +}; + +export default MenuNavigator; diff --git a/src/screens/DiscountReservationScreen/DiscountReservationScreen.style.tsx b/src/screens/DiscountReservationScreen/DiscountReservationScreen.style.tsx new file mode 100644 index 0000000..a76ce0f --- /dev/null +++ b/src/screens/DiscountReservationScreen/DiscountReservationScreen.style.tsx @@ -0,0 +1,72 @@ +import styled from '@emotion/native'; + +const S = { + Container: styled.View` + flex: 1; + padding: 16px; + background-color: white; + `, + + CreateButton: styled.TouchableOpacity` + background-color: ${({theme}) => theme.colors.primary}; + padding: 12px; + border-radius: 8px; + align-items: center; + margin-bottom: 16px; + `, + + ItemContainer: styled.View` + display: flex; + flex-direction: column; + width: 100%; + gap: 8px; + background-color: white; + padding: 16px; + border-radius: 8px; + margin-bottom: 12px; + border: 1px solid ${({theme}) => theme.colors.tertiaryDisabled}; + `, + + ItemText: styled.Text` + font-size: 16px; + `, + + ProductList: styled.View` + display: flex; + flex-direction: column; + gap: 6px; + `, + + ProductListItem: styled.Text` + ${({theme}) => theme.fonts.body2} + color: ${({theme}) => theme.colors.tertiaryDisabled}; + padding-left: 8px; + `, + + EditButton: styled.TouchableOpacity` + background-color: ${({theme}) => theme.colors.primary}; + padding: 8px; + border-radius: 4px; + align-items: center; + margin-top: 8px; + `, + + ButtonText: styled.Text` + color: white; + font-weight: bold; + `, + + LoadingText: styled.Text` + text-align: center; + margin-top: 20px; + `, + + ErrorText: styled.Text` + ${({theme}) => theme.fonts.subtitle1} + text-align: center; + margin-top: 20px; + color: ${({theme}) => theme.colors.error}; + `, +}; + +export default S; diff --git a/src/screens/DiscountReservationScreen/index.tsx b/src/screens/DiscountReservationScreen/index.tsx new file mode 100644 index 0000000..d1e3cbc --- /dev/null +++ b/src/screens/DiscountReservationScreen/index.tsx @@ -0,0 +1,68 @@ +import React, {useState} from 'react'; +import {ScrollView} from 'react-native'; +import {useDiscountReservations} from '@/apis/discount/query'; +import {DiscountReservation} from '@/apis/discount/model'; +import DiscountReservationModal from '@/components/discount/DiscountReservationModal'; +import S from './DiscountReservationScreen.style'; + +const DiscountReservationScreen = () => { + const {reservations, isLoading, error} = useDiscountReservations(); + const [modalVisible, setModalVisible] = useState(false); + const [selectedReservation, setSelectedReservation] = + useState(null); + + const handleOpenModal = (reservation: DiscountReservation | null) => { + setSelectedReservation(reservation); + setModalVisible(true); + }; + + const handleCloseModal = () => { + setModalVisible(false); + setSelectedReservation(null); + }; + + if (isLoading || !reservations) { + return 로딩 중...; + } + + if (error) { + return 오류가 발생했습니다.; + } + + return ( + + + {reservations.map(item => ( + + + 할인율: {item.discountRate}% ({item.startAt} - {item.endAt}) + + + + 적용 상품: + {item.products.map(product => ( + + - {product.name} + + ))} + + + handleOpenModal(item)}> + 수정 + + + ))} + handleOpenModal(null)}> + 예약 할인하기 + + + + + ); +}; + +export default DiscountReservationScreen; diff --git a/src/screens/MenuManageScreen/MenuManageDetailScreen.style.tsx b/src/screens/MenuManageScreen/MenuManageDetailScreen.style.tsx index 3cdacc1..7748f99 100644 --- a/src/screens/MenuManageScreen/MenuManageDetailScreen.style.tsx +++ b/src/screens/MenuManageScreen/MenuManageDetailScreen.style.tsx @@ -1,7 +1,16 @@ import styled from '@emotion/native'; -import {Button, Text} from 'react-native-paper'; +import {Button} from 'react-native-paper'; const S = { + Container: styled.ScrollView` + display: flex; + flex: 1; + flex-direction: column; + background-color: white; + width: 100%; + height: 100%; + `, + MainText: styled.Text` font-size: 20px; font-style: normal; @@ -10,7 +19,9 @@ const S = { `, AddProductView: styled.View` - align-items: center; + flex-direction: row; + padding: 0px 24px; + justify-content: space-between; `, AddButton: styled(Button)` @@ -22,17 +33,15 @@ const S = { background-color: ${({theme}) => theme.colors.primary}; - padding: 4px 24px; - margin: 12px; + padding: 4px 12px; + margin: 8px; `, - AddButtonText: styled(Text)` + AddButtonText: styled.Text` color: rgba(255, 255, 255, 1); padding: 4px 12px; - - margin-left: 8px; - - ${({theme}) => theme.fonts.default} + ${props => props.theme.fonts.subtitle2}; + font-size: 16px; `, }; diff --git a/src/screens/MenuManageScreen/MenuManageDetailScreen.tsx b/src/screens/MenuManageScreen/MenuManageDetailScreen.tsx index 53647eb..128490d 100644 --- a/src/screens/MenuManageScreen/MenuManageDetailScreen.tsx +++ b/src/screens/MenuManageScreen/MenuManageDetailScreen.tsx @@ -1,6 +1,9 @@ +import {useNavigation} from '@react-navigation/native'; import React, {useState} from 'react'; import {Alert} from 'react-native'; import {RefreshControl, ScrollView} from 'react-native-gesture-handler'; +import {MenuStackParamList} from '@/types/StackNavigationType'; +import {StackNavigationProp} from '@react-navigation/stack'; import Icon from 'react-native-vector-icons/Feather'; @@ -28,6 +31,7 @@ const MenuManageDetailScreen = ({menus, updateMenus}: Props) => { const [modalVisible, setModalVisible] = useState(false); const [currentMenu, setCurrentMenu] = useState(null); + const navigation = useNavigation>(); const {marketInfo} = useMarket(); const {profile} = useProfile(); const {refresh} = useProduct(); @@ -39,6 +43,12 @@ const MenuManageDetailScreen = ({menus, updateMenus}: Props) => { setModalVisible(true); }; + const handleGoToDiscountReservation = () => { + navigation.navigate('Menu', { + screen: 'DiscountReservation', + }); + }; + const handleEditProduct = (menu: MenuType) => { setCurrentMenu(menu); setModalVisible(true); @@ -65,6 +75,7 @@ const MenuManageDetailScreen = ({menus, updateMenus}: Props) => { id: menuData.id, image: menuData.image, name: menuData.name, + reservationStatus: menuData.reservationStatus, originPrice: Number(menuData.originPrice.toString().replace(/,/g, '')), discountPrice: Number( menuData.discountPrice.toString().replace(/,/g, ''), @@ -99,6 +110,7 @@ const MenuManageDetailScreen = ({menus, updateMenus}: Props) => { if (targetMenu.stock === 0) { const body: MenuType = { id: menuData.id, + reservationStatus: null, image: menuData.image, name: menuData.name, originPrice: Number(menuData.originPrice.toString().replace(/,/g, '')), @@ -148,6 +160,7 @@ const MenuManageDetailScreen = ({menus, updateMenus}: Props) => { id: menuData.id, image: menuData.image, name: menuData.name, + reservationStatus: null, originPrice: Number(menuData.originPrice.toString().replace(/,/g, '')), discountPrice: Number( menuData.discountPrice.toString().replace(/,/g, ''), @@ -196,34 +209,40 @@ const MenuManageDetailScreen = ({menus, updateMenus}: Props) => { }; return ( - - }> - {menus.map(menu => ( - handleEditProduct(menu)} - onIncreaseStock={() => handleIncreaseStock(menu)} - onDecreaseStock={() => handleDecreaseStock(menu)} + + + }> + {menus.map(menu => ( + handleEditProduct(menu)} + onIncreaseStock={() => handleIncreaseStock(menu)} + onDecreaseStock={() => handleDecreaseStock(menu)} + /> + ))} + + + + 상품 추가하기 + + + + 예약 할인 관리 + + + + - ))} - - - - 상품 추가하기 - - - - - + + ); }; diff --git a/src/types/ProductType.ts b/src/types/ProductType.ts index 471f528..6c2b43a 100644 --- a/src/types/ProductType.ts +++ b/src/types/ProductType.ts @@ -1,6 +1,8 @@ import {ProductType} from '@ummgoban/shared'; -export type MenuType = ProductType; +export type MenuType = ProductType & { + reservationStatus: 'PENDING' | 'ACTIVE' | null; +}; export type TagType = { id: number; diff --git a/src/types/StackNavigationType.ts b/src/types/StackNavigationType.ts index e036e23..0936365 100644 --- a/src/types/StackNavigationType.ts +++ b/src/types/StackNavigationType.ts @@ -11,7 +11,7 @@ type StackParamType = { export interface HomeStackParamList extends ParamListBase { MarketInfo: undefined; MyPage: undefined; - MenuManage: undefined; + Menu: undefined; Order: undefined; Review: undefined; } @@ -44,6 +44,11 @@ export interface ReviewStackParamList extends ParamListBase { }; } +export interface MenuStackParamList extends ParamListBase { + MenuManage: undefined; + DiscountReservation: undefined; +} + export interface MyPageStackParamList extends ParamListBase { MyPageRoot: undefined; Notice: undefined; @@ -58,4 +63,5 @@ export interface RootStackParamList extends ParamListBase { Order: StackParamType; Review: StackParamType; MyPageRoot: StackParamType; + Menu: StackParamType; }