diff --git a/HDesign/package-lock.json b/HDesign/package-lock.json index 192588f72..274804d74 100644 --- a/HDesign/package-lock.json +++ b/HDesign/package-lock.json @@ -1,12 +1,13 @@ { "name": "haengdong-design", - "version": "0.1.69", + "version": "0.1.74", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "haengdong-design", - "version": "0.1.69", + "version": "0.1.74", + "license": "ISC", "dependencies": { "@emotion/react": "^11.11.4", diff --git a/HDesign/package.json b/HDesign/package.json index 4e555b3fc..e01107490 100644 --- a/HDesign/package.json +++ b/HDesign/package.json @@ -1,6 +1,6 @@ { "name": "haengdong-design", - "version": "0.1.69", + "version": "0.1.74", "description": "", "main": "./dist/index.js", "module": "./dist/index.js", diff --git a/HDesign/src/components/EditableItem/EditableItem.Input.style.ts b/HDesign/src/components/EditableItem/EditableItem.Input.style.ts new file mode 100644 index 000000000..fc346d77b --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.Input.style.ts @@ -0,0 +1,75 @@ +import {css} from '@emotion/react'; + +import {TextSize} from '@components/Text/Text.type'; + +import {Theme} from '@theme/theme.type'; + +import TYPOGRAPHY from '@token/typography'; + +interface InputWrapperStyleProps { + theme: Theme; + hasFocus: boolean; + hasError: boolean; +} + +interface InputStyleProps { + theme: Theme; + textSize: TextSize; +} + +interface InputSizeStyleProps { + textSize: TextSize; +} + +interface InputBaseStyleProps { + theme: Theme; +} + +export const inputWrapperStyle = ({theme, hasFocus, hasError}: InputWrapperStyleProps) => + css({ + position: 'relative', + display: 'inline-block', + + '&::after': { + content: '""', + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + height: '0.125rem', + backgroundColor: hasFocus ? theme.colors.primary : hasError ? theme.colors.error : 'transparent', + transition: 'background-color 0.2s', + transitionTimingFunction: 'cubic-bezier(0.7, 0.62, 0.62, 1.16)', + }, + }); + +export const inputStyle = ({theme, textSize}: InputStyleProps) => [inputSizeStyle({textSize}), inputBaseStyle({theme})]; + +const inputSizeStyle = ({textSize}: InputSizeStyleProps) => { + const style = { + head: css(TYPOGRAPHY.head), + title: css(TYPOGRAPHY.title), + subTitle: css(TYPOGRAPHY.subTitle), + bodyBold: css(TYPOGRAPHY.bodyBold), + body: css(TYPOGRAPHY.body), + smallBodyBold: css(TYPOGRAPHY.smallBodyBold), + smallBody: css(TYPOGRAPHY.smallBody), + captionBold: css(TYPOGRAPHY.captionBold), + caption: css(TYPOGRAPHY.caption), + tiny: css(TYPOGRAPHY.tiny), + }; + + return [style[textSize]]; +}; + +const inputBaseStyle = ({theme}: InputBaseStyleProps) => + css({ + border: 'none', + outline: 'none', + paddingBottom: '0.125rem', + + color: theme.colors.black, + '&:placeholder': { + color: theme.colors.gray, + }, + }); diff --git a/HDesign/src/components/EditableItem/EditableItem.Input.tsx b/HDesign/src/components/EditableItem/EditableItem.Input.tsx new file mode 100644 index 000000000..b6af0c144 --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.Input.tsx @@ -0,0 +1,27 @@ +/** @jsxImportSource @emotion/react */ +import React, {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; + +import {InputProps} from '@components/EditableItem/EditableItem.Input.type'; + +import {useTheme} from '@theme/HDesignProvider'; + +import {inputStyle, inputWrapperStyle} from './EditableItem.Input.style'; +import useEditableItemInput from './useEditableItemInput'; + +export const EditableItemInput: React.FC = forwardRef(function Input( + {textSize = 'body', hasError = false, ...htmlProps}, + ref, +) { + const {theme} = useTheme(); + const inputRef = useRef(null); + const {hasFocus} = useEditableItemInput({inputRef}); + useImperativeHandle(ref, () => inputRef.current!); + + return ( +
+ +
+ ); +}); + +export default EditableItemInput; diff --git a/HDesign/src/components/EditableItem/EditableItem.Input.type.ts b/HDesign/src/components/EditableItem/EditableItem.Input.type.ts new file mode 100644 index 000000000..5d58238e8 --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.Input.type.ts @@ -0,0 +1,18 @@ +import {TextSize} from '@components/Text/Text.type'; + +import {Theme} from '@theme/theme.type'; + +export interface InputStyleProps { + hasError?: boolean; + textSize?: TextSize; +} + +export interface InputCustomProps {} + +export interface InputStylePropsWithTheme extends InputStyleProps { + theme: Theme; +} + +export type InputOptionProps = InputStyleProps & InputCustomProps; + +export type InputProps = React.ComponentProps<'input'> & InputOptionProps; diff --git a/HDesign/src/components/EditableItem/EditableItem.context.tsx b/HDesign/src/components/EditableItem/EditableItem.context.tsx new file mode 100644 index 000000000..a2aeef4bc --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.context.tsx @@ -0,0 +1,23 @@ +/** @jsxImportSource @emotion/react */ +import {createContext, PropsWithChildren, useContext, useState} from 'react'; + +interface EditableItemContextProps { + hasAnyFocus: boolean; + setHasAnyFocus: React.Dispatch>; +} + +const EditableItemContext = createContext(null); + +export const useEditableItemContext = () => { + const context = useContext(EditableItemContext); + if (!context) { + throw new Error('useEditableItemContext must be used within an EditableItemProvider'); + } + return context; +}; + +export const EditableItemProvider: React.FC = ({children}: React.PropsWithChildren) => { + const [hasAnyFocus, setHasAnyFocus] = useState(false); + + return {children}; +}; diff --git a/HDesign/src/components/EditableItem/EditableItem.input.stories.tsx b/HDesign/src/components/EditableItem/EditableItem.input.stories.tsx new file mode 100644 index 000000000..a23483b78 --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.input.stories.tsx @@ -0,0 +1,44 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import EditableItemInput from '@components/EditableItem/EditableItem.Input'; + +import EditableItem from './EditableItem'; +import {EditableItemProvider} from './EditableItem.context'; + +const meta = { + title: 'Components/EditableItemInput', + component: EditableItemInput, + tags: ['autodocs'], + parameters: {}, + argTypes: { + textSize: { + description: '', + control: {type: 'select'}, + }, + hasError: { + description: '', + control: {type: 'boolean'}, + }, + }, + args: { + placeholder: '지출 내역', + textSize: 'body', + hasError: false, + autoFocus: true, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + render: ({...args}) => { + return ( + + + + ); + }, +}; diff --git a/HDesign/src/components/EditableItem/EditableItem.stories.tsx b/HDesign/src/components/EditableItem/EditableItem.stories.tsx new file mode 100644 index 000000000..976a4aa90 --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.stories.tsx @@ -0,0 +1,44 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import EditableItem from '@components/EditableItem/EditableItem'; +import Flex from '@components/Flex/Flex'; +import Text from '@components/Text/Text'; + +const meta = { + title: 'Components/EditableItem', + component: EditableItem, + tags: ['autodocs'], + parameters: {}, + argTypes: { + backgroundColor: { + description: '', + control: {type: 'select'}, + }, + }, + args: { + backgroundColor: 'lightGrayContainer', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + render: ({...args}) => { + return ( + console.log('focus')} + onBlur={() => console.log('blur')} + > + + + + + + + ); + }, +}; diff --git a/HDesign/src/components/EditableItem/EditableItem.style.ts b/HDesign/src/components/EditableItem/EditableItem.style.ts new file mode 100644 index 000000000..c817e5f7d --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.style.ts @@ -0,0 +1,14 @@ +import {css} from '@emotion/react'; + +import {Theme} from '@theme/theme.type'; + +import {ColorKeys} from '@token/colors'; + +export const editableItemStyle = (theme: Theme, backgroundColor: ColorKeys) => + css({ + display: 'flex', + justifyContent: 'space-between', + padding: '0.5rem', + borderRadius: '0.5rem', + backgroundColor: theme.colors[backgroundColor], + }); diff --git a/HDesign/src/components/EditableItem/EditableItem.tsx b/HDesign/src/components/EditableItem/EditableItem.tsx new file mode 100644 index 000000000..cc1892370 --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.tsx @@ -0,0 +1,40 @@ +/** @jsxImportSource @emotion/react */ +import React, {useEffect} from 'react'; + +import {useTheme} from '@theme/HDesignProvider'; + +import {editableItemStyle} from './EditableItem.style'; +import EditableItemInput from './EditableItem.Input'; +import {EditableItemProps} from './EditableItem.type'; +import {EditableItemProvider} from './EditableItem.context'; +import useEditableItem from './useEditableItem'; + +const EditableItemBase = ({ + onInputFocus, + onInputBlur, + backgroundColor = 'white', + children, + ...htmlProps +}: EditableItemProps) => { + const {theme} = useTheme(); + + useEditableItem({onInputFocus, onInputBlur}); + + return ( +
+ {children} +
+ ); +}; + +export const EditableItem = (props: EditableItemProps) => { + return ( + + + + ); +}; + +EditableItem.Input = EditableItemInput; + +export default EditableItem; diff --git a/HDesign/src/components/EditableItem/EditableItem.type.ts b/HDesign/src/components/EditableItem/EditableItem.type.ts new file mode 100644 index 000000000..b19a76427 --- /dev/null +++ b/HDesign/src/components/EditableItem/EditableItem.type.ts @@ -0,0 +1,20 @@ +import {Theme} from '@theme/theme.type'; + +import {ColorKeys} from '@token/colors'; + +export interface EditableItemStyleProps { + backgroundColor: ColorKeys; +} + +export interface EditableItemCustomProps { + onInputFocus?: () => void; + onInputBlur?: () => void; +} + +export interface EditableItemStylePropsWithTheme extends EditableItemStyleProps { + theme: Theme; +} + +export type EditableItemOptionProps = EditableItemStyleProps & EditableItemCustomProps; + +export type EditableItemProps = React.ComponentProps<'div'> & EditableItemOptionProps; diff --git a/HDesign/src/components/EditableItem/useEditableItem.ts b/HDesign/src/components/EditableItem/useEditableItem.ts new file mode 100644 index 000000000..86880deb6 --- /dev/null +++ b/HDesign/src/components/EditableItem/useEditableItem.ts @@ -0,0 +1,23 @@ +import {useEffect} from 'react'; + +import {useEditableItemContext} from './EditableItem.context'; + +interface UseEditableItemProps { + onInputFocus?: () => void; + onInputBlur?: () => void; +} + +const useEditableItem = ({onInputFocus, onInputBlur}: UseEditableItemProps) => { + const {hasAnyFocus} = useEditableItemContext(); + + useEffect(() => { + if (hasAnyFocus && onInputFocus) { + onInputFocus(); + } + if (!hasAnyFocus && onInputBlur) { + onInputBlur(); + } + }, [hasAnyFocus, onInputFocus, onInputBlur]); +}; + +export default useEditableItem; diff --git a/HDesign/src/components/EditableItem/useEditableItemInput.ts b/HDesign/src/components/EditableItem/useEditableItemInput.ts new file mode 100644 index 000000000..3253ad0ea --- /dev/null +++ b/HDesign/src/components/EditableItem/useEditableItemInput.ts @@ -0,0 +1,46 @@ +import {useCallback, useEffect, useState} from 'react'; + +import {useEditableItemContext} from './EditableItem.context'; + +interface UseEditableItemInputProps { + inputRef: React.RefObject; +} + +const useEditableItemInput = ({inputRef}: UseEditableItemInputProps) => { + const [hasFocus, setHasFocus] = useState(false); + const {setHasAnyFocus} = useEditableItemContext(); + + const handleFocus = useCallback(() => { + setHasFocus(true); + setHasAnyFocus(true); + }, [setHasAnyFocus]); + + const handleBlur = useCallback(() => { + setHasFocus(false); + setHasAnyFocus(false); + }, [setHasAnyFocus]); + + useEffect(() => { + const input = inputRef.current; + + if (input) { + input.addEventListener('focus', handleFocus); + input.addEventListener('blur', handleBlur); + + return () => { + input.removeEventListener('focus', handleFocus); + input.removeEventListener('blur', handleBlur); + }; + } + }, [handleFocus, handleBlur, inputRef]); + + useEffect(() => { + if (document.activeElement === inputRef.current) { + handleFocus(); + } + }, [handleFocus, inputRef]); + + return {hasFocus}; +}; + +export default useEditableItemInput; diff --git a/HDesign/src/components/Tabs/Tabs.style.ts b/HDesign/src/components/Tabs/Tabs.style.ts index 99a514547..3ea38924c 100644 --- a/HDesign/src/components/Tabs/Tabs.style.ts +++ b/HDesign/src/components/Tabs/Tabs.style.ts @@ -10,6 +10,8 @@ export const tabListStyle = (theme: Theme) => cursor: 'pointer', + WebkitTapHighlightColor: 'transparent', + '&::after': { position: 'absolute', left: 0, diff --git a/HDesign/src/components/Text/Text.style.ts b/HDesign/src/components/Text/Text.style.ts index 6560c8274..650260e62 100644 --- a/HDesign/src/components/Text/Text.style.ts +++ b/HDesign/src/components/Text/Text.style.ts @@ -21,5 +21,9 @@ export const getSizeStyling = ({size, textColor, theme}: Required; diff --git a/HDesign/src/components/Title/Title.tsx b/HDesign/src/components/Title/Title.tsx index 759588b5e..e17de5e88 100644 --- a/HDesign/src/components/Title/Title.tsx +++ b/HDesign/src/components/Title/Title.tsx @@ -1,4 +1,5 @@ /** @jsxImportSource @emotion/react */ +import Flex from '@components/Flex/Flex'; import Text from '@components/Text/Text'; import {priceContainerStyle, titleContainerStyle} from '@components/Title/Title.style'; import {TitleProps} from '@components/Title/Title.type'; @@ -11,7 +12,7 @@ export const Title: React.FC = ({title, description, price}: TitlePr
{title} {description && ( - + {description} )} @@ -20,7 +21,10 @@ export const Title: React.FC = ({title, description, price}: TitlePr 전체 지출 금액 - {price.toLocaleString('ko-kr')}원 + + {price.toLocaleString('ko-kr')} + +
)} diff --git a/HDesign/src/index.tsx b/HDesign/src/index.tsx index e6498965e..656690951 100644 --- a/HDesign/src/index.tsx +++ b/HDesign/src/index.tsx @@ -2,6 +2,7 @@ import BottomSheet from '@components/BottomSheet/BottomSheet'; import Button from '@components/Button/Button'; import DragHandleItem from '@components/DragHandleItem/DragHandleItem'; import DragHandleItemContainer from '@components/DragHandleItemContainer/DragHandleItemContainer'; +import EditableItem from '@components/EditableItem/EditableItem'; import ExpenseList from '@components/ExpenseList/ExpenseList'; import FixedButton from '@components/FixedButton/FixedButton'; import Flex from '@components/Flex/Flex'; @@ -33,6 +34,7 @@ export { Button, DragHandleItem, DragHandleItemContainer, + EditableItem, ExpenseList, FixedButton, Flex, diff --git a/client/package-lock.json b/client/package-lock.json index f3731b138..0a884603e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", - "haengdong-design": "^0.1.69", + "haengdong-design": "^0.1.72", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", @@ -8123,9 +8123,9 @@ } }, "node_modules/haengdong-design": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.69.tgz", - "integrity": "sha512-XlZ7hnKn51aQOOz/x+hNM6tLjJDvvTOqLiVfOSjEvRpkMKquNWFoijclXHLNhK7tqrb6m+hDREf12mIXwSN/Ew==", + "version": "0.1.72", + "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.72.tgz", + "integrity": "sha512-7Qtk/HygT5IhLzUTQzcx7FbgZo/ZrqVZhKA08zJaf1wcOlc3CkXO3Ljl1wQloHGejg71K1N1BO3L208tppxycQ==", "dependencies": { "@emotion/react": "^11.11.4", "@storybook/addon-webpack5-compiler-swc": "^1.0.5", diff --git a/client/package.json b/client/package.json index c73dae15d..b0d9e7ff0 100644 --- a/client/package.json +++ b/client/package.json @@ -54,7 +54,7 @@ "dependencies": { "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", - "haengdong-design": "^0.1.69", + "haengdong-design": "^0.1.72", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", diff --git a/client/src/apis/baseUrl.ts b/client/src/apis/baseUrl.ts index e508ba375..37fc00a94 100644 --- a/client/src/apis/baseUrl.ts +++ b/client/src/apis/baseUrl.ts @@ -1,3 +1,3 @@ export const BASE_URL = { - HD: process.env.API_BASE_URL ?? '', + HD: process.env.API_BASE_URL, }; diff --git a/client/src/apis/request/member.ts b/client/src/apis/request/member.ts index 48329e78f..141061bc1 100644 --- a/client/src/apis/request/member.ts +++ b/client/src/apis/request/member.ts @@ -73,7 +73,7 @@ export const requestDeleteAllMemberList = async ({eventId, memberName}: WithEven }; export type ResponseGetCurrentInMemberList = { - members: Array<{name: string}>; + memberNames: string[]; }; export const requestGetCurrentInMemberList = async (eventId: string) => { diff --git a/client/src/apis/useFetch.ts b/client/src/apis/useFetch.ts index b5f94df84..512ef4a96 100644 --- a/client/src/apis/useFetch.ts +++ b/client/src/apis/useFetch.ts @@ -64,6 +64,9 @@ export const useFetch = () => { }; const captureError = async (error: Error, navigate: NavigateFunction, eventId: string) => { + // prod 환경에서만 Sentry capture 실행 + if (process.env.NODE_ENV !== 'production') return; + const errorBody: ServerError = error instanceof FetchError ? error.errorBody : {message: error.message, errorCode: error.name}; diff --git a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx b/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx index a509b5d54..18b7e13ac 100644 --- a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx +++ b/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx @@ -2,6 +2,8 @@ import type {MemberAction, MemberType} from 'types/serviceType'; import {BottomSheet, Flex, Input, Text, IconButton, FixedButton, Icon} from 'haengdong-design'; +import {useToast} from '@components/Toast/ToastProvider'; + import useDeleteMemberAction from '@hooks/useDeleteMemberAction'; import {bottomSheetHeaderStyle, bottomSheetStyle, inputGroupStyle} from './DeleteMemberActionModal.style'; @@ -19,10 +21,38 @@ const DeleteMemberActionModal = ({ isBottomSheetOpened, setIsBottomSheetOpened, }: DeleteMemberActionModalProps) => { - const {aliveActionList, deleteMemberActionList, addDeleteMemberAction} = useDeleteMemberAction( + 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)}> diff --git a/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx index ac9375d52..623d9f98f 100644 --- a/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx +++ b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx @@ -37,7 +37,7 @@ const SetInitialMemberListModal = ({isOpenBottomSheet, setIsOpenBottomSheet}: Se return ( setIsOpenBottomSheet(false)}>
- 초기 인원 설정하기 + 시작 인원 추가하기
{inputList.map(({value, index}) => ( diff --git a/client/src/global.d.ts b/client/src/global.d.ts index bff94710c..dbf6320af 100644 --- a/client/src/global.d.ts +++ b/client/src/global.d.ts @@ -1 +1,11 @@ declare module '*.svg'; + +declare namespace NodeJS { + interface ProcessEnv { + readonly NODE_ENV: 'development' | 'production' | 'test'; + + // env keys + readonly API_BASE_URL: string; + readonly AMPLITUDE_KEY: string; + } +} diff --git a/client/src/hooks/useDeleteMemberAction.tsx b/client/src/hooks/useDeleteMemberAction.tsx index 1883929f2..be16baf02 100644 --- a/client/src/hooks/useDeleteMemberAction.tsx +++ b/client/src/hooks/useDeleteMemberAction.tsx @@ -2,7 +2,6 @@ import type {MemberAction} from 'types/serviceType'; import {useState} from 'react'; -import {useToast} from '@components/Toast/ToastProvider'; import {requestDeleteMemberAction} from '@apis/request/member'; import {useStepList} from '@hooks/useStepList'; @@ -11,14 +10,22 @@ import {useFetch} from '@apis/useFetch'; import getEventIdByUrl from '@utils/getEventIdByUrl'; -const useDeleteMemberAction = ( - memberActionList: MemberAction[], - setIsBottomSheetOpened: React.Dispatch>, -) => { +type UseDeleteMemberActionProps = { + memberActionList: MemberAction[]; + setIsBottomSheetOpened: React.Dispatch>; + showToastAlreadyExistMemberAction: () => void; + showToastExistSameMemberFromAfterStep: (name: string) => void; +}; + +const useDeleteMemberAction = ({ + memberActionList, + setIsBottomSheetOpened, + showToastAlreadyExistMemberAction, + showToastExistSameMemberFromAfterStep, +}: UseDeleteMemberActionProps) => { const {stepList, refreshStepList} = useStepList(); const [aliveActionList, setAliveActionList] = useState(memberActionList); const eventId = getEventIdByUrl(); - const {showToast} = useToast(); const {fetch} = useFetch(); const deleteMemberAction = async (actionId: number) => { @@ -46,36 +53,24 @@ const useDeleteMemberAction = ( } }; - const addDeleteMemberAction = (memberAction: MemberAction) => { + const checkAlreadyExistMemberAction = (memberAction: MemberAction, showToast: () => void) => { if (!memberActionList.includes(memberAction)) { - showToast({ - isClickToClose: true, - showingTime: 3000, - message: '이미 삭제된 인원입니다.', - type: 'error', - bottom: '160px', - }); - return; + showToast(); } + }; + const checkExistSameMemberFromAfterStep = (memberAction: MemberAction, showToast: () => void) => { if (isExistSameMemberFromAfterStep(memberAction)) { - showToast({ - isClickToClose: true, - showingTime: 3000, - message: `이후의 ${memberAction.name}가 사라져요`, - type: 'error', - position: 'top', - top: '30px', - style: { - zIndex: 9000, - }, - }); + showToast(); } + }; + const addDeleteMemberAction = (memberAction: MemberAction) => { + checkAlreadyExistMemberAction(memberAction, showToastAlreadyExistMemberAction); + checkExistSameMemberFromAfterStep(memberAction, () => showToastExistSameMemberFromAfterStep(memberAction.name)); setAliveActionList(prev => prev.filter(aliveMember => aliveMember.actionId !== memberAction.actionId)); }; - // 현재 선택된 액션의 인덱스를 구해서 뒤의 동일인물의 액션이 있는지를 파악하는 기능 const isExistSameMemberFromAfterStep = (memberAction: MemberAction) => { const memberActionList = stepList .filter(step => step.type !== 'BILL') diff --git a/client/src/hooks/useSearchInMemberList.ts b/client/src/hooks/useSearchInMemberList.ts index 66e65b887..04514d689 100644 --- a/client/src/hooks/useSearchInMemberList.ts +++ b/client/src/hooks/useSearchInMemberList.ts @@ -23,7 +23,7 @@ const useSearchInMemberList = ( const [currentInputIndex, setCurrentInputIndex] = useState(-1); // 서버에서 가져온 전체 리스트 - const [currentInMemberList, setCurrentInMemberList] = useState>([]); + const [currentInMemberList, setCurrentInMemberList] = useState>([]); // 검색된 리스트 (따로 둔 이유는 검색 후 클릭했을 때 리스트를 비워주어야하기 때문) const [filteredInMemberList, setFilteredInMemberList] = useState>([]); @@ -31,7 +31,7 @@ const useSearchInMemberList = ( useEffect(() => { const getCurrentInMembers = async () => { const currentInMemberListFromServer = await fetch({queryFunction: () => requestGetCurrentInMemberList(eventId)}); - setCurrentInMemberList(currentInMemberListFromServer.members); + setCurrentInMemberList(currentInMemberListFromServer.memberNames); }; getCurrentInMembers(); @@ -40,11 +40,9 @@ const useSearchInMemberList = ( const filterMatchItems = (keyword: string) => { if (keyword.trim() === '') return []; - const MatchItems = currentInMemberList.map(({name}) => name); - - return MatchItems.filter( - matchItem => matchItem.toLocaleLowerCase().indexOf(keyword.toLocaleLowerCase()) > -1, - ).slice(0, 3); + return currentInMemberList + .filter(member => member.toLocaleLowerCase().indexOf(keyword.toLocaleLowerCase()) > -1) + .slice(0, 3); }; const chooseMember = (inputIndex: number, name: string) => { diff --git a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx index 200642f30..d9c4f6bce 100644 --- a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx +++ b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx @@ -1,4 +1,3 @@ -import {useEffect, useState} from 'react'; import {useLocation, useNavigate} from 'react-router-dom'; import {Button, FixedButton, Flex, Input, MainLayout, Text, Title, TopNav} from 'haengdong-design'; import {CopyToClipboard} from 'react-copy-to-clipboard'; @@ -6,28 +5,20 @@ import {css} from '@emotion/react'; import {useToast} from '@components/Toast/ToastProvider'; +import getEventPageUrlByEnvironment from '@utils/getEventPageUrlByEnvironment'; + import {ROUTER_URLS} from '@constants/routerUrls'; const CompleteCreateEventPage = () => { const navigate = useNavigate(); const location = useLocation(); - const [url, setUrl] = useState(''); - useEffect(() => { - const getUrl = async () => { - // TODO: (@weadie) eventId를 location에서 불러오는 로직 함수로 분리해서 재사용 - const params = new URLSearchParams(location.search); - const eventId = params.get('eventId'); - // TODO: (@weadie) eventId가 없는 경우에 대한 처리 필요 - setUrl(eventId ?? ''); - }; - getUrl(); - }, []); + const params = new URLSearchParams(location.search); + const eventId = params.get('eventId'); const {showToast} = useToast(); - const env = process.env.NODE_ENV || ''; - const homeUrlByEnvironment = `https://${env.includes('development') ? 'dev.' : ''}haengdong.pro${ROUTER_URLS.event}/${url}/home`; + const homePageUrl = getEventPageUrlByEnvironment(url, 'home'); return ( @@ -41,10 +32,10 @@ const CompleteCreateEventPage = () => { 링크가 없으면 페이지에 접근할 수 없어요. 관리를 위해서 행사 링크를 복사 후 보관해 주세요. - + showToast({ showingTime: 3000, @@ -61,7 +52,7 @@ const CompleteCreateEventPage = () => {
- navigate(`${ROUTER_URLS.event}/${url}/admin`)}>관리 페이지로 이동 + navigate(`${ROUTER_URLS.event}/${eventId}/admin`)}>관리 페이지로 이동 ); }; diff --git a/client/src/pages/CreateEventPage/SetEventNamePage.tsx b/client/src/pages/CreateEventPage/SetEventNamePage.tsx index ef0417d8a..68f57a242 100644 --- a/client/src/pages/CreateEventPage/SetEventNamePage.tsx +++ b/client/src/pages/CreateEventPage/SetEventNamePage.tsx @@ -4,8 +4,6 @@ import {FixedButton, MainLayout, LabelInput, Title, TopNav, Back} from 'haengdon import validateEventName from '@utils/validate/validateEventName'; -import useEvent from '@hooks/useEvent'; - import {ROUTER_URLS} from '@constants/routerUrls'; const SetEventNamePage = () => { @@ -13,7 +11,6 @@ const SetEventNamePage = () => { const [errorMessage, setErrorMessage] = useState(''); const [canSubmit, setCanSubmit] = useState(false); const navigate = useNavigate(); - const {createNewEvent} = useEvent(); const submitEventName = async (event: React.FormEvent) => { event.preventDefault(); diff --git a/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx b/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx index 04c8dd760..a3572d24d 100644 --- a/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx +++ b/client/src/pages/CreateEventPage/SetEventPasswordPage.tsx @@ -2,60 +2,38 @@ import {useEffect, useState} from 'react'; import {useLocation, useNavigate} from 'react-router-dom'; import {FixedButton, MainLayout, LabelInput, Title, TopNav, Back} from 'haengdong-design'; -import validateEventPassword from '@utils/validate/validateEventPassword'; -import {requestPostNewEvent} from '@apis/request/event'; - -import useEvent from '@hooks/useEvent'; +import useSetPassword from '@hooks/useSetPassword'; import RULE from '@constants/rule'; import {ROUTER_URLS} from '@constants/routerUrls'; const SetEventPasswordPage = () => { - const [eventName, setEventName] = useState(''); - const [password, setPassword] = useState(''); - const [errorMessage, setErrorMessage] = useState(''); - const [canSubmit, setCanSubmit] = useState(false); - const {createNewEvent} = useEvent(); const navigate = useNavigate(); const location = useLocation(); useEffect(() => { if (!location.state) { navigate(ROUTER_URLS.main); - } else { - setEventName(location.state.eventName); } - }, []); + }, [location.state]); - const submitPassword = async (event: React.FormEvent) => { - event.preventDefault(); + const {password, errorMessage, canSubmit, submitPassword, handlePasswordChange} = useSetPassword( + location.state?.eventName, + ); - const {eventId} = await createNewEvent({eventName, password: parseInt(password)}); + const onSubmit = async (event: React.FormEvent) => { + const eventId = await submitPassword(event); navigate(`${ROUTER_URLS.eventCreateComplete}?${new URLSearchParams({eventId})}`); }; - const handleChange = (event: React.ChangeEvent) => { - const newValue = event.target.value; - const validation = validateEventPassword(newValue); - - setCanSubmit(newValue.length === RULE.maxEventPasswordLength); - - if (validation.isValid) { - setPassword(newValue); - setErrorMessage(''); - } else { - event.target.value = password; - setErrorMessage(validation.errorMessage ?? ''); - } - }; return ( - <form onSubmit={submitPassword} style={{padding: '0 1rem'}}> + <form onSubmit={onSubmit} style={{padding: '0 1rem'}}> <LabelInput labelText="비밀번호" errorText={errorMessage} @@ -63,10 +41,10 @@ const SetEventPasswordPage = () => { type="secret" maxLength={RULE.maxEventPasswordLength} placeholder="비밀번호" - onChange={e => handleChange(e)} + onChange={handlePasswordChange} isError={!!errorMessage} autoFocus - ></LabelInput> + /> <FixedButton disabled={!canSubmit}>행동 개시!</FixedButton> </form> </MainLayout> diff --git a/client/src/pages/CreateEventPage/index.ts b/client/src/pages/CreateEventPage/index.ts index 9b66c3ba5..6d3d6c808 100644 --- a/client/src/pages/CreateEventPage/index.ts +++ b/client/src/pages/CreateEventPage/index.ts @@ -1,2 +1,3 @@ export {default as SetEventNamePage} from './SetEventNamePage'; +export {default as SetEventPasswordPage} from './SetEventPasswordPage'; export {default as CompleteCreateEventPage} from './CompleteCreateEventPage'; diff --git a/client/src/pages/EventPage/AdminPage/AdminPage.tsx b/client/src/pages/EventPage/AdminPage/AdminPage.tsx index a1ea053d9..ac0d85c05 100644 --- a/client/src/pages/EventPage/AdminPage/AdminPage.tsx +++ b/client/src/pages/EventPage/AdminPage/AdminPage.tsx @@ -36,8 +36,9 @@ const AdminPage = () => { const getTitleDescriptionByInitialMemberSetting = () => { return allMemberList.length > 0 - ? '“행동 추가하기” 버튼을 눌러서 지출 내역 및 인원 변동사항을 추가해 주세요.' - : '“초기인원 설정하기” 버튼을 눌러서 행사 초기 인원을 설정해 주세요.'; + ? `지출 내역 및 인원 변동을 추가해 주세요. + 인원 변동을 기준으로 몇 차인지 나뉘어져요.` + : '“시작 인원 추가” 버튼을 눌러 행사의 시작부터 참여하는 사람들의 이름을 입력해 주세요.'; }; return ( @@ -55,7 +56,7 @@ const AdminPage = () => { <section css={receiptStyle}> <StepList /> <FixedButton - children={allMemberList.length === 0 ? '초기인원 설정하기' : '행동 추가하기'} + children={allMemberList.length === 0 ? '시작인원 추가하기' : '행동 추가하기'} onClick={() => setIsOpenFixedBottomBottomSheet(prev => !prev)} /> {isOpenFixedButtonBottomSheet && ( diff --git a/client/src/pages/EventPage/EventPageLayout.tsx b/client/src/pages/EventPage/EventPageLayout.tsx index 13a6aa67a..771422087 100644 --- a/client/src/pages/EventPage/EventPageLayout.tsx +++ b/client/src/pages/EventPage/EventPageLayout.tsx @@ -10,6 +10,7 @@ import useNavSwitch from '@hooks/useNavSwitch'; import StepListProvider from '@hooks/useStepList'; import getEventIdByUrl from '@utils/getEventIdByUrl'; +import getEventPageUrlByEnvironment from '@utils/getEventPageUrlByEnvironment'; import {ROUTER_URLS} from '@constants/routerUrls'; @@ -44,8 +45,7 @@ const EventPageLayout = () => { }; const {showToast} = useToast(); - - const env = process.env.NODE_ENV || ''; + const url = getEventPageUrlByEnvironment(eventId, 'home'); return ( <StepListProvider> @@ -53,7 +53,7 @@ const EventPageLayout = () => { <TopNav> <Switch value={nav} values={paths} onChange={onChange} /> <CopyToClipboard - text={`[행동대장]\n"${eventName}"에 대한 정산을 시작할게요:)\n아래 링크에 접속해서 정산 내역을 확인해 주세요!\nhttps://${env.includes('development') ? 'dev.' : ''}haengdong.pro${ROUTER_URLS.event}/${eventId}/home`} + text={`[행동대장]\n"${eventName}"에 대한 정산을 시작할게요:)\n아래 링크에 접속해서 정산 내역을 확인해 주세요!\n${url}`} onCopy={() => showToast({ showingTime: 3000, diff --git a/client/src/router.tsx b/client/src/router.tsx index 52d6da760..a6af2184d 100644 --- a/client/src/router.tsx +++ b/client/src/router.tsx @@ -3,10 +3,9 @@ import {createBrowserRouter} from 'react-router-dom'; import {AdminPage} from '@pages/EventPage/AdminPage'; import {HomePage} from '@pages/EventPage/HomePage'; import ErrorPage from '@pages/ErrorPage/ErrorPage'; -import SetEventPasswordPage from '@pages/CreateEventPage/SetEventPasswordPage'; import EventLoginPage from '@pages/EventPage/AdminPage/EventLoginPage'; -import {CompleteCreateEventPage, SetEventNamePage} from '@pages/CreateEventPage'; +import {CompleteCreateEventPage, SetEventNamePage, SetEventPasswordPage} from '@pages/CreateEventPage'; import {MainPage} from '@pages/MainPage'; import {EventPage} from '@pages/EventPage'; diff --git a/client/src/utils/getEventPageUrlByEnvironment.ts b/client/src/utils/getEventPageUrlByEnvironment.ts new file mode 100644 index 000000000..bcfce54e9 --- /dev/null +++ b/client/src/utils/getEventPageUrlByEnvironment.ts @@ -0,0 +1,11 @@ +import {ROUTER_URLS} from '@constants/routerUrls'; + +type EventPageTab = 'home' | 'admin'; + +const getEventPageUrlByEnvironment = (eventId: string, tab: EventPageTab) => { + const isDevelopment = process.env.NODE_ENV === 'development'; + + return `https://${isDevelopment ? 'dev.' : ''}haengdong.pro${ROUTER_URLS.event}/${eventId}/${tab}`; +}; + +export default getEventPageUrlByEnvironment;