diff --git a/client/package-lock.json b/client/package-lock.json index d01bee964..d9f21f883 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", "@tanstack/react-query": "^5.51.23", - "haengdong-design": "^0.1.74", + "haengdong-design": "^0.1.76", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", @@ -10137,9 +10137,9 @@ } }, "node_modules/haengdong-design": { - "version": "0.1.74", - "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.74.tgz", - "integrity": "sha512-K75LDIR4wqR+Z8YDTMsYXm9sWQ60Qw4DLPSOSnsb5mzncX1u3/z+zLOb1gs/zS8YZznUwzu6HzavWh6Sl8guNQ==", + "version": "0.1.76", + "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.76.tgz", + "integrity": "sha512-wTZv/3XK9I8N5NAyCAoYyykWu76MzWQlQ+jDZDuTFuzyV2c+MoYI2Ouvyu+9rl+HDYRuBVIaQ6ysfM6hQcwVYg==", "dependencies": { "@emotion/react": "^11.11.4", "@storybook/addon-webpack5-compiler-swc": "^1.0.5", diff --git a/client/package.json b/client/package.json index 5de424ec7..ff2abe9d8 100644 --- a/client/package.json +++ b/client/package.json @@ -68,7 +68,7 @@ "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", "@tanstack/react-query": "^5.51.23", - "haengdong-design": "^0.1.74", + "haengdong-design": "^0.1.76", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", @@ -80,4 +80,4 @@ "npm": ">=10.7.0", "node": ">=20.15.1" } -} \ No newline at end of file +} diff --git a/client/src/components/StepList/BillStepItem.tsx b/client/src/components/StepList/BillStepItem.tsx index 7c49d8efa..89fbe7e2e 100644 --- a/client/src/components/StepList/BillStepItem.tsx +++ b/client/src/components/StepList/BillStepItem.tsx @@ -1,26 +1,41 @@ -import type {BillStep, MemberReport} from 'types/serviceType'; - -import {DragHandleItem, DragHandleItemContainer} from 'haengdong-design'; +import {DragHandleItem, DragHandleItemContainer, EditableItem, Flex, Text} from 'haengdong-design'; import {Fragment, useState} from 'react'; import {useOutletContext} from 'react-router-dom'; +import {BillStep, MemberReport} from 'types/serviceType'; import {PutAndDeleteBillActionModal} from '@components/Modal/SetActionModal/PutAndDeleteBillActionModal'; import {MemberListInBillStep} from '@components/Modal/MemberListInBillStep'; import {EventPageContextProps} from '@pages/EventPage/EventPageLayout'; +import useSetBillInput from '@hooks/useSetBillInput'; + interface BillStepItemProps { step: BillStep; isOpenBottomSheet: boolean; setIsOpenBottomSheet: React.Dispatch>; + isAddEditableItem: boolean; + setIsAddEditableItem: React.Dispatch>; + isLastBillItem: boolean; + index: number; } -const BillStepItem: React.FC = ({step, isOpenBottomSheet, setIsOpenBottomSheet}) => { +const BillStepItem: React.FC = ({ + step, + isOpenBottomSheet, + setIsOpenBottomSheet, + isAddEditableItem, + setIsAddEditableItem, + isLastBillItem, + index, +}) => { const {isAdmin} = useOutletContext(); + const {handleBlurBillRequest, handleChangeBillInput, billInput} = useSetBillInput({setIsAddEditableItem}); + const [clickedIndex, setClickedIndex] = useState(-1); const [isOpenMemberListInBillStep, setIsOpenMemberListInBillStep] = useState(false); const stepName = `차`; - const totalPrice = step.actions.reduce((acc, cur) => acc + cur.price, 0); + const totalPrice = step.actions && step.type === 'BILL' ? step.actions.reduce((acc, cur) => acc + cur.price, 0) : 0; const handleDragHandleItemClick = (index: number) => { setClickedIndex(index); @@ -39,6 +54,7 @@ const BillStepItem: React.FC = ({step, isOpenBottomSheet, set return ( <> = ({step, isOpenBottomSheet, set backgroundColor="white" onTopRightTextClick={handleTopRightTextClick} > - {step.actions.map((action, index) => ( - - handleDragHandleItemClick(index)} - /> - {isOpenBottomSheet && clickedIndex === index && isAdmin && ( - ( + + handleDragHandleItemClick(index)} /> - )} - - ))} + + {isOpenBottomSheet && clickedIndex === index && isAdmin && ( + + )} + + ))} + + {isAddEditableItem && isLastBillItem && ( + + handleChangeBillInput('title', e)} + > + + handleChangeBillInput('price', e)} + style={{textAlign: 'right'}} + > + + + + )} {isOpenMemberListInBillStep && ( >; } -const Step = ({step}: StepProps) => { +const Step = ({step, isAddEditableItem, lastBillItemIndex, lastItemIndex, setIsAddEditableItem, index}: StepProps) => { const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false); + const [isLastBillItem, setIsLastBillItem] = useState(false); - if (step.type === 'BILL') { + useEffect(() => { + if (index === lastBillItemIndex && lastBillItemIndex === lastItemIndex) { + // index를 사용하여 마지막 BillStep인지 확인 + setIsLastBillItem(true); + } else { + setIsLastBillItem(false); + } + }, [index, lastBillItemIndex]); + + if (step.actions && step.type === 'BILL') { return ( - + ); - } else if (step.type === 'IN' || step.type === 'OUT') { + } else if (step.actions && (step.type === 'IN' || step.type === 'OUT')) { return ( ); diff --git a/client/src/components/StepList/StepList.tsx b/client/src/components/StepList/StepList.tsx index be9f40998..755bf8ac6 100644 --- a/client/src/components/StepList/StepList.tsx +++ b/client/src/components/StepList/StepList.tsx @@ -1,18 +1,69 @@ import {Flex} from 'haengdong-design'; +import {useEffect, useMemo, useState} from 'react'; import {BillStep, MemberStep} from 'types/serviceType'; import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; import Step from './Step'; -const StepList = () => { +interface StepListProps { + isAddEditableItem: boolean; + setIsAddEditableItem: React.Dispatch>; +} + +const StepList = ({isAddEditableItem, setIsAddEditableItem}: StepListProps) => { const {data: stepListData} = useRequestGetStepList(); - const stepList = stepListData ?? ([] as (MemberStep | BillStep)[]); + const [stepList, setStepList] = useState<(MemberStep | BillStep)[]>([]); + const existIndexInStepList = stepList.map((step, index) => ({...step, index})); + const [hasAddedItem, setHasAddedItem] = useState(false); + + useEffect(() => { + if (stepListData) { + setStepList(stepListData); + } + }, [stepListData]); + + const lastBillItemIndex = useMemo(() => { + const billSteps = existIndexInStepList.filter(step => step.type === 'BILL'); + + // billSteps 배열이 비어 있지 않으면 마지막 항목의 index를 반환, 그렇지 않으면 -1을 반환 + return billSteps.length > 0 ? billSteps.slice(-1)[0].index : -1; + }, [stepList]); + + const lastItemIndex = useMemo(() => { + return existIndexInStepList.length > 0 ? existIndexInStepList.slice(-1)[0].index : -1; + }, [existIndexInStepList]); + + useEffect(() => { + // 최초로 빈 stepList가 생성되고 난 후, 다시 hasAddedItem을 false로 변환하기 위한 조건문 + if (hasAddedItem) setHasAddedItem(prev => !prev); + + if (isAddEditableItem && lastBillItemIndex !== lastItemIndex && !hasAddedItem) { + setStepList(prev => [ + ...prev, + { + type: 'BILL', + stepName: '', + members: [], + actions: [], + }, + ]); + setHasAddedItem(prev => !prev); + } + }, [isAddEditableItem, lastBillItemIndex, lastItemIndex, hasAddedItem, existIndexInStepList, stepListData]); return ( {stepList.map((step, index) => ( - + ))} ); diff --git a/client/src/hooks/useSetBillInput.ts b/client/src/hooks/useSetBillInput.ts new file mode 100644 index 000000000..c1dd4a394 --- /dev/null +++ b/client/src/hooks/useSetBillInput.ts @@ -0,0 +1,66 @@ +import {useState} from 'react'; + +import validatePurchase from '@utils/validate/validatePurchase'; +import {Bill} from 'types/serviceType'; + +import useRequestPostBillList from './queries/useRequestPostBillList'; +import {BillInputType, InputPair} from './useDynamicBillActionInput'; + +interface UseSetBillInputProps { + setIsAddEditableItem: React.Dispatch>; +} + +interface UseSetBillInputReturns { + billInput: Bill; + handleChangeBillInput: (field: BillInputType, event: React.ChangeEvent) => void; + handleBlurBillRequest: () => void; +} + +const useSetBillInput = ({setIsAddEditableItem}: UseSetBillInputProps): UseSetBillInputReturns => { + const initialInput = {title: '', price: 0}; + const [billInput, setBillInput] = useState(initialInput); + + const {mutate: postBillList} = useRequestPostBillList(); + + const handleChangeBillInput = (field: BillInputType, event: React.ChangeEvent) => { + const {value} = event.target; + const {isValid} = validatePurchase({ + ...billInput, + [field]: value, + }); + + if (isValid) { + setBillInput(prev => ({ + ...prev, + [field]: value, + })); + } + }; + + const handleBlurBillRequest = () => { + const isEmptyTitle = billInput.title.trim().length; + const isEmptyPrice = Number(billInput.price); + + // 두 input의 값이 모두 채워졌을 때 api 요청 + // api 요청을 하면 Input을 띄우지 않음 + if (isEmptyTitle && isEmptyPrice) { + postBillList( + {billList: [billInput]}, + { + onSuccess: () => { + setBillInput(initialInput); + setIsAddEditableItem(false); + }, + }, + ); + } + }; + + return { + billInput, + handleBlurBillRequest, + handleChangeBillInput, + }; +}; + +export default useSetBillInput; diff --git a/client/src/pages/EventPage/AdminPage/AdminPage.style.ts b/client/src/pages/EventPage/AdminPage/AdminPage.style.ts index 6fea4be3b..1a711bbbd 100644 --- a/client/src/pages/EventPage/AdminPage/AdminPage.style.ts +++ b/client/src/pages/EventPage/AdminPage/AdminPage.style.ts @@ -4,8 +4,7 @@ export const receiptStyle = () => css({ display: 'flex', flexDirection: 'column', - gap: '8px', - padding: '0 8px', + gap: '1rem', paddingBottom: '8.75rem', }); @@ -14,3 +13,11 @@ export const titleAndListButtonContainerStyle = () => display: 'flex', flexDirection: 'column', }); + +export const buttonGroupStyle = () => + css({ + display: 'flex', + width: '100%', + padding: '0 0.5rem', + gap: '0.5rem', + }); diff --git a/client/src/pages/EventPage/AdminPage/AdminPage.tsx b/client/src/pages/EventPage/AdminPage/AdminPage.tsx index 9fd10f8ef..65e6f42cc 100644 --- a/client/src/pages/EventPage/AdminPage/AdminPage.tsx +++ b/client/src/pages/EventPage/AdminPage/AdminPage.tsx @@ -1,5 +1,5 @@ import {useEffect, useState} from 'react'; -import {Title, FixedButton, ListButton} from 'haengdong-design'; +import {Title, FixedButton, ListButton, Button} from 'haengdong-design'; import {useOutletContext} from 'react-router-dom'; import StepList from '@components/StepList/StepList'; @@ -11,11 +11,12 @@ import {useTotalExpenseAmountStore} from '@store/totalExpenseAmountStore'; import {EventPageContextProps} from '../EventPageLayout'; -import {receiptStyle, titleAndListButtonContainerStyle} from './AdminPage.style'; +import {receiptStyle, titleAndListButtonContainerStyle, buttonGroupStyle} from './AdminPage.style'; const AdminPage = () => { const [isOpenFixedButtonBottomSheet, setIsOpenFixedButtonBottomSheet] = useState(false); const [isOpenAllMemberListButton, setIsOpenAllMemberListButton] = useState(false); + const [isAddEditableItem, setIsAddEditableItem] = useState(false); const {eventName} = useOutletContext(); const {data: allMemberListData} = useRequestGetAllMemberList(); @@ -54,11 +55,24 @@ const AdminPage = () => { )}
- - setIsOpenFixedButtonBottomSheet(prev => !prev)} - /> + + {allMemberList.length === 0 ? ( + setIsOpenFixedButtonBottomSheet(prev => !prev)} /> + ) : ( +
+ + +
+ )} {isOpenFixedButtonBottomSheet && (