diff --git a/client/src/apis/tempPrefix.ts b/client/src/apis/endpointPrefix.ts similarity index 63% rename from client/src/apis/tempPrefix.ts rename to client/src/apis/endpointPrefix.ts index 949981b7..18b5756c 100644 --- a/client/src/apis/tempPrefix.ts +++ b/client/src/apis/endpointPrefix.ts @@ -1,2 +1,3 @@ // TODO: (@weadie) 반복되서 쓰이는 이 api/events가 추후 수정 가능성이 있어서 일단 편집하기 편하게 이 변수를 재사용하도록 했습니다. -export const TEMP_PREFIX = '/api/events'; +export const USER_API_PREFIX = '/api/events'; +export const ADMIN_API_PREFIX = '/api/admin/events'; diff --git a/client/src/apis/request/auth.ts b/client/src/apis/request/auth.ts index 9c56d1f6..aac5c622 100644 --- a/client/src/apis/request/auth.ts +++ b/client/src/apis/request/auth.ts @@ -1,23 +1,23 @@ import {BASE_URL} from '@apis/baseUrl'; -import {TEMP_PREFIX} from '@apis/tempPrefix'; +import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix'; import {requestPostWithoutResponse} from '@apis/fetcher'; -import {WithEventId} from '@apis/withEventId.type'; - -export type RequestToken = { - password: string; -}; +import {WithEventId} from '@apis/withId.type'; export const requestPostAuthentication = async ({eventId}: WithEventId) => { await requestPostWithoutResponse({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/auth`, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/auth`, }); }; -export const requestPostToken = async ({eventId, password}: WithEventId) => { +export interface RequestPostToken { + password: string; +} + +export const requestPostToken = async ({eventId, password}: WithEventId) => { await requestPostWithoutResponse({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/login`, + endpoint: `${USER_API_PREFIX}/${eventId}/login`, body: { password: password, }, diff --git a/client/src/apis/request/bill.ts b/client/src/apis/request/bill.ts index c0816852..561afcc5 100644 --- a/client/src/apis/request/bill.ts +++ b/client/src/apis/request/bill.ts @@ -1,69 +1,73 @@ -import type {Bill, MemberReportInAction} from 'types/serviceType'; +import type {BillDetails} from 'types/serviceType'; import {BASE_URL} from '@apis/baseUrl'; -import {TEMP_PREFIX} from '@apis/tempPrefix'; +import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix'; import {requestDelete, requestGet, requestPostWithoutResponse, requestPut} from '@apis/fetcher'; -import {WithEventId} from '@apis/withEventId.type'; +import {WithBillId, WithEventId} from '@apis/withId.type'; -type RequestPostBillList = { - billList: Bill[]; -}; +export interface RequestPostBill { + title: string; + price: number; + members: number[]; +} -export const requestPostBillList = async ({eventId, billList}: WithEventId) => { +export const requestPostBill = async ({eventId, title, price, members}: WithEventId) => { await requestPostWithoutResponse({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/bill-actions`, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/bills`, body: { - actions: billList, + title, + price, + members, }, }); }; -type RequestBillAction = { - actionId: number; -}; - -export const requestDeleteBillAction = async ({eventId, actionId}: WithEventId) => { +export const requestDeleteBill = async ({eventId, billId}: WithEventId) => { await requestDelete({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/bill-actions/${actionId}`, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/bills/${billId}`, }); }; -type RequestPutBillAction = Bill & RequestBillAction; +export interface RequestPutBill { + title: string; + price: number; +} -export const requestPutBillAction = async ({eventId, actionId, title, price}: WithEventId) => { +export const requestPutBill = async ({eventId, billId, title, price}: WithEventId>) => { await requestPut({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/bill-actions/${actionId}`, - body: { - title, - price, - }, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/bills/${billId}`, + body: {title, price}, }); }; -export type MemberReportList = {members: MemberReportInAction[]}; - -export const requestGetMemberReportListInAction = async ({eventId, actionId}: WithEventId) => { - return requestGet({ +export const requestGetBillDetails = async ({eventId, billId}: WithEventId) => { + return requestGet({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/bill-actions/${actionId}/fixed`, + endpoint: `${USER_API_PREFIX}/${eventId}/bills/${billId}/fixed`, }); }; -type RequestPutMemberReportList = RequestBillAction & MemberReportList; +interface PutBillDetail { + id: number; + price: number; + isFixed: boolean; +} + +export interface RequestPutBillDetails { + billDetails: PutBillDetail[]; +} -export const requestPutMemberReportListInAction = async ({ +export const requestPutBillDetails = async ({ eventId, - actionId, - members, -}: WithEventId) => { - return requestPut({ + billId, + billDetails, +}: WithEventId>) => { + await requestPut({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/bill-actions/${actionId}/fixed`, - body: { - members, - }, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/bills/${billId}/fixed`, + body: {billDetails}, }); }; diff --git a/client/src/apis/request/event.ts b/client/src/apis/request/event.ts index 6d559e94..c8c218f3 100644 --- a/client/src/apis/request/event.ts +++ b/client/src/apis/request/event.ts @@ -1,32 +1,43 @@ -import {TEMP_PREFIX} from '@apis/tempPrefix'; -import {requestGet, requestPostWithResponse} from '@apis/fetcher'; -import {WithEventId} from '@apis/withEventId.type'; +import {Event, EventId} from 'types/serviceType'; -export type RequestPostNewEvent = { +import {USER_API_PREFIX} from '@apis/endpointPrefix'; +import {requestGet, requestPostWithResponse, requestPut} from '@apis/fetcher'; +import {WithEventId} from '@apis/withId.type'; + +export interface RequestPostEvent { eventName: string; password: string; -}; +} -export type ResponsePostNewEvent = { - eventId: string; -}; - -export const requestPostNewEvent = async ({eventName, password}: RequestPostNewEvent) => { - return await requestPostWithResponse({ - endpoint: TEMP_PREFIX, +export const requestPostEvent = async ({eventName, password}: RequestPostEvent) => { + return await requestPostWithResponse({ + endpoint: USER_API_PREFIX, body: { - eventName: eventName, - password: password, + eventName, + password, }, }); }; -type ResponseGetEventName = { - eventName: string; +export const requestGetEvent = async ({eventId}: WithEventId) => { + return await requestGet({ + endpoint: `${USER_API_PREFIX}/${eventId}`, + }); }; -export const requestGetEventName = async ({eventId}: WithEventId) => { - return requestGet({ - endpoint: `${TEMP_PREFIX}/${eventId}`, +export interface RequestPutEvent { + eventName?: string; + bankName?: string; + accountNumber?: string; +} + +export const requestPutEvent = async ({eventId, eventName, bankName, accountNumber}: WithEventId) => { + return await requestPut({ + endpoint: `${USER_API_PREFIX}/${eventId}`, + body: { + eventName, + bankName, + accountNumber, + }, }); }; diff --git a/client/src/apis/request/member.ts b/client/src/apis/request/member.ts index dd715257..9c0a0506 100644 --- a/client/src/apis/request/member.ts +++ b/client/src/apis/request/member.ts @@ -1,84 +1,68 @@ -import type {MemberType} from 'types/serviceType'; +import type {AllMembers, Members} from 'types/serviceType'; import {BASE_URL} from '@apis/baseUrl'; -import {TEMP_PREFIX} from '@apis/tempPrefix'; -import {requestDelete, requestGet, requestPut, requestPostWithoutResponse} from '@apis/fetcher'; -import {WithEventId} from '@apis/withEventId.type'; +import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix'; +import {requestDelete, requestGet, requestPut, requestPostWithResponse} from '@apis/fetcher'; +import {WithEventId} from '@apis/withId.type'; -type RequestPostMemberList = { - memberNameList: string[]; - type: MemberType; -}; +interface PostMember { + name: string; +} + +export interface RequestPostMembers { + members: PostMember[]; +} -export const requestPostMemberList = async ({eventId, type, memberNameList}: WithEventId) => { - await requestPostWithoutResponse({ +export const requestPostMembers = async ({eventId, members: newMembers}: WithEventId) => { + return await requestPostWithResponse({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/member-actions`, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/members`, body: { - members: memberNameList, - status: type, + members: newMembers, }, }); }; -type RequestDeleteMemberAction = { - actionId: number; -}; +export interface RequestDeleteMember { + memberId: number; +} -export const requestDeleteMemberAction = async ({eventId, actionId}: WithEventId) => { +export const requestDeleteMember = async ({eventId, memberId}: WithEventId) => { await requestDelete({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/member-actions/${actionId}`, - }); -}; - -type ResponseGetAllMemberList = { - memberNames: string[]; -}; - -export const requestGetAllMemberList = async ({eventId}: WithEventId) => { - return requestGet({ - endpoint: `${TEMP_PREFIX}/${eventId}/members`, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/members/${memberId}`, }); }; -export type MemberChange = { - before: string; - after: string; -}; +interface PutMember { + id: number; + name: string; + isDeposited: boolean; +} -type RequestPutAllMemberList = { - members: MemberChange[]; -}; +export interface RequestPutMembers { + members: PutMember[]; +} -export const requestPutAllMemberList = async ({eventId, members}: WithEventId) => { +export const requestPutMembers = async ({eventId, members}: WithEventId) => { await requestPut({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/members/nameChange`, + endpoint: `${ADMIN_API_PREFIX}/${eventId}/members`, body: { - members, + members: members, }, }); }; -type RequestDeleteAllMemberList = { - memberName: string; -}; - -export const requestDeleteAllMemberList = async ({eventId, memberName}: WithEventId) => { - await requestDelete({ +export const requestGetCurrentMembers = async ({eventId}: WithEventId) => { + return await requestGet({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/members/${memberName}`, + endpoint: `${USER_API_PREFIX}/${eventId}/members/current`, }); }; -export type ResponseGetCurrentInMemberList = { - memberNames: string[]; -}; - -export const requestGetCurrentInMemberList = async ({eventId}: WithEventId) => { - return await requestGet({ - baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/members/current`, +export const requestGetAllMembers = async ({eventId}: WithEventId) => { + return await requestGet({ + endpoint: `${USER_API_PREFIX}/${eventId}/members`, }); }; diff --git a/client/src/apis/request/report.ts b/client/src/apis/request/report.ts index 8992f9c3..6d73ff16 100644 --- a/client/src/apis/request/report.ts +++ b/client/src/apis/request/report.ts @@ -1,19 +1,13 @@ -import type {MemberReport} from 'types/serviceType'; +import type {Reports} from 'types/serviceType'; import {BASE_URL} from '@apis/baseUrl'; -import {TEMP_PREFIX} from '@apis/tempPrefix'; +import {USER_API_PREFIX} from '@apis/endpointPrefix'; import {requestGet} from '@apis/fetcher'; -import {WithEventId} from '@apis/withEventId.type'; +import {WithEventId} from '@apis/withId.type'; -type ResponseGetMemberReportList = { - reports: MemberReport[]; -}; - -export const requestGetMemberReportList = async ({eventId}: WithEventId) => { - const {reports} = await requestGet({ +export const requestGetReports = async ({eventId}: WithEventId) => { + return await requestGet({ baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/actions/reports`, + endpoint: `${USER_API_PREFIX}/${eventId}/reports`, }); - - return reports; }; diff --git a/client/src/apis/request/step.ts b/client/src/apis/request/step.ts new file mode 100644 index 00000000..f67615e2 --- /dev/null +++ b/client/src/apis/request/step.ts @@ -0,0 +1,15 @@ +import {Steps} from 'types/serviceType'; + +import {BASE_URL} from '@apis/baseUrl'; +import {USER_API_PREFIX} from '@apis/endpointPrefix'; +import {requestGet} from '@apis/fetcher'; +import {WithEventId} from '@apis/withId.type'; + +export const requestGetSteps = async ({eventId}: WithEventId) => { + const {steps} = await requestGet({ + baseUrl: BASE_URL.HD, + endpoint: `${USER_API_PREFIX}/${eventId}/bills`, + }); + + return steps; +}; diff --git a/client/src/apis/request/stepList.ts b/client/src/apis/request/stepList.ts deleted file mode 100644 index 0d47ab90..00000000 --- a/client/src/apis/request/stepList.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type {StepList} from 'types/serviceType'; - -import {BASE_URL} from '@apis/baseUrl'; -import {TEMP_PREFIX} from '@apis/tempPrefix'; -import {requestGet} from '@apis/fetcher'; -import {WithEventId} from '@apis/withEventId.type'; - -// TODO: (@weadie) 현재 토큰을 어떻게 관리할지.. 계속 사용되는데 -export const requestGetStepList = async ({eventId}: WithEventId) => { - // TODO: (@weadie) response가 어떻게 오는지 안나와서 data로만 써뒀어요. - const {steps} = await requestGet({ - baseUrl: BASE_URL.HD, - endpoint: `${TEMP_PREFIX}/${eventId}/actions`, - }); - - return steps; -}; diff --git a/client/src/apis/withEventId.type.ts b/client/src/apis/withId.type.ts similarity index 50% rename from client/src/apis/withEventId.type.ts rename to client/src/apis/withId.type.ts index b88160e3..2b1d3c2e 100644 --- a/client/src/apis/withEventId.type.ts +++ b/client/src/apis/withId.type.ts @@ -1,3 +1,7 @@ export type WithEventId

= P & { eventId: string; }; + +export type WithBillId

= P & { + billId: number; +}; diff --git a/client/src/assets/image/meatballs.svg b/client/src/assets/image/meatballs.svg new file mode 100644 index 00000000..bc3e17d2 --- /dev/null +++ b/client/src/assets/image/meatballs.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/src/components/AmountInput/AmountInput.stories.tsx b/client/src/components/AmountInput/AmountInput.stories.tsx new file mode 100644 index 00000000..84cedb17 --- /dev/null +++ b/client/src/components/AmountInput/AmountInput.stories.tsx @@ -0,0 +1,23 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import AmountInput from './AmountInput'; + +const meta = { + title: 'Components/AmountInput', + component: AmountInput, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: {}, + args: { + value: '112,000', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/client/src/components/AmountInput/AmountInput.tsx b/client/src/components/AmountInput/AmountInput.tsx new file mode 100644 index 00000000..c8267295 --- /dev/null +++ b/client/src/components/AmountInput/AmountInput.tsx @@ -0,0 +1,37 @@ +/** @jsxImportSource @emotion/react */ +import {css} from '@emotion/react'; + +import {Text} from '@components/Design'; + +interface Props { + value: string; +} + +const AmountInput = ({value}: Props) => { + return ( +

+ + {value ? value : '0'} + + + 원 + +
+ ); +}; + +export default AmountInput; diff --git a/client/src/components/Design/components/Amount/Amount.stories.tsx b/client/src/components/Design/components/Amount/Amount.stories.tsx new file mode 100644 index 00000000..bb9ce5b5 --- /dev/null +++ b/client/src/components/Design/components/Amount/Amount.stories.tsx @@ -0,0 +1,28 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import Amount from '@HDcomponents/Amount/Amount'; + +const meta = { + title: 'Components/Amount', + component: Amount, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + amount: { + description: '', + control: {type: 'number'}, + }, + }, + args: { + amount: 112000, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/client/src/components/Design/components/Amount/Amount.tsx b/client/src/components/Design/components/Amount/Amount.tsx new file mode 100644 index 00000000..f491df6b --- /dev/null +++ b/client/src/components/Design/components/Amount/Amount.tsx @@ -0,0 +1,20 @@ +/** @jsxImportSource @emotion/react */ +import Flex from '../Flex/Flex'; +import Text from '../Text/Text'; + +interface Props { + amount: number; +} + +const Amount = ({amount}: Props) => { + return ( + + {amount ? amount.toLocaleString('ko-kr') : 0} + + 원 + + + ); +}; + +export default Amount; diff --git a/client/src/components/Design/components/Button/Button.style.ts b/client/src/components/Design/components/Button/Button.style.ts index 7b76cf78..0d274f11 100644 --- a/client/src/components/Design/components/Button/Button.style.ts +++ b/client/src/components/Design/components/Button/Button.style.ts @@ -23,7 +23,7 @@ const getButtonDefaultStyle = (theme: Theme) => whiteSpace: 'nowrap', '&:disabled': { - backgroundColor: theme.colors.tertiary, + backgroundColor: theme.colors.grayContainer, color: theme.colors.onPrimary, cursor: 'default', }, diff --git a/client/src/components/Design/components/Chip/Chip.stories.tsx b/client/src/components/Design/components/Chip/Chip.stories.tsx new file mode 100644 index 00000000..08e7f259 --- /dev/null +++ b/client/src/components/Design/components/Chip/Chip.stories.tsx @@ -0,0 +1,36 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import Chip from '@HDcomponents/Chip/Chip'; + +import Text from '../Text/Text'; +import Amount from '../Amount/Amount'; + +const meta = { + title: 'Components/Chip', + component: Chip, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + color: { + description: '', + control: {type: 'select'}, + }, + text: { + description: '', + control: {type: 'text'}, + }, + }, + args: { + color: 'gray', + text: '망쵸', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/client/src/components/Design/components/Chip/Chip.style.ts b/client/src/components/Design/components/Chip/Chip.style.ts new file mode 100644 index 00000000..a7ca3eaf --- /dev/null +++ b/client/src/components/Design/components/Chip/Chip.style.ts @@ -0,0 +1,20 @@ +import {css} from '@emotion/react'; + +import {ColorKeys} from '@components/Design/token/colors'; +import {Theme} from '@theme/theme.type'; + +interface ChipStyleProps { + theme: Theme; + color: ColorKeys; +} + +export const chipStyle = ({theme, color}: ChipStyleProps) => + css({ + display: 'flex', + padding: '0.125rem 0.5rem ', + borderRadius: '0.5rem', + color: `${theme.colors[color]}`, + boxSizing: 'border-box', + outline: 'none', + boxShadow: `inset 0 0 0 1px ${theme.colors[color]}`, + }); diff --git a/client/src/components/Design/components/Chip/Chip.tsx b/client/src/components/Design/components/Chip/Chip.tsx new file mode 100644 index 00000000..9566dfe7 --- /dev/null +++ b/client/src/components/Design/components/Chip/Chip.tsx @@ -0,0 +1,26 @@ +/** @jsxImportSource @emotion/react */ + +import {ColorKeys} from '@components/Design/token/colors'; +import {useTheme} from '@components/Design/theme/HDesignProvider'; + +import Text from '../Text/Text'; + +import {chipStyle} from './Chip.style'; + +interface Props { + color: ColorKeys; + text: string; +} + +const Chip = ({color, text}: Props) => { + const {theme} = useTheme(); + return ( +
+ + {text} + +
+ ); +}; + +export default Chip; diff --git a/client/src/components/Design/components/ChipButton/ChipButton.stories.tsx b/client/src/components/Design/components/ChipButton/ChipButton.stories.tsx new file mode 100644 index 00000000..b3d3ea53 --- /dev/null +++ b/client/src/components/Design/components/ChipButton/ChipButton.stories.tsx @@ -0,0 +1,34 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import ChipButton from '@HDcomponents/ChipButton/ChipButton'; + +const meta = { + title: 'Components/ChipButton', + component: ChipButton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + color: { + description: '', + control: {type: 'select'}, + }, + text: { + description: '', + control: {type: 'text'}, + }, + }, + args: { + color: 'gray', + text: '망쵸', + onClick: () => {}, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/client/src/components/Design/components/ChipButton/ChipButton.style.ts b/client/src/components/Design/components/ChipButton/ChipButton.style.ts new file mode 100644 index 00000000..ec0a19c0 --- /dev/null +++ b/client/src/components/Design/components/ChipButton/ChipButton.style.ts @@ -0,0 +1,21 @@ +import {css} from '@emotion/react'; + +import {ColorKeys} from '@components/Design/token/colors'; +import {Theme} from '@theme/theme.type'; + +interface ChipStyleProps { + theme: Theme; + color: ColorKeys; +} + +export const chipButtonStyle = ({theme, color}: ChipStyleProps) => + css({ + display: 'flex', + padding: '0.25rem 0.375rem 0.25rem 0.75rem', + gap: '0.5rem', + borderRadius: '1rem', + color: `${theme.colors[color]}`, + boxSizing: 'border-box', + outline: 'none', + boxShadow: `inset 0 0 0 1px ${theme.colors[color]}`, + }); diff --git a/client/src/components/Design/components/ChipButton/ChipButton.tsx b/client/src/components/Design/components/ChipButton/ChipButton.tsx new file mode 100644 index 00000000..7b0e1aa5 --- /dev/null +++ b/client/src/components/Design/components/ChipButton/ChipButton.tsx @@ -0,0 +1,27 @@ +/** @jsxImportSource @emotion/react */ + +import {ColorKeys} from '@components/Design/token/colors'; +import {useTheme} from '@components/Design/theme/HDesignProvider'; + +import Text from '../Text/Text'; +import Icon from '../Icon/Icon'; + +import {chipButtonStyle} from './ChipButton.style'; + +interface Props { + color: ColorKeys; + text: string; + onClick: () => void; +} + +const ChipButton = ({color, text, onClick}: Props) => { + const {theme} = useTheme(); + return ( +
+ {text} + +
+ ); +}; + +export default ChipButton; diff --git a/client/src/components/Design/components/ChipGroup/ChipGroup.stories.tsx b/client/src/components/Design/components/ChipGroup/ChipGroup.stories.tsx new file mode 100644 index 00000000..f60a84a0 --- /dev/null +++ b/client/src/components/Design/components/ChipGroup/ChipGroup.stories.tsx @@ -0,0 +1,29 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import ChipGroup from '@HDcomponents/ChipGroup/ChipGroup'; + +const meta = { + title: 'Components/ChipGroup', + component: ChipGroup, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + color: { + description: '', + control: {type: 'select'}, + }, + }, + args: { + color: 'gray', + texts: ['망쵸', '감자', '백호', '이상'], + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = {}; diff --git a/client/src/components/Design/components/ChipGroup/ChipGroup.style.ts b/client/src/components/Design/components/ChipGroup/ChipGroup.style.ts new file mode 100644 index 00000000..f85afd3a --- /dev/null +++ b/client/src/components/Design/components/ChipGroup/ChipGroup.style.ts @@ -0,0 +1,6 @@ +import {css} from '@emotion/react'; + +export const chipGroupStyle = css({ + display: 'flex', + gap: '0.25rem', +}); diff --git a/client/src/components/Design/components/ChipGroup/ChipGroup.tsx b/client/src/components/Design/components/ChipGroup/ChipGroup.tsx new file mode 100644 index 00000000..79222821 --- /dev/null +++ b/client/src/components/Design/components/ChipGroup/ChipGroup.tsx @@ -0,0 +1,27 @@ +/** @jsxImportSource @emotion/react */ + +import {ColorKeys} from '@components/Design/token/colors'; +import {useTheme} from '@components/Design/theme/HDesignProvider'; + +import {chipStyle} from '../Chip/Chip.style'; +import Text from '../Text/Text'; +import Chip from '../Chip/Chip'; + +import {chipGroupStyle} from './ChipGroup.style'; + +interface Props { + color: ColorKeys; + texts: string[]; +} + +const ChipGroup = ({color, texts}: Props) => { + return ( +
+ {texts.map(text => ( + + ))} +
+ ); +}; + +export default ChipGroup; diff --git a/client/src/components/Design/components/FixedButton/FixedButton.style.ts b/client/src/components/Design/components/FixedButton/FixedButton.style.ts index a0128a3e..00a7ea25 100644 --- a/client/src/components/Design/components/FixedButton/FixedButton.style.ts +++ b/client/src/components/Design/components/FixedButton/FixedButton.style.ts @@ -59,6 +59,32 @@ export const fixedButtonStyle = (props: Required) => { return [getFixedButtonDefaultStyle(props.theme), getFixedButtonVariantsStyle(props.variants, props.theme)]; }; +export const cancleButtonStyle = (theme: Theme) => + css({ + display: 'flex', + justifyContent: 'center', + padding: '1rem 1.5rem', + borderRadius: '1rem', + width: '100%', + + fontFamily: 'Pretendard', + fontSize: '1.25rem', + fontWeight: '700', + lineHeight: '1', + + transition: '0.2s', + transitionTimingFunction: 'cubic-bezier(0.7, 0.62, 0.62, 1.16)', + + backgroundColor: theme.colors.tertiary, + color: theme.colors.onTertiary, + + '&:disabled': { + backgroundColor: theme.colors.grayContainer, + color: theme.colors.onPrimary, + cursor: 'default', + }, + }); + const getFixedButtonDefaultStyle = (theme: Theme) => css({ display: 'flex', @@ -76,7 +102,7 @@ const getFixedButtonDefaultStyle = (theme: Theme) => transitionTimingFunction: 'cubic-bezier(0.7, 0.62, 0.62, 1.16)', '&:disabled': { - backgroundColor: theme.colors.tertiary, + backgroundColor: theme.colors.grayContainer, color: theme.colors.onPrimary, cursor: 'default', }, diff --git a/client/src/components/Design/components/FixedButton/FixedButton.tsx b/client/src/components/Design/components/FixedButton/FixedButton.tsx index 3114bb9c..62ea0127 100644 --- a/client/src/components/Design/components/FixedButton/FixedButton.tsx +++ b/client/src/components/Design/components/FixedButton/FixedButton.tsx @@ -7,6 +7,7 @@ import { fixedButtonContainerStyle, fixedButtonStyle, buttonContainerStyle, + cancleButtonStyle, } from '@HDcomponents/FixedButton/FixedButton.style'; import {FixedButtonProps} from '@HDcomponents/FixedButton/FixedButton.type'; import IconButton from '@HDcomponents/IconButton/IconButton'; @@ -14,7 +15,7 @@ import Icon from '@HDcomponents/Icon/Icon'; import {useTheme} from '@theme/HDesignProvider'; export const FixedButton: React.FC = forwardRef(function Button( - {variants = 'primary', onDeleteClick, disabled, children, ...htmlProps}: FixedButtonProps, + {variants = 'primary', onDeleteClick, onBackClick, disabled, children, ...htmlProps}: FixedButtonProps, ref, ) { const {theme} = useTheme(); @@ -26,6 +27,11 @@ export const FixedButton: React.FC = forwardRef )} + {onBackClick && ( + + )} + ); +} diff --git a/client/src/components/Design/components/NumberKeyboard/NumberKeyboard.stories.tsx b/client/src/components/Design/components/NumberKeyboard/NumberKeyboard.stories.tsx new file mode 100644 index 00000000..a6a52555 --- /dev/null +++ b/client/src/components/Design/components/NumberKeyboard/NumberKeyboard.stories.tsx @@ -0,0 +1,42 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import {useRef, useState} from 'react'; + +import {Flex, Input} from '@components/Design'; + +import NumberKeyboard from './NumberKeyboard'; + +const meta = { + title: 'Components/NumberKeyboard', + component: NumberKeyboard, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + type: {description: '', control: {type: 'select'}, options: ['amount', 'number', 'string']}, + }, + args: { + type: 'amount', + maxNumber: 10000000, + onChange: () => {}, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + render: ({type, maxNumber}) => { + const inputRef = useRef(null); + const [value, setValue] = useState(''); + return ( + + + + + ); + }, +}; diff --git a/client/src/components/Design/components/NumberKeyboard/NumberKeyboard.tsx b/client/src/components/Design/components/NumberKeyboard/NumberKeyboard.tsx new file mode 100644 index 00000000..0da3862d --- /dev/null +++ b/client/src/components/Design/components/NumberKeyboard/NumberKeyboard.tsx @@ -0,0 +1,72 @@ +/** @jsxImportSource @emotion/react */ +import {css} from '@emotion/react'; + +import {Button, useTheme} from '@components/Design'; + +import {Keypad} from './Keypad'; +import useNumberKeyboard from './useNumberKeyboard'; + +export type KeyboardType = 'number' | 'string' | 'amount'; + +interface Props { + type: KeyboardType; + maxNumber: number; + onChange: (value: string) => void; +} + +export default function NumberKeyboard({type, maxNumber, 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, + maxNumber, + onChange, + }); + + return ( +
+ {type === 'amount' && ( +
+ + + + +
+ )} + {(type === 'amount' ? amountKeypads : numberKeypads).map(el => ( + onClickKeypad(el)} + /> + ))} +
+ ); +} diff --git a/client/src/components/Design/components/NumberKeyboard/useNumberKeyboard.tsx b/client/src/components/Design/components/NumberKeyboard/useNumberKeyboard.tsx new file mode 100644 index 00000000..f8d5fbae --- /dev/null +++ b/client/src/components/Design/components/NumberKeyboard/useNumberKeyboard.tsx @@ -0,0 +1,53 @@ +import {useEffect, useState} from 'react'; + +import {KeyboardType} from './NumberKeyboard'; + +interface Props { + type: KeyboardType; + maxNumber?: number; + onChange: (value: string) => void; +} + +const useNumberKeyboard = ({type, maxNumber, onChange}: Props) => { + const [value, setValue] = useState(''); + + const onClickKeypad = (inputValue: string) => { + const newValue = (value + inputValue).replace(/,/g, ''); + setValueByType(newValue); + }; + + const onClickDelete = () => { + const newValue = value.slice(0, value.length - 1).replace(/,/g, ''); + setValueByType(newValue); + }; + + const onClickDeleteAll = () => { + setValue(''); + }; + + const onClickAddAmount = (amount: number) => { + const newValue = `${Number(value.replace(/,/g, '')) + amount}`; + setValueByType(newValue); + }; + + const setValueByType = (value: string) => { + if (type === 'string') { + setValue(value); + } else { + const limitedValue = maxNumber && Number(value) > maxNumber ? `${maxNumber}` : value; + if (Number(limitedValue) === 0) { + setValue(''); + } else { + setValue(type === 'amount' ? Number(limitedValue).toLocaleString() : `${limitedValue}`); + } + } + }; + + useEffect(() => { + onChange(value); + }, [value]); + + return {value, onClickKeypad, onClickDelete, onClickDeleteAll, onClickAddAmount}; +}; + +export default useNumberKeyboard; diff --git a/client/src/components/Design/components/Title/Title.stories.tsx b/client/src/components/Design/components/Title/Title.stories.tsx index 4eab2d5c..899c78b8 100644 --- a/client/src/components/Design/components/Title/Title.stories.tsx +++ b/client/src/components/Design/components/Title/Title.stories.tsx @@ -14,20 +14,14 @@ const meta = { description: '', control: {type: 'text'}, }, - description: { - description: '', - control: {type: 'text'}, - }, - price: { + amount: { description: '', control: {type: 'number'}, }, }, args: { - title: '페이지 제목이에요', - description: `이곳에는 페이지 설명이 들어가요. - 페이지에 대한 설명을 자세하게 적어주면 좋아요 :)`, - price: 100000, + title: '행동대장 야유회', + amount: 100000, }, } satisfies Meta; diff --git a/client/src/components/Design/components/Title/Title.style.ts b/client/src/components/Design/components/Title/Title.style.ts index ab347a21..bd19002d 100644 --- a/client/src/components/Design/components/Title/Title.style.ts +++ b/client/src/components/Design/components/Title/Title.style.ts @@ -2,18 +2,26 @@ import {css} from '@emotion/react'; import {Theme} from '@theme/theme.type'; -export const titleContainerStyle = (theme: Theme) => +export const titleStyle = (theme: Theme) => css({ display: 'flex', flexDirection: 'column', width: '100%', gap: '0.5rem', backgroundColor: theme.colors.white, - padding: '1rem', + padding: '0.5rem', + borderRadius: '0.75rem', }); -export const priceContainerStyle = css({ +export const titleContainerStyle = css({ + display: 'flex', + justifyContent: 'space-between', + paddingLeft: '0.5rem', +}); + +export const amountContainerStyle = css({ display: 'flex', justifyContent: 'space-between', alignItems: 'end', + paddingInline: '0.5rem', }); diff --git a/client/src/components/Design/components/Title/Title.tsx b/client/src/components/Design/components/Title/Title.tsx index 4050399e..1c28c332 100644 --- a/client/src/components/Design/components/Title/Title.tsx +++ b/client/src/components/Design/components/Title/Title.tsx @@ -1,31 +1,27 @@ /** @jsxImportSource @emotion/react */ import Flex from '@HDcomponents/Flex/Flex'; import Text from '@HDcomponents/Text/Text'; -import {priceContainerStyle, titleContainerStyle} from '@HDcomponents/Title/Title.style'; +import {amountContainerStyle, titleContainerStyle, titleStyle} from '@HDcomponents/Title/Title.style'; import {TitleProps} from '@HDcomponents/Title/Title.type'; import {useTheme} from '@theme/HDesignProvider'; -export const Title: React.FC = ({title, description, price}: TitleProps) => { +import Icon from '../Icon/Icon'; +import Amount from '../Amount/Amount'; + +export const Title: React.FC = ({title, amount}: TitleProps) => { const {theme} = useTheme(); return ( -
- {title} - {description && ( - - {description} +
+
+ {title} + +
+
+ + 전체 지출 금액 - )} - {price !== undefined && ( -
- - 전체 지출 금액 - - - {price.toLocaleString('ko-kr')} - - -
- )} + +
); }; diff --git a/client/src/components/Design/components/Title/Title.type.ts b/client/src/components/Design/components/Title/Title.type.ts index 4b12f23b..2b0718dd 100644 --- a/client/src/components/Design/components/Title/Title.type.ts +++ b/client/src/components/Design/components/Title/Title.type.ts @@ -2,8 +2,7 @@ export interface TitleStyleProps {} export interface TitleCustomProps { title: string; - description?: string; - price?: number; + amount?: number; } export type TitleOptionProps = TitleStyleProps & TitleCustomProps; diff --git a/client/src/components/Design/components/Top/Line.tsx b/client/src/components/Design/components/Top/Line.tsx new file mode 100644 index 00000000..552dc027 --- /dev/null +++ b/client/src/components/Design/components/Top/Line.tsx @@ -0,0 +1,36 @@ +/** @jsxImportSource @emotion/react */ +import {css} from '@emotion/react'; + +import Text from '../Text/Text'; + +interface Props { + text: string; + emphasize?: string[]; +} + +export default function Line({text, emphasize = []}: Props) { + const getTextElements = ({text, emphasize = []}: Props) => { + if (emphasize.length === 0) return [text]; + + const regexPattern = new RegExp(`(${emphasize.join('|')})`, 'g'); + return text.split(regexPattern).filter(Boolean); + }; + + const elements = getTextElements({text, emphasize}); + + return ( +
+ {elements.map(text => { + return ( + + {`${text}`} + + ); + })} +
+ ); +} diff --git a/client/src/components/Design/components/Top/Top.stories.tsx b/client/src/components/Design/components/Top/Top.stories.tsx new file mode 100644 index 00000000..20aa07b8 --- /dev/null +++ b/client/src/components/Design/components/Top/Top.stories.tsx @@ -0,0 +1,30 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import Top from '@HDcomponents/Top/Top'; + +const meta = { + title: 'Components/Top', + component: Top, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: {}, + args: {}, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + render: ({...args}) => { + return ( + + + + + ); + }, +}; diff --git a/client/src/components/Design/components/Top/Top.tsx b/client/src/components/Design/components/Top/Top.tsx new file mode 100644 index 00000000..9d00801d --- /dev/null +++ b/client/src/components/Design/components/Top/Top.tsx @@ -0,0 +1,19 @@ +/** @jsxImportSource @emotion/react */ +import {css} from '@emotion/react'; + +import Line from './Line'; + +Top.Line = Line; + +export default function Top({children}: React.PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/client/src/components/Design/layouts/MainLayout.tsx b/client/src/components/Design/layouts/MainLayout.tsx index 22012fde..d7a69966 100644 --- a/client/src/components/Design/layouts/MainLayout.tsx +++ b/client/src/components/Design/layouts/MainLayout.tsx @@ -15,7 +15,7 @@ export function MainLayout({backgroundColor, children}: MainLayoutProps) { justifyContent="flexStart" flexDirection="column" padding="1rem 0 0 0" - gap="1rem" + gap="0.5rem" width="100%" height="100%" minHeight="100vh" diff --git a/client/src/components/ExpenseDetailModal/ExpenseDetailModal.tsx b/client/src/components/ExpenseDetailModal/ExpenseDetailModal.tsx new file mode 100644 index 00000000..ddc15138 --- /dev/null +++ b/client/src/components/ExpenseDetailModal/ExpenseDetailModal.tsx @@ -0,0 +1,130 @@ +// import type {BillAction} from 'types/serviceType'; + +// import validatePurchase from '@utils/validate/validatePurchase'; +// import useRequestGetSteps from '@hooks/queries/step/useRequestGetSteps'; +// import useMemberReportListInAction from '@hooks/useMemberReportListInAction/useMemberReportListInAction'; +// import useMemberReportInput from '@hooks/useMemberReportListInAction/useMemberReportInput'; + +// import usePutAndDeleteBill from '@hooks/usePutAndDeleteBill'; + +// import {BottomSheet, EditableItem, FixedButton, Flex, Text} from '@HDesign/index'; + +// type ExpenseDetailModalProps = { +// billAction: BillAction; +// isBottomSheetOpened: boolean; +// setIsBottomSheetOpened: React.Dispatch>; +// }; + +// const ExpenseDetailModal = ({billAction, isBottomSheetOpened, setIsBottomSheetOpened}: ExpenseDetailModalProps) => { +// const { +// inputPair, +// handleInputChange, +// // handleOnBlur, +// // errorMessage, +// // errorInfo, +// canSubmit, +// onDelete, +// onSubmit: putBillAction, +// } = usePutAndDeleteBill({title: billAction.name, price: String(billAction.price), index: 0}, validatePurchase, () => +// setIsBottomSheetOpened(false), +// ); + +// const { +// memberReportListInAction, +// addAdjustedMember, +// onSubmit: putMemberReportListInAction, +// getIsSamePriceStateAndServerState, +// getOnlyOneNotAdjustedRemainMemberIndex, +// isExistAdjustedPrice, +// } = useMemberReportListInAction(billAction.actionId, Number(inputPair.price), () => setIsBottomSheetOpened(false)); +// const { +// inputList, +// onChange, +// canEditList, +// canSubmit: isChangedMemberReportInput, +// } = useMemberReportInput({ +// data: memberReportListInAction, +// addAdjustedMember, +// totalPrice: Number(inputPair.price), +// getIsSamePriceStateAndServerState, +// getOnlyOneNotAdjustedRemainMemberIndex, +// }); + +// const {steps} = useRequestGetSteps(); + +// const actionMemberList = steps.filter(({actions}) => +// actions.find(({actionId}) => actionId === billAction.actionId), +// )[0].members; + +// return ( +// setIsBottomSheetOpened(false)}> +//
setIsBottomSheetOpened(false)}> +//

+// 지출 내역 상세 +//

+//
+// +// ) => handleInputChange('title', event)} +// disabled +// /> +// +// ) => handleInputChange('price', event)} +// isFixed={isExistAdjustedPrice()} +// disabled +// /> +// +// +// + +// +// +// {inputList.map(({name, price, isFixed}, index) => ( +// +// +// +// onChange(event, index)} +// isFixed={isFixed} +// textSize="smallBody" +// value={price} +// placeholder="0" +// type="number" +// disabled +// style={{textAlign: 'right'}} +// > +// +// +// +// ))} +// +// +//
+// +// 닫기 +// +//
+//
+// ); +// }; + +// export default ExpenseDetailModal; diff --git a/client/src/components/Modal/ExpenseDetailModal/ExpenseDetailModal.tsx b/client/src/components/Modal/ExpenseDetailModal/ExpenseDetailModal.tsx deleted file mode 100644 index 93a95008..00000000 --- a/client/src/components/Modal/ExpenseDetailModal/ExpenseDetailModal.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import type {BillAction} from 'types/serviceType'; - -import validatePurchase from '@utils/validate/validatePurchase'; -import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; -import useMemberReportListInAction from '@hooks/useMemberReportListInAction/useMemberReportListInAction'; -import useMemberReportInput from '@hooks/useMemberReportListInAction/useMemberReportInput'; - -import usePutAndDeleteBillAction from '@hooks/usePutAndDeleteBillAction'; - -import {BottomSheet, EditableItem, FixedButton, Flex, Text} from '@HDesign/index'; - -import { - bottomSheetHeaderStyle, - bottomSheetStyle, - inputContainerStyle, -} from '../SetActionModal/PutAndDeleteBillActionModal/PutAndDeltetBillActionModal.style'; - -type PutAndDeleteBillActionModalProps = { - billAction: BillAction; - isBottomSheetOpened: boolean; - setIsBottomSheetOpened: React.Dispatch>; -}; - -const ExpenseDetailModal = ({ - billAction, - isBottomSheetOpened, - setIsBottomSheetOpened, -}: PutAndDeleteBillActionModalProps) => { - const { - inputPair, - handleInputChange, - // handleOnBlur, - // errorMessage, - // errorInfo, - canSubmit, - onDelete, - onSubmit: putBillAction, - } = usePutAndDeleteBillAction( - {title: billAction.name, price: String(billAction.price), index: 0}, - validatePurchase, - () => setIsBottomSheetOpened(false), - ); - - const { - memberReportListInAction, - addAdjustedMember, - onSubmit: putMemberReportListInAction, - getIsSamePriceStateAndServerState, - getOnlyOneNotAdjustedRemainMemberIndex, - isExistAdjustedPrice, - } = useMemberReportListInAction(billAction.actionId, Number(inputPair.price), () => setIsBottomSheetOpened(false)); - const { - inputList, - onChange, - canEditList, - canSubmit: isChangedMemberReportInput, - } = useMemberReportInput({ - data: memberReportListInAction, - addAdjustedMember, - totalPrice: Number(inputPair.price), - getIsSamePriceStateAndServerState, - getOnlyOneNotAdjustedRemainMemberIndex, - }); - - const {data: stepListData = []} = useRequestGetStepList(); - - const actionMemberList = stepListData.filter(({actions}) => - actions.find(({actionId}) => actionId === billAction.actionId), - )[0].members; - - return ( - setIsBottomSheetOpened(false)}> -
setIsBottomSheetOpened(false)}> -

- 지출 내역 상세 -

-
- - ) => handleInputChange('title', event)} - disabled - /> - - ) => handleInputChange('price', event)} - isFixed={isExistAdjustedPrice()} - disabled - /> - - - - - - - {inputList.map(({name, price, isFixed}, index) => ( - - - - onChange(event, index)} - isFixed={isFixed} - textSize="smallBody" - value={price} - placeholder="0" - type="number" - disabled - style={{textAlign: 'right'}} - > - - - - ))} - - -
- - 닫기 - -
-
- ); -}; - -export default ExpenseDetailModal; diff --git a/client/src/components/Modal/MemberListInBillStep/MemberListInBillStep.style.ts b/client/src/components/Modal/MemberListInBillStep/MemberListInBillStep.style.ts deleted file mode 100644 index 4bbefdba..00000000 --- a/client/src/components/Modal/MemberListInBillStep/MemberListInBillStep.style.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {css} from '@emotion/react'; - -export const bottomSheetStyle = css({ - display: 'flex', - flexDirection: 'column', - gap: '1.5rem', - width: '100%', - height: '100%', - padding: '0 1.5rem', -}); diff --git a/client/src/components/Modal/MemberListInBillStep/MemberListInBillStep.tsx b/client/src/components/Modal/MemberListInBillStep/MemberListInBillStep.tsx deleted file mode 100644 index f7596927..00000000 --- a/client/src/components/Modal/MemberListInBillStep/MemberListInBillStep.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type {MemberReport} from 'types/serviceType'; - -import {BottomSheet, FixedButton, Flex, Text} from '@HDesign/index'; - -import {bottomSheetStyle} from './MemberListInBillStep.style'; - -type MemberListInBillStepProps = { - stepName: string; - memberList: MemberReport[]; - isOpenBottomSheet: boolean; - setIsOpenBottomSheet: React.Dispatch>; -}; - -const MemberListInBillStep = ({ - stepName, - memberList, - isOpenBottomSheet, - setIsOpenBottomSheet, -}: MemberListInBillStepProps) => { - const closeModal = () => setIsOpenBottomSheet(false); - - return ( - -
- - {`${stepName} 참석자`} - {`총 ${memberList.length}명`} - - -
    - {memberList.map(member => ( -
  • - - - {member.name} - - - {`${member.price.toLocaleString('ko-kr')} 원`} - - -
  • - ))} -
-
-
- - 닫기 - -
- ); -}; - -export default MemberListInBillStep; diff --git a/client/src/components/Modal/MemberListInBillStep/index.ts b/client/src/components/Modal/MemberListInBillStep/index.ts deleted file mode 100644 index 137df8c8..00000000 --- a/client/src/components/Modal/MemberListInBillStep/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as MemberListInBillStep} from './MemberListInBillStep'; diff --git a/client/src/components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount.tsx b/client/src/components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount.tsx deleted file mode 100644 index 51e33589..00000000 --- a/client/src/components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import {SetAllMemberListModal, SetInitialMemberListModal, SetActionListModal} from '@components/Modal/index'; - -interface ModalBasedOnMemberCountProps { - allMemberList: string[]; - isOpenBottomSheet: boolean; - isOpenAllMemberListButton: boolean; - setIsOpenBottomSheet: React.Dispatch>; - setIsOpenAllMemberListButton: React.Dispatch>; -} - -const ModalBasedOnMemberCount = ({ - allMemberList, - isOpenBottomSheet, - isOpenAllMemberListButton, - setIsOpenBottomSheet, - setIsOpenAllMemberListButton, -}: ModalBasedOnMemberCountProps) => { - if (isOpenAllMemberListButton) { - return ( - - ); - } - switch (allMemberList.length) { - case 0: - return ( - - ); - - default: - return ; - } -}; - -export default ModalBasedOnMemberCount; diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts deleted file mode 100644 index 7fad2e00..00000000 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {css} from '@emotion/react'; - -const container = css({ - display: 'flex', - flexDirection: 'column', - gap: '1.5rem', - height: '100%', -}); - -const inputGroup = css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - overflow: 'auto', - paddingBottom: '14rem', -}); - -const addMemberActionListModalContentStyle = { - container, - inputGroup, -}; - -export default addMemberActionListModalContentStyle; diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx deleted file mode 100644 index a15681e4..00000000 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type {MemberType} from 'types/serviceType'; - -import validateMemberName from '@utils/validate/validateMemberName'; -import useRequestPostMemberList from '@hooks/queries/useRequestPostMemberList'; - -import useDynamicInput from '@hooks/useDynamicInput'; - -import {FixedButton, LabelGroupInput} from '@HDesign/index'; - -import style from './AddMemberActionListModalContent.style'; -import InMember from './InMember'; -import OutMember from './OutMember'; - -interface AddMemberActionListModalContentProps { - inOutAction: MemberType; - setIsOpenBottomSheet: React.Dispatch>; -} - -const AddMemberActionListModalContent = ({inOutAction, setIsOpenBottomSheet}: AddMemberActionListModalContentProps) => { - const dynamicProps = useDynamicInput(validateMemberName); - const {inputList, getFilledInputList, errorMessage, canSubmit, resetInputValue} = dynamicProps; - - const {mutate: postMemberList} = useRequestPostMemberList(); - - const handleUpdateMemberListSubmit = () => { - postMemberList({memberNameList: getFilledInputList().map(({value}) => value), type: inOutAction}); - setIsOpenBottomSheet(false); - }; - - return ( -
-
- - {inOutAction === 'IN' ? : } - -
- { - handleUpdateMemberListSubmit(); - resetInputValue(); - }} - /> -
- ); -}; - -export default AddMemberActionListModalContent; diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/InMember.tsx b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/InMember.tsx deleted file mode 100644 index 44da530f..00000000 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/InMember.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import {ReturnUseDynamicInput} from '@hooks/useDynamicInput'; - -import {LabelGroupInput} from '@HDesign/index'; - -interface InMemberProps { - dynamicProps: ReturnUseDynamicInput; -} - -const InMember = ({dynamicProps}: InMemberProps) => { - const { - inputList, - inputRefList, - handleInputChange, - deleteEmptyInputElementOnBlur, - focusNextInputOnEnter, - errorIndexList, - } = dynamicProps; - return inputList.map(({value, index}) => ( - (inputRefList.current[index] = el)} - isError={errorIndexList.includes(index)} - onChange={e => handleInputChange(index, e)} - onBlur={() => deleteEmptyInputElementOnBlur()} - onKeyDown={e => focusNextInputOnEnter(e, index)} - placeholder="이름" - autoFocus={inputList.length === 1 && index === 0} - /> - )); -}; - -export default InMember; diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx deleted file mode 100644 index 2ed739b6..00000000 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import {ReturnUseDynamicInput} from '@hooks/useDynamicInput'; -import useSearchInMemberList from '@hooks/useSearchInMemberList'; - -import {LabelGroupInput, Search} from '@HDesign/index'; - -interface OutMemberProps { - dynamicProps: ReturnUseDynamicInput; -} - -const OutMember = ({dynamicProps}: OutMemberProps) => { - const { - inputList, - inputRefList, - errorIndexList, - deleteEmptyInputElementOnBlur, - focusNextInputOnEnter, - handleInputChange, - handleChange, - } = dynamicProps; - const {currentInputIndex, filteredInMemberList, handleCurrentInputIndex, searchCurrentInMember, chooseMember} = - useSearchInMemberList(handleChange); - - const validationAndSearchOnChange = (inputIndex: number, event: React.ChangeEvent) => { - handleCurrentInputIndex(inputIndex); - handleInputChange(inputIndex, event); - searchCurrentInMember(event); - }; - - return inputList.map(({value, index}) => ( - chooseMember(currentInputIndex, term)} - > - (inputRefList.current[index] = el)} - isError={errorIndexList.includes(index)} - onChange={e => validationAndSearchOnChange(index, e)} - onBlur={() => deleteEmptyInputElementOnBlur()} - onKeyDown={e => focusNextInputOnEnter(e, index)} - placeholder="이름" - autoFocus={inputList.length === 1 && index === 0} - /> - - )); -}; - -export default OutMember; diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/index.ts b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/index.ts deleted file mode 100644 index 6519283f..00000000 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as AddMemberActionListModalContent} from './AddMemberActionListModalContent'; diff --git a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.style.ts b/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.style.ts deleted file mode 100644 index 7be2f214..00000000 --- a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.style.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {css} from '@emotion/react'; - -export const bottomSheetStyle = css({ - display: 'flex', - flexDirection: 'column', - gap: '1.5rem', - width: '100%', - height: '100%', - padding: '0 1rem', -}); - -export const bottomSheetHeaderStyle = css({ - display: 'flex', - justifyContent: 'space-between', - alignContent: 'center', - - width: '100%', - padding: '0 0.5rem', -}); - -export const inputGroupStyle = css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - overflow: 'auto', - paddingBottom: '11rem', -}); diff --git a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx b/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx deleted file mode 100644 index 7d657319..00000000 --- a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type {MemberAction, MemberType} from 'types/serviceType'; - -import useDeleteMemberAction from '@hooks/useDeleteMemberAction/useDeleteMemberAction'; -import {useToast} from '@hooks/useToast/useToast'; - -import {BottomSheet, Flex, Input, Text, IconButton, FixedButton, Icon} from '@HDesign/index'; - -import {bottomSheetHeaderStyle, bottomSheetStyle, inputGroupStyle} from './DeleteMemberActionModal.style'; - -type DeleteMemberActionModalProps = { - memberActionType: MemberType; - memberActionList: MemberAction[]; - isBottomSheetOpened: boolean; - setIsBottomSheetOpened: React.Dispatch>; -}; - -const DeleteMemberActionModal = ({ - memberActionType, - memberActionList, - isBottomSheetOpened, - setIsBottomSheetOpened, -}: DeleteMemberActionModalProps) => { - const {showToast} = useToast(); - - const showToastAlreadyExistMemberAction = () => { - showToast({ - isClickToClose: true, - showingTime: 3000, - message: '이미 삭제된 인원입니다.', - type: 'error', - bottom: '160px', - }); - }; - - const showToastExistSameMemberFromAfterStep = (name: string) => { - showToast({ - isClickToClose: true, - showingTime: 3000, - message: `이후의 ${name}가 사라져요`, - type: 'error', - position: 'top', - top: '30px', - style: { - zIndex: 9000, - }, - }); - }; - - const {aliveActionList, deleteMemberActionList, addDeleteMemberAction} = useDeleteMemberAction({ - memberActionList, - setIsBottomSheetOpened, - showToastAlreadyExistMemberAction, - showToastExistSameMemberFromAfterStep, - }); - - return ( - setIsBottomSheetOpened(false)}> -
-
- {memberActionType === 'IN' ? '들어온' : '나간'} 인원 수정하기 - {`${aliveActionList.length}명`} -
-
    - {aliveActionList.map(member => ( -
  • - -
    - -
    - addDeleteMemberAction(member)}> - - -
    -
  • - ))} -
- -
-
- ); -}; - -export default DeleteMemberActionModal; diff --git a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/index.ts b/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/index.ts deleted file mode 100644 index 2a6b52b4..00000000 --- a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as DeleteMemberActionModal} from './DeleteMemberActionModal'; diff --git a/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/PutAndDeleteBillActionModal.tsx b/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/PutAndDeleteBillActionModal.tsx deleted file mode 100644 index 95a72c99..00000000 --- a/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/PutAndDeleteBillActionModal.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import type {BillAction} from 'types/serviceType'; - -import {useEffect} from 'react'; - -import validatePurchase from '@utils/validate/validatePurchase'; -import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; -import useMemberReportListInAction from '@hooks/useMemberReportListInAction/useMemberReportListInAction'; -import useMemberReportInput from '@hooks/useMemberReportListInAction/useMemberReportInput'; - -import usePutAndDeleteBillAction from '@hooks/usePutAndDeleteBillAction'; - -import {BottomSheet, EditableItem, FixedButton, Flex, Text} from '@HDesign/index'; - -import {bottomSheetHeaderStyle, bottomSheetStyle, inputContainerStyle} from './PutAndDeltetBillActionModal.style'; - -type PutAndDeleteBillActionModalProps = { - billAction: BillAction; - isBottomSheetOpened: boolean; - setIsBottomSheetOpened: React.Dispatch>; -}; - -const PutAndDeleteBillActionModal = ({ - billAction, - isBottomSheetOpened, - setIsBottomSheetOpened, -}: PutAndDeleteBillActionModalProps) => { - const { - inputPair, - handleInputChange, - // handleOnBlur, - // errorMessage, - // errorInfo, - canSubmit, - onDelete, - onSubmit: putBillAction, - } = usePutAndDeleteBillAction( - {title: billAction.name, price: String(billAction.price), index: 0}, - validatePurchase, - () => setIsBottomSheetOpened(false), - ); - - const { - memberReportListInAction, - addAdjustedMember, - onSubmit: putMemberReportListInAction, - getIsSamePriceStateAndServerState, - getOnlyOneNotAdjustedRemainMemberIndex, - isExistAdjustedPrice, - } = useMemberReportListInAction(billAction.actionId, Number(inputPair.price), () => setIsBottomSheetOpened(false)); - const { - inputList, - onChange, - canEditList, - canSubmit: isChangedMemberReportInput, - } = useMemberReportInput({ - data: memberReportListInAction, - addAdjustedMember, - totalPrice: Number(inputPair.price), - getIsSamePriceStateAndServerState, - getOnlyOneNotAdjustedRemainMemberIndex, - }); - - const {data: stepListData = []} = useRequestGetStepList(); - - const actionMemberList = stepListData.filter(({actions}) => - actions.find(({actionId}) => actionId === billAction.actionId), - )[0].members; - - useEffect(() => { - console.log(inputList); - }, [inputList]); - - return ( - setIsBottomSheetOpened(false)}> -
{ - event.preventDefault(); - - if (canSubmit) await putBillAction(event, inputPair, billAction.actionId); - if (isChangedMemberReportInput) putMemberReportListInAction(); - }} - > -

- 지출 내역 수정하기 -

-
- - ) => handleInputChange('title', event)} - /> - - ) => handleInputChange('price', event)} - isFixed={isExistAdjustedPrice()} - /> - - - - - - - {inputList.map(({name, price, isFixed}, index) => ( - - - - onChange(event, index)} - isFixed={isFixed} - textSize="smallBody" - value={price} - placeholder="0" - type="number" - readOnly={!canEditList[index]} - style={{textAlign: 'right'}} - > - - - - ))} - - -
- onDelete(billAction.actionId)} - > - 수정 완료 - -
-
- ); -}; - -export default PutAndDeleteBillActionModal; diff --git a/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/PutAndDeltetBillActionModal.style.ts b/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/PutAndDeltetBillActionModal.style.ts deleted file mode 100644 index f58f4342..00000000 --- a/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/PutAndDeltetBillActionModal.style.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {css} from '@emotion/react'; - -export const bottomSheetStyle = css({ - display: 'flex', - flexDirection: 'column', - gap: '1.5rem', - width: '100%', - height: '100%', - padding: '0 1rem', -}); - -export const bottomSheetHeaderStyle = css({ - display: 'flex', - justifyContent: 'space-between', - alignContent: 'center', - - width: '100%', - padding: '0 0.5rem', -}); - -export const inputContainerStyle = css({ - display: 'flex', - height: '100%', - flexDirection: 'column', - gap: '1.5rem', - overflow: 'auto', - paddingBottom: '14rem', -}); diff --git a/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/index.ts b/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/index.ts deleted file mode 100644 index c7cbcfcb..00000000 --- a/client/src/components/Modal/SetActionModal/PutAndDeleteBillActionModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as PutAndDeleteBillActionModal} from './PutAndDeleteBillActionModal'; diff --git a/client/src/components/Modal/SetActionModal/SetActionListModal.style.ts b/client/src/components/Modal/SetActionModal/SetActionListModal.style.ts deleted file mode 100644 index ca670330..00000000 --- a/client/src/components/Modal/SetActionModal/SetActionListModal.style.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {css} from '@emotion/react'; - -export const container = css({ - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - padding: '0 1.5rem', - gap: '1.5rem', -}); - -export const switchContainer = css({ - display: 'flex', - width: '100%', - justifyContent: 'space-between', -}); - -const setActionListModalStyle = {container, switchContainer}; - -export default setActionListModalStyle; diff --git a/client/src/components/Modal/SetActionModal/SetActionListModal.tsx b/client/src/components/Modal/SetActionModal/SetActionListModal.tsx deleted file mode 100644 index 6550175e..00000000 --- a/client/src/components/Modal/SetActionModal/SetActionListModal.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import type {InOutType} from 'types/serviceType'; - -import {useState} from 'react'; - -import {BottomSheet, Switch, Text} from '@HDesign/index'; - -import {AddMemberActionListModalContent} from './AddMemberActionListModalContent'; -import style from './SetActionListModal.style'; - -export type ActionType = '지출' | '인원'; - -interface SetActionModalContentProps { - isOpenBottomSheet: boolean; - setIsOpenBottomSheet: React.Dispatch>; -} - -const SetActionListModal = ({isOpenBottomSheet, setIsOpenBottomSheet}: SetActionModalContentProps) => { - const [inOutAction, setInOutAction] = useState('탈주'); - - const handleParticipantTypeChange = (value: string) => { - setInOutAction(value as InOutType); - }; - - return ( - setIsOpenBottomSheet(false)}> -
-
- - 인원 변동 - - -
- - -
-
- ); -}; - -export default SetActionListModal; diff --git a/client/src/components/Modal/SetActionModal/index.ts b/client/src/components/Modal/SetActionModal/index.ts deleted file mode 100644 index f689cb59..00000000 --- a/client/src/components/Modal/SetActionModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as SetActionListModal} from './SetActionListModal'; diff --git a/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.style.ts b/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.style.ts deleted file mode 100644 index f54166c0..00000000 --- a/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.style.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {css} from '@emotion/react'; - -export const allMemberListModalStyle = () => - css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - width: '100%', - height: '100%', - }); - -export const allMemberListModalTitleStyle = () => - css({ - display: 'flex', - justifyContent: 'space-between', - width: '100%', - padding: '0 1.5rem', - }); - -export const allMemberListModalLabelGroupInputStyle = () => - css({ - display: 'flex', - flexDirection: 'column', - padding: '0 1rem', - paddingBottom: '10rem', - - overflow: 'auto', - }); - -export const InputAndDeleteButtonContainer = () => - css({ - display: 'flex', - alignItems: 'center', - width: '100%', - gap: '1rem', - }); diff --git a/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.tsx b/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.tsx deleted file mode 100644 index 62853f91..00000000 --- a/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import validateMemberName from '@utils/validate/validateMemberName'; - -import useSetAllMemberList from '@hooks/useSetAllMemberList'; - -import {BottomSheet, Text, LabelGroupInput, FixedButton, IconButton, Icon} from '@HDesign/index'; - -import { - allMemberListModalLabelGroupInputStyle, - allMemberListModalStyle, - allMemberListModalTitleStyle, - InputAndDeleteButtonContainer, -} from './SetAllMemberListModal.style'; - -interface SetAllMemberListModalProps { - isOpenBottomSheet: boolean; - allMemberList: string[]; - setIsOpenBottomSheet: React.Dispatch>; - setIsOpenAllMemberListButton: React.Dispatch>; -} - -const SetAllMemberListModal = ({ - isOpenBottomSheet, - allMemberList, - setIsOpenBottomSheet, - setIsOpenAllMemberListButton, -}: SetAllMemberListModalProps) => { - const handleCloseAllMemberListModal = () => { - setIsOpenAllMemberListButton(prev => !prev); - setIsOpenBottomSheet(false); - }; - - const { - editedAllMemberList, - canSubmit, - errorMessage, - errorIndexList, - handleNameChange, - handleClickDeleteButton, - handlePutAllMemberList, - } = useSetAllMemberList({ - validateFunc: validateMemberName, - allMemberList, - handleCloseAllMemberListModal, - }); - - return ( - -
-
- 전체 참여자 수정하기 - - 총 {allMemberList.length}명 - -
-
- - {editedAllMemberList.map((member, index) => ( -
-
- handleNameChange(index, e)} - /> -
- handleClickDeleteButton(index)}> - - -
- ))} -
-
- -
-
- ); -}; - -export default SetAllMemberListModal; diff --git a/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.style.ts b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.style.ts deleted file mode 100644 index ed835682..00000000 --- a/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.style.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {css} from '@emotion/react'; - -export const setInitialMemberListModalStyle = () => - css({ - display: 'flex', - flexDirection: 'column', - gap: '1.5rem', - width: '100%', - height: '100%', - padding: '0 1.5rem', - }); - -export const setInitialMemberListModalInputGroupStyle = () => - css({ - display: 'flex', - flexDirection: 'column', - gap: '1rem', - overflow: 'auto', - paddingBottom: '11rem', - }); diff --git a/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx deleted file mode 100644 index f4f2dc22..00000000 --- a/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import validateMemberName from '@utils/validate/validateMemberName'; -import useRequestPostMemberList from '@hooks/queries/useRequestPostMemberList'; - -import useDynamicInput from '@hooks/useDynamicInput'; - -import {Text, BottomSheet, FixedButton, LabelGroupInput} from '@HDesign/index'; - -import { - setInitialMemberListModalInputGroupStyle, - setInitialMemberListModalStyle, -} from './SetInitialMemberListModal.style'; - -interface SetInitialMemberListProps { - isOpenBottomSheet: boolean; - setIsOpenBottomSheet: React.Dispatch>; -} - -const SetInitialMemberListModal = ({isOpenBottomSheet, setIsOpenBottomSheet}: SetInitialMemberListProps) => { - const { - inputList, - inputRefList, - handleInputChange, - deleteEmptyInputElementOnBlur, - getFilledInputList, - errorMessage, - canSubmit, - errorIndexList, - focusNextInputOnEnter, - } = useDynamicInput(validateMemberName); - const {mutate: postMemberList} = useRequestPostMemberList(); - - const handleSubmit = () => { - postMemberList({memberNameList: getFilledInputList().map(({value}) => value), type: 'IN'}); - setIsOpenBottomSheet(false); - }; - - return ( - setIsOpenBottomSheet(false)}> -
- 시작 인원 추가하기 -
- - {inputList.map(({value, index}) => ( - (inputRefList.current[index] = el)} - onChange={e => handleInputChange(index, e)} - onBlur={() => deleteEmptyInputElementOnBlur()} - onKeyDown={e => focusNextInputOnEnter(e, index)} - autoFocus={inputList.length === 1 && index === 0} - /> - ))} - -
-
- -
- ); -}; - -export default SetInitialMemberListModal; diff --git a/client/src/components/Modal/SetInitialMemberListModal/index.ts b/client/src/components/Modal/SetInitialMemberListModal/index.ts deleted file mode 100644 index 18098e4f..00000000 --- a/client/src/components/Modal/SetInitialMemberListModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as SetInitialMemberListModal} from './SetInitialMemberListModal'; diff --git a/client/src/components/Modal/index.ts b/client/src/components/Modal/index.ts deleted file mode 100644 index 180063b0..00000000 --- a/client/src/components/Modal/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export {default as SetActionListModal} from './SetActionModal/SetActionListModal'; -export {default as SetInitialMemberListModal} from './SetInitialMemberListModal/SetInitialMemberListModal'; -export {default as SetAllMemberListModal} from './SetAllMemberListModal/SetAllMemberListModal'; -export {default as ModalBasedOnMemberCount} from './ModalBasedOnMemberCount/ModalBasedOnMemberCount'; diff --git a/client/src/components/MemberReportList/MemberReportList.tsx b/client/src/components/Reports/Reports.tsx similarity index 67% rename from client/src/components/MemberReportList/MemberReportList.tsx rename to client/src/components/Reports/Reports.tsx index 8d58030a..028c55d1 100644 --- a/client/src/components/MemberReportList/MemberReportList.tsx +++ b/client/src/components/Reports/Reports.tsx @@ -1,12 +1,12 @@ import React, {useState} from 'react'; -import useSearchMemberReportList from '@hooks/useSearchMemberReportList/useSearchMemberReportList'; +import {useSearchReports} from '@hooks/useSearchReports'; import {ExpenseList, Flex, Input, Text} from '@HDesign/index'; -const MemberReportList = () => { +const Reports = () => { const [name, setName] = useState(''); - const {memberReportSearchList, memberReportList} = useSearchMemberReportList({name}); + const {matchedReports, reports} = useSearchReports({name}); const changeName = ({target}: React.ChangeEvent) => { setName(target.value); @@ -17,7 +17,7 @@ const MemberReportList = () => { window.location.href = url; }; - const expenseListProp = memberReportList.map(member => ({ + const expenseListProp = matchedReports.map(member => ({ ...member, clipboardText: `계좌번호 받아와야 함 ${member.price}원`, onBankButtonClick, @@ -25,10 +25,10 @@ const MemberReportList = () => { return ( - {memberReportList.length > 0 ? ( + {reports.length > 0 ? ( <> - {memberReportSearchList.length !== 0 && } + {matchedReports.length !== 0 && } ) : ( @@ -41,4 +41,4 @@ const MemberReportList = () => { ); }; -export default MemberReportList; +export default Reports; diff --git a/client/src/components/StepList/BillStepItem.tsx b/client/src/components/StepList/BillStepItem.tsx deleted file mode 100644 index ca5f4ee0..00000000 --- a/client/src/components/StepList/BillStepItem.tsx +++ /dev/null @@ -1,139 +0,0 @@ -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 ExpenseDetailModal from '@components/Modal/ExpenseDetailModal/ExpenseDetailModal'; - -import useSetBillInput from '@hooks/useSetBillInput'; - -import {DragHandleItem, DragHandleItemContainer, EditableItem, Flex, Text} from '@HDesign/index'; - -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, - isAddEditableItem, - setIsAddEditableItem, - isLastBillItem, - index, -}) => { - const {isAdmin} = useOutletContext(); - const {handleBlurBillRequest, handleChangeBillInput, billInput} = useSetBillInput({setIsAddEditableItem}); - - const [clickedIndex, setClickedIndex] = useState(-1); - const [isOpenMemberListInBillStep, setIsOpenMemberListInBillStep] = useState(false); - - const totalPrice = step.actions && step.type === 'BILL' ? step.actions.reduce((acc, cur) => acc + cur.price, 0) : 0; - - const handleDragHandleItemClick = (index: number) => { - setClickedIndex(index); - setIsOpenBottomSheet(true); - }; - - const memberList: MemberReport[] = step.members.map(member => ({ - name: member, - price: totalPrice / step.members.length, - })); - - const handleTopRightTextClick = () => { - setIsOpenMemberListInBillStep(true); - }; - - return ( - <> - - {step.actions && - step.type === 'BILL' && - step.actions.map((action, index) => ( - - handleDragHandleItemClick(index)} - isFixed={action.isFixed} - /> - - {isOpenBottomSheet && clickedIndex === index && isAdmin && ( - - )} - {isOpenBottomSheet && clickedIndex === index && !isAdmin && ( - - )} - - ))} - - {isAddEditableItem && isLastBillItem && ( - { - if (e.key === 'Enter') { - handleBlurBillRequest(); - } - }} - > - handleChangeBillInput('title', e)} - autoFocus - > - - handleChangeBillInput('price', e)} - style={{textAlign: 'right'}} - > - - - - )} - - - - ); -}; - -export default BillStepItem; diff --git a/client/src/components/StepList/MemberStepItem.tsx b/client/src/components/StepList/MemberStepItem.tsx deleted file mode 100644 index b0ef25de..00000000 --- a/client/src/components/StepList/MemberStepItem.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type {MemberStep} from 'types/serviceType'; - -import {useOutletContext} from 'react-router-dom'; - -import {DeleteMemberActionModal} from '@components/Modal/SetActionModal/DeleteMemberActionModal'; -import {EventPageContextProps} from '@pages/EventPage/EventPageLayout'; - -import {DragHandleItem} from '@HDesign/index'; - -interface MemberStepItemProps { - step: MemberStep; - isOpenBottomSheet: boolean; - setIsOpenBottomSheet: React.Dispatch>; -} - -const MemberStepItem: React.FC = ({step, isOpenBottomSheet, setIsOpenBottomSheet}) => { - const {isAdmin} = useOutletContext(); - - return ( - <> - name).join(', ')} ${step.type === 'IN' ? '들어옴' : '나감'}`} - onClick={() => setIsOpenBottomSheet(prev => !prev)} - /> - {isOpenBottomSheet && isAdmin && ( - - )} - - ); -}; - -export default MemberStepItem; diff --git a/client/src/components/StepList/Step.stories.tsx b/client/src/components/StepList/Step.stories.tsx new file mode 100644 index 00000000..076879f6 --- /dev/null +++ b/client/src/components/StepList/Step.stories.tsx @@ -0,0 +1,60 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import Step from './Step'; + +const meta = { + title: 'Components/Step', + component: Step, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + step: { + description: '', + }, + }, + args: { + step: { + bills: [ + { + id: 1, + title: '커피', + price: 10000, + isFixed: false, + }, + { + id: 2, + title: '인생네컷', + price: 20000, + isFixed: false, + }, + ], + members: [ + { + id: 1, + name: '망쵸', + }, + { + id: 2, + name: '백호', + }, + ], + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + render: ({...args}) => { + return ( +
+ +
+ ); + }, +}; diff --git a/client/src/components/StepList/Step.tsx b/client/src/components/StepList/Step.tsx index f6c89475..1fec736f 100644 --- a/client/src/components/StepList/Step.tsx +++ b/client/src/components/StepList/Step.tsx @@ -1,51 +1,34 @@ -import type {BillStep, MemberStep} from 'types/serviceType'; +/** @jsxImportSource @emotion/react */ +import Amount from '@components/Design/components/Amount/Amount'; +import ChipGroup from '@components/Design/components/ChipGroup/ChipGroup'; +import ListItem from '@components/Design/components/ListItem/ListItem'; +import {Step as StepType} from 'types/serviceType'; -import {useEffect, useState} from 'react'; +import {Text} from '@components/Design'; -import BillStepItem from './BillStepItem'; -import MemberStepItem from './MemberStepItem'; - -interface StepProps { - step: BillStep | MemberStep; - isAddEditableItem: boolean; - lastBillItemIndex: number; - lastItemIndex: number; - index: number; - setIsAddEditableItem: React.Dispatch>; +interface Prop { + step: StepType; } -const Step = ({step, isAddEditableItem, lastBillItemIndex, lastItemIndex, setIsAddEditableItem, index}: StepProps) => { - const [isOpenBottomSheet, setIsOpenBottomSheet] = useState(false); - const [isLastBillItem, setIsLastBillItem] = useState(false); - - 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.actions && (step.type === 'IN' || step.type === 'OUT')) { - return ( - - ); - } else { - return <>; - } +const Step = ({step}: Prop) => { + return ( + + + member.name)} /> + + {step.members.length}명 + + + {step.bills.map(bill => { + return ( + + {bill.title} + + + ); + })} + + ); }; export default Step; diff --git a/client/src/components/StepList/StepList.tsx b/client/src/components/StepList/StepList.tsx deleted file mode 100644 index ccdf547f..00000000 --- a/client/src/components/StepList/StepList.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import {useEffect, useMemo, useState} from 'react'; - -import {BillStep, MemberStep} from 'types/serviceType'; -import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; - -import {Flex, Text} from '@HDesign/index'; - -import Step from './Step'; - -interface StepListProps { - isAddEditableItem?: boolean; - setIsAddEditableItem?: React.Dispatch>; -} - -const StepList = ({isAddEditableItem = false, setIsAddEditableItem = () => {}}: StepListProps) => { - const {data: stepListData} = useRequestGetStepList(); - const [stepList, setStepList] = useState<(MemberStep | BillStep)[]>([]); - const existIndexInStepList = stepList.map((step, index) => ({...step, index})); - const [hasAddedItem, setHasAddedItem] = useState(false); - const nextStepName = stepList.length > 0 ? String(stepList[stepList.length - 1].stepName).match(/.*(?=차)/) : ''; - - 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(() => { - if (hasAddedItem) { - setHasAddedItem(false); - - setStepList(prev => [...prev.filter(({actions}) => actions.length !== 0)]); - } - - if (isAddEditableItem && lastBillItemIndex !== lastItemIndex && !hasAddedItem) { - setStepList(prev => [ - ...prev, - { - type: 'BILL', - stepName: `${Number(nextStepName) + 1}차`, - members: [], - actions: [], - }, - ]); - setHasAddedItem(prev => !prev); - } - }, [isAddEditableItem]); - - return ( - - {stepList.length > 0 ? ( - stepList.map((step, index) => ( - - )) - ) : ( - - - 지금은 지출 내역이 없어요. :( - - - )} - - ); -}; - -export default StepList; diff --git a/client/src/components/StepList/Steps.tsx b/client/src/components/StepList/Steps.tsx new file mode 100644 index 00000000..3146e355 --- /dev/null +++ b/client/src/components/StepList/Steps.tsx @@ -0,0 +1,27 @@ +import {Step as StepType} from 'types/serviceType'; + +import {Flex, Text} from '@HDesign/index'; + +import Step from './Step'; + +interface Props { + data: StepType[]; +} + +const Steps = ({data}: Props) => { + return ( + + {data.length > 0 ? ( + data.map(step => ) + ) : ( + + + 지금은 지출 내역이 없어요. :( + + + )} + + ); +}; + +export default Steps; diff --git a/client/src/constants/queryKeys.ts b/client/src/constants/queryKeys.ts index 17f3f458..476c0576 100644 --- a/client/src/constants/queryKeys.ts +++ b/client/src/constants/queryKeys.ts @@ -1,10 +1,10 @@ const QUERY_KEYS = { - stepList: 'stepList', + steps: 'steps', eventName: 'eventName', - allMemberList: 'allMemberList', - currentInMember: 'currentInMember', - memberReport: 'memberReport', - memberReportInAction: 'memberReportInAction', + allMembers: 'allMembers', + currentMembers: 'currentMembers', + reports: 'reports', + billDetails: 'billDetails', }; export default QUERY_KEYS; diff --git a/client/src/constants/routerUrls.ts b/client/src/constants/routerUrls.ts index 6a3634c3..99e82b5f 100644 --- a/client/src/constants/routerUrls.ts +++ b/client/src/constants/routerUrls.ts @@ -7,4 +7,5 @@ export const ROUTER_URLS = { eventLogin: '/event/:eventId/login', eventManage: '/event/:eventId/admin', home: '/event/:eventId/home', + addBill: 'event/:eventId/addBill', }; diff --git a/client/src/hooks/queries/useRequestPostAuthentication.ts b/client/src/hooks/queries/auth/useRequestPostAuthentication.ts similarity index 74% rename from client/src/hooks/queries/useRequestPostAuthentication.ts rename to client/src/hooks/queries/auth/useRequestPostAuthentication.ts index e533a501..b44cfcaf 100644 --- a/client/src/hooks/queries/useRequestPostAuthentication.ts +++ b/client/src/hooks/queries/auth/useRequestPostAuthentication.ts @@ -7,17 +7,22 @@ import getEventIdByUrl from '@utils/getEventIdByUrl'; import {ROUTER_URLS} from '@constants/routerUrls'; -const useRequestPostAuthenticate = () => { +const useRequestPostAuthentication = () => { const eventId = getEventIdByUrl(); const navigate = useNavigate(); - return useMutation({ + const {mutate, ...rest} = useMutation({ mutationFn: () => requestPostAuthentication({eventId}), onError: () => { // 에러가 발생하면 로그인 페이지로 리다이렉트 navigate(`${ROUTER_URLS.event}/${eventId}/login`); }, }); + + return { + postAuthenticate: mutate, + ...rest, + }; }; -export default useRequestPostAuthenticate; +export default useRequestPostAuthentication; diff --git a/client/src/hooks/queries/useRequestPostLogin.ts b/client/src/hooks/queries/auth/useRequestPostLogin.ts similarity index 65% rename from client/src/hooks/queries/useRequestPostLogin.ts rename to client/src/hooks/queries/auth/useRequestPostLogin.ts index 21dce179..f2f0d7c4 100644 --- a/client/src/hooks/queries/useRequestPostLogin.ts +++ b/client/src/hooks/queries/auth/useRequestPostLogin.ts @@ -1,26 +1,24 @@ import {useMutation} from '@tanstack/react-query'; import {useNavigate} from 'react-router-dom'; -import {requestPostToken} from '@apis/request/auth'; +import {RequestPostToken, requestPostToken} from '@apis/request/auth'; import getEventIdByUrl from '@utils/getEventIdByUrl'; import {ROUTER_URLS} from '@constants/routerUrls'; -interface PostLoginMutationProps { - password: string; -} - const useRequestPostLogin = () => { const eventId = getEventIdByUrl(); const navigate = useNavigate(); - return useMutation({ - mutationFn: ({password}: PostLoginMutationProps) => requestPostToken({eventId, password}), + const {mutate, ...rest} = useMutation({ + mutationFn: ({password}: RequestPostToken) => requestPostToken({eventId, password}), onSuccess: () => { navigate(`${ROUTER_URLS.event}/${eventId}/admin`); }, }); + + return {postLogin: mutate, ...rest}; }; export default useRequestPostLogin; diff --git a/client/src/hooks/queries/bill/useRequestDeleteBill.ts b/client/src/hooks/queries/bill/useRequestDeleteBill.ts new file mode 100644 index 00000000..68083d52 --- /dev/null +++ b/client/src/hooks/queries/bill/useRequestDeleteBill.ts @@ -0,0 +1,29 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {requestDeleteBill} from '@apis/request/bill'; + +import {WithBillId} from '@apis/withId.type'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestDeleteBill = () => { + const eventId = getEventIdByUrl(); + const queryClient = useQueryClient(); + + const {mutate, ...rest} = useMutation({ + mutationFn: ({billId}: WithBillId) => requestDeleteBill({eventId, billId}), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]}); + }, + }); + + return { + deleteBill: mutate, + ...rest, + }; +}; + +export default useRequestDeleteBill; diff --git a/client/src/hooks/queries/bill/useRequestGetBillDetails.ts b/client/src/hooks/queries/bill/useRequestGetBillDetails.ts new file mode 100644 index 00000000..604bd7cd --- /dev/null +++ b/client/src/hooks/queries/bill/useRequestGetBillDetails.ts @@ -0,0 +1,23 @@ +import {useQuery} from '@tanstack/react-query'; + +import {requestGetBillDetails} from '@apis/request/bill'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestGetBillDetails = (billId: number) => { + const eventId = getEventIdByUrl(); + + const {data, ...rest} = useQuery({ + queryKey: [QUERY_KEYS.billDetails, billId], + queryFn: () => requestGetBillDetails({eventId, billId}), + }); + + return { + reportFromServer: data?.billDetails ?? [], + ...rest, + }; +}; + +export default useRequestGetBillDetails; diff --git a/client/src/hooks/queries/bill/useRequestPostBill.ts b/client/src/hooks/queries/bill/useRequestPostBill.ts new file mode 100644 index 00000000..80e4b685 --- /dev/null +++ b/client/src/hooks/queries/bill/useRequestPostBill.ts @@ -0,0 +1,27 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {RequestPostBill, requestPostBill} from '@apis/request/bill'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestPostBill = () => { + const eventId = getEventIdByUrl(); + const queryClient = useQueryClient(); + + const {mutate, ...rest} = useMutation({ + mutationFn: ({title, price, members}: RequestPostBill) => requestPostBill({eventId, title, price, members}), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]}); + }, + }); + + return { + postBill: mutate, + ...rest, + }; +}; + +export default useRequestPostBill; diff --git a/client/src/hooks/queries/bill/useRequestPutBill.ts b/client/src/hooks/queries/bill/useRequestPutBill.ts new file mode 100644 index 00000000..280436f7 --- /dev/null +++ b/client/src/hooks/queries/bill/useRequestPutBill.ts @@ -0,0 +1,26 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {RequestPutBill, requestPutBill} from '@apis/request/bill'; + +import {WithBillId} from '@apis/withId.type'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestPutBill = () => { + const eventId = getEventIdByUrl(); + const queryClient = useQueryClient(); + + const {mutate, ...rest} = useMutation({ + mutationFn: ({billId, title, price}: WithBillId) => requestPutBill({eventId, billId, title, price}), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]}); + }, + }); + + return {pulBill: mutate, ...rest}; +}; + +export default useRequestPutBill; diff --git a/client/src/hooks/queries/bill/useRequestPutBillDetails.ts b/client/src/hooks/queries/bill/useRequestPutBillDetails.ts new file mode 100644 index 00000000..a63c9d96 --- /dev/null +++ b/client/src/hooks/queries/bill/useRequestPutBillDetails.ts @@ -0,0 +1,48 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {RequestPutBillDetails, requestPutBillDetails} from '@apis/request/bill'; + +import {WithBillId} from '@apis/withId.type'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestPutBillDetails = (billId: number) => { + const eventId = getEventIdByUrl(); + const queryClient = useQueryClient(); + + const {mutate, ...rest} = useMutation({ + mutationFn: ({billId, billDetails}: WithBillId) => + requestPutBillDetails({eventId, billId, billDetails}), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]}); + queryClient.removeQueries({queryKey: [QUERY_KEYS.billDetails, billId]}); + }, + // onMutate: async (newMembers: MemberReportInAction[]) => { + // await queryClient.cancelQueries({queryKey: [QUERY_KEYS.memberReportInAction, actionId]}); + + // const previousMembers = queryClient.getQueryData([QUERY_KEYS.memberReportInAction, actionId]); + + // queryClient.setQueryData([QUERY_KEYS.memberReportInAction, actionId], (oldData: MemberReportList) => ({ + // ...oldData, + // members: oldData.members.map((member: MemberReportInAction) => { + // const updatedMember = newMembers.find(m => m.name === member.name); + // return updatedMember ? {...member, ...updatedMember} : member; + // }), + // })); + + // return {previousMembers}; + // }, + // onError: (err, newMembers, context) => { + // if (context?.previousMembers) { + // queryClient.setQueryData([QUERY_KEYS.memberReportInAction, actionId], context.previousMembers); + // } + // }, + }); + + return {putBillDetails: mutate, ...rest}; +}; + +export default useRequestPutBillDetails; diff --git a/client/src/hooks/queries/event/useRequestGetEvent.ts b/client/src/hooks/queries/event/useRequestGetEvent.ts new file mode 100644 index 00000000..c85faae8 --- /dev/null +++ b/client/src/hooks/queries/event/useRequestGetEvent.ts @@ -0,0 +1,25 @@ +import {useQuery} from '@tanstack/react-query'; + +import {requestGetEvent} from '@apis/request/event'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestGetEvent = () => { + const eventId = getEventIdByUrl(); + + const {data, ...rest} = useQuery({ + queryKey: [QUERY_KEYS.eventName], + queryFn: () => requestGetEvent({eventId}), + }); + + return { + eventName: data?.eventName ?? '', + bankName: data?.bankName, + accountName: data?.accountNumber, + ...rest, + }; +}; + +export default useRequestGetEvent; diff --git a/client/src/hooks/queries/event/useRequestPostEvent.ts b/client/src/hooks/queries/event/useRequestPostEvent.ts new file mode 100644 index 00000000..ec4c6f2b --- /dev/null +++ b/client/src/hooks/queries/event/useRequestPostEvent.ts @@ -0,0 +1,17 @@ +import {useMutation} from '@tanstack/react-query'; + +import {RequestPostEvent, requestPostEvent} from '@apis/request/event'; + +const useRequestPostEvent = () => { + const {mutate, ...rest} = useMutation({ + mutationFn: ({eventName, password}: RequestPostEvent) => requestPostEvent({eventName, password}), + }); + + return { + postEvent: mutate, + isPostEventPending: rest.isPending, + ...rest, + }; +}; + +export default useRequestPostEvent; diff --git a/client/src/hooks/queries/member/useRequestDeleteMember.ts b/client/src/hooks/queries/member/useRequestDeleteMember.ts new file mode 100644 index 00000000..ed3b191b --- /dev/null +++ b/client/src/hooks/queries/member/useRequestDeleteMember.ts @@ -0,0 +1,26 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {RequestDeleteMember, requestDeleteMember} from '@apis/request/member'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestDeleteMember = () => { + const eventId = getEventIdByUrl(); + const queryClient = useQueryClient(); + + const {mutate, ...rest} = useMutation({ + mutationFn: ({memberId}: RequestDeleteMember) => requestDeleteMember({eventId, memberId}), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMembers]}); + queryClient.removeQueries({queryKey: [QUERY_KEYS.billDetails]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]}); + }, + }); + + return {deleteMember: mutate, ...rest}; +}; + +export default useRequestDeleteMember; diff --git a/client/src/hooks/queries/member/useRequestGetAllMembers.ts b/client/src/hooks/queries/member/useRequestGetAllMembers.ts new file mode 100644 index 00000000..0efe290b --- /dev/null +++ b/client/src/hooks/queries/member/useRequestGetAllMembers.ts @@ -0,0 +1,20 @@ +import {useQuery} from '@tanstack/react-query'; + +import {requestGetAllMembers} from '@apis/request/member'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestGetAllMembers = () => { + const eventId = getEventIdByUrl(); + + const {data, ...rest} = useQuery({ + queryKey: [QUERY_KEYS.allMembers], + queryFn: () => requestGetAllMembers({eventId}), + }); + + return {members: data?.members ?? [], ...rest}; +}; + +export default useRequestGetAllMembers; diff --git a/client/src/hooks/queries/member/useRequestGetCurrentMembers.ts b/client/src/hooks/queries/member/useRequestGetCurrentMembers.ts new file mode 100644 index 00000000..06c595e3 --- /dev/null +++ b/client/src/hooks/queries/member/useRequestGetCurrentMembers.ts @@ -0,0 +1,20 @@ +import {useQuery} from '@tanstack/react-query'; + +import {requestGetCurrentMembers} from '@apis/request/member'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestGetCurrentMembers = () => { + const eventId = getEventIdByUrl(); + + const {data, ...rest} = useQuery({ + queryKey: [QUERY_KEYS.currentMembers], + queryFn: () => requestGetCurrentMembers({eventId}), + }); + + return {currentMembers: data?.members ?? [], ...rest}; +}; + +export default useRequestGetCurrentMembers; diff --git a/client/src/hooks/queries/member/useRequestPostMembers.ts b/client/src/hooks/queries/member/useRequestPostMembers.ts new file mode 100644 index 00000000..72da9f36 --- /dev/null +++ b/client/src/hooks/queries/member/useRequestPostMembers.ts @@ -0,0 +1,32 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {RequestPostMembers, requestPostMembers} from '@apis/request/member'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestPostMembers = () => { + const eventId = getEventIdByUrl(); + const queryClient = useQueryClient(); + + const {mutate, ...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: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMembers]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]}); + }, + }); + + return {postMember: mutate, ...rest}; +}; + +export default useRequestPostMembers; diff --git a/client/src/hooks/queries/member/useRequestPutMembers.ts b/client/src/hooks/queries/member/useRequestPutMembers.ts new file mode 100644 index 00000000..656866a3 --- /dev/null +++ b/client/src/hooks/queries/member/useRequestPutMembers.ts @@ -0,0 +1,26 @@ +import {useMutation, useQueryClient} from '@tanstack/react-query'; + +import {RequestPutMembers, requestPutMembers} from '@apis/request/member'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestPutMembers = () => { + const eventId = getEventIdByUrl(); + const queryClient = useQueryClient(); + + const {mutate, ...rest} = useMutation({ + mutationFn: ({members}: RequestPutMembers) => requestPutMembers({eventId, members}), + onSuccess: () => { + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.steps]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMembers]}); + queryClient.removeQueries({queryKey: [QUERY_KEYS.billDetails]}); + queryClient.invalidateQueries({queryKey: [QUERY_KEYS.reports]}); + }, + }); + + return {putMember: mutate, ...rest}; +}; + +export default useRequestPutMembers; diff --git a/client/src/hooks/queries/report/useRequestGetReports.ts b/client/src/hooks/queries/report/useRequestGetReports.ts new file mode 100644 index 00000000..5cb4de12 --- /dev/null +++ b/client/src/hooks/queries/report/useRequestGetReports.ts @@ -0,0 +1,22 @@ +import {useQuery} from '@tanstack/react-query'; + +import {requestGetReports} from '@apis/request/report'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; + +import QUERY_KEYS from '@constants/queryKeys'; + +const useRequestGetReports = () => { + const eventId = getEventIdByUrl(); + + const {data, ...rest} = useQuery({ + queryKey: [QUERY_KEYS.reports], + queryFn: () => requestGetReports({eventId}), + }); + + return { + reports: data?.reports ?? [], + ...rest, + }; +}; +export default useRequestGetReports; diff --git a/client/src/hooks/queries/useRequestGetStepList.ts b/client/src/hooks/queries/step/useRequestGetSteps.ts similarity index 69% rename from client/src/hooks/queries/useRequestGetStepList.ts rename to client/src/hooks/queries/step/useRequestGetSteps.ts index 7dfc1799..312c0316 100644 --- a/client/src/hooks/queries/useRequestGetStepList.ts +++ b/client/src/hooks/queries/step/useRequestGetSteps.ts @@ -1,7 +1,7 @@ import {useQuery} from '@tanstack/react-query'; import {useEffect} from 'react'; -import {requestGetStepList} from '@apis/request/stepList'; +import {requestGetSteps} from '@apis/request/step'; import {useTotalExpenseAmountStore} from '@store/totalExpenseAmountStore'; @@ -9,13 +9,13 @@ import getEventIdByUrl from '@utils/getEventIdByUrl'; import QUERY_KEYS from '@constants/queryKeys'; -const useRequestGetStepList = () => { +const useRequestGetSteps = () => { const eventId = getEventIdByUrl(); const {updateTotalExpenseAmount} = useTotalExpenseAmountStore(); const queryResult = useQuery({ - queryKey: [QUERY_KEYS.stepList], - queryFn: () => requestGetStepList({eventId}), + queryKey: [QUERY_KEYS.steps], + queryFn: () => requestGetSteps({eventId}), }); useEffect(() => { @@ -24,7 +24,10 @@ const useRequestGetStepList = () => { } }, [queryResult.data, queryResult.isSuccess, updateTotalExpenseAmount]); - return queryResult; + return { + steps: queryResult.data ?? [], + ...queryResult, + }; }; -export default useRequestGetStepList; +export default useRequestGetSteps; diff --git a/client/src/hooks/queries/useRequestDeleteAllMemberList.ts b/client/src/hooks/queries/useRequestDeleteAllMemberList.ts deleted file mode 100644 index da6fc422..00000000 --- a/client/src/hooks/queries/useRequestDeleteAllMemberList.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {requestDeleteAllMemberList} from '@apis/request/member'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -interface DeleteAllMemberListMutationProps { - memberName: string; -} - -const useRequestDeleteAllMemberList = () => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({memberName}: DeleteAllMemberListMutationProps) => requestDeleteAllMemberList({eventId, memberName}), - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMemberList]}); - queryClient.removeQueries({queryKey: [QUERY_KEYS.memberReportInAction]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - }, - }); -}; - -export default useRequestDeleteAllMemberList; diff --git a/client/src/hooks/queries/useRequestDeleteBillAction.ts b/client/src/hooks/queries/useRequestDeleteBillAction.ts deleted file mode 100644 index 28be9dfd..00000000 --- a/client/src/hooks/queries/useRequestDeleteBillAction.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {requestDeleteBillAction} from '@apis/request/bill'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -interface DeleteBillActionMutationProps { - actionId: number; -} - -const useRequestDeleteBillAction = () => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({actionId}: DeleteBillActionMutationProps) => requestDeleteBillAction({eventId, actionId}), - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - }, - }); -}; - -export default useRequestDeleteBillAction; diff --git a/client/src/hooks/queries/useRequestDeleteMemberAction.ts b/client/src/hooks/queries/useRequestDeleteMemberAction.ts deleted file mode 100644 index 08e69ac1..00000000 --- a/client/src/hooks/queries/useRequestDeleteMemberAction.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {requestDeleteMemberAction} from '@apis/request/member'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -interface DeleteMemberActionMutationProps { - actionId: number; -} - -const useRequestDeleteMemberAction = () => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({actionId}: DeleteMemberActionMutationProps) => requestDeleteMemberAction({eventId, actionId}), - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReportInAction]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMemberList]}); - }, - }); -}; - -export default useRequestDeleteMemberAction; diff --git a/client/src/hooks/queries/useRequestGetAllMemberList.ts b/client/src/hooks/queries/useRequestGetAllMemberList.ts deleted file mode 100644 index 531285fd..00000000 --- a/client/src/hooks/queries/useRequestGetAllMemberList.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {useQuery} from '@tanstack/react-query'; - -import {requestGetAllMemberList} from '@apis/request/member'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -const useRequestGetAllMemberList = () => { - const eventId = getEventIdByUrl(); - - return useQuery({ - queryKey: [QUERY_KEYS.allMemberList], - queryFn: () => requestGetAllMemberList({eventId}), - }); -}; - -export default useRequestGetAllMemberList; diff --git a/client/src/hooks/queries/useRequestGetCurrentInMemberList.ts b/client/src/hooks/queries/useRequestGetCurrentInMemberList.ts deleted file mode 100644 index 31dc058c..00000000 --- a/client/src/hooks/queries/useRequestGetCurrentInMemberList.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {useQuery} from '@tanstack/react-query'; - -import {requestGetCurrentInMemberList} from '@apis/request/member'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -const useRequestGetCurrentInMemberList = () => { - const eventId = getEventIdByUrl(); - - return useQuery({ - queryKey: [QUERY_KEYS.currentInMember], - queryFn: () => requestGetCurrentInMemberList({eventId}), - }); -}; - -export default useRequestGetCurrentInMemberList; diff --git a/client/src/hooks/queries/useRequestGetEventName.ts b/client/src/hooks/queries/useRequestGetEventName.ts deleted file mode 100644 index 6ddb2185..00000000 --- a/client/src/hooks/queries/useRequestGetEventName.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {useQuery} from '@tanstack/react-query'; - -import {requestGetEventName} from '@apis/request/event'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -const useRequestGetEventName = () => { - const eventId = getEventIdByUrl(); - - return useQuery({ - queryKey: [QUERY_KEYS.eventName], - queryFn: () => requestGetEventName({eventId}), - }); -}; - -export default useRequestGetEventName; diff --git a/client/src/hooks/queries/useRequestGetMemberReportList.ts b/client/src/hooks/queries/useRequestGetMemberReportList.ts deleted file mode 100644 index fe3372d3..00000000 --- a/client/src/hooks/queries/useRequestGetMemberReportList.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {useQuery} from '@tanstack/react-query'; - -import {requestGetMemberReportList} from '@apis/request/report'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -const useRequestGetMemberReportList = () => { - const eventId = getEventIdByUrl(); - - return useQuery({ - queryKey: [QUERY_KEYS.memberReport], - queryFn: () => requestGetMemberReportList({eventId}), - }); -}; -export default useRequestGetMemberReportList; diff --git a/client/src/hooks/queries/useRequestGetMemberReportListInAction.ts b/client/src/hooks/queries/useRequestGetMemberReportListInAction.ts deleted file mode 100644 index 85b45822..00000000 --- a/client/src/hooks/queries/useRequestGetMemberReportListInAction.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {useQuery} from '@tanstack/react-query'; - -import {requestGetMemberReportListInAction} from '@apis/request/bill'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -const useRequestGetMemberReportListInAction = (actionId: number) => { - const eventId = getEventIdByUrl(); - - const {data, ...queryResult} = useQuery({ - queryKey: [QUERY_KEYS.memberReportInAction, actionId], - queryFn: () => requestGetMemberReportListInAction({eventId, actionId}), - select: data => data.members, - }); - - return { - memberReportListInActionFromServer: data ?? [], - queryResult, - }; -}; - -export default useRequestGetMemberReportListInAction; diff --git a/client/src/hooks/queries/useRequestPostBillList.ts b/client/src/hooks/queries/useRequestPostBillList.ts deleted file mode 100644 index 676da9d8..00000000 --- a/client/src/hooks/queries/useRequestPostBillList.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {requestPostBillList} from '@apis/request/bill'; -import {Bill} from 'types/serviceType'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -interface PostBillActionMutationProps { - billList: Bill[]; -} - -const useRequestPostBillList = () => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({billList}: PostBillActionMutationProps) => requestPostBillList({eventId, billList}), - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - }, - }); -}; - -export default useRequestPostBillList; diff --git a/client/src/hooks/queries/useRequestPostEvent.ts b/client/src/hooks/queries/useRequestPostEvent.ts deleted file mode 100644 index 5767738e..00000000 --- a/client/src/hooks/queries/useRequestPostEvent.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {useMutation} from '@tanstack/react-query'; - -import {requestPostNewEvent} from '@apis/request/event'; - -interface PostEventMutationProps { - eventName: string; - password: string; -} - -const usePostEvent = () => { - return useMutation({ - mutationFn: ({eventName, password}: PostEventMutationProps) => requestPostNewEvent({eventName, password}), - }); -}; - -export default usePostEvent; diff --git a/client/src/hooks/queries/useRequestPostMemberList.ts b/client/src/hooks/queries/useRequestPostMemberList.ts deleted file mode 100644 index 8ed69db0..00000000 --- a/client/src/hooks/queries/useRequestPostMemberList.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {MemberType} from 'types/serviceType'; -import {requestPostMemberList} from '@apis/request/member'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -interface PostMemberListMutationProps { - type: MemberType; - memberNameList: string[]; -} - -const useRequestPostMemberList = () => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({type, memberNameList}: PostMemberListMutationProps) => - requestPostMemberList({eventId, type, memberNameList}), - // TODO: (@todari) : 낙관적 업데이트 적고 있었어용 - // onMutate: async ({type, memberNameList}) => { - // await queryClient.cancelQueries({queryKey: [QUERY_KEYS.stepList]}); - // const previousStepList = queryClient.getQueryData([QUERY_KEYS.stepList]); - // queryClient.setQueryData([QUERY_KEYS.stepList], (prev: (MemberStep | BillStep)[]) => prev && { - // }); - // }, - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMemberList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - }, - }); -}; - -export default useRequestPostMemberList; diff --git a/client/src/hooks/queries/useRequestPutAllMemberList.ts b/client/src/hooks/queries/useRequestPutAllMemberList.ts deleted file mode 100644 index 4bd746d0..00000000 --- a/client/src/hooks/queries/useRequestPutAllMemberList.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {MemberChange, requestPutAllMemberList} from '@apis/request/member'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -interface PutAllMemberListMutationProps { - members: MemberChange[]; -} - -const useRequestPutAllMemberList = () => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({members}: PutAllMemberListMutationProps) => requestPutAllMemberList({eventId, members}), - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.allMemberList]}); - queryClient.removeQueries({queryKey: [QUERY_KEYS.memberReportInAction]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - }, - }); -}; - -export default useRequestPutAllMemberList; diff --git a/client/src/hooks/queries/useRequestPutBillAction.ts b/client/src/hooks/queries/useRequestPutBillAction.ts deleted file mode 100644 index a58da8ea..00000000 --- a/client/src/hooks/queries/useRequestPutBillAction.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {requestPutBillAction} from '@apis/request/bill'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -interface PutBillActionMutationProps { - actionId: number; - title: string; - price: number; -} - -const useRequestPutBillAction = () => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({actionId, title, price}: PutBillActionMutationProps) => - requestPutBillAction({eventId, actionId, title, price}), - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - }, - }); -}; - -export default useRequestPutBillAction; diff --git a/client/src/hooks/queries/useRequestPutMemberReportListInAction.ts b/client/src/hooks/queries/useRequestPutMemberReportListInAction.ts deleted file mode 100644 index a87fa44f..00000000 --- a/client/src/hooks/queries/useRequestPutMemberReportListInAction.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {useMutation, useQueryClient} from '@tanstack/react-query'; - -import {MemberReportList, requestPutMemberReportListInAction} from '@apis/request/bill'; -import {MemberReportInAction} from 'types/serviceType'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -import QUERY_KEYS from '@constants/queryKeys'; - -const useRequestPutMemberReportListInAction = (actionId: number) => { - const eventId = getEventIdByUrl(); - const queryClient = useQueryClient(); - - const {mutate, ...mutationProps} = useMutation({ - mutationFn: (members: MemberReportInAction[]) => requestPutMemberReportListInAction({eventId, actionId, members}), - onSuccess: () => { - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.stepList]}); - queryClient.invalidateQueries({queryKey: [QUERY_KEYS.memberReport]}); - queryClient.removeQueries({queryKey: [QUERY_KEYS.memberReportInAction, actionId]}); - }, - onMutate: async (newMembers: MemberReportInAction[]) => { - await queryClient.cancelQueries({queryKey: [QUERY_KEYS.memberReportInAction, actionId]}); - - const previousMembers = queryClient.getQueryData([QUERY_KEYS.memberReportInAction, actionId]); - - queryClient.setQueryData([QUERY_KEYS.memberReportInAction, actionId], (oldData: MemberReportList) => ({ - ...oldData, - members: oldData.members.map((member: MemberReportInAction) => { - const updatedMember = newMembers.find(m => m.name === member.name); - return updatedMember ? {...member, ...updatedMember} : member; - }), - })); - - return {previousMembers}; - }, - onError: (err, newMembers, context) => { - if (context?.previousMembers) { - queryClient.setQueryData([QUERY_KEYS.memberReportInAction, actionId], context.previousMembers); - } - }, - }); - - return { - putMemberReportListInAction: mutate, - mutationProps, - }; -}; - -export default useRequestPutMemberReportListInAction; diff --git a/client/src/hooks/useMemberReportListInAction/useMemberReportInput.tsx b/client/src/hooks/useBillDetails/useBillDetailInput.tsx similarity index 69% rename from client/src/hooks/useMemberReportListInAction/useMemberReportInput.tsx rename to client/src/hooks/useBillDetails/useBillDetailInput.tsx index 9c9cd80d..6b335049 100644 --- a/client/src/hooks/useMemberReportListInAction/useMemberReportInput.tsx +++ b/client/src/hooks/useBillDetails/useBillDetailInput.tsx @@ -1,29 +1,29 @@ -import type {MemberReportInAction} from 'types/serviceType'; +import type {BillDetail} from 'types/serviceType'; import {useEffect, useState} from 'react'; -import validateMemberReportInAction from '@utils/validate/validateMemberReportInAction'; +import validateBillDetails from '@utils/validate/validateBillDetails'; -type MemberReportInput = MemberReportInAction & { +type BillDetailInput = BillDetail & { index: number; }; -type UseMemberReportProps = { - data: MemberReportInAction[]; - addAdjustedMember: (memberReport: MemberReportInAction) => void; +type UseBillDetailInput = { + data: BillDetail[]; + addAdjustedMember: (billDetails: BillDetail) => void; getOnlyOneNotAdjustedRemainMemberIndex: () => number | null; getIsSamePriceStateAndServerState: () => boolean; totalPrice: number; }; -const useMemberReportInput = ({ +const useBillDetailInput = ({ data, addAdjustedMember, totalPrice, getOnlyOneNotAdjustedRemainMemberIndex, getIsSamePriceStateAndServerState, -}: UseMemberReportProps) => { - const [inputList, setInputList] = useState(data.map((item, index) => ({...item, index}))); +}: UseBillDetailInput) => { + const [inputList, setInputList] = useState(data.map((item, index) => ({...item, index}))); const [canSubmit, setCanSubmit] = useState(false); const [canEditList, setCanEditList] = useState([]); @@ -35,26 +35,27 @@ const useMemberReportInput = ({ }; const validateAndAddAdjustedMember = (price: string, index: number) => { - const {isValid} = validateMemberReportInAction(price, totalPrice); + const {isValid} = validateBillDetails(price, totalPrice); if (isValid) { const newInputList = [...inputList]; newInputList[index].price = Number(price); setInputList(newInputList); - const memberReportData: MemberReportInAction = { - name: newInputList[index].name, + const reportData: BillDetail = { + ...newInputList[index], + memberName: newInputList[index].memberName, price: newInputList[index].price, isFixed: newInputList[index].isFixed, }; - addAdjustedMember(memberReportData); + addAdjustedMember(reportData); } }; // 서버와 값이 같지 않고 input price의 상태가 모두 valid하다면 can submit true useEffect(() => { const isSamePriceState = getIsSamePriceStateAndServerState(); - const isAllValid = inputList.every(input => validateMemberReportInAction(String(input.price), totalPrice)); + const isAllValid = inputList.every(input => validateBillDetails(String(input.price), totalPrice)); setCanSubmit(!isSamePriceState && isAllValid); }, [inputList]); @@ -86,4 +87,4 @@ const useMemberReportInput = ({ }; }; -export default useMemberReportInput; +export default useBillDetailInput; diff --git a/client/src/hooks/useBillDetails/useBillDetails.ts b/client/src/hooks/useBillDetails/useBillDetails.ts new file mode 100644 index 00000000..408bd043 --- /dev/null +++ b/client/src/hooks/useBillDetails/useBillDetails.ts @@ -0,0 +1,157 @@ +import type {BillDetail} from 'types/serviceType'; + +import {useEffect, useState} from 'react'; + +import useRequestGetBillDetails from '@hooks/queries/bill/useRequestGetBillDetails'; +import useRequestPutBillDetails from '@hooks/queries/bill/useRequestPutBillDetails'; + +const useBillDetails = (billId: number, totalPrice: number, onClose: () => void) => { + const {reportFromServer, isSuccess} = useRequestGetBillDetails(billId); + const {putBillDetails} = useRequestPutBillDetails(billId); + + const [billDetails, setBillDetails] = useState(reportFromServer); + + // isFixed를 모두 풀고 계산값으로 모두 처리하는 기능 + const reCalculatePriceByTotalPriceChange = () => { + const {divided, remainder} = calculateDividedPrice(billDetails.length, 0); + + const resetBillDetails = [...billDetails]; + resetBillDetails.forEach((member, index) => { + if (index !== resetBillDetails.length - 1) { + member.price = divided; + } else { + member.price = divided + remainder; + } + member.isFixed = false; + }); + + setBillDetails(resetBillDetails); + }; + + // 총 금액이 변동됐을 때 (서버에서 온 값과 다를 때) 재계산 실행 + useEffect(() => { + const totalPriceFromServer = reportFromServer.reduce((acc, cur) => acc + cur.price, 0); + + if (totalPriceFromServer !== totalPrice && totalPriceFromServer !== 0) { + reCalculatePriceByTotalPriceChange(); + } + }, [totalPrice, reportFromServer]); + + useEffect(() => { + if (isSuccess) { + setBillDetails(reportFromServer); + } + }, [reportFromServer, isSuccess]); + + const isExistAdjustedPrice = () => { + return billDetails.some(member => member.isFixed === true); + }; + + // 조정되지 않은 인원이 단 1명인 경우의 index + const getOnlyOneNotAdjustedRemainMemberIndex = (): number | null => { + const adjustedPriceCount = getAdjustedMemberCount(billDetails); + + if (adjustedPriceCount < billDetails.length - 1) return null; + + return billDetails.findIndex(member => member.isFixed === false); + }; + + // 조정값 멤버의 수를 구하는 함수 + const getAdjustedMemberCount = (billDetails: BillDetail[]) => { + return billDetails.filter(member => member.isFixed === true).length; + }; + + const addAdjustedMember = (memberReport: BillDetail) => { + const newBillDetails = billDetails.map(member => + member.memberName === memberReport.memberName ? {...member, price: memberReport.price, isFixed: true} : member, + ); + + calculateAnotherMemberPrice(newBillDetails); + }; + + const calculateDividedPrice = (remainMemberCount: number, totalAdjustedPrice: number) => { + return { + divided: Math.floor((totalPrice - totalAdjustedPrice) / remainMemberCount), + remainder: (totalPrice - totalAdjustedPrice) % remainMemberCount, + }; + }; + + // 계산값으로 값을 변경했을 때 isFixed를 푸는 함수 + // 100 true 33300 true 33300 false 33300 false + const setIsFixedFalseAtResetToDividedPrice = (billDetails: BillDetail[], divided: number) => { + return billDetails.map(bill => { + if (bill.isFixed === true && bill.price === divided) { + return {...bill, isFixed: false}; + } + + return bill; + }); + }; + + const calculateAnotherMemberPrice = (billDetails: BillDetail[]) => { + // 총 조정치 금액 + const totalAdjustedPrice = billDetails + .filter(memberReport => memberReport.isFixed === true) + .reduce((acc, cur) => acc + cur.price, 0); + + const remainMemberCount = billDetails.length - getAdjustedMemberCount(billDetails); + const {divided, remainder} = calculateDividedPrice(remainMemberCount, totalAdjustedPrice); + + const updatedList = billDetails.map(member => (member.isFixed === true ? member : {...member, price: divided})); + + // 나머지를 조정되지 않은 멤버 중 마지막 멤버에게 추가 + if (remainder !== 0) { + const nonAdjustedMembers = updatedList.filter(member => member.isFixed === false); + const lastNonAdjustedMemberIndex = updatedList.findIndex( + member => member.memberName === nonAdjustedMembers[nonAdjustedMembers.length - 1].memberName, + ); + + if (lastNonAdjustedMemberIndex !== -1) { + updatedList[lastNonAdjustedMemberIndex].price += remainder; + } + } + + // 조정됐지만 계산값으로 가격이 변한 경우 fixed 상태를 풀어야한다. + const result = setIsFixedFalseAtResetToDividedPrice(updatedList, divided); + setBillDetails(result); + }; + + const onSubmit = () => { + const withoutMemberName = billDetails.map(billDetail => { + const {memberName, ...rest} = billDetail; + return {...rest}; + }); + + putBillDetails({billId, billDetails: withoutMemberName}); + + onClose(); + }; + + const getIsSamePriceStateAndServerState = () => { + const serverStatePriceList = reportFromServer.map(({price}) => price); + const clientStatePriceList = billDetails.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 { + billDetails, + addAdjustedMember, + isExistAdjustedPrice, + onSubmit, + getOnlyOneNotAdjustedRemainMemberIndex, + getIsSamePriceStateAndServerState, + isSuccess, + }; +}; + +export default useBillDetails; diff --git a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx deleted file mode 100644 index 7168070c..00000000 --- a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import {renderHook, waitFor} from '@testing-library/react'; -import {MemoryRouter} from 'react-router-dom'; -import {act} from 'react'; - -import {BillStep, MemberAction, MemberStep} from 'types/serviceType'; -import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; -import AppErrorBoundary from '@components/AppErrorBoundary/ErrorCatcher'; -import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; -import {ToastProvider} from '@hooks/useToast/ToastProvider'; - -import {HDesignProvider} from '@HDesign/index'; - -import stepListJson from '@mocks/stepList.json'; -import invalidMemberStepListJson from '@mocks/invalidMemberStepList.json'; - -import useDeleteMemberAction from './useDeleteMemberAction'; - -const stepListMockData = stepListJson as (BillStep | MemberStep)[]; -let memberActionList: MemberAction[] = []; - -// filter로는 type narrowing이 안되어 부득이하게 for 문을 사용했습니다. -for (let i = 0; i < stepListMockData.length; i++) { - const curAction = stepListMockData[i]; - if (curAction.type !== 'BILL') curAction.actions.forEach(action => memberActionList.push(action)); -} - -describe('useDeleteMemberAction', () => { - const initializeProvider = (list: MemberAction[] = memberActionList) => - renderHook( - () => { - return { - stepListResult: useRequestGetStepList(), - deleteMemberActionList: useDeleteMemberAction({ - memberActionList: list, - setIsBottomSheetOpened: () => {}, - showToastAlreadyExistMemberAction: () => {}, - showToastExistSameMemberFromAfterStep: () => {}, - }), - }; - }, - { - wrapper: ({children}) => ( - - - - - {children} - - - - - ), - }, - ); - - it('멤버를 삭제할 멤버 목록에 추가한다.', async () => { - const {result} = initializeProvider(); - - // stepList 값이 채워지길 대기합니다. - await waitFor(() => { - expect(result.current.stepListResult.data).not.toStrictEqual([]); - }); - - act(() => { - const memberAction = { - actionId: 1, - name: '망쵸', - price: null, - sequence: 1, - isFixed: false, - }; - - result.current.deleteMemberActionList.addDeleteMemberAction(memberAction); - }); - - expect(result.current.deleteMemberActionList.aliveActionList).not.toContainEqual({ - actionId: 1, - name: '망쵸', - price: null, - sequence: 1, - }); - }); - - it('삭제할 멤버 목록에 있는 멤버들을 모두 삭제해 삭제할 멤버 목록을 비운다.', async () => { - const {result} = initializeProvider(); - - // stepList 값이 채워지길 대기합니다. - await waitFor(() => { - expect(result.current.stepListResult.data).not.toStrictEqual([]); - }); - - await act(async () => { - memberActionList.forEach(memberAction => { - result.current.deleteMemberActionList.addDeleteMemberAction(memberAction); - }); - }); - - await act(async () => result.current.deleteMemberActionList.deleteMemberActionList()); - - await waitFor(() => { - expect(result.current.deleteMemberActionList.aliveActionList).toHaveLength(0); - }); - }); - - it('삭제 요청에서 오류가 발생했을 경우 삭제할 멤버 목록을 처음의 상태로 돌려놓는다.', async () => { - /** - * 이 테스트는 deleteMemberAction을 실행했을 때 오류가 나는 경우를 테스트하기 위해 작성되었습니다. - * 여기서 사용하는 stepList 목데이터는 999 actionId를 가진 MemberAction이 있는 데이터입니다. - * 999 actionId는 현재 모킹되어있는 msw에서 오류를 뱉도록 하는 id입니다. 이는 오류 상황을 시연하기 위한 임의 모킹입니다. - * (999가 아닌 경우는 모두 정상 응답 반환) - */ - - // 999 actionId를 가진 MemberAction이 있는 stepListJson 데이터를 사용해 MemberActions []으로 정제합니다. - const invalidMemberStepListMockData = invalidMemberStepListJson as (BillStep | MemberStep)[]; - let memberActionList: MemberAction[] = []; - - for (let i = 0; i < invalidMemberStepListMockData.length; i++) { - const curAction = invalidMemberStepListMockData[i]; - if (curAction.type !== 'BILL') curAction.actions.forEach(action => memberActionList.push(action)); - } - - // 오류 멤버가 포함된 memberAction[]을 useDeleteMemberAction의 초기값으로 주입합니다. - const {result} = initializeProvider(memberActionList); - - // stepList 값이 채워지길 대기합니다. - await waitFor(() => { - expect(result.current.stepListResult.data).not.toStrictEqual([]); - }); - - await act(async () => { - const memberAction = memberActionList[0]; // 현재 0번 참여자는 actionId가 999 이고, 999 actionId는 서버에서 에러를 뱉도록 조작된 상황입니다. - result.current.deleteMemberActionList.addDeleteMemberAction(memberAction); - }); - - await act(async () => result.current.deleteMemberActionList.deleteMemberActionList()); - - // 삭제 요청에서 오류가 발생했기 때문에 aliveActionList를 초기에 주입했던 값으로 다시 돌려놓는지 확인합니다. - expect(result.current.deleteMemberActionList.aliveActionList).toStrictEqual(memberActionList); - }); -}); diff --git a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.tsx b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.tsx deleted file mode 100644 index 913087a5..00000000 --- a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import type {MemberAction} from 'types/serviceType'; - -import {useState} from 'react'; - -import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; -import useRequestDeleteMemberAction from '@hooks/queries/useRequestDeleteMemberAction'; - -import getEventIdByUrl from '@utils/getEventIdByUrl'; - -type UseDeleteMemberActionProps = { - memberActionList: MemberAction[]; - setIsBottomSheetOpened: React.Dispatch>; - showToastAlreadyExistMemberAction: () => void; - showToastExistSameMemberFromAfterStep: (name: string) => void; -}; - -const useDeleteMemberAction = ({ - memberActionList, - setIsBottomSheetOpened, - showToastAlreadyExistMemberAction, - showToastExistSameMemberFromAfterStep, -}: UseDeleteMemberActionProps) => { - const {data: stepListData} = useRequestGetStepList(); - const stepList = stepListData ?? []; - const {mutate: deleteMemberActionMutate} = useRequestDeleteMemberAction(); - - const [deleteActionList, setDeleteActionList] = useState([]); - - const eventId = getEventIdByUrl(); - - const deleteMemberAction = async (actionId: number) => { - deleteMemberActionMutate( - {actionId}, - { - onError: () => { - setDeleteActionList([]); - }, - }, - ); - setIsBottomSheetOpened(false); - }; - - // TODO: (@cookie: 추후에 반복문으로 delete하는 것이 아니라 한 번에 모아서 delete 처리하기 (backend에 문의)) - const deleteMemberActionList = async () => { - for (const {actionId} of deleteActionList) { - await deleteMemberAction(actionId); - } - }; - - const checkAlreadyExistMemberAction = (memberAction: MemberAction, showToast: () => void) => { - if (!memberActionList.includes(memberAction)) { - showToast(); - } - }; - - const checkExistSameMemberFromAfterStep = (memberAction: MemberAction, showToast: () => void) => { - if (isExistSameMemberFromAfterStep(memberAction)) { - showToast(); - } - }; - - const addDeleteMemberAction = (memberAction: MemberAction) => { - checkAlreadyExistMemberAction(memberAction, showToastAlreadyExistMemberAction); - checkExistSameMemberFromAfterStep(memberAction, () => showToastExistSameMemberFromAfterStep(memberAction.name)); - setDeleteActionList(prev => [...prev, memberAction]); - }; - - const isExistSameMemberFromAfterStep = (memberAction: MemberAction) => { - const memberActionList = stepList - .filter(step => step.type !== 'BILL') - .map(({actions}) => actions) - .flat(); - const currentActionIndex = memberActionList.findIndex(action => action.actionId === memberAction.actionId); - const memberActionListAfterCurrentAction = memberActionList.slice(Math.max(currentActionIndex - 1, 0)); - const memberNameList = memberActionListAfterCurrentAction.map(({name}) => name); - - return memberNameList.filter(member => member === memberAction.name).length >= 2; - }; - - const aliveActionList = memberActionList.filter( - memberAction => !deleteActionList.some(deleteAction => deleteAction.actionId === memberAction.actionId), - ); - return {aliveActionList, deleteMemberActionList, addDeleteMemberAction}; -}; - -export default useDeleteMemberAction; diff --git a/client/src/hooks/useDynamicInput.tsx b/client/src/hooks/useDynamicInput.tsx deleted file mode 100644 index 452add19..00000000 --- a/client/src/hooks/useDynamicInput.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import {useEffect, useRef} from 'react'; - -import {ValidateResult} from '@utils/validate/type'; - -import useInput from './useInput'; - -type InputValue = { - value: string; - index: number; -}; - -export type ReturnUseDynamicInput = { - inputList: InputValue[]; - inputRefList: React.MutableRefObject<(HTMLInputElement | null)[]>; - errorMessage: string | null; - canSubmit: boolean; - errorIndexList: number[]; - handleChange: (index: number, value: string) => void; - handleInputChange: (index: number, event: React.ChangeEvent) => void; - deleteEmptyInputElementOnBlur: () => void; - getFilledInputList: (list?: InputValue[]) => InputValue[]; - focusNextInputOnEnter: (e: React.KeyboardEvent, index: number) => void; - setInputValueTargetIndex: (index: number, value: string) => void; - resetInputValue: () => void; -}; - -const useDynamicInput = (validateFunc: (name: string) => ValidateResult): ReturnUseDynamicInput => { - const initialInputList = [{index: 0, value: ''}]; - const inputRefList = useRef<(HTMLInputElement | null)[]>([]); - - const {inputList, errorMessage, errorIndexList, canSubmit, handleChange, setInputList} = useInput({ - validateFunc, - initialInputList, - }); - - useEffect(() => { - if (inputRefList.current.length <= 0) return; - - const lastInput = inputRefList.current[inputRefList.current.length - 1]; - - if (lastInput) { - lastInput.scrollIntoView({behavior: 'smooth', block: 'center'}); - } - }, [inputList]); - - const handleInputChange = (index: number, event: React.ChangeEvent) => { - const {value} = event.target; - - makeNewInputWhenFirstCharacterInput(index, value); - handleChange(index, value); - }; - - const makeNewInputWhenFirstCharacterInput = (index: number, value: string) => { - if (isLastInputFilled(index, value) && value.trim().length !== 0) { - // 마지막 인풋이 한 자라도 채워진다면 새로운 인풋을 생성해 간편한 다음 입력을 유도합니다. - setInputList(prevInputs => { - const updatedInputList = [...prevInputs]; - - // 새로운 인덱스를 inputs 배열 길이를 기준으로 설정 - const newIndex = updatedInputList[updatedInputList.length - 1].index + 1; - - return [...updatedInputList, {index: newIndex, value: ''}]; - }); - } - }; - - const deleteEmptyInputElementOnBlur = () => { - // 0, 1번 input이 값이 있는 상태에서 두 input의 값을 모두 x버튼으로 제거해도 input이 2개 남아있는 문제를 위해 조건문을 추가했습니다. - if (getFilledInputList().length === 0 && inputList.length > 1) { - setInputList([{index: 0, value: ''}]); - return; - } - - // *표시 조건문은 처음에 input을 클릭했다가 블러시켰을 때 filledInputList가 아예 없어 .index에 접근할 때 오류가 납니다. 이를 위한 얼리리턴을 두었습니다. - if (getFilledInputList().length === 0) return; - - if (getFilledInputList().length !== inputList.length) { - setInputList(inputList => { - const filledInputList = getFilledInputList(inputList); - - // 새 입력의 인덱스를 inputs 길이를 기준으로 설정 - const newIndex = filledInputList[filledInputList.length - 1].index + 1; - - return [...filledInputList, {index: newIndex, value: ''}]; - }); - } - }; - - const setInputValueTargetIndex = (index: number, value: string) => { - setInputList(prevInputs => { - const updatedInputList = [...prevInputs]; - const targetInput = findInputByIndex(index, updatedInputList); - - targetInput.value = value; - - return updatedInputList; - }); - }; - - const resetInputValue = () => { - setInputList(initialInputList); - }; - - const focusNextInputOnEnter = (e: React.KeyboardEvent, index: number) => { - if (e.nativeEvent.isComposing) return; - - if (e.key === 'Enter') { - inputRefList.current[index + 1]?.focus(); - } - }; - - const findInputByIndex = (index: number, list?: InputValue[]) => { - return (list ?? inputList).filter(input => input.index === index)[0]; - }; - - const getFilledInputList = (list?: InputValue[]) => { - return (list ?? inputList).filter(({value}) => value.trim().length !== 0); - }; - - const isLastInputFilled = (index: number, value: string) => { - const lastInputIndex = inputList[inputList.length - 1].index; - - return value !== '' && index === lastInputIndex; - }; - - return { - inputList, - inputRefList, - errorMessage, - canSubmit, - errorIndexList, - handleInputChange, - handleChange, - deleteEmptyInputElementOnBlur, - getFilledInputList, - focusNextInputOnEnter, - setInputValueTargetIndex, - resetInputValue, - }; -}; - -export default useDynamicInput; diff --git a/client/src/hooks/useEventLogin.ts b/client/src/hooks/useEventLogin.ts index e6f2cf2e..15d9b267 100644 --- a/client/src/hooks/useEventLogin.ts +++ b/client/src/hooks/useEventLogin.ts @@ -4,13 +4,13 @@ import validateEventPassword from '@utils/validate/validateEventPassword'; import RULE from '@constants/rule'; -import useRequestPostLogin from './queries/useRequestPostLogin'; +import useRequestPostLogin from './queries/auth/useRequestPostLogin'; const useEventLogin = () => { const [password, setPassword] = useState(''); const [errorMessage, setErrorMessage] = useState(null); const [canSubmit, setCanSubmit] = useState(false); - const {mutate: postLogin} = useRequestPostLogin(); + const {postLogin} = useRequestPostLogin(); const submitPassword = async (event: React.FormEvent) => { event.preventDefault(); diff --git a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx deleted file mode 100644 index 78c29ee0..00000000 --- a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import type {MemberReportInAction} from 'types/serviceType'; - -import {renderHook, waitFor, act} from '@testing-library/react'; -import {MemoryRouter} from 'react-router-dom'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; - -import memberReportListInActionJson from '../../mocks/memberReportListInAction.json'; - -import useMemberReportListInAction from './useMemberReportListInAction'; - -describe('useMemberReportListInActionTest', () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 0, - }, - }, - }); - - const initializeProvider = (actionId: number, totalPrice: number) => - renderHook(() => useMemberReportListInAction(actionId, totalPrice, () => {}), { - wrapper: ({children}) => ( - - {children} - - ), - }); - - const actionId = 123; - const totalPrice = 100000; - - describe('Flow: 유저가 정상적으로 값을 불러왔을 때의 test', () => { - it('초기값을 정상적으로 불러온다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - expect(result.current.memberReportListInAction).toStrictEqual(memberReportListInActionJson); - }); - - it('망쵸의 가격을 100원으로 바꾸면 망쵸의 가격은 100원으로 설정된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMember: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMember); - }); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '망쵸'); - - expect(targetMember?.price).toBe(100); - }); - - it('망쵸의 가격을 100원으로 바꾸면 망쵸의 가격은 100원으로 설정되고 나머지 인원의 가격이 33,300원으로 설정된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMember: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMember); - }); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '망쵸'); - expect(targetMember?.price).toBe(100); - - const anotherMemberList = result.current.memberReportListInAction.filter(member => member.name !== '망쵸'); - - anotherMemberList.forEach(member => { - expect(member.price).toBe(33300); - }); - }); - - it('망쵸의 가격을 100원 쿠키의 가격을 100원으로 바꾸면 나머지 인원의 가격이 49,900원으로 설정된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - const adjustedMemberCookie: MemberReportInAction = {name: '쿠키', price: 100, isFixed: true}; - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangcho); - }); - - act(() => { - result.current.addAdjustedMember(adjustedMemberCookie); - }); - - const anotherMemberList = result.current.memberReportListInAction.filter( - member => !(member.name === '망쵸' || member.name === '쿠키'), - ); - - anotherMemberList.forEach(member => { - expect(member.price).toBe(49900); - }); - }); - - it('망쵸의 가격을 100원 쿠키의 가격을 100원으로 바꾸면 나머지 인원의 가격이 49,900원으로 설정된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - const adjustedMemberCookie: MemberReportInAction = {name: '쿠키', price: 100, isFixed: false}; - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangcho); - }); - - act(() => { - result.current.addAdjustedMember(adjustedMemberCookie); - }); - - const anotherMemberList = result.current.memberReportListInAction.filter( - member => !(member.name === '망쵸' || member.name === '쿠키'), - ); - - anotherMemberList.forEach(member => { - expect(member.price).toBe(49900); - }); - }); - - it('망쵸의 가격을 100원으로 바꾸고 다시 망쵸의 가격을 10,000원으로 바꾸면 나머지 인원의 가격이 30,000원으로 설정된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - const adjustedMemberMangchoAfter: MemberReportInAction = {name: '망쵸', price: 10000, isFixed: true}; - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangcho); - }); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangchoAfter); - }); - - const anotherMemberList = result.current.memberReportListInAction.filter(member => member.name !== '망쵸'); - - anotherMemberList.forEach(member => { - expect(member.price).toBe(30000); - }); - }); - }); - - describe('예외 & 엣지케이스', () => { - it('동일한 인원의 가격을 동일하게 바꾸면 변함없다.', 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); - }); - - act(() => { - result.current.addAdjustedMember({...adjustedMemberMangcho, isFixed: true}); - }); - - const anotherMemberList = result.current.memberReportListInAction.filter(member => member.name !== '망쵸'); - - expect(anotherMemberList[0].price).toBe(33300); - }); - - it('망쵸에게 300원을 주면 나머지 사람들은 33233원이고 마지막 사람은 33234원이 된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 300, isFixed: false}; - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangcho); - }); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '망쵸'); - expect(targetMember?.price).toBe(300); - - const anotherMemberList = result.current.memberReportListInAction.filter( - member => member.name === '이상' || member.name === '소하', - ); - - anotherMemberList.forEach(member => { - expect(member.price).toBe(33233); - }); - - const lastMember = result.current.memberReportListInAction.find(member => member.name === '쿠키'); - - expect(lastMember?.price).toBe(33234); - }); - - it('망쵸, 쿠키의 가격을 100원으로 바꾼 후 다시 쿠키의 가격을 33000원으로 바꾸면 쿠키의 isFixed는 false가 된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - const adjustedMemberCookie: MemberReportInAction = {name: '쿠키', price: 100, isFixed: false}; - const adjustedMemberCookieReset: MemberReportInAction = {name: '쿠키', price: 33300, isFixed: true}; - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangcho); - }); - - act(() => { - result.current.addAdjustedMember(adjustedMemberCookie); - }); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '쿠키'); - - expect(targetMember?.isFixed).toBe(true); - - act(() => { - result.current.addAdjustedMember(adjustedMemberCookieReset); - }); - - const targetMemberReset = result.current.memberReportListInAction.find(member => member.name === '쿠키'); - - expect(targetMemberReset?.isFixed).toBe(false); - }); - - it('망쵸의 가격을 100원으로 바꾼 후 다시 망쵸의 가격을 25000원으로 바꾸면 망쵸의 isFixed는 false가 된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - const adjustedMemberMangchoAfter: MemberReportInAction = {name: '망쵸', price: 25000, isFixed: true}; - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangcho); - }); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '망쵸'); - expect(targetMember?.isFixed).toBe(true); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangchoAfter); - }); - - 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); - }); - }); - - describe('지출 인원이 2명인 상황', () => { - const actionId = 1; - const totalPrice = 50000; - - // 망쵸 이상 - it('망쵸의 가격을 100원으로 수정한 경우, 이상의 가격이 49900원으로 수정된다.', 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); - }); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '이상'); - - expect(targetMember?.price).toBe(49900); - }); - - it('망쵸의 가격을 100원으로 수정하고 다시 200원으로 수정한 경우, 이상의 가격이 49800원으로 수정된다.', async () => { - const {result} = initializeProvider(actionId, totalPrice); - const adjustedMemberMangcho: MemberReportInAction = {name: '망쵸', price: 100, isFixed: false}; - const adjustedMemberMangchoOther: MemberReportInAction = {name: '망쵸', price: 200, isFixed: true}; - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangcho); - }); - - act(() => { - result.current.addAdjustedMember(adjustedMemberMangchoOther); - }); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '이상'); - - expect(targetMember?.price).toBe(49800); - }); - }); - - // last - describe('onSubmit 실행 시 반영 테스트', () => { - it('망쵸의 가격을 100원으로 바꾸고 저장하면 망쵸 100원이 반영된다.', 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); - }); - - await waitFor(() => { - result.current.onSubmit(); - }); - - await waitFor(() => expect(result.current.queryResult.isSuccess).toBe(true)); - - const targetMember = result.current.memberReportListInAction.find(member => member.name === '망쵸'); - expect(targetMember?.price).toBe(100); - }); - }); -}); diff --git a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.ts b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.ts deleted file mode 100644 index 0e32d740..00000000 --- a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type {MemberReportInAction} from 'types/serviceType'; - -import {useEffect, useState} from 'react'; - -import useRequestGetMemberReportListInAction from '@hooks/queries/useRequestGetMemberReportListInAction'; -import useRequestPutMemberReportListInAction from '@hooks/queries/useRequestPutMemberReportListInAction'; - -const useMemberReportListInAction = (actionId: number, totalPrice: number, onClose: () => void) => { - const {memberReportListInActionFromServer, queryResult} = useRequestGetMemberReportListInAction(actionId); - const {putMemberReportListInAction} = useRequestPutMemberReportListInAction(actionId); - - const [memberReportListInAction, setMemberReportListInAction] = useState( - 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 && totalPriceFromServer !== 0) { - reCalculatePriceByTotalPriceChange(); - } - }, [totalPrice, memberReportListInActionFromServer]); - - 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; - }; - - const addAdjustedMember = (memberReport: MemberReportInAction) => { - const newMemberReportListInAction = memberReportListInAction.map(member => - member.name === memberReport.name ? {...member, price: memberReport.price, isFixed: true} : member, - ); - - calculateAnotherMemberPrice(newMemberReportListInAction); - }; - - const calculateDividedPrice = (remainMemberCount: number, totalAdjustedPrice: number) => { - return { - divided: Math.floor((totalPrice - totalAdjustedPrice) / remainMemberCount), - remainder: (totalPrice - totalAdjustedPrice) % remainMemberCount, - }; - }; - - // 계산값으로 값을 변경했을 때 isFixed를 푸는 함수 - // 100 true 33300 true 33300 false 33300 false - const setIsFixedFalseAtResetToDividedPrice = (memberReportListInAction: MemberReportInAction[], divided: number) => { - return memberReportListInAction.map(memberReport => { - if (memberReport.isFixed === true && memberReport.price === divided) { - return {...memberReport, isFixed: false}; - } - - return memberReport; - }); - }; - - const calculateAnotherMemberPrice = (memberReportListInAction: MemberReportInAction[]) => { - // 총 조정치 금액 - const totalAdjustedPrice = memberReportListInAction - .filter(memberReport => memberReport.isFixed === true) - .reduce((acc, cur) => acc + cur.price, 0); - - const remainMemberCount = memberReportListInAction.length - getAdjustedMemberCount(memberReportListInAction); - const {divided, remainder} = calculateDividedPrice(remainMemberCount, totalAdjustedPrice); - - const updatedList = memberReportListInAction.map(member => - member.isFixed === true ? member : {...member, price: divided}, - ); - - // 나머지를 조정되지 않은 멤버 중 마지막 멤버에게 추가 - if (remainder !== 0) { - const nonAdjustedMembers = updatedList.filter(member => member.isFixed === false); - const lastNonAdjustedMemberIndex = updatedList.findIndex( - member => member.name === nonAdjustedMembers[nonAdjustedMembers.length - 1].name, - ); - - if (lastNonAdjustedMemberIndex !== -1) { - updatedList[lastNonAdjustedMemberIndex].price += remainder; - } - } - - // 조정됐지만 계산값으로 가격이 변한 경우 fixed 상태를 풀어야한다. - const result = setIsFixedFalseAtResetToDividedPrice(updatedList, divided); - setMemberReportListInAction(result); - }; - - const onSubmit = () => { - putMemberReportListInAction(memberReportListInAction); - - onClose(); - }; - - 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, - }; -}; - -export default useMemberReportListInAction; diff --git a/client/src/hooks/usePutAndDeleteBill.ts b/client/src/hooks/usePutAndDeleteBill.ts new file mode 100644 index 00000000..60c0ea35 --- /dev/null +++ b/client/src/hooks/usePutAndDeleteBill.ts @@ -0,0 +1,115 @@ +// import type {Bill, BillInputType, InputPair} from 'types/serviceType'; + +// import {useEffect, useState} from 'react'; + +// import {ValidateResult} from '@utils/validate/type'; + +// import {ERROR_MESSAGE} from '@constants/errorMessage'; + +// import usePutBillAction from './queries/'; +// import useDeleteBillAction from './queries/useRequestDeleteBill'; + +// const usePutAndDeleteBill = ( +// initialValue: InputPair, +// validateFunc: (inputPair: Bill) => ValidateResult, +// onClose: () => void, +// ) => { +// const [inputPair, setInputPair] = useState(initialValue); +// const [canSubmit, setCanSubmit] = useState(false); +// const [errorInfo, setErrorInfo] = useState>({title: false, price: false}); +// const [errorMessage, setErrorMessage] = useState(null); + +// const {mutateAsync: putBillAction} = usePutBillAction(); +// const {mutate: deleteBillAction} = useDeleteBillAction(); + +// // 현재 타겟의 event.target.value를 넣어주기 위해서 +// const getFieldValue = (field: BillInputType, value: string) => { +// if (field === 'title') { +// return {title: value, price: Number(inputPair.price)}; +// } else { +// return {title: inputPair.title, price: Number(value)}; +// } +// }; + +// // TODO: (@weadie) getFieldValue 를 리펙토링해야한다. + +// const handleInputChange = (field: BillInputType, event: React.ChangeEvent) => { +// const {value} = event.target; + +// if (value.length > 20) return; + +// const {isValid, errorMessage, errorInfo} = validateFunc(getFieldValue(field, value)); + +// setErrorMessage(errorMessage); + +// if (isValid) { +// // valid일 경우 에러메시지 nope, setValue, submit은 value가 비지 않았을 때 true를 설정 +// setInputPair(prevInputPair => { +// return { +// ...prevInputPair, +// [field]: value, +// }; +// }); +// setCanSubmit(value.length !== 0); +// } else { +// const {isValid: isValidName} = validateFunc(getFieldValue('title', inputPair.title)); +// const {isValid: isValidPrice} = validateFunc(getFieldValue('price', inputPair.price)); + +// setCanSubmit(isValidName && isValidPrice); +// // valid하지 않으면 event.target.value 덮어쓰기 +// event.target.value = inputPair[field]; +// } + +// if (field === 'title') { +// // 현재 field가 title일 때는 title의 errorInfo만 반영해줌 (blur에서도 errorInfo를 조작하기 때문) +// setErrorInfo(prev => ({title: errorInfo?.title ?? false, price: prev.price})); +// } else { +// setErrorInfo(prev => ({title: prev.title, price: errorInfo?.price ?? false})); +// } +// }; + +// const handleOnBlur = () => { +// const {isValid, errorMessage, errorInfo} = validateFunc({title: inputPair.title, price: Number(inputPair.price)}); + +// // blur시 값이 비었을 때 error state 반영 +// if (inputPair.price.length === 0 || inputPair.title.length === 0) { +// setErrorMessage(ERROR_MESSAGE.preventEmpty); +// setErrorInfo({title: inputPair.title.length === 0, price: inputPair.price.length === 0}); +// setCanSubmit(false); +// return; +// } + +// // 이외 blur시에 추가로 검증함 +// setErrorMessage(errorMessage); +// setCanSubmit(isValid); +// setErrorInfo(errorInfo ?? {title: false, price: false}); +// }; + +// const onSubmit = async (event: React.FormEvent, inputPair: InputPair, actionId: number) => { +// event.preventDefault(); + +// const {title, price} = inputPair; + +// await putBillAction({actionId, title, price: Number(price)}); + +// onClose(); +// }; + +// const onDelete = async (actionId: number) => { +// deleteBillAction({actionId}); +// onClose(); +// }; + +// return { +// inputPair, +// handleInputChange, +// handleOnBlur, +// onSubmit, +// onDelete, +// canSubmit, +// errorMessage, +// errorInfo, +// }; +// }; + +// export default usePutAndDeleteBill; diff --git a/client/src/hooks/usePutAndDeleteBillAction.ts b/client/src/hooks/usePutAndDeleteBillAction.ts deleted file mode 100644 index 9126bf90..00000000 --- a/client/src/hooks/usePutAndDeleteBillAction.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type {Bill, BillInputType, InputPair} from 'types/serviceType'; - -import {useEffect, useState} from 'react'; - -import {ValidateResult} from '@utils/validate/type'; - -import {ERROR_MESSAGE} from '@constants/errorMessage'; - -import usePutBillAction from './queries/useRequestPutBillAction'; -import useDeleteBillAction from './queries/useRequestDeleteBillAction'; - -const usePutAndDeleteBillAction = ( - initialValue: InputPair, - validateFunc: (inputPair: Bill) => ValidateResult, - onClose: () => void, -) => { - const [inputPair, setInputPair] = useState(initialValue); - const [canSubmit, setCanSubmit] = useState(false); - const [errorInfo, setErrorInfo] = useState>({title: false, price: false}); - const [errorMessage, setErrorMessage] = useState(null); - - const {mutateAsync: putBillAction} = usePutBillAction(); - const {mutate: deleteBillAction} = useDeleteBillAction(); - - // 현재 타겟의 event.target.value를 넣어주기 위해서 - const getFieldValue = (field: BillInputType, value: string) => { - if (field === 'title') { - return {title: value, price: Number(inputPair.price)}; - } else { - return {title: inputPair.title, price: Number(value)}; - } - }; - - // TODO: (@weadie) getFieldValue 를 리펙토링해야한다. - - const handleInputChange = (field: BillInputType, event: React.ChangeEvent) => { - const {value} = event.target; - - if (value.length > 20) return; - - const {isValid, errorMessage, errorInfo} = validateFunc(getFieldValue(field, value)); - - setErrorMessage(errorMessage); - - if (isValid) { - // valid일 경우 에러메시지 nope, setValue, submit은 value가 비지 않았을 때 true를 설정 - setInputPair(prevInputPair => { - return { - ...prevInputPair, - [field]: value, - }; - }); - setCanSubmit(value.length !== 0); - } else { - const {isValid: isValidName} = validateFunc(getFieldValue('title', inputPair.title)); - const {isValid: isValidPrice} = validateFunc(getFieldValue('price', inputPair.price)); - - setCanSubmit(isValidName && isValidPrice); - // valid하지 않으면 event.target.value 덮어쓰기 - event.target.value = inputPair[field]; - } - - if (field === 'title') { - // 현재 field가 title일 때는 title의 errorInfo만 반영해줌 (blur에서도 errorInfo를 조작하기 때문) - setErrorInfo(prev => ({title: errorInfo?.title ?? false, price: prev.price})); - } else { - setErrorInfo(prev => ({title: prev.title, price: errorInfo?.price ?? false})); - } - }; - - const handleOnBlur = () => { - const {isValid, errorMessage, errorInfo} = validateFunc({title: inputPair.title, price: Number(inputPair.price)}); - - // blur시 값이 비었을 때 error state 반영 - if (inputPair.price.length === 0 || inputPair.title.length === 0) { - setErrorMessage(ERROR_MESSAGE.preventEmpty); - setErrorInfo({title: inputPair.title.length === 0, price: inputPair.price.length === 0}); - setCanSubmit(false); - return; - } - - // 이외 blur시에 추가로 검증함 - setErrorMessage(errorMessage); - setCanSubmit(isValid); - setErrorInfo(errorInfo ?? {title: false, price: false}); - }; - - const onSubmit = async (event: React.FormEvent, inputPair: InputPair, actionId: number) => { - event.preventDefault(); - - const {title, price} = inputPair; - - await putBillAction({actionId, title, price: Number(price)}); - - onClose(); - }; - - const onDelete = async (actionId: number) => { - deleteBillAction({actionId}); - onClose(); - }; - - return { - inputPair, - handleInputChange, - handleOnBlur, - onSubmit, - onDelete, - canSubmit, - errorMessage, - errorInfo, - }; -}; - -export default usePutAndDeleteBillAction; diff --git a/client/src/hooks/useSearchInMemberList.ts b/client/src/hooks/useSearchInMemberList.ts deleted file mode 100644 index 09f227b8..00000000 --- a/client/src/hooks/useSearchInMemberList.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {useState} from 'react'; - -import useRequestGetCurrentInMemberList from './queries/useRequestGetCurrentInMemberList'; - -export type ReturnUseSearchInMemberList = { - currentInputIndex: number; - handleCurrentInputIndex: (inputIndex: number) => void; - filteredInMemberList: string[]; - searchCurrentInMember: (event: React.ChangeEvent) => void; - chooseMember: (inputIndex: number, name: string) => void; -}; - -const useSearchInMemberList = (handleChange: (index: number, value: string) => void): ReturnUseSearchInMemberList => { - const [currentInputIndex, setCurrentInputIndex] = useState(-1); - - // 서버에서 가져온 전체 리스트 - const {data} = useRequestGetCurrentInMemberList(); - const currentInMemberList = data?.memberNames ?? []; - - // 검색된 리스트 (따로 둔 이유는 검색 후 클릭했을 때 리스트를 비워주어야하기 때문) - const [filteredInMemberList, setFilteredInMemberList] = useState>([]); - - const filterMatchItems = (keyword: string) => { - if (keyword.trim() === '') return []; - - return currentInMemberList - .filter(member => member.toLocaleLowerCase().indexOf(keyword.toLocaleLowerCase()) > -1) - .slice(0, 3); - }; - - const chooseMember = (inputIndex: number, name: string) => { - setFilteredInMemberList([]); - handleChange(inputIndex, name); - }; - - const searchCurrentInMember = (event: React.ChangeEvent) => { - const {value} = event.target; - setFilteredInMemberList(filterMatchItems(value)); - }; - - const handleCurrentInputIndex = (inputIndex: number) => { - setCurrentInputIndex(inputIndex); - }; - - return { - currentInputIndex, - handleCurrentInputIndex, - filteredInMemberList, - searchCurrentInMember, - chooseMember, - }; -}; - -export default useSearchInMemberList; diff --git a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx deleted file mode 100644 index 90782cbd..00000000 --- a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import {renderHook, waitFor} from '@testing-library/react'; -import {MemoryRouter} from 'react-router-dom'; - -import AppErrorBoundary from '@components/AppErrorBoundary/ErrorCatcher'; -import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; -import {ToastProvider} from '@hooks/useToast/ToastProvider'; - -import reportListJson from '../../mocks/reportList.json'; - -import useSearchMemberReportList from './useSearchMemberReportList'; - -describe('useSearchMemberReportList', () => { - const initializeProvider = (name: string) => - renderHook(() => useSearchMemberReportList({name}), { - wrapper: ({children}) => ( - - - - {children} - - - - ), - }); - - it('빈 값을 검색한다면 검색 목록은 비어있다.', async () => { - const {result} = initializeProvider(''); - - await waitFor(() => expect(result.current.memberReportSearchList).toStrictEqual(reportListJson)); - }); - - it('검색어의 일부와 일치하는 이름이 있다면 해당 이름을 목록에 반환한다.', async () => { - const keyword = '소'; - const {result} = initializeProvider(keyword); - const expectedMemberReportSearchList = reportListJson.filter(memberReport => memberReport.name.includes(keyword)); - - await waitFor(() => { - expect(result.current.memberReportSearchList).toStrictEqual(expectedMemberReportSearchList); - }); - }); -}); diff --git a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.tsx b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.tsx deleted file mode 100644 index 58608062..00000000 --- a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import useRequestGetMemberReportList from '@hooks/queries/useRequestGetMemberReportList'; - -type UseSearchMemberReportListParams = { - name: string; -}; - -const useSearchMemberReportList = ({name}: UseSearchMemberReportListParams) => { - const {data} = useRequestGetMemberReportList(); - const memberReportList = data ?? []; - - return { - memberReportSearchList: memberReportList.filter(memberReport => memberReport.name.includes(name)), - memberReportList, - }; -}; - -export default useSearchMemberReportList; diff --git a/client/src/hooks/useSearchReports/index.ts b/client/src/hooks/useSearchReports/index.ts new file mode 100644 index 00000000..619af549 --- /dev/null +++ b/client/src/hooks/useSearchReports/index.ts @@ -0,0 +1 @@ +export {default as useSearchReports} from './useSearchReports'; diff --git a/client/src/hooks/useSearchReports/useSearchReports.tsx b/client/src/hooks/useSearchReports/useSearchReports.tsx new file mode 100644 index 00000000..3387c230 --- /dev/null +++ b/client/src/hooks/useSearchReports/useSearchReports.tsx @@ -0,0 +1,16 @@ +import useRequestGetReports from '@hooks/queries/report/useRequestGetReports'; + +type UseSearchReportsParams = { + name: string; +}; + +const useSearchReports = ({name}: UseSearchReportsParams) => { + const {reports} = useRequestGetReports(); + + return { + matchedReports: reports.filter(memberReport => memberReport.name.includes(name)), + reports, + }; +}; + +export default useSearchReports; diff --git a/client/src/hooks/useSetAllMemberList.tsx b/client/src/hooks/useSetAllMemberList.tsx index 02241d3e..bf5580dd 100644 --- a/client/src/hooks/useSetAllMemberList.tsx +++ b/client/src/hooks/useSetAllMemberList.tsx @@ -1,100 +1,100 @@ -import {useEffect, useState} from 'react'; - -import {ValidateResult} from '@utils/validate/type'; -import {MemberChange} from '@apis/request/member'; - -import isArraysEqual from '@utils/isArraysEqual'; - -import useDeleteAllMemberList from './queries/useRequestDeleteAllMemberList'; -import usePutAllMemberList from './queries/useRequestPutAllMemberList'; -import useInput from './useInput'; - -interface UseSetAllMemberListProps { - validateFunc: (name: string) => ValidateResult; - allMemberList: string[]; - handleCloseAllMemberListModal: () => void; -} - -interface UseSetAllMemberListReturns { - editedAllMemberList: string[]; - canSubmit: boolean; - errorMessage: string; - errorIndexList: number[]; - handleNameChange: (index: number, event: React.ChangeEvent) => void; - handleClickDeleteButton: (index: number) => Promise; - handlePutAllMemberList: () => Promise; -} - -const useSetAllMemberList = ({ - validateFunc, - allMemberList, - handleCloseAllMemberListModal, -}: UseSetAllMemberListProps): UseSetAllMemberListReturns => { - const initialInputList = allMemberList.map((name, index) => ({index, value: name})); - const { - inputList, - errorMessage, - errorIndexList, - canSubmit, - handleChange, - setInputList: setEditedAllMemberList, - setCanSubmit, - } = useInput({validateFunc, initialInputList}); - - const [deleteInOriginal, setDeleteInOriginal] = useState(allMemberList); - const [deleteMemberList, setDeleteMemberList] = useState([]); - - const {mutateAsync: deleteAllMemberList} = useDeleteAllMemberList(); - const {mutate: putAllMemberList} = usePutAllMemberList(); - - const editedAllMemberList = inputList.map(input => input.value); - - useEffect(() => { - setCanSubmit(!isArraysEqual(editedAllMemberList, allMemberList)); - }, [editedAllMemberList]); - - const handleNameChange = (index: number, event: React.ChangeEvent) => { - const {value} = event.target; - - handleChange(index, value); - }; - - const handleClickDeleteButton = async (index: number) => { - const memberToDelete = editedAllMemberList[index]; - - setDeleteMemberList(prev => [...prev, memberToDelete]); - setDeleteInOriginal(prev => [...prev.slice(0, index), ...prev.slice(index + 1)]); - setEditedAllMemberList(prev => [...prev.slice(0, index), ...prev.slice(index + 1)]); - }; - - const handlePutAllMemberList = async () => { - // deleteMemberList가 비어있지 않은 경우에만 반복문 실행 (삭제 api 요청) - if (deleteMemberList.length > 0) { - for (const deleteMember of deleteMemberList) { - await deleteAllMemberList({memberName: deleteMember}); - } - } - - const editedMemberName: MemberChange[] = deleteInOriginal - .map((originalName, index) => { - if (editedAllMemberList[index] !== originalName) { - return {before: originalName, after: editedAllMemberList[index]}; - } - return null; // 조건에 맞지 않으면 null을 반환 - }) - .filter(item => item !== null); // null인 항목을 필터링하여 제거 - if (!isArraysEqual(editedAllMemberList, deleteInOriginal)) putAllMemberList({members: editedMemberName}); - handleCloseAllMemberListModal(); - }; - - return { - editedAllMemberList, - canSubmit, - errorMessage, - errorIndexList, - handleNameChange, - handleClickDeleteButton, - handlePutAllMemberList, - }; -}; -export default useSetAllMemberList; +// import {useEffect, useState} from 'react'; + +// import {ValidateResult} from '@utils/validate/type'; +// import {MemberChange} from '@apis/request/member'; + +// import isArraysEqual from '@utils/isArraysEqual'; + +// import useDeleteAllMemberList from './queries/useRequestDeleteMember'; +// import usePutAllMemberList from './queries/useRequestPutMember'; +// import useInput from './useInput'; + +// interface UseSetAllMemberListProps { +// validateFunc: (name: string) => ValidateResult; +// allMemberList: string[]; +// handleCloseAllMemberListModal: () => void; +// } + +// interface UseSetAllMemberListReturns { +// editedAllMemberList: string[]; +// canSubmit: boolean; +// errorMessage: string; +// errorIndexList: number[]; +// handleNameChange: (index: number, event: React.ChangeEvent) => void; +// handleClickDeleteButton: (index: number) => Promise; +// handlePutAllMemberList: () => Promise; +// } + +// const useSetAllMemberList = ({ +// validateFunc, +// allMemberList, +// handleCloseAllMemberListModal, +// }: UseSetAllMemberListProps): UseSetAllMemberListReturns => { +// const initialInputList = allMemberList.map((name, index) => ({index, value: name})); +// const { +// inputList, +// errorMessage, +// errorIndexList, +// canSubmit, +// handleChange, +// setInputList: setEditedAllMemberList, +// setCanSubmit, +// } = useInput({validateFunc, initialInputList}); + +// const [deleteInOriginal, setDeleteInOriginal] = useState(allMemberList); +// const [deleteMemberList, setDeleteMemberList] = useState([]); + +// const {mutateAsync: deleteAllMemberList} = useDeleteAllMemberList(); +// const {mutate: putAllMemberList} = usePutAllMemberList(); + +// const editedAllMemberList = inputList.map(input => input.value); + +// useEffect(() => { +// setCanSubmit(!isArraysEqual(editedAllMemberList, allMemberList)); +// }, [editedAllMemberList]); + +// const handleNameChange = (index: number, event: React.ChangeEvent) => { +// const {value} = event.target; + +// handleChange(index, value); +// }; + +// const handleClickDeleteButton = async (index: number) => { +// const memberToDelete = editedAllMemberList[index]; + +// setDeleteMemberList(prev => [...prev, memberToDelete]); +// setDeleteInOriginal(prev => [...prev.slice(0, index), ...prev.slice(index + 1)]); +// setEditedAllMemberList(prev => [...prev.slice(0, index), ...prev.slice(index + 1)]); +// }; + +// const handlePutAllMemberList = async () => { +// // deleteMemberList가 비어있지 않은 경우에만 반복문 실행 (삭제 api 요청) +// if (deleteMemberList.length > 0) { +// for (const deleteMember of deleteMemberList) { +// await deleteAllMemberList({memberName: deleteMember}); +// } +// } + +// const editedMemberName: MemberChange[] = deleteInOriginal +// .map((originalName, index) => { +// if (editedAllMemberList[index] !== originalName) { +// return {before: originalName, after: editedAllMemberList[index]}; +// } +// return null; // 조건에 맞지 않으면 null을 반환 +// }) +// .filter(item => item !== null); // null인 항목을 필터링하여 제거 +// if (!isArraysEqual(editedAllMemberList, deleteInOriginal)) putAllMemberList({members: editedMemberName}); +// handleCloseAllMemberListModal(); +// }; + +// return { +// editedAllMemberList, +// canSubmit, +// errorMessage, +// errorIndexList, +// handleNameChange, +// handleClickDeleteButton, +// handlePutAllMemberList, +// }; +// }; +// export default useSetAllMemberList; diff --git a/client/src/hooks/useSetBillInput.ts b/client/src/hooks/useSetBillInput.ts deleted file mode 100644 index 4f534791..00000000 --- a/client/src/hooks/useSetBillInput.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {useState} from 'react'; - -import validatePurchase from '@utils/validate/validatePurchase'; -import {Bill, BillInputType} from 'types/serviceType'; - -import useRequestPostBillList from './queries/useRequestPostBillList'; - -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; - - if (value.length > 20) return; - - 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); - }, - }, - ); - } else if (!isEmptyTitle && !isEmptyPrice) { - setIsAddEditableItem(false); - } - }; - - return { - billInput, - handleBlurBillRequest, - handleChangeBillInput, - }; -}; - -export default useSetBillInput; diff --git a/client/src/hooks/useSetEventPasswordPage.ts b/client/src/hooks/useSetEventPasswordPage.ts index 17079f3a..849f6f5f 100644 --- a/client/src/hooks/useSetEventPasswordPage.ts +++ b/client/src/hooks/useSetEventPasswordPage.ts @@ -6,7 +6,7 @@ import validateEventPassword from '@utils/validate/validateEventPassword'; import {ROUTER_URLS} from '@constants/routerUrls'; import RULE from '@constants/rule'; -import usePostEvent from './queries/useRequestPostEvent'; +import useRequestPostEvent from './queries/event/useRequestPostEvent'; const useSetEventPasswordPage = () => { const [eventName, setEventName] = useState(''); @@ -15,7 +15,7 @@ const useSetEventPasswordPage = () => { const [canSubmit, setCanSubmit] = useState(false); const navigate = useNavigate(); const location = useLocation(); - const {mutate: postEvent, isPending: isPostEventPending} = usePostEvent(); + const {postEvent, isPostEventPending} = useRequestPostEvent(); useEffect(() => { if (!location.state) { @@ -28,6 +28,10 @@ const useSetEventPasswordPage = () => { const submitPassword = async (event: React.FormEvent) => { event.preventDefault(); + onSuccess(); + }; + + const onSuccess = () => { postEvent( {eventName, password: String(password).padStart(4, '0')}, { @@ -55,6 +59,6 @@ const useSetEventPasswordPage = () => { } }; - return {submitPassword, errorMessage, password, handleChange, canSubmit, isPostEventPending}; + return {submitPassword, errorMessage, password, handleChange, onSuccess, canSubmit, isPostEventPending}; }; export default useSetEventPasswordPage; diff --git a/client/src/index.tsx b/client/src/index.tsx index 17f98655..cb3fcb95 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -5,11 +5,6 @@ import * as Sentry from '@sentry/react'; import router from './router'; -// async function enableMocking() { -// const {worker} = await import('./mocks/browser'); -// return worker.start(); -// } - Sentry.init({ dsn: 'https://81685591a3234c689be8c48959b04c88@o4507739935997952.ingest.us.sentry.io/4507739943272448', integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()], @@ -23,6 +18,11 @@ Sentry.init({ }); // MSW 모킹을 사용하려면 아래 주석을 해제하고 save해주세요. +// async function enableMocking() { +// const {worker} = await import('./mocks/browser'); +// return worker.start(); +// } + // enableMocking().then(() => { ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/client/src/mocks/handlers.ts b/client/src/mocks/handlers.ts index 4189b2d6..d4347bc5 100644 --- a/client/src/mocks/handlers.ts +++ b/client/src/mocks/handlers.ts @@ -1,15 +1,15 @@ import {authHandler} from './handlers/authHandlers'; import {eventHandler} from './handlers/eventHandlers'; import {reportHandlers} from './handlers/reportHandlers'; -import {stepListHandler} from './handlers/stepListHandler'; import {testHandler} from './handlers/testHandlers'; -import {memberReportInActionHandler} from './handlers/memberReportInActionHandlers'; +import {billHandler} from './handlers/billHandler'; +import {memberHandler} from './handlers/memberHandler'; export const handlers = [ ...authHandler, ...eventHandler, + ...billHandler, + ...memberHandler, ...testHandler, - ...stepListHandler, ...reportHandlers, - ...memberReportInActionHandler, ]; diff --git a/client/src/mocks/handlers/authHandlers.ts b/client/src/mocks/handlers/authHandlers.ts index 545c7723..00000b1e 100644 --- a/client/src/mocks/handlers/authHandlers.ts +++ b/client/src/mocks/handlers/authHandlers.ts @@ -1,99 +1,56 @@ -import {HttpResponse, http} from 'msw'; +import {http, HttpResponse} from 'msw'; -import {PASSWORD_LENGTH} from '@constants/password'; - -import { - EXPIRED_TOKEN_FOR_TEST, - FORBIDDEN_TOKEN_FOR_TEST, - VALID_PASSWORD_FOR_TEST, - VALID_TOKEN_FOR_TEST, -} from '@mocks/validValueForTest'; -import {MSW_TEMP_PRIFIX} from '@mocks/serverConstants'; +import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix'; -type PostLoginParams = { - eventId: string; -}; +import {PASSWORD_LENGTH} from '@constants/password'; -type PostLoginRequestBody = { - password: string; -}; +import {MOCK_API_PREFIX} from '@mocks/mockEndpointPrefix'; export const authHandler = [ - http.post(`${MSW_TEMP_PRIFIX}/:eventId/auth`, ({cookies}) => { - const token = cookies['eventToken']; - - if (token === VALID_TOKEN_FOR_TEST) { - return new HttpResponse(null, { - status: 200, - }); - } else if (token === EXPIRED_TOKEN_FOR_TEST) { - return HttpResponse.json( - { - errorCode: 'TOKEN_EXPIRED', - message: '만료된 토큰입니다.', - }, - {status: 401}, - ); - } else if (token === FORBIDDEN_TOKEN_FOR_TEST) { - return HttpResponse.json( - { - errorCode: 'FORBIDDEN', - message: '접근할 수 없는 행사입니다.', - }, - {status: 401}, - ); - } else if (token === undefined) { - return HttpResponse.json( - { - errorCode: 'TOKEN_NOT_FOUND', - message: '토큰이 존재하지 않습니다.', - }, - {status: 401}, - ); - } else { - return HttpResponse.json( - { - errorCode: 'TOKEN_INVALID', - message: '유효하지 않은 토큰입니다.', - }, - {status: 401}, - ); - } + // POST /api/eventId/auth (requestPostAuthentication) + http.post(`${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/auth`, () => { + return new HttpResponse(null, {status: 200}); }), - // TODO: (@weadie) any를 사용한 이유는.. any가 있는 위치가 이 handler함수의 responseBody타입인데, 아래처럼 return하는 것에 대한 예시가 공문에 없습니다. 함수를 까면 되겠지만 시간이 아깝고 알아낸다고 해서 이 responseBody 타입은 사실 중요한게 아니기 때문에 any로 대체하였습니다. - http.post( - `${MSW_TEMP_PRIFIX}/:eventId/login`, - async ({request}) => { + // POST /api/eventId/login (requestPostToken) + http.post<{eventId: string}, {password: string}>( + `${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/login`, + async ({params, request}) => { + const {eventId} = params; const {password} = await request.json(); - if (password === String(VALID_PASSWORD_FOR_TEST)) { - return new HttpResponse(null, { - headers: { - 'Set-Cookie': 'eventToken=abc-123', - }, - }); - } else if (password.length < PASSWORD_LENGTH) { + if (!password) { return HttpResponse.json( { - errorCode: 'EVENT_PASSWORD_FORMAT_INVALID', - message: `비밀번호는 ${PASSWORD_LENGTH}자리 숫자만 가능합니다.`, + errorCode: 'REQUEST_EMPTY', + message: '비밀번호는 공백일 수 없습니다.', }, - {status: 401}, + {status: 400}, ); - } else if (password === undefined) { + } + if (password.length !== PASSWORD_LENGTH) { return HttpResponse.json( { - errorCode: 'REQUEST_EMPTY', - message: '비밀번호는 공백일 수 없습니다.', + errorCode: 'PASSWORD_INVALID', + message: `비밀번호는 ${PASSWORD_LENGTH}자리여야 합니다.`, }, - {status: 401}, + {status: 400}, ); + } + + // 비밀번호가 1234인 경우만 성공으로 처리 + if (password === '1234') { + return new HttpResponse(null, { + status: 200, + headers: { + 'Set-Cookie': `eventToken=${eventId}-token`, + }, + }); } else { return HttpResponse.json( { - errorCode: 'PASSWORD_INVALID', - message: '비밀번호가 일치하지 않습니다.', + errorCode: 'PASSWORD_INCORRECT', + message: '비밀번호가 올바르지 않습니다.', }, {status: 401}, ); diff --git a/client/src/mocks/handlers/billHandler.ts b/client/src/mocks/handlers/billHandler.ts new file mode 100644 index 00000000..0130b52a --- /dev/null +++ b/client/src/mocks/handlers/billHandler.ts @@ -0,0 +1,105 @@ +import {http, HttpResponse, PathParams} from 'msw'; + +import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix'; + +import {billData, billDetailsData} from '@mocks/sharedState'; + +import {MOCK_API_PREFIX} from './../mockEndpointPrefix'; + +interface BillDetailsData { + [key: string]: {billDetails: {id: number; memberName: string; price: string}[]}; +} + +export const billHandler = [ + // GET /api/eventId/bills + http.get(`${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/bills`, () => { + return HttpResponse.json(billData); + }), + + // GET /api/eventId/bills/billId/fixed + http.get(`${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/bills/:billId/fixed`, ({params}) => { + const {billId} = params; + const billDetails = (billDetailsData as unknown as BillDetailsData)[billId as keyof BillDetailsData]; + return HttpResponse.json({billDetails}); + }), + + // POST /api/eventId/bills + http.post( + `${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/bills`, + async ({request}) => { + try { + 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}`})); + + (billDetailsData as unknown as BillDetailsData)[newBill.id.toString()] = { + billDetails: members.map((id, index) => ({ + id, + memberName: `Member ${id}`, + price: (Math.floor(price / members.length) + (index < price % members.length ? 1 : 0)).toString(), + })), + }; + + return HttpResponse.json({status: 200}); + } catch (error) { + return HttpResponse.json({message: 'Internal Server Error'}, {status: 500}); + } + }, + ), + + // DELETE /api/eventId/bills/billId + http.delete(`${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/bills/:billId`, ({params}) => { + const {billId} = params; + billData.steps.forEach(step => { + step.bills = step.bills.filter(bill => bill.id !== Number(billId)); + }); + delete (billDetailsData as unknown as BillDetailsData)[billId as keyof BillDetailsData]; + return HttpResponse.json({status: 200}); + }), + + // PUT /api/eventId/bills/billId + http.put( + `${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/bills/:billId`, + async ({params, request}) => { + const {billId} = params; + const {title, price} = await request.json(); + + billData.steps.forEach(step => { + const billIndex = step.bills.findIndex(bill => bill.id === Number(billId)); + if (billIndex !== -1) { + step.bills[billIndex] = {...step.bills[billIndex], title, price}; + } + }); + + return HttpResponse.json({status: 200}); + }, + ), + + // PUT /api/eventId/bills/billId/fixed + http.put( + `${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/bills/:billId/fixed`, + async ({params, request}) => { + const {billId} = params; + const {billDetails} = await request.json(); + + (billDetailsData as unknown as BillDetailsData)[billId as keyof BillDetailsData] = { + billDetails: billDetails.map(detail => ({ + id: detail.id, + memberName: 'Unknown', + price: detail.price.toString(), + })), + }; + + billData.steps.forEach(step => { + const billIndex = step.bills.findIndex(bill => bill.id === Number(billId)); + if (billIndex !== -1) { + step.bills[billIndex].isFixed = true; + } + }); + + return HttpResponse.json({status: 200}); + }, + ), +]; diff --git a/client/src/mocks/handlers/eventHandlers.ts b/client/src/mocks/handlers/eventHandlers.ts index 9e852612..23fefaa4 100644 --- a/client/src/mocks/handlers/eventHandlers.ts +++ b/client/src/mocks/handlers/eventHandlers.ts @@ -1,54 +1,64 @@ -import {HttpResponse, http} from 'msw'; +import type {EventId} from 'types/serviceType'; -import {RequestPostNewEvent, ResponsePostNewEvent} from '@apis/request/event'; +import {http, HttpResponse} from 'msw'; -import {PASSWORD_LENGTH} from '@constants/password'; +import {USER_API_PREFIX} from '@apis/endpointPrefix'; -import {MSW_TEMP_PRIFIX} from '@mocks/serverConstants'; import {VALID_EVENT_NAME_LENGTH_IN_SERVER} from '@mocks/serverConstants'; - -type ErrorResponseBody = { - errorCode: string; - message: string; -}; +import {MOCK_API_PREFIX} from '@mocks/mockEndpointPrefix'; +import {eventData} from '@mocks/sharedState'; export const eventHandler = [ - http.post( - `${MSW_TEMP_PRIFIX}`, + // POST /api/events (requestPostEvent) + http.post(`${MOCK_API_PREFIX}${USER_API_PREFIX}`, async ({request}) => { + const {eventName, password} = await request.json(); + + if ( + eventName.length < VALID_EVENT_NAME_LENGTH_IN_SERVER.min || + eventName.length > VALID_EVENT_NAME_LENGTH_IN_SERVER.max + ) { + return HttpResponse.json( + { + errorCode: 'EVENT_NAME_LENGTH_INVALID', + message: '행사 이름은 2자 이상 30자 이하만 입력 가능합니다.', + }, + {status: 400}, + ); + } + + if (password.length !== 4) { + return HttpResponse.json( + { + errorCode: 'EVENT_PASSWORD_FORMAT_INVALID', + message: '비밀번호는 4자리 숫자만 가능합니다.', + }, + {status: 400}, + ); + } + + const eventId: EventId = {eventId: 'mock-event-id'}; + return HttpResponse.json(eventId, { + status: 201, + headers: { + 'Set-Cookie': 'eventToken=mock-event-token', + }, + }); + }), + + // GET /api/events/:eventId (requestGetEvent) + http.get(`${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId`, () => { + return HttpResponse.json(eventData); + }), + + // PUT /api/events/:eventId (requestPutEvent) + http.put( + `${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId`, async ({request}) => { - const {eventName, password} = await request.json(); - - if (String(password).length < PASSWORD_LENGTH) { - return HttpResponse.json( - { - errorCode: 'EVENT_PASSWORD_FORMAT_INVALID', - message: '비밀번호는 4자리 숫자만 가능합니다.', - }, - {status: 401}, - ); - } else if ( - eventName.length < VALID_EVENT_NAME_LENGTH_IN_SERVER.min || - eventName.length > VALID_EVENT_NAME_LENGTH_IN_SERVER.max - ) { - return HttpResponse.json( - { - errorCode: 'EVENT_NAME_LENGTH_INVALID', - message: `행사 이름은 2자 이상 30자 이하만 입력 가능합니다. 입력한 이름 길이 : ${eventName.length}`, - }, - {status: 401}, - ); - } else { - return HttpResponse.json( - { - eventId: 'eventId', - }, - { - headers: { - 'Set-Cookie': 'eventToken=abc-123', - }, - }, - ); - } + const updates = await request.json(); + + Object.assign(eventData, updates); + + return HttpResponse.json({status: 200}); }, ), ]; diff --git a/client/src/mocks/handlers/memberHandler.ts b/client/src/mocks/handlers/memberHandler.ts new file mode 100644 index 00000000..97233c8b --- /dev/null +++ b/client/src/mocks/handlers/memberHandler.ts @@ -0,0 +1,62 @@ +import {http, HttpResponse, PathParams} from 'msw'; + +import {AllMembers, Members} from 'types/serviceType'; + +import {ADMIN_API_PREFIX, USER_API_PREFIX} from '@apis/endpointPrefix'; + +import {MOCK_API_PREFIX} from '@mocks/mockEndpointPrefix'; +import {memberData} from '@mocks/sharedState'; + +export const memberHandler = [ + // POST /api/eventId/members (requestPostMember) + http.post( + `${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/members`, + async ({request}) => { + const {members: newMembers} = await request.json(); + const addedMembers = newMembers.map((member, index) => ({ + id: memberData.members.length + index + 1, + name: member.name, + isDeposited: false, + })); + + memberData.members = [...memberData.members, ...addedMembers]; + + return HttpResponse.json({members: addedMembers}, {status: 201}); + }, + ), + + // DELETE /api/eventId/members/memberId (requestDeleteMember) + http.delete(`${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/members/:memberId`, ({params}) => { + const {memberId} = params; + memberData.members = memberData.members.filter(member => member.id !== Number(memberId)); + return HttpResponse.json({status: 200}); + }), + + // PUT /api/eventId/members (requestPutMember) + http.put( + `${MOCK_API_PREFIX}${ADMIN_API_PREFIX}/:eventId/members`, + async ({request}) => { + const {members: updatedMembers} = await request.json(); + + memberData.members = memberData.members.map(member => { + const updatedMember = updatedMembers.find(m => m.id === member.id); + return updatedMember ? {...member, ...updatedMember} : member; + }); + + return HttpResponse.json({status: 200}); + }, + ), + + // GET /api/eventId/members/current (requestGetCurrentMember) + http.get(`${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/members/current`, () => { + const currentMembers: Members = { + members: memberData.members.map(({id, name}) => ({id, name})), + }; + return HttpResponse.json(currentMembers); + }), + + // GET /api/eventId/members (requestGetAllMember) + http.get(`${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/members`, () => { + return HttpResponse.json(memberData); + }), +]; diff --git a/client/src/mocks/handlers/memberReportInActionHandlers.ts b/client/src/mocks/handlers/memberReportInActionHandlers.ts deleted file mode 100644 index 41ab23f9..00000000 --- a/client/src/mocks/handlers/memberReportInActionHandlers.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {http, HttpResponse} from 'msw'; - -import {MemberReport} from 'types/serviceType'; - -import {MSW_TEMP_PRIFIX} from '@mocks/serverConstants'; - -import memberReportInActionJson from '../memberReportListInAction.json'; - -let memberReportInActionMockData = memberReportInActionJson as MemberReport[]; - -type MemberReportListRequestParams = { - eventId: string; - actionId: string; -}; -type MemberReportListBody = {members: MemberReport[]}; - -export const memberReportInActionHandler = [ - http.get< - MemberReportListRequestParams, - MemberReportListBody, - any, - `${typeof MSW_TEMP_PRIFIX}/:eventId/bill-actions/:actionId/fixed` - >(`${MSW_TEMP_PRIFIX}/:eventId/bill-actions/:actionId/fixed`, ({params}) => { - const {actionId} = params; - - if (Number(actionId) === 123) { - return HttpResponse.json({ - members: memberReportInActionMockData, - }); - } - - return HttpResponse.json({ - members: memberReportInActionMockData.slice(0, 2), - }); - }), - - http.put( - `${MSW_TEMP_PRIFIX}/:eventId/bill-actions/:actionId/fixed`, - async ({request}) => { - const {members} = await request.json(); - - memberReportInActionMockData = members; - - return HttpResponse.json({ - status: 200, - }); - }, - ), -]; diff --git a/client/src/mocks/handlers/reportHandlers.ts b/client/src/mocks/handlers/reportHandlers.ts index 368a6828..5248fe82 100644 --- a/client/src/mocks/handlers/reportHandlers.ts +++ b/client/src/mocks/handlers/reportHandlers.ts @@ -1,13 +1,13 @@ -import {HttpResponse, http} from 'msw'; +import {http, HttpResponse} from 'msw'; -import {MSW_TEMP_PRIFIX} from '@mocks/serverConstants'; +import {USER_API_PREFIX} from '@apis/endpointPrefix'; -import reportListJson from '../reportList.json'; +import {MOCK_API_PREFIX} from '@mocks/mockEndpointPrefix'; +import {reportData} from '@mocks/sharedState'; export const reportHandlers = [ - http.get(`${MSW_TEMP_PRIFIX}/:eventId/actions/reports`, () => { - return HttpResponse.json({ - reports: reportListJson, - }); + // GET /api/eventId/reports (requestGetMemberReport) + http.get(`${MOCK_API_PREFIX}${USER_API_PREFIX}/:eventId/reports`, () => { + return HttpResponse.json(reportData); }), ]; diff --git a/client/src/mocks/handlers/stepListHandler.ts b/client/src/mocks/handlers/stepListHandler.ts deleted file mode 100644 index 1d900a1f..00000000 --- a/client/src/mocks/handlers/stepListHandler.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {HttpResponse, http} from 'msw'; - -import {Bill, MemberType, StepList} from 'types/serviceType'; - -import {MSW_TEMP_PRIFIX} from '@mocks/serverConstants'; - -import stepListJson from '../stepList.json'; - -type StepListResponseBody = { - step: StepList; -}; - -type PostMemberListRequestBody = { - members: string[]; - status: MemberType; -}; - -type PostBillListRequestBody = { - actions: Bill[]; -}; - -let stepListMockData = stepListJson; - -export const stepListHandler = [ - http.get( - `${MSW_TEMP_PRIFIX}/:eventId/actions`, - () => { - return HttpResponse.json({ - steps: stepListMockData, - }); - }, - ), - - http.get(`${MSW_TEMP_PRIFIX}/:eventId/members`, () => { - return HttpResponse.json({ - memberNames: stepListMockData - .filter(({type}) => type !== 'BILL') - .map(({actions}) => actions.map(({name}) => name)) - .flat(), - }); - }), - - http.delete<{actionId: string}>(`${MSW_TEMP_PRIFIX}/:eventId/member-actions/:actionId`, ({params}) => { - const {actionId} = params; - - if (parseInt(actionId) === 999) { - return HttpResponse.json( - { - errorCode: 'MEMBER_ACTION_STATUS_INVALID', - message: 'actionId는 999일 수 없습니다.(고의로 만든 에러임)', - }, - {status: 401}, - ); - } else { - return HttpResponse.json({ - status: 200, - }); - } - }), - - http.post( - `${MSW_TEMP_PRIFIX}/:eventId/member-actions`, - async ({request}) => { - const {members, status} = await request.json(); - stepListMockData = [ - ...stepListJson, - { - type: status, - stepName: '영차영차', - members: status === 'IN' ? members : [], - actions: members.map(name => ({ - actionId: 999, - name, - price: 0, - sequence: 999, - isFixed: false, - })), - }, - ]; - - return HttpResponse.json({ - status: 200, - }); - }, - ), - - http.post( - `${MSW_TEMP_PRIFIX}/:eventId/bill-actions`, - async ({request}) => { - const {actions} = await request.json(); - - stepListMockData = [ - ...stepListJson, - { - type: 'BILL', - stepName: '밥스카이', - members: [], - actions: actions.map(({title, price}) => ({ - actionId: 999, - name: title, - price, - sequence: 999, - isFixed: false, - })), - }, - ]; - - return HttpResponse.json({ - status: 200, - }); - }, - ), -]; diff --git a/client/src/mocks/invalidMemberStepList.json b/client/src/mocks/invalidMemberStepList.json deleted file mode 100644 index 33ca77e4..00000000 --- a/client/src/mocks/invalidMemberStepList.json +++ /dev/null @@ -1,100 +0,0 @@ -[ - { - "type": "IN", - "stepName": null, - "members": [], - "actions": [ - { - "actionId": 999, - "name": "망쵸", - "price": null, - "sequence": 1, - "isFixed": false - }, - { - "actionId": 2, - "name": "백호", - "price": null, - "sequence": 2, - "isFixed": false - } - ] - }, - { - "type": "BILL", - "stepName": "1차", - "members": ["망쵸", "백호"], - "actions": [ - { - "actionId": 3, - "name": "감자탕", - "price": 10000, - "sequence": 3, - "isFixed": false - }, - { - "actionId": 4, - "name": "인생네컷", - "price": 10000, - "sequence": 4, - "isFixed": false - } - ] - }, - { - "type": "IN", - "stepName": null, - "members": [], - "actions": [ - { - "actionId": 5, - "name": "소하", - "price": null, - "sequence": 5, - "isFixed": false - }, - { - "actionId": 6, - "name": "웨디", - "price": null, - "sequence": 6, - "isFixed": false - } - ] - }, - { - "type": "BILL", - "stepName": "2차", - "members": ["소하", "웨디"], - "actions": [ - { - "actionId": 9, - "name": "노래방", - "price": 20000, - "sequence": 10, - "isFixed": false - } - ] - }, - { - "type": "OUT", - "stepName": null, - "members": [], - "actions": [ - { - "actionId": 7, - "name": "망쵸", - "price": null, - "sequence": 7, - "isFixed": false - }, - { - "actionId": 8, - "name": "백호", - "price": null, - "sequence": 8, - "isFixed": false - } - ] - } -] diff --git a/client/src/mocks/memberActionStepList.json b/client/src/mocks/memberActionStepList.json deleted file mode 100644 index 734db4a7..00000000 --- a/client/src/mocks/memberActionStepList.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "type": "IN", - "stepName": null, - "members": [], - "actions": [ - { - "actionId": 1, - "name": "망쵸", - "price": null, - "sequence": 1, - "isFixed": false - }, - { - "actionId": 2, - "name": "백호", - "price": null, - "sequence": 2, - "isFixed": false - } - ] - } -] diff --git a/client/src/mocks/memberReportListInAction.json b/client/src/mocks/memberReportListInAction.json deleted file mode 100644 index 2e24670c..00000000 --- a/client/src/mocks/memberReportListInAction.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - {"name": "망쵸", "price": 25000, "isFixed": false}, - {"name": "이상", "price": 25000, "isFixed": false}, - {"name": "소하", "price": 25000, "isFixed": false}, - {"name": "쿠키", "price": 25000, "isFixed": false} -] diff --git a/client/src/mocks/memberReportSearchList.json b/client/src/mocks/memberReportSearchList.json deleted file mode 100644 index dfcb684b..00000000 --- a/client/src/mocks/memberReportSearchList.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - {"name": "망쵸", "price": 1033200}, - {"name": "이상", "price": 10100}, - {"name": "소하", "price": 10000}, - {"name": "쿠키", "price": 100012}, - {"name": "토다리", "price": 1001230}, - {"name": "감자", "price": 1012300}, - {"name": "백호", "price": 10300}, - {"name": "웨디", "price": 1000} -] diff --git a/client/src/mocks/mockEndpointPrefix.ts b/client/src/mocks/mockEndpointPrefix.ts new file mode 100644 index 00000000..d02762e5 --- /dev/null +++ b/client/src/mocks/mockEndpointPrefix.ts @@ -0,0 +1,3 @@ +import {BASE_URL} from '@apis/baseUrl'; + +export const MOCK_API_PREFIX = typeof window !== 'undefined' ? `${BASE_URL.HD}` : ''; diff --git a/client/src/mocks/reportList.json b/client/src/mocks/reportList.json deleted file mode 100644 index a663eb9f..00000000 --- a/client/src/mocks/reportList.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "name": "소하", - "price": 40000 - }, - { - "name": "감자", - "price": 20000 - }, - { - "name": "쿠키", - "price": 40000 - }, - { - "name": "토다리", - "price": 0 - } -] diff --git a/client/src/mocks/sharedState.ts b/client/src/mocks/sharedState.ts new file mode 100644 index 00000000..d35b7926 --- /dev/null +++ b/client/src/mocks/sharedState.ts @@ -0,0 +1,109 @@ +export let eventData = { + eventName: 'MSW 야유회', + bankName: '', + accountNumber: '', +}; + +export let memberData = { + members: [ + {id: 1, name: '망쵸', isDeposited: false}, + {id: 2, name: '백호', isDeposited: true}, + {id: 3, name: '감자', isDeposited: true}, + ], +}; + +export let billData = { + steps: [ + { + bills: [ + {id: 1, title: '커피', price: 10000, isFixed: false}, + {id: 2, title: '인생네컷', price: 20000, isFixed: false}, + ], + members: [ + {id: 1, name: '망쵸'}, + {id: 2, name: '백호'}, + ], + }, + { + bills: [{id: 3, title: '맥주', price: 20000, isFixed: true}], + members: [ + {id: 1, name: '망쵸'}, + {id: 2, name: '백호'}, + {id: 3, name: '감자'}, + ], + }, + ], +}; + +export let billDetailsData = { + '1': { + billDetails: [ + { + id: 1, + memberName: '망쵸', + price: 5000, + }, + { + id: 2, + memberName: '백호', + price: 5000, + }, + ], + }, + '2': { + billDetails: [ + { + id: 1, + memberName: '망쵸', + price: 10000, + }, + { + id: 2, + memberName: '백호', + price: 10000, + }, + ], + }, + '3': { + billDetails: [ + { + id: 1, + memberName: '망쵸', + price: 5000, + }, + { + id: 2, + memberName: '백호', + price: 10000, + }, + { + id: 2, + memberName: '감자', + price: 5000, + }, + ], + }, +}; + +export let reportData = { + reports: [ + { + memberId: 1, + name: '망쵸', + price: 20000, + isDeposited: false, + }, + { + memberId: 2, + name: '백호', + price: 25000, + isDeposited: true, + }, + { + memberId: 3, + name: '감자', + price: 5000, + isDeposited: true, + }, + ], +}; diff --git a/client/src/mocks/stepList.json b/client/src/mocks/stepList.json deleted file mode 100644 index 355692d5..00000000 --- a/client/src/mocks/stepList.json +++ /dev/null @@ -1,100 +0,0 @@ -[ - { - "type": "IN", - "stepName": null, - "members": [], - "actions": [ - { - "actionId": 1, - "name": "망쵸", - "price": null, - "sequence": 1, - "isFixed": false - }, - { - "actionId": 2, - "name": "백호", - "price": null, - "sequence": 2, - "isFixed": false - } - ] - }, - { - "type": "BILL", - "stepName": "1차", - "members": ["망쵸", "백호"], - "actions": [ - { - "actionId": 3, - "name": "감자탕", - "price": 10000, - "sequence": 3, - "isFixed": false - }, - { - "actionId": 4, - "name": "인생네컷", - "price": 10000, - "sequence": 4, - "isFixed": false - } - ] - }, - { - "type": "IN", - "stepName": null, - "members": [], - "actions": [ - { - "actionId": 5, - "name": "소하", - "price": null, - "sequence": 5, - "isFixed": false - }, - { - "actionId": 6, - "name": "웨디", - "price": null, - "sequence": 6, - "isFixed": false - } - ] - }, - { - "type": "BILL", - "stepName": "2차", - "members": ["소하", "웨디"], - "actions": [ - { - "actionId": 9, - "name": "노래방", - "price": 20000, - "sequence": 10, - "isFixed": false - } - ] - }, - { - "type": "OUT", - "stepName": null, - "members": [], - "actions": [ - { - "actionId": 7, - "name": "망쵸", - "price": null, - "sequence": 7, - "isFixed": false - }, - { - "actionId": 8, - "name": "백호", - "price": null, - "sequence": 8, - "isFixed": false - } - ] - } -] diff --git a/client/src/pages/BillPage/AddBillFunnel.tsx b/client/src/pages/BillPage/AddBillFunnel.tsx new file mode 100644 index 00000000..d8ff1484 --- /dev/null +++ b/client/src/pages/BillPage/AddBillFunnel.tsx @@ -0,0 +1,220 @@ +import {css} from '@emotion/react'; +import {useEffect, useState} from 'react'; +import {useNavigate} from 'react-router-dom'; + +import NumberKeyboard from '@components/Design/components/NumberKeyboard/NumberKeyboard'; +import useRequestGetCurrentMembers from '@hooks/queries/member/useRequestGetCurrentMembers'; +import Top from '@components/Design/components/Top/Top'; +import ChipButton from '@components/Design/components/ChipButton/ChipButton'; +import AmountInput from '@components/AmountInput/AmountInput'; + +import {Back, FixedButton, Flex, LabelInput, MainLayout, Text, TopNav} from '@components/Design'; + +type BillStep = 'title' | 'price' | 'members'; + +interface BillInfo { + price: string; + title: string; + members: string[]; +} + +const AddBillFunnel = () => { + const {currentMembers} = useRequestGetCurrentMembers(); + const [step, setStep] = useState('price'); + const [billInfo, setBillInfo] = useState({ + price: '', + title: '', + members: [], + }); + const [errorMessage, setErrorMessage] = useState(''); + const [nameInput, setNameInput] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + currentMembers && setBillInfo(prev => ({...prev, members: currentMembers.map(member => member.name)})); + }, [currentMembers]); + + const handleNumberKeyboardChange = (value: string) => { + setBillInfo(prev => ({...prev, price: value || prev.price})); + }; + + const handleTitleInputChange = (event: React.ChangeEvent) => { + setBillInfo(prev => ({...prev, title: event.target.value})); + }; + + const handleTitleInputEnter = (event: React.KeyboardEvent) => { + if (event.nativeEvent.isComposing) { + return; + } + if (event.key === 'Enter') { + event.preventDefault(); + setStep('members'); + } + }; + + const handleNameInputChange = (event: React.ChangeEvent) => setNameInput(event.target.value); + + const handleNameInputEnter = (event: React.KeyboardEvent) => { + if (event.nativeEvent.isComposing) { + return; + } + if (event.key === 'Enter') { + console.log(nameInput); + event.preventDefault(); + if (!billInfo.members.includes(nameInput)) { + setBillInfo(prev => ({...prev, members: [...prev.members, nameInput]})); + } + setNameInput(''); + } + }; + + const setStepPrice = () => { + setStep('price'); + }; + + const setStepTitle = () => { + setStep('title'); + }; + + const setStepMembers = () => { + setStep('members'); + }; + + const priceStep = () => ( + <> +
+ + + + +
+
+ +
+ navigate(-1)}> + 다음으로 + + + ); + + const titleStep = () => ( + <> +
+ + + + + +
+ + 다음으로 + + + ); + + const membersStep = () => ( + <> +
+ + + + + +
+ + + 참여 인원 + + {`총 ${billInfo.members.length}명`} + +
+ {billInfo.members.map(member => ( + setBillInfo(prev => ({...prev, members: prev.members.filter(name => name !== member)}))} + /> + ))} +
+
+
+ + 추가완료 + + + ); + + return ( + + + + + {step === 'price' && priceStep()} + {step === 'title' && titleStep()} + {step === 'members' && membersStep()} + + ); +}; + +export default AddBillFunnel; diff --git a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx index 831bb1fd..385d8c96 100644 --- a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx +++ b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx @@ -1,6 +1,8 @@ import {useLocation, useNavigate} from 'react-router-dom'; +import {css} from '@emotion/react'; import {RunningDog} from '@components/Common/Logo'; +import Top from '@components/Design/components/Top/Top'; import {FixedButton, MainLayout, Title, TopNav} from '@HDesign/index'; @@ -16,12 +18,20 @@ const CompleteCreateEventPage = () => { return ( - - <RunningDog /> + <div + css={css` + display: flex; + flex-direction: column; + gap: 3rem; + padding: 1rem; + `} + > + <Top> + <Top.Line text="행사가 생성되었어요!" emphasize={['행사가 생성되었어요!']} /> + <Top.Line text="관리 페이지에서 정산을 시작하세요" emphasize={['정산을 시작하세요']} /> + </Top> + <RunningDog /> + </div> <FixedButton onClick={() => navigate(`${ROUTER_URLS.event}/${eventId}/admin`)}>관리 페이지로 이동</FixedButton> </MainLayout> ); diff --git a/client/src/pages/CreateEventPage/SetEventNamePage.tsx b/client/src/pages/CreateEventPage/SetEventNamePage.tsx index 73fca320..f9fc9f3f 100644 --- a/client/src/pages/CreateEventPage/SetEventNamePage.tsx +++ b/client/src/pages/CreateEventPage/SetEventNamePage.tsx @@ -1,9 +1,11 @@ import {useNavigate} from 'react-router-dom'; import {css} from '@emotion/react'; +import Top from '@components/Design/components/Top/Top'; + import useSetEventNamePage from '@hooks/useSetEventNamePage'; -import {FixedButton, MainLayout, LabelInput, Title, TopNav, Back} from '@HDesign/index'; +import {FixedButton, MainLayout, LabelInput, Title, TopNav, Back, Flex} from '@HDesign/index'; import {ROUTER_URLS} from '@constants/routerUrls'; @@ -14,28 +16,51 @@ const SetEventNamePage = () => { const submitEventName = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); + onSuccessSubmint(); + }; + + const onSuccessSubmint = () => { navigate(ROUTER_URLS.eventCreatePassword, {state: {eventName}}); }; + const handleGoNextStep = (event: React.KeyboardEvent<HTMLInputElement>) => { + if (event.key === 'Enter') { + onSuccessSubmint(); + } + }; + return ( <MainLayout backgroundColor="white"> <TopNav> <Back /> </TopNav> - <Title title="행사 이름 입력" description="시작할 행사 이름을 입력해 주세요." /> - <form onSubmit={submitEventName} css={css({padding: '0 1rem'})}> - <LabelInput - labelText="행사 이름" - errorText={errorMessage ?? ''} - value={eventName} - type="text" - placeholder="행사 이름" - onChange={handleEventNameChange} - isError={!!errorMessage} - autoFocus - ></LabelInput> - <FixedButton disabled={!canSubmit}>다음</FixedButton> - </form> + <div + css={css` + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + `} + > + <Top> + <Top.Line text="정산을 시작하려는" /> + <Top.Line text="행사의 이름은 무엇인가요?" emphasize={['행사의 이름']} /> + </Top> + <form onSubmit={submitEventName}> + <LabelInput + labelText="행사 이름" + errorText={errorMessage ?? ''} + value={eventName} + type="text" + placeholder="행동대장 야유회" + onChange={handleEventNameChange} + isError={!!errorMessage} + autoFocus + onKeyDown={handleGoNextStep} + ></LabelInput> + <FixedButton disabled={!canSubmit}>다음</FixedButton> + </form> + </div> </MainLayout> ); }; diff --git a/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx b/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx index 9f6ba6a4..914f0a25 100644 --- a/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx +++ b/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx @@ -1,3 +1,7 @@ +import {css} from '@emotion/react'; + +import Top from '@components/Design/components/Top/Top'; + import useSetEventPasswordPage from '@hooks/useSetEventPasswordPage'; import {FixedButton, MainLayout, LabelInput, Title, TopNav, Back} from '@HDesign/index'; @@ -6,34 +10,51 @@ import RULE from '@constants/rule'; import {PASSWORD_LENGTH} from '@constants/password'; const SetEventPasswordPage = () => { - const {submitPassword, errorMessage, password, handleChange, canSubmit, isPostEventPending} = + const {submitPassword, onSuccess, errorMessage, password, handleChange, canSubmit, isPostEventPending} = useSetEventPasswordPage(); + const handleGoNextStep = (event: React.KeyboardEvent<HTMLInputElement>) => { + if (event.key === 'Enter') { + onSuccess(); + } + }; + return ( <MainLayout backgroundColor="white"> <TopNav> <Back /> </TopNav> - <Title - title="행사 비밀번호 설정" - description={`행사 관리에 필요한 ${PASSWORD_LENGTH} 자리의 숫자 비밀번호를 입력해 주세요.`} - /> - <form onSubmit={submitPassword} style={{padding: '0 1rem'}}> - <LabelInput - labelText="비밀번호" - errorText={errorMessage} - value={password} - type="text" - maxLength={RULE.maxEventPasswordLength} - placeholder="비밀번호" - onChange={handleChange} - isError={!!errorMessage} - autoFocus - /> - <FixedButton variants={isPostEventPending ? 'loading' : 'primary'} disabled={!canSubmit}> - 행동 개시! - </FixedButton> - </form> + <div + css={css` + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + `} + > + <Top> + <Top.Line text="관리에 필요한 네자리 숫자" emphasize={['네자리 숫자']} /> + <Top.Line text="비밀번호는 무엇으로 할까요?" emphasize={['비밀번호']} /> + </Top> + <form onSubmit={submitPassword}> + <LabelInput + labelText="비밀번호" + errorText={errorMessage} + value={password} + type="text" + maxLength={RULE.maxEventPasswordLength} + placeholder="1234" + onChange={handleChange} + isError={!!errorMessage} + autoFocus + onKeyDown={handleGoNextStep} + /> + {/* 가상 키패드 적용 예정 */} + <FixedButton variants={isPostEventPending ? 'loading' : 'primary'} disabled={!canSubmit}> + 행동 개시! + </FixedButton> + </form> + </div> </MainLayout> ); }; diff --git a/client/src/pages/ErrorPage/ErrorPage.tsx b/client/src/pages/ErrorPage/ErrorPage.tsx index a405112e..84f61b2a 100644 --- a/client/src/pages/ErrorPage/ErrorPage.tsx +++ b/client/src/pages/ErrorPage/ErrorPage.tsx @@ -3,10 +3,7 @@ import {MainLayout, Title} from '@HDesign/index'; const ErrorPage = () => { return ( <MainLayout> - <Title - title="알 수 없는 오류입니다." - description="오류가 난 상황에 대해 haengdongdj@gmail.com 로 연락주시면 소정의 상품을 드립니다." - /> + <Title title="알 수 없는 오류입니다." /> </MainLayout> ); }; diff --git a/client/src/pages/EventPage/AdminPage/AdminPage.style.ts b/client/src/pages/EventPage/AdminPage/AdminPage.style.ts index cf6bb004..830ec74a 100644 --- a/client/src/pages/EventPage/AdminPage/AdminPage.style.ts +++ b/client/src/pages/EventPage/AdminPage/AdminPage.style.ts @@ -4,7 +4,8 @@ export const receiptStyle = () => css({ display: 'flex', flexDirection: 'column', - gap: '1rem', + gap: '0.5rem', + paddingInline: '1rem', paddingBottom: '2rem', }); diff --git a/client/src/pages/EventPage/AdminPage/AdminPage.tsx b/client/src/pages/EventPage/AdminPage/AdminPage.tsx index 855c7905..7a5168d2 100644 --- a/client/src/pages/EventPage/AdminPage/AdminPage.tsx +++ b/client/src/pages/EventPage/AdminPage/AdminPage.tsx @@ -1,90 +1,42 @@ -import {useEffect, useState} from 'react'; -import {useOutletContext} from 'react-router-dom'; +import {useEffect} from 'react'; +import {useNavigate, useOutletContext} from 'react-router-dom'; -import StepList from '@components/StepList/StepList'; -import {ModalBasedOnMemberCount, SetAllMemberListModal} from '@components/Modal/index'; -import useRequestGetAllMemberList from '@hooks/queries/useRequestGetAllMemberList'; -import useRequestPostAuthenticate from '@hooks/queries/useRequestPostAuthentication'; +import StepList from '@components/StepList/Steps'; +import useRequestPostAuthenticate from '@hooks/queries/auth/useRequestPostAuthentication'; +import useRequestGetSteps from '@hooks/queries/step/useRequestGetSteps'; import {useTotalExpenseAmountStore} from '@store/totalExpenseAmountStore'; -import {Title, FixedButton, ListButton, Button} from '@HDesign/index'; +import {Title, Button} from '@HDesign/index'; + +import getEventIdByUrl from '@utils/getEventIdByUrl'; import {EventPageContextProps} from '../EventPageLayout'; -import {receiptStyle, titleAndListButtonContainerStyle, buttonGroupStyle} from './AdminPage.style'; +import {receiptStyle} from './AdminPage.style'; const AdminPage = () => { - const [isOpenFixedButtonBottomSheet, setIsOpenFixedButtonBottomSheet] = useState(false); - const [isOpenAllMemberListButton, setIsOpenAllMemberListButton] = useState(false); - const [isAddEditableItem, setIsAddEditableItem] = useState(false); - + const navigate = useNavigate(); + const eventId = getEventIdByUrl(); const {eventName} = useOutletContext<EventPageContextProps>(); - const {data: allMemberListData} = useRequestGetAllMemberList(); - const allMemberList = allMemberListData?.memberNames ?? []; const {totalExpenseAmount} = useTotalExpenseAmountStore(); - const {mutate: postAuthentication} = useRequestPostAuthenticate(); + const {steps} = useRequestGetSteps(); + const {postAuthenticate} = useRequestPostAuthenticate(); useEffect(() => { - postAuthentication(); - }, [postAuthentication]); - - const handleOpenAllMemberListButton = () => { - setIsOpenFixedButtonBottomSheet(prev => !prev); - setIsOpenAllMemberListButton(prev => !prev); - }; - - const getTitleDescriptionByInitialMemberSetting = () => { - return allMemberList.length > 0 - ? `지출 내역 및 인원 변동을 추가해 주세요. - 인원 변동을 기준으로 몇 차인지 나뉘어져요.` - : '“시작 인원 추가” 버튼을 눌러 행사의 시작부터 참여하는 사람들의 이름을 입력해 주세요.'; - }; + postAuthenticate(); + }, [postAuthenticate]); return ( - <> - <div css={titleAndListButtonContainerStyle}> - <Title title={eventName} description={getTitleDescriptionByInitialMemberSetting()} price={totalExpenseAmount} /> - {allMemberList.length !== 0 && ( - <ListButton - prefix="전체 참여자" - suffix={`${allMemberList.length}명`} - onClick={handleOpenAllMemberListButton} - /> - )} - </div> - <section css={receiptStyle}> - <StepList isAddEditableItem={isAddEditableItem} setIsAddEditableItem={setIsAddEditableItem} /> - {allMemberList.length === 0 ? ( - <FixedButton children={'시작인원 추가하기'} onClick={() => setIsOpenFixedButtonBottomSheet(prev => !prev)} /> - ) : ( - <div css={buttonGroupStyle}> - <Button - size="medium" - variants="tertiary" - style={{width: '100%'}} - onClick={() => setIsOpenFixedButtonBottomSheet(prev => !prev)} - > - 인원 변동 추가 - </Button> - <Button size="medium" onClick={() => setIsAddEditableItem(true)} style={{width: '100%'}}> - 지출 내역 추가 - </Button> - </div> - )} - {isOpenFixedButtonBottomSheet && ( - <ModalBasedOnMemberCount - allMemberList={allMemberList} - setIsOpenBottomSheet={setIsOpenFixedButtonBottomSheet} - isOpenBottomSheet={isOpenFixedButtonBottomSheet} - isOpenAllMemberListButton={isOpenAllMemberListButton} - setIsOpenAllMemberListButton={setIsOpenAllMemberListButton} - /> - )} - </section> - </> + <section css={receiptStyle}> + <Title title={eventName} amount={totalExpenseAmount} /> + <StepList data={steps ?? []} /> + <Button size="medium" onClick={() => navigate(`/event/${eventId}/addBill`)} style={{width: '100%'}}> + 지출내역 추가하기 + </Button> + </section> ); }; diff --git a/client/src/pages/EventPage/AdminPage/EventLoginPage.tsx b/client/src/pages/EventPage/AdminPage/EventLoginPage.tsx index d099835b..e126d158 100644 --- a/client/src/pages/EventPage/AdminPage/EventLoginPage.tsx +++ b/client/src/pages/EventPage/AdminPage/EventLoginPage.tsx @@ -1,19 +1,20 @@ +import Top from '@components/Design/components/Top/Top'; + import useEventLogin from '@hooks/useEventLogin'; -import {FixedButton, LabelInput, Title} from '@HDesign/index'; +import {FixedButton, LabelInput} from '@HDesign/index'; import RULE from '@constants/rule'; -import {PASSWORD_LENGTH} from '@constants/password'; const EventLoginPage = () => { const {password, errorMessage, handleChange, canSubmit, submitPassword} = useEventLogin(); return ( <> - <Title - title="행사 비밀번호 입력" - description={`관리를 위해선 비밀번호가 필요해요. 행사 생성 시 설정한 ${PASSWORD_LENGTH} 자리의 숫자 비밀번호를 입력해 주세요.`} - /> + <Top> + <Top.Line text="행사 생성 시 설정한" /> + <Top.Line text="네자리 숫자 비밀번호를 입력해 주세요." /> + </Top> <form onSubmit={submitPassword} style={{padding: '0 1rem'}}> <LabelInput labelText="비밀번호" diff --git a/client/src/pages/EventPage/EventPageLayout.tsx b/client/src/pages/EventPage/EventPageLayout.tsx index e76649f4..3eb21a90 100644 --- a/client/src/pages/EventPage/EventPageLayout.tsx +++ b/client/src/pages/EventPage/EventPageLayout.tsx @@ -2,7 +2,7 @@ import {Outlet, useMatch} from 'react-router-dom'; import CopyToClipboard from 'react-copy-to-clipboard'; import {useToast} from '@hooks/useToast/useToast'; -import useRequestGetEventName from '@hooks/queries/useRequestGetEventName'; +import useRequestGetEvent from '@hooks/queries/event/useRequestGetEvent'; import useNavSwitch from '@hooks/useNavSwitch'; @@ -20,8 +20,7 @@ export type EventPageContextProps = { const EventPageLayout = () => { const {nav, paths, onChange} = useNavSwitch(); - const {data} = useRequestGetEventName(); - const eventName = data?.eventName ?? ''; + const {eventName} = useRequestGetEvent(); const eventId = getEventIdByUrl(); const isAdmin = useMatch(ROUTER_URLS.eventManage) !== null; diff --git a/client/src/pages/EventPage/HomePage/HomePage.tsx b/client/src/pages/EventPage/HomePage/HomePage.tsx index 0521d386..34b91a76 100644 --- a/client/src/pages/EventPage/HomePage/HomePage.tsx +++ b/client/src/pages/EventPage/HomePage/HomePage.tsx @@ -1,7 +1,8 @@ import {useOutletContext} from 'react-router-dom'; -import MemberReportList from '@components/MemberReportList/MemberReportList'; -import StepList from '@components/StepList/StepList'; +import StepList from '@components/StepList/Steps'; +import useRequestGetSteps from '@hooks/queries/step/useRequestGetSteps'; +import Reports from '@components/Reports/Reports'; import {useTotalExpenseAmountStore} from '@store/totalExpenseAmountStore'; @@ -11,14 +12,15 @@ import {EventPageContextProps} from '../EventPageLayout'; const HomePage = () => { const {eventName} = useOutletContext<EventPageContextProps>(); + const {steps} = useRequestGetSteps(); const {totalExpenseAmount} = useTotalExpenseAmountStore(); return ( <div style={{paddingBottom: '2rem'}}> - <Title title={eventName} price={totalExpenseAmount} /> + <Title title={eventName} amount={totalExpenseAmount} /> <Tabs tabsContainerStyle={{gap: '1rem'}}> - <Tab label="전체 지출 내역" content={<StepList />} /> - <Tab label="참여자 별 내역" content={<MemberReportList />} /> + <Tab label="전체 지출 내역" content={<StepList data={steps ?? []} />} /> + <Tab label="참여자 별 내역" content={<Reports />} /> </Tabs> </div> ); diff --git a/client/src/pages/MainPage/MainPage.tsx b/client/src/pages/MainPage/MainPage.tsx index 16d710e3..702972ef 100644 --- a/client/src/pages/MainPage/MainPage.tsx +++ b/client/src/pages/MainPage/MainPage.tsx @@ -5,7 +5,7 @@ import MainSection from './Section/MainSection'; import DescriptionSection from './Section/DescriptionSection'; import AddBillSection from './Section/AddBillSection'; import AddMemberSection from './Section/AddMemberSection'; -import MemberReportSection from './Section/MemberReportSection'; +import ReportSection from './Section/ReportSection'; const MainPage = () => { return ( @@ -15,7 +15,7 @@ const MainPage = () => { <DescriptionSection /> <AddBillSection /> <AddMemberSection /> - <MemberReportSection /> + <ReportSection /> </MainLayout> ); }; diff --git a/client/src/pages/MainPage/Section/MemberReportSection.tsx b/client/src/pages/MainPage/Section/ReportSection.tsx similarity index 91% rename from client/src/pages/MainPage/Section/MemberReportSection.tsx rename to client/src/pages/MainPage/Section/ReportSection.tsx index 6e51ae2b..452365ea 100644 --- a/client/src/pages/MainPage/Section/MemberReportSection.tsx +++ b/client/src/pages/MainPage/Section/ReportSection.tsx @@ -4,7 +4,7 @@ import MemberReportMockup from '@assets/image/memberReportMockup.svg'; import {Text} from '@HDesign/index'; -const MemberReportSection = () => { +const ReportSection = () => { return ( <div css={css({ @@ -29,4 +29,4 @@ const MemberReportSection = () => { ); }; -export default MemberReportSection; +export default ReportSection; diff --git a/client/src/router.tsx b/client/src/router.tsx index 31bbd386..e1592bd2 100644 --- a/client/src/router.tsx +++ b/client/src/router.tsx @@ -5,6 +5,7 @@ import {HomePage} from '@pages/EventPage/HomePage'; import ErrorPage from '@pages/ErrorPage/ErrorPage'; import EventLoginPage from '@pages/EventPage/AdminPage/EventLoginPage'; import Account from '@pages/Account/Account'; +import AddBillFunnel from '@pages/BillPage/AddBillFunnel'; import {CompleteCreateEventPage, SetEventNamePage, SetEventPasswordPage} from '@pages/CreateEventPage'; import {MainPage} from '@pages/MainPage'; @@ -52,6 +53,10 @@ const router = createBrowserRouter([ }, ], }, + { + path: ROUTER_URLS.addBill, + element: <AddBillFunnel />, + }, { path: '*', element: <ErrorPage />, diff --git a/client/src/store/stepListStore.ts b/client/src/store/stepListStore.ts deleted file mode 100644 index 07f3b107..00000000 --- a/client/src/store/stepListStore.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {create} from 'zustand'; - -import {ConvertedAction} from 'types/serviceType'; - -type State = { - stepList: ConvertedAction[]; -}; - -type Action = { - updateStepList: (stepList: State['stepList']) => void; -}; - -export const useStepListStore = create<State & Action>(set => ({ - stepList: [], - updateStepList: stepList => set(() => ({stepList})), -})); diff --git a/client/src/store/stepsStore.ts b/client/src/store/stepsStore.ts new file mode 100644 index 00000000..e89adf39 --- /dev/null +++ b/client/src/store/stepsStore.ts @@ -0,0 +1,16 @@ +import {create} from 'zustand'; + +import {Steps} from 'types/serviceType'; + +type State = { + steps: Steps[]; +}; + +type Action = { + updateSteps: (stepList: State['steps']) => void; +}; + +export const useBillsStore = create<State & Action>(set => ({ + steps: [], + updateSteps: steps => set(() => ({steps})), +})); diff --git a/client/src/store/totalExpenseAmountStore.ts b/client/src/store/totalExpenseAmountStore.ts index e9ebcf52..cce46575 100644 --- a/client/src/store/totalExpenseAmountStore.ts +++ b/client/src/store/totalExpenseAmountStore.ts @@ -1,6 +1,6 @@ import {create} from 'zustand'; -import {BillStep, MemberStep} from 'types/serviceType'; +import {Step as StepType} from 'types/serviceType'; import {getTotalExpenseAmount} from '@utils/caculateExpense'; @@ -9,10 +9,10 @@ type State = { }; type Action = { - updateTotalExpenseAmount: (stepList: (MemberStep | BillStep)[]) => void; + updateTotalExpenseAmount: (steps: StepType[]) => void; }; export const useTotalExpenseAmountStore = create<State & Action>(set => ({ totalExpenseAmount: 0, - updateTotalExpenseAmount: stepList => set({totalExpenseAmount: getTotalExpenseAmount(stepList)}), + updateTotalExpenseAmount: (steps: StepType[]) => set({totalExpenseAmount: getTotalExpenseAmount(steps)}), })); diff --git a/client/src/types/serviceType.ts b/client/src/types/serviceType.ts index 0467995f..39fa1995 100644 --- a/client/src/types/serviceType.ts +++ b/client/src/types/serviceType.ts @@ -1,80 +1,67 @@ -export type MemberType = 'IN' | 'OUT'; +// ******************************************************************* +// ******************** UX 개선 이후 변경된 부분들 24.09.19 **************** +// ******************************************************************* -export type InOutType = '늦참' | '탈주'; +export interface Steps { + steps: Step[]; +} -export type MemberReport = { - name: string; - price: number; -}; - -export type MemberReportInAction = MemberReport & { - isFixed: boolean; -}; +export interface Step { + bills: Bill[]; + members: Member[]; +} -export type Bill = { +export interface Bill { + id: number; title: string; price: number; -}; - -type StepBase = { - members: string[]; -}; - -export type MemberStep = StepBase & { - type: MemberType; - stepName: null; - actions: MemberAction[]; -}; - -export type BillStep = StepBase & { - type: 'BILL'; - stepName: string; - actions: BillAction[]; -}; - -// (@weadie) 준 데이터 형식에서 steps를 빼내 flat하게 사용중. 일관성있게 하는게 좋긴 하나 사용시 번거로움이 있을 거라고 판단. -export type StepList = { - steps: (MemberStep | BillStep)[]; -}; - -export type Action = { - actionId: number; - name: string; - price: number | null; - sequence: number; isFixed: boolean; -}; +} -export type BillAction = Omit<Action, 'price'> & { +export interface BillDetail { + id: number; + memberName: string; price: number; -}; + isFixed: boolean; +} -export type MemberAction = Omit<Action, 'price'> & { - price: null; -}; +export interface BillDetails { + billDetails: BillDetail[]; +} -export type Member = { +export interface Member { + id: number; name: string; - status: MemberType; -}; - -export type ActionType = 'IN' | 'OUT' | 'BILL'; - -// export type StepList = { -// actions: Action[]; -// }; - -export type ConvertedAction = { - actionId: number; +} + +export interface Members { + members: Member[]; +} + +export interface MemberWithDeposited extends Member { + isDeposited: boolean; +} + +export interface AllMembers { + members: MemberWithDeposited[]; +} +export interface EventId { + eventId: string; +} + +export interface Event { + eventName: string; + bankName: string; + accountNumber: string; +} + +export interface Report { + memberId: number; name: string; - price: string | null; - sequence: number; - type: ActionType; -}; - -export type InputPair = Omit<Bill, 'price'> & { - price: string; - index: number; -}; + isDeposited: boolean; + price: number; +} -export type BillInputType = 'title' | 'price'; +export interface Reports { + reports: Report[]; +} diff --git a/client/src/utils/caculateExpense.ts b/client/src/utils/caculateExpense.ts index 8b41af8c..2ee8c2fa 100644 --- a/client/src/utils/caculateExpense.ts +++ b/client/src/utils/caculateExpense.ts @@ -1,14 +1,8 @@ -import {BillAction, BillStep, MemberStep} from 'types/serviceType'; +import {Step} from 'types/serviceType'; -export const calculateStepExpense = (actions: BillAction[]) => { - return actions.reduce((sum, {price}) => sum + price, 0); -}; - -export const getTotalExpenseAmount = (stepList: (MemberStep | BillStep)[]) => { - return stepList.reduce((sum, {type, actions}) => { - if (type === 'BILL') { - return sum + calculateStepExpense(actions); - } - return sum; +export const getTotalExpenseAmount = (steps: Step[]) => { + return steps.reduce((total, step) => { + const stepTotal = step.bills.reduce((sum, bill) => sum + bill.price, 0); + return total + stepTotal; }, 0); }; diff --git a/client/src/utils/groupActions.ts b/client/src/utils/groupActions.ts deleted file mode 100644 index a6347167..00000000 --- a/client/src/utils/groupActions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {BillStep, ConvertedAction, MemberStep} from 'types/serviceType'; - -import stepListToAction from './stepListToActions'; - -const groupActions = (stepList: (BillStep | MemberStep)[]) => { - const actions = stepListToAction(stepList); - const groupedActions: ConvertedAction[][] = []; - - let group: ConvertedAction[] = []; - - actions.forEach((action, index) => { - if (group.length === 0 || group[group.length - 1].type === action.type) { - group.push(action); - } else { - groupedActions.push(group); - group = []; - } - - if (index === actions.length - 1) { - groupedActions.push(group); - } - }); - - return groupedActions; -}; - -export default groupActions; diff --git a/client/src/utils/stepListToActions.ts b/client/src/utils/stepListToActions.ts deleted file mode 100644 index 5fe1bb3a..00000000 --- a/client/src/utils/stepListToActions.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {BillStep, ConvertedAction, MemberStep} from 'types/serviceType'; - -const stepListToAction = (stepList: (BillStep | MemberStep)[]) => { - // (@todari) test용이라 임시로 any 사용할게용... - // Action을 사용하려고 하는데 serviceType의 기존 Action이랑 겹쳐서요~~~ - const actions: ConvertedAction[] = []; - - stepList.forEach(step => { - step.actions.forEach(action => { - actions.push({ - actionId: action.actionId, - name: action.name, - price: action.price ? action.price.toLocaleString() : null, - sequence: action.sequence, - type: step.type, - }); - }); - }); - - return actions; -}; - -export default stepListToAction; diff --git a/client/src/utils/validate/validateMemberReportInAction.ts b/client/src/utils/validate/validateBillDetails.ts similarity index 85% rename from client/src/utils/validate/validateMemberReportInAction.ts rename to client/src/utils/validate/validateBillDetails.ts index b4e6c2a9..a77a154a 100644 --- a/client/src/utils/validate/validateMemberReportInAction.ts +++ b/client/src/utils/validate/validateBillDetails.ts @@ -3,7 +3,7 @@ import RULE from '@constants/rule'; import {ValidateResult} from './type'; -const validateMemberReportInAction = (price: string, totalPrice: number): ValidateResult => { +const validateBillDetails = (price: string, totalPrice: number): ValidateResult => { let errorMessage = null; const numberTypePrice = Number(price); @@ -30,4 +30,4 @@ const validateMemberReportInAction = (price: string, totalPrice: number): Valida return {isValid: false, errorMessage: errorMessage || ERROR_MESSAGE.invalidInput}; }; -export default validateMemberReportInAction; +export default validateBillDetails;