Skip to content
Merged
81 changes: 81 additions & 0 deletions src/apis/discount/client.ts
Original file line number Diff line number Diff line change
@@ -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<DiscountReservation[] | null> => {
try {
const res = await apiClient.get<DiscountReservation[] | null>(
`/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<boolean> => {
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<boolean> => {
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<boolean> => {
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);
}
};
25 changes: 25 additions & 0 deletions src/apis/discount/model.ts
Original file line number Diff line number Diff line change
@@ -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;
};
56 changes: 56 additions & 0 deletions src/apis/discount/query.ts
Original file line number Diff line number Diff line change
@@ -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'>) =>
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,
};
};
2 changes: 1 addition & 1 deletion src/components/common/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type TabBarComponentType = {
};
};
const tabBarData: TabBarComponentType = {
MenuManage: {
Menu: {
label: '메뉴 관리',
icon: {
family: 'Feather',
Expand Down
142 changes: 142 additions & 0 deletions src/components/discount/DiscountReservationModal.style.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading