From 78d20a398b459eccc1ced55b4dfb61df23ef7e61 Mon Sep 17 00:00:00 2001 From: JinHo Kim <81083461+jinhokim98@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:08:46 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B0=A8=EB=93=B1=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9=20=EC=A4=91=20=EC=A7=80=EC=9B=90=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#423)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 0원을 허용하도록 수정 * feat: 총 금액이 변동됐을 때 재계산을 실행하는 기능 추가 * feat: 하나라도 조정값이 있는지를 판단하는 기능 추가 * test: 조정된 값이 있는지 여부를 주는 기능 test 작성 * feat: 조정되지 않은 인원이 1명일 때, input을 막아버리는 기능 추가 * feat: 서버상태와 클라이언트 상태 가격이 전부 동일하면 can submit false 기능 추가 --- .../useMemberReportInput.tsx | 26 ++++++++- .../useMemberReportListInAction.test.tsx | 21 +++++++ .../useMemberReportListInAction.ts | 58 +++++++++++++++++++ .../validate/validateMemberReportInAction.ts | 10 +--- 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/client/src/hooks/useMemberReportListInAction/useMemberReportInput.tsx b/client/src/hooks/useMemberReportListInAction/useMemberReportInput.tsx index 638cff2e5..9f0cb4449 100644 --- a/client/src/hooks/useMemberReportListInAction/useMemberReportInput.tsx +++ b/client/src/hooks/useMemberReportListInAction/useMemberReportInput.tsx @@ -11,13 +11,23 @@ type MemberReportInput = MemberReportInAction & { type UseMemberReportProps = { data: MemberReportInAction[]; addAdjustedMember: (memberReport: MemberReportInAction) => void; + getOnlyOneNotAdjustedRemainMemberIndex: () => number | null; + getIsSamePriceStateAndServerState: () => boolean; totalPrice: number; }; -const useMemberReportInput = ({data, addAdjustedMember, totalPrice}: UseMemberReportProps) => { +const useMemberReportInput = ({ + data, + addAdjustedMember, + totalPrice, + getOnlyOneNotAdjustedRemainMemberIndex, + getIsSamePriceStateAndServerState, +}: UseMemberReportProps) => { const [inputList, setInputList] = useState(data.map((item, index) => ({...item, index}))); const [canSubmit, setCanSubmit] = useState(false); + const [canEditList, setCanEditList] = useState(new Array(data.length).fill(true)); + const onChange = (event: React.ChangeEvent, index: number) => { const {value} = event.target; @@ -25,8 +35,8 @@ const useMemberReportInput = ({data, addAdjustedMember, totalPrice}: UseMemberRe }; const validateAndAddAdjustedMember = (price: string, index: number) => { - const {isValid, errorMessage} = validateMemberReportInAction(price, totalPrice); - setCanSubmit(errorMessage === null); + const {isValid} = validateMemberReportInAction(price, totalPrice); + setCanSubmit(isValid); if (isValid) { const newInputList = [...inputList]; @@ -44,12 +54,22 @@ const useMemberReportInput = ({data, addAdjustedMember, totalPrice}: UseMemberRe // addAdjustedMember로 인해 data가 변했을 때 input list의 값을 맞춰주기 위함 useEffect(() => { + setCanSubmit(!getIsSamePriceStateAndServerState()); setInputList(data.map((item, index) => ({...item, index}))); + + // 남은 인원이 1명일 때 수정을 불가능하도록 설정 + const onlyOneIndex = getOnlyOneNotAdjustedRemainMemberIndex(); + if (onlyOneIndex !== null) { + const newCanEditList = new Array(data.length).fill(true); + newCanEditList[onlyOneIndex] = false; + setCanEditList(newCanEditList); + } }, [data]); return { inputList, onChange, + canEditList, canSubmit, }; }; diff --git a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx index e40e1694f..99d3b1814 100644 --- a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx +++ b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx @@ -275,6 +275,27 @@ describe('useMemberReportListInActionTest', () => { const targetMemberReset = result.current.memberReportListInAction.find(member => member.name === '망쵸'); expect(targetMemberReset?.isFixed).toBe(false); }); + + it('아무도 조정된 값이 없다면 조정값이 있는지 확인 결과 false다.', async () => { + const {result} = initializeProvider(actionId, totalPrice); + + await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); + + expect(result.current.isExistAdjustedPrice()).toBe(false); + }); + + it('망쵸의 가격을 100원으로 바꾼 후 리스트 중 조정값이 있는지 확인 결과 true다.', async () => { + const {result} = initializeProvider(actionId, totalPrice); + const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; + + await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); + + act(() => { + result.current.addAdjustedMember(adjustedMemberMangcho); + }); + + expect(result.current.isExistAdjustedPrice()).toBe(true); + }); }); // last diff --git a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.ts b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.ts index 9549c7e91..3d332ea25 100644 --- a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.ts +++ b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.ts @@ -13,12 +13,51 @@ const useMemberReportListInAction = (actionId: number, totalPrice: number) => { memberReportListInActionFromServer, ); + // isFixed를 모두 풀고 계산값으로 모두 처리하는 기능 + const reCalculatePriceByTotalPriceChange = () => { + const {divided, remainder} = calculateDividedPrice(memberReportListInAction.length, 0); + + const resetMemberReportList = [...memberReportListInAction]; + resetMemberReportList.forEach((member, index) => { + if (index !== resetMemberReportList.length - 1) { + member.price = divided; + } else { + member.price = divided + remainder; + } + member.isFixed = false; + }); + + setMemberReportListInAction(resetMemberReportList); + }; + + // 총 금액이 변동됐을 때 (서버에서 온 값과 다를 때) 재계산 실행 + useEffect(() => { + const totalPriceFromServer = memberReportListInActionFromServer.reduce((acc, cur) => acc + cur.price, 0); + + if (totalPriceFromServer !== totalPrice) { + reCalculatePriceByTotalPriceChange(); + } + }, [totalPrice]); + useEffect(() => { if (queryResult.isSuccess) { setMemberReportListInAction(memberReportListInActionFromServer); } }, [memberReportListInActionFromServer, queryResult.isSuccess]); + const isExistAdjustedPrice = () => { + return memberReportListInAction.some(member => member.isFixed === true); + }; + + // 조정되지 않은 인원이 단 1명인 경우의 index + const getOnlyOneNotAdjustedRemainMemberIndex = (): number | null => { + const adjustedPriceCount = getAdjustedMemberCount(memberReportListInAction); + + if (adjustedPriceCount < memberReportListInAction.length - 1) return null; + + return memberReportListInAction.findIndex(member => member.isFixed === false); + }; + // 조정값 멤버의 수를 구하는 함수 const getAdjustedMemberCount = (memberReportListInAction: MemberReportInAction[]) => { return memberReportListInAction.filter(member => member.isFixed === true).length; @@ -89,10 +128,29 @@ const useMemberReportListInAction = (actionId: number, totalPrice: number) => { putMemberReportListInAction(memberReportListInAction); }; + const getIsSamePriceStateAndServerState = () => { + const serverStatePriceList = memberReportListInActionFromServer.map(({price}) => price); + const clientStatePriceList = memberReportListInAction.map(({price}) => price); + + let isSame = true; + + // isArrayEqual을 사용하지 않은 이유는 정렬이 영향을 받으면 안 되기 때문입니다 + for (let i = 0; i < serverStatePriceList.length; i++) { + if (serverStatePriceList[i] !== clientStatePriceList[i]) { + isSame = false; + } + } + + return isSame; + }; + return { memberReportListInAction, addAdjustedMember, + isExistAdjustedPrice, onSubmit, + getOnlyOneNotAdjustedRemainMemberIndex, + getIsSamePriceStateAndServerState, queryResult, }; }; diff --git a/client/src/utils/validate/validateMemberReportInAction.ts b/client/src/utils/validate/validateMemberReportInAction.ts index 8e02c3dde..b4e6c2a9a 100644 --- a/client/src/utils/validate/validateMemberReportInAction.ts +++ b/client/src/utils/validate/validateMemberReportInAction.ts @@ -17,21 +17,13 @@ const validateMemberReportInAction = (price: string, totalPrice: number): Valida return true; }; - const validateEmpty = () => { - if (!price.trim().length) { - errorMessage = ERROR_MESSAGE.preventEmpty; - return false; - } - return true; - }; - const validateUnderTotalPrice = () => { if (numberTypePrice > totalPrice) return false; return true; }; - if (validateOnlyNaturalNumber() && validatePrice() && validateEmpty() && validateUnderTotalPrice()) { + if (validateOnlyNaturalNumber() && validatePrice() && validateUnderTotalPrice()) { return {isValid: true, errorMessage: null}; }