Skip to content

Commit

Permalink
feat: 지출 내역 생성하기 기능 추가 (#587)
Browse files Browse the repository at this point in the history
* feat: 지출 내역 생성 api 연결 및 유효성 검사

* feat: Funnel 파일 분리 및 member 생성 api 로직 변경

* fix: AddBillFunnel route 수정

* style: lint 적용

* refactor: BillStep hook 분리

* style: lint 적용

* fix: postMember api 요청 변경

* fix: addBill route 수정

* fix: TitleStep에서 PriceStep으로 이동했을 때, billInfo.price 초기화되던 오류 수정

* fix: MSW mocking 해제

* rename: AddBillFunnel 경로 수정

* fix: mutateAsync를 이용하여 postMembers 이후 postBills 요청을 보내도록 수정

* fix: API 변경에 맞추어 수정

* style: lint 적용
  • Loading branch information
Todari authored Sep 24, 2024
1 parent 396ff78 commit 8271194
Show file tree
Hide file tree
Showing 20 changed files with 539 additions and 292 deletions.
6 changes: 3 additions & 3 deletions client/src/apis/request/bill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ import {WithBillId, WithEventId} from '@apis/withId.type';
export interface RequestPostBill {
title: string;
price: number;
members: number[];
memberIds: number[];
}

export const requestPostBill = async ({eventId, title, price, members}: WithEventId<RequestPostBill>) => {
export const requestPostBill = async ({eventId, title, price, memberIds}: WithEventId<RequestPostBill>) => {
await requestPostWithoutResponse({
baseUrl: BASE_URL.HD,
endpoint: `${ADMIN_API_PREFIX}/${eventId}/bills`,
body: {
title,
price,
members,
memberIds,
},
});
};
Expand Down
2 changes: 1 addition & 1 deletion client/src/apis/request/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix';
import {requestDelete, requestGet, requestPut, requestPostWithResponse} from '@apis/fetcher';
import {WithEventId} from '@apis/withId.type';

interface PostMember {
export interface PostMember {
name: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ export type KeyboardType = 'number' | 'string' | 'amount';
interface Props {
type: KeyboardType;
maxNumber: number;
initialValue?: string;
onChange: (value: string) => void;
}

export default function NumberKeyboard({type, maxNumber, onChange}: Props) {
export default function NumberKeyboard({type, maxNumber, initialValue, onChange}: Props) {
const {theme} = useTheme();
const amountKeypads = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '00', '0', '<-'];
const numberKeypads = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '', '0', '<-'];

const {onClickKeypad, onClickDelete, onClickDeleteAll, onClickAddAmount} = useNumberKeyboard({
type,
initialValue,
maxNumber,
onChange,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import {KeyboardType} from './NumberKeyboard';
interface Props {
type: KeyboardType;
maxNumber?: number;
initialValue?: string;
onChange: (value: string) => void;
}

const useNumberKeyboard = ({type, maxNumber, onChange}: Props) => {
const [value, setValue] = useState('');
const useNumberKeyboard = ({type, maxNumber, initialValue, onChange}: Props) => {
const [value, setValue] = useState(initialValue ?? '');

const onClickKeypad = (inputValue: string) => {
const newValue = (value + inputValue).replace(/,/g, '');
Expand Down
4 changes: 2 additions & 2 deletions client/src/constants/regExp.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const REGEXP = {
eventPassword: /^[0-9]*$/,
memberName: /^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z\s]*$/,
purchaseTitle: /^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9\s]*$/,
eventUrl: /\/event\/([a-zA-Z0-9-]+)\//,
billTitle: /^([ㄱ-ㅎ가-힣a-zA-Z0-9ㆍᆢ]\s?)*$/,
memberName: /^([ㄱ-ㅎ가-힣a-zA-Zㆍᆢ]\s?)*$/,
};

export default REGEXP;
2 changes: 1 addition & 1 deletion client/src/constants/routerUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export const ROUTER_URLS = {
eventLogin: '/event/:eventId/login',
eventManage: '/event/:eventId/admin',
home: '/event/:eventId/home',
addBill: 'event/:eventId/addBill',
addBill: '/event/:eventId/add-bill',
eventEdit: 'event/:eventId/admin/edit',
};
2 changes: 1 addition & 1 deletion client/src/hooks/queries/bill/useRequestPostBill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const useRequestPostBill = () => {
const queryClient = useQueryClient();

const {mutate, ...rest} = useMutation({
mutationFn: ({title, price, members}: RequestPostBill) => requestPostBill({eventId, title, price, members}),
mutationFn: ({title, price, memberIds}: RequestPostBill) => requestPostBill({eventId, title, price, memberIds}),
onSuccess: () => {
queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]});
queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]});
Expand Down
14 changes: 4 additions & 10 deletions client/src/hooks/queries/member/useRequestPostMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,17 @@ const useRequestPostMembers = () => {
const eventId = getEventIdByUrl();
const queryClient = useQueryClient();

const {mutate, ...rest} = useMutation({
const {mutate, mutateAsync, data, ...rest} = useMutation({
mutationFn: ({members}: RequestPostMembers) => requestPostMembers({eventId, members}),
// TODO: (@todari) : 낙관적 업데이트 적고 있었어용
// onMutate: async ({type, memberName}) => {
// await queryClient.cancelQueries({queryKey: [QUERY_KEYS.step]});
// const previousStep = queryClient.getQueryData([QUERY_KEYS.step]);
// queryClient.setQueryData([QUERY_KEYS.step], (prev: (MemberStep | BillStep)[]) => prev && {
// });
// },
onSuccess: () => {
onSuccess: responseData => {
queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMembers]});
queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]});
queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]});
return responseData;
},
});

return {postMember: mutate, ...rest};
return {postMembers: mutate, postMembersAsync: mutateAsync, responseMemberIds: data, ...rest};
};

export default useRequestPostMembers;
33 changes: 33 additions & 0 deletions client/src/hooks/useAddBillFunnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {useEffect, useState} from 'react';

import {BillInfo} from '@pages/AddBillFunnel/AddBillFunnel';

import useRequestGetCurrentMembers from './queries/member/useRequestGetCurrentMembers';

export type BillStep = 'title' | 'price' | 'members';

const useAddBillFunnel = () => {
const {currentMembers} = useRequestGetCurrentMembers();
const [step, setStep] = useState<BillStep>('price');
const [billInfo, setBillInfo] = useState<BillInfo>({
price: '',
title: '',
members: [],
});

useEffect(() => {
document.body.style.overflow = 'hidden';

return () => {
document.body.style.overflow = 'auto';
};
}, []);

useEffect(() => {
currentMembers && setBillInfo(prev => ({...prev, members: currentMembers}));
}, [currentMembers]);

return {step, setStep, billInfo, setBillInfo, currentMembers};
};

export default useAddBillFunnel;
122 changes: 122 additions & 0 deletions client/src/hooks/useMembersStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {useEffect, useState} from 'react';
import {useNavigate} from 'react-router-dom';

import {BillInfo} from '@pages/AddBillFunnel/AddBillFunnel';
import {Member} from 'types/serviceType';

import getEventIdByUrl from '@utils/getEventIdByUrl';

import REGEXP from '@constants/regExp';

import useRequestPostMembers from './queries/member/useRequestPostMembers';
import useRequestPostBill from './queries/bill/useRequestPostBill';
import {BillStep} from './useAddBillFunnel';

interface Props {
billInfo: BillInfo;
setBillInfo: React.Dispatch<React.SetStateAction<BillInfo>>;
setStep: React.Dispatch<React.SetStateAction<BillStep>>;
currentMembers: Member[];
}

const useMembersStep = ({billInfo, setBillInfo, currentMembers, setStep}: Props) => {
const [errorMessage, setErrorMessage] = useState('');
const [nameInput, setNameInput] = useState('');

const {postMembersAsync, isPending: isPendingPostMembers} = useRequestPostMembers();

const {postBill, isSuccess: isSuccessPostBill, isPending: isPendingPostBill} = useRequestPostBill();
const navigate = useNavigate();
const eventId = getEventIdByUrl();

const onNameInputChange = (value: string) => {
if (REGEXP.memberName.test(value)) {
setNameInput(value);
}
};

const handleNameInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value.length > 4) {
setErrorMessage('이름은 4자까지 입력 가능해요');
onNameInputChange(nameInput.slice(0, 4));
} else {
setErrorMessage('');
onNameInputChange(event.target.value);
}
};

const canAddMembers = nameInput && !errorMessage;

const canSubmitMembers = billInfo.members.length !== 0;

const setBillInfoMemberWithId = (name: string) => {
const existingMember = currentMembers.find(currentMember => currentMember.name === name);
if (existingMember) {
setBillInfo(prev => ({...prev, members: [...prev.members, {id: existingMember.id, name: name}]}));
} else {
setBillInfo(prev => ({...prev, members: [...prev.members, {id: -1, name: name}]}));
}
};

const handleNameInputEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.nativeEvent.isComposing) {
return;
}
if (event.key === 'Enter' && canAddMembers) {
event.preventDefault();
if (!billInfo.members.map(({name}) => name).includes(nameInput)) {
setBillInfoMemberWithId(nameInput);
}
setNameInput('');
}
};

const handlePostBill = async () => {
if (billInfo.members.map(({id}) => id).includes(-1)) {
const newMembers = await postMembersAsync({
members: billInfo.members
.filter(({id}) => id === -1)
.map(({name}) => ({
name,
})),
});
postBill({
title: billInfo.title,
price: Number(billInfo.price.replace(',', '')),
memberIds: billInfo.members.map(member =>
member.id === -1 ? newMembers.members.find(m => m.name === member.name)?.id || member.id : member.id,
),
});
} else {
postBill({
title: billInfo.title,
price: Number(billInfo.price.replace(',', '')),
memberIds: billInfo.members.map(({id}) => id),
});
}
};

useEffect(() => {
if (isSuccessPostBill) {
navigate(`/event/${eventId}/admin`);
}
}, [isSuccessPostBill]);

const handlePrevStep = () => {
setStep('title');
};

return {
errorMessage,
nameInput,
handleNameInputChange,
handleNameInputEnter,
isPendingPostBill,
isPendingPostMembers,
canSubmitMembers,
handlePostBill,
handlePrevStep,
};
};

export default useMembersStep;
27 changes: 27 additions & 0 deletions client/src/hooks/usePriceStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {useCallback} from 'react';

import {BillInfo} from '@pages/AddBillFunnel/AddBillFunnel';

import {BillStep} from './useAddBillFunnel';

interface Props {
setStep: React.Dispatch<React.SetStateAction<BillStep>>;
setBillInfo: React.Dispatch<React.SetStateAction<BillInfo>>;
}

const usePriceStep = ({setStep, setBillInfo}: Props) => {
const handleNumberKeyboardChange = useCallback(
(value: string) => {
setBillInfo(prev => ({...prev, price: value}));
},
[setBillInfo],
);

const handleNextStep = () => {
setStep('title');
};

return {handleNumberKeyboardChange, handleNextStep};
};

export default usePriceStep;
64 changes: 64 additions & 0 deletions client/src/hooks/useTitleStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {useState} from 'react';

import {BillInfo} from '@pages/AddBillFunnel/AddBillFunnel';

import REGEXP from '@constants/regExp';

import {BillStep} from './useAddBillFunnel';

interface Props {
billInfo: BillInfo;
setBillInfo: React.Dispatch<React.SetStateAction<BillInfo>>;
setStep: React.Dispatch<React.SetStateAction<BillStep>>;
}

const useTitleStep = ({billInfo, setBillInfo, setStep}: Props) => {
const [errorMessage, setErrorMessage] = useState('');

const onTitleInputChange = (value: string) => {
if (REGEXP.billTitle.test(value)) {
setBillInfo(prev => ({...prev, title: value}));
}
};

const handleTitleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.value.length > 12) {
setErrorMessage('지출내역은 12자까지 입력 가능해요');
onTitleInputChange(billInfo.title.slice(0, 12));
} else {
setErrorMessage('');
onTitleInputChange(event.target.value);
}
};

const canSubmitTitleInput = billInfo.title && !errorMessage;

const handleTitleInputEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.nativeEvent.isComposing) {
return;
}
if (event.key === 'Enter' && canSubmitTitleInput) {
event.preventDefault();
setStep('members');
}
};

const handleNextStep = () => {
setStep('members');
};

const handlePrevStep = () => {
setStep('price');
};

return {
errorMessage,
handleTitleInputChange,
handleTitleInputEnter,
canSubmitTitleInput,
handleNextStep,
handlePrevStep,
};
};

export default useTitleStep;
16 changes: 13 additions & 3 deletions client/src/mocks/handlers/billHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ export const billHandler = [
const {title, price, members} = await request.json();
const newBill = {id: Date.now(), title, price, isFixed: false};

billData.steps[0].bills.push(newBill);
billData.steps[0].members = members.map(id => ({id, name: `Member ${id}`}));
const lastStep = billData.steps[billData.steps.length - 1];
const isSameMembers = JSON.stringify(lastStep.members.map(m => m.id).sort()) === JSON.stringify(members.sort());

if (isSameMembers) {
lastStep.bills.push(newBill);
} else {
billData.steps.push({
bills: [newBill],
members: members.map(id => ({id, name: `Member ${id}`})),
});
}

(billDetailsData as unknown as BillDetailsData)[newBill.id.toString()] = {
billDetails: members.map((id, index) => ({
id,
memberName: `Member ${id}`,
memberName:
billData.steps.flatMap(step => step.members).find(member => member.id === id)?.name || `Member ${id}`,
price: (Math.floor(price / members.length) + (index < price % members.length ? 1 : 0)).toString(),
})),
};
Expand Down
Loading

0 comments on commit 8271194

Please sign in to comment.