diff --git a/HDesign/package-lock.json b/HDesign/package-lock.json index f465f5c3a..0faaccf1d 100644 --- a/HDesign/package-lock.json +++ b/HDesign/package-lock.json @@ -1,12 +1,12 @@ { "name": "haengdong-design", - "version": "0.1.60", + "version": "0.1.65", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "haengdong-design", - "version": "0.1.60", + "version": "0.1.65", "license": "ISC", "dependencies": { "@emotion/react": "^11.11.4", diff --git a/HDesign/package.json b/HDesign/package.json index 737f07b5b..b0ad0c465 100644 --- a/HDesign/package.json +++ b/HDesign/package.json @@ -1,6 +1,6 @@ { "name": "haengdong-design", - "version": "0.1.60", + "version": "0.1.65", "description": "", "main": "./dist/index.js", "module": "./dist/index.js", diff --git a/HDesign/src/components/DragHandleItem/DragHandleItem.style.ts b/HDesign/src/components/DragHandleItem/DragHandleItem.style.ts index 117ae93c2..c23166272 100644 --- a/HDesign/src/components/DragHandleItem/DragHandleItem.style.ts +++ b/HDesign/src/components/DragHandleItem/DragHandleItem.style.ts @@ -18,8 +18,3 @@ export const dragHandlerStyle = css({ gap: '0.25rem', width: '100%', }); - -export const textStyle = (theme: Theme) => - css({ - color: theme.colors.black, - }); diff --git a/HDesign/src/components/DragHandleItem/DragHandleItem.tsx b/HDesign/src/components/DragHandleItem/DragHandleItem.tsx index 1a2df5471..df6ac2b27 100644 --- a/HDesign/src/components/DragHandleItem/DragHandleItem.tsx +++ b/HDesign/src/components/DragHandleItem/DragHandleItem.tsx @@ -9,7 +9,7 @@ import {useTheme} from '@theme/HDesignProvider'; import IconButton from '../IconButton/IconButton'; -import {dragHandleItemStyle, dragHandlerStyle, textStyle} from './DragHandleItem.style'; +import {dragHandleItemStyle, dragHandlerStyle} from './DragHandleItem.style'; import {DragHandleItemProps} from './DragHandleItem.type'; export const DragHandleItem = ({ @@ -31,12 +31,8 @@ export const DragHandleItem = ({ )} - - {prefix} - - - {suffix} - + {prefix} + {suffix} diff --git a/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.style.ts b/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.style.ts index d1a74247f..0054817fa 100644 --- a/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.style.ts +++ b/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.style.ts @@ -13,25 +13,3 @@ export const containerStyle = (theme: Theme, backgroundColor: ColorKeys) => borderRadius: '0.75rem', backgroundColor: theme.colors[backgroundColor], }); - -export const topLeftStyle = (theme: Theme) => - css({ - color: theme.colors.gray, - }); - -//TODO: (@todari) : 추후 클릭 기능을 넣었을 때 underline -export const topRightStyle = (theme: Theme) => - css({ - color: theme.colors.gray, - // textDecoration: 'underline', - }); - -export const bottomLeftTextStyle = (theme: Theme) => - css({ - color: theme.colors.gray, - }); - -export const bottomRightTextStyle = (theme: Theme) => - css({ - color: theme.colors.gray, - }); diff --git a/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.tsx b/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.tsx index b255f3391..c439b98c2 100644 --- a/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.tsx +++ b/HDesign/src/components/DragHandleItemContainer/DragHandleItemContainer.tsx @@ -4,13 +4,7 @@ import {useTheme} from '@theme/HDesignProvider'; import Text from '../Text/Text'; import Flex from '../Flex/Flex'; -import { - bottomLeftTextStyle, - bottomRightTextStyle, - containerStyle, - topLeftStyle, - topRightStyle, -} from './DragHandleItemContainer.style'; +import {containerStyle} from './DragHandleItemContainer.style'; import {DragHandleItemContainerProps} from './DragHandleItemContainer.type'; export const DragHandleItemContainer: React.FC = ({ @@ -27,19 +21,19 @@ export const DragHandleItemContainer: React.FC = ( return (
- + {topLeftText} - + {topRightText} {children} - + {bottomLeftText} - + {bottomRightText} diff --git a/HDesign/src/components/ExpenseList/ExpenseList.style.ts b/HDesign/src/components/ExpenseList/ExpenseList.style.ts index dd05c067f..8dbd227aa 100644 --- a/HDesign/src/components/ExpenseList/ExpenseList.style.ts +++ b/HDesign/src/components/ExpenseList/ExpenseList.style.ts @@ -19,11 +19,6 @@ export const expenseItemLeftStyle = () => gap: '1rem', }); -export const TextStyle = (theme: Theme) => - css({ - color: theme.colors.onTertiary, - }); - export const expenseListStyle = (theme: Theme) => css({ width: '100%', diff --git a/HDesign/src/components/ExpenseList/ExpenseList.tsx b/HDesign/src/components/ExpenseList/ExpenseList.tsx index 3cc5e8354..1f57810e5 100644 --- a/HDesign/src/components/ExpenseList/ExpenseList.tsx +++ b/HDesign/src/components/ExpenseList/ExpenseList.tsx @@ -6,7 +6,7 @@ import Icon from '@components/Icon/Icon'; import {useTheme} from '@theme/HDesignProvider'; import {ExpenseItemProps, ExpenseListProps} from './ExpenseList.type'; -import {expenseItemStyle, expenseListStyle, expenseItemLeftStyle, TextStyle} from './ExpenseList.style'; +import {expenseItemStyle, expenseListStyle, expenseItemLeftStyle} from './ExpenseList.style'; // TODO: (@soha) 따로 파일 분리할까 고민중.. 여기서만 사용할 것 같긴 한데.. 흠 // TODO: (@todari) : 추후 클릭 시 상호작용이 생기면 iconButton으로 변경할 수 있음 @@ -14,11 +14,9 @@ function ExpenseItem({name, price, ...buttonProps}: ExpenseItemProps) { const {theme} = useTheme(); return ( diff --git a/HDesign/src/components/Input/Input.stories.tsx b/HDesign/src/components/Input/Input.stories.tsx index 31a226771..dc46e022f 100644 --- a/HDesign/src/components/Input/Input.stories.tsx +++ b/HDesign/src/components/Input/Input.stories.tsx @@ -3,6 +3,8 @@ import type {Meta, StoryObj} from '@storybook/react'; import React, {useEffect, useState} from 'react'; import Input from '@components/Input/Input'; +import Flex from '@components/Flex/Flex'; +import Button from '@components/Button/Button'; const meta = { title: 'Components/Input', @@ -26,21 +28,29 @@ type Story = StoryObj; export const Playground: Story = { render: ({...args}) => { + const regex = /^[ㄱ-ㅎ가-힣]*$/; const [value, setValue] = useState(''); const [isError, setIsError] = useState(false); + const handleChange = (event: React.ChangeEvent) => { - if (event.target.value.length < 4) { - setValue(event.target.value); + const newValue = event.target.value; + if (regex.test(newValue)) { + setValue(newValue); setIsError(false); } else { - event.target.value = value; setIsError(true); } }; - const handleBlur = () => { - console.log('blur'); + + const changeRandomValue = () => { + setValue('외부에서 값 변경됨'); }; - return handleChange(e)} isError={isError} onBlur={handleBlur} {...args} />; + return ( +
+ + +
+ ); }, }; diff --git a/HDesign/src/components/Input/Input.tsx b/HDesign/src/components/Input/Input.tsx index a1b495afd..37945e434 100644 --- a/HDesign/src/components/Input/Input.tsx +++ b/HDesign/src/components/Input/Input.tsx @@ -13,7 +13,6 @@ export const Input: React.FC = forwardRef inputRef.current!); const {theme} = useTheme(); const inputRef = useRef(null); const {value, handleChange, hasFocus, handleClickDelete, handleBlur, handleFocus, handleKeyDown} = useInput({ @@ -24,6 +23,7 @@ export const Input: React.FC = forwardRef inputRef.current!); return (
diff --git a/HDesign/src/components/Input/useInput.ts b/HDesign/src/components/Input/useInput.ts index 0ff7206c4..a9e531a07 100644 --- a/HDesign/src/components/Input/useInput.ts +++ b/HDesign/src/components/Input/useInput.ts @@ -9,17 +9,20 @@ interface UseInputProps { autoFocus?: boolean; } -export const useInput = ({propsValue, onChange, onBlur, onFocus, inputRef}: UseInputProps) => { +export const useInput = ({propsValue, onChange, onBlur, onFocus, autoFocus, inputRef}: UseInputProps) => { const [value, setValue] = useState(propsValue); const [hasFocus, setHasFocus] = useState(inputRef.current === document.activeElement); useEffect(() => { - setHasFocus(inputRef.current === document.activeElement); - }, []); + if (autoFocus && inputRef.current) { + inputRef.current.focus(); + setHasFocus(true); + } + }, [autoFocus, inputRef]); useEffect(() => { setValue(propsValue); - }, [value]); + }, [propsValue, value]); const handleClickDelete = (event: React.MouseEvent) => { event.preventDefault(); diff --git a/HDesign/src/components/ListButton/ListButton.style.ts b/HDesign/src/components/ListButton/ListButton.style.ts index ca6a686ff..fcca0ca01 100644 --- a/HDesign/src/components/ListButton/ListButton.style.ts +++ b/HDesign/src/components/ListButton/ListButton.style.ts @@ -14,8 +14,3 @@ export const listButtonStyle = (theme: Theme) => boxShadow: `0 1px 0 0 ${theme.colors.grayContainer} inset `, }); - -export const textStyle = (theme: Theme) => - css({ - color: theme.colors.gray, - }); diff --git a/HDesign/src/components/ListButton/ListButton.tsx b/HDesign/src/components/ListButton/ListButton.tsx index c90bbd122..10c417310 100644 --- a/HDesign/src/components/ListButton/ListButton.tsx +++ b/HDesign/src/components/ListButton/ListButton.tsx @@ -9,7 +9,7 @@ import Icon from '@components/Icon/Icon'; import {useTheme} from '@theme/HDesignProvider'; import {ListButtonProps} from './ListButton.type'; -import {listButtonStyle, textStyle} from './ListButton.style'; +import {listButtonStyle} from './ListButton.style'; export const ListButton: React.FC = forwardRef(function Button( {prefix, suffix, ...htmlProps}: ListButtonProps, @@ -18,11 +18,11 @@ export const ListButton: React.FC = forwardRef - + {prefix} - + {suffix} diff --git a/HDesign/src/components/Search/Search.stories.tsx b/HDesign/src/components/Search/Search.stories.tsx index 2ce93f3a5..6b5d821cd 100644 --- a/HDesign/src/components/Search/Search.stories.tsx +++ b/HDesign/src/components/Search/Search.stories.tsx @@ -11,20 +11,17 @@ const meta = { parameters: { // layout: 'centered', }, - argTypes: { - value: { - description: '', - control: {type: 'text'}, - }, - inputType: { - control: {type: 'radio'}, - }, - }, + decorators: [ + Story => ( +
+ +
+ ), + ], args: { - disabled: false, - placeholder: 'placeholder', - searchTerms: ['todari', 'cookie'], - setState: keyword => console.log(keyword), + isShowTargetInput: true, + matchItems: ['todari', 'cookie'], + onMatchItemClick: keyword => alert(keyword), }, } satisfies Meta; diff --git a/HDesign/src/components/Search/Search.style.ts b/HDesign/src/components/Search/Search.style.ts index 0c5203aed..d75e8fe25 100644 --- a/HDesign/src/components/Search/Search.style.ts +++ b/HDesign/src/components/Search/Search.style.ts @@ -12,21 +12,23 @@ export const searchTermsStyle = (theme: Theme) => css({ position: 'absolute', top: '3.5rem', + zIndex: 1, - width: 'calc(100% - 2rem)', - margin: '0 1rem', - padding: '0.5rem 0', + width: '100%', + padding: '0.5rem 1rem', borderRadius: '1rem', backgroundColor: theme.colors.white, + + boxShadow: '0 0.25rem 0.5rem 0 rgba(0, 0, 0, 0.12)', }); export const searchTermStyle = (theme: Theme) => css( { width: '100%', - padding: '0.5rem 1rem', + padding: '0.5rem', color: theme.colors.onTertiary, diff --git a/HDesign/src/components/Search/Search.tsx b/HDesign/src/components/Search/Search.tsx index cecdffa52..410a0bc97 100644 --- a/HDesign/src/components/Search/Search.tsx +++ b/HDesign/src/components/Search/Search.tsx @@ -3,34 +3,27 @@ import Flex from '@components/Flex/Flex'; import {useTheme} from '@theme/HDesignProvider'; -import Input from '../Input/Input'; -import {InputProps} from '../Input/Input.type'; - import {searchStyle, searchTermsStyle, searchTermStyle} from './Search.style'; -import useSearch from './useSearch'; -export interface SearchProps extends InputProps { - searchTerms: string[]; - setState: React.Dispatch>; +export interface SearchProps { + isShowTargetInput: boolean; + matchItems: string[]; + onMatchItemClick: (term: string) => void; } -const Search: React.FC = ({searchTerms, setState, ...inputProps}: SearchProps) => { +const Search = ({isShowTargetInput, matchItems, onMatchItemClick, children}: React.PropsWithChildren) => { const {theme} = useTheme(); - const {value, showSearchTerms, handleOnChange, handleOnClick, filterSearchTerms} = useSearch({ - searchTerms, - setState, - }); return (
- - {showSearchTerms && ( + {children} + {matchItems.length > 0 && isShowTargetInput && (
    - {filterSearchTerms(value).map((searchTerm, index) => ( -
  • -
  • ))} diff --git a/HDesign/src/components/Search/useSearch.ts b/HDesign/src/components/Search/useSearch.ts deleted file mode 100644 index fbb9cf4c3..000000000 --- a/HDesign/src/components/Search/useSearch.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {useState} from 'react'; - -interface UseSearchProps { - searchTerms: string[]; - setState: React.Dispatch>; -} - -const useSearch = ({searchTerms, setState}: UseSearchProps) => { - const [value, setValue] = useState(''); - const [showSearchTerms, setShowSearchTerms] = useState(false); - - const handleOnClick = (searchTerm: string) => { - setValue(searchTerm); - setState(searchTerm); - setShowSearchTerms(false); - }; - - const handleOnChange = (event: React.ChangeEvent) => { - const {value} = event.target; - setValue(value); - setShowSearchTerms(value.trim() !== '' && filterSearchTerms(value).length !== 0); - }; - - const filterSearchTerms = (keyword: string) => { - if (keyword.trim() === '') return []; - - return searchTerms.filter(terms => terms.toLocaleLowerCase().indexOf(keyword.toLocaleLowerCase()) > -1); - }; - - return { - value, - showSearchTerms, - handleOnClick, - handleOnChange, - filterSearchTerms, - }; -}; - -export default useSearch; diff --git a/HDesign/src/components/Text/Text.style.ts b/HDesign/src/components/Text/Text.style.ts index 8767cdb0f..6560c8274 100644 --- a/HDesign/src/components/Text/Text.style.ts +++ b/HDesign/src/components/Text/Text.style.ts @@ -1,11 +1,11 @@ -import type {TextProps} from './Text.type'; +import type {TextStylePropsWithTheme} from './Text.type'; import {css} from '@emotion/react'; // TODO: (@todari) themeProvider 이용하도록 변경 import TYPOGRAPHY from '@token/typography'; -export const getSizeStyling = (size: Required['size']) => { +export const getSizeStyling = ({size, textColor, theme}: Required) => { const style = { head: css(TYPOGRAPHY.head), title: css(TYPOGRAPHY.title), @@ -19,5 +19,7 @@ export const getSizeStyling = (size: Required['size']) => { tiny: css(TYPOGRAPHY.tiny), }; - return style[size]; + const colorStyle = css({color: theme.colors[textColor]}); + + return [style[size], colorStyle]; }; diff --git a/HDesign/src/components/Text/Text.tsx b/HDesign/src/components/Text/Text.tsx index e0e2a8ec8..9b7853aa8 100644 --- a/HDesign/src/components/Text/Text.tsx +++ b/HDesign/src/components/Text/Text.tsx @@ -3,11 +3,14 @@ import type {TextProps} from '@components/Text/Text.type'; import React from 'react'; +import {useTheme} from '@theme/HDesignProvider'; + import {getSizeStyling} from './Text.style'; -const Text: React.FC = ({size = 'body', children, ...attributes}: TextProps) => { +const Text: React.FC = ({size = 'body', textColor = 'black', children, ...attributes}: TextProps) => { + const {theme} = useTheme(); return ( -

    +

    {children}

    ); diff --git a/HDesign/src/components/Text/Text.type.ts b/HDesign/src/components/Text/Text.type.ts index db75b09be..157591abf 100644 --- a/HDesign/src/components/Text/Text.type.ts +++ b/HDesign/src/components/Text/Text.type.ts @@ -1,3 +1,7 @@ +import {Theme} from '@theme/theme.type'; + +import {ColorKeys} from '@token/colors'; + export type TextSize = | 'head' | 'title' @@ -12,6 +16,11 @@ export type TextSize = export interface TextStyleProps { size?: TextSize; + textColor?: ColorKeys; +} + +export interface TextStylePropsWithTheme extends TextStyleProps { + theme: Theme; } export interface TextCustomProps {} diff --git a/HDesign/src/components/TextButton/TextButton.style.ts b/HDesign/src/components/TextButton/TextButton.style.ts index c9ec31a4e..c8d8738fe 100644 --- a/HDesign/src/components/TextButton/TextButton.style.ts +++ b/HDesign/src/components/TextButton/TextButton.style.ts @@ -2,14 +2,9 @@ import {css} from '@emotion/react'; import {Theme} from '@theme/theme.type'; -import {TextColor} from './TextButton.type'; +import {ColorKeys} from '@token/colors'; interface TextButtonStyleProps { - textColor: TextColor; + textColor: ColorKeys; theme: Theme; } - -export const textButtonStyle = ({textColor, theme}: TextButtonStyleProps) => - css({ - color: theme.colors[textColor], - }); diff --git a/HDesign/src/components/TextButton/TextButton.tsx b/HDesign/src/components/TextButton/TextButton.tsx index c211f05e4..9e73667d5 100644 --- a/HDesign/src/components/TextButton/TextButton.tsx +++ b/HDesign/src/components/TextButton/TextButton.tsx @@ -6,7 +6,6 @@ import {useTheme} from '@theme/HDesignProvider'; import Text from '../Text/Text'; import {TextButtonProps} from './TextButton.type'; -import {textButtonStyle} from './TextButton.style'; export const TextButton: React.FC = forwardRef(function Button( {textColor, textSize, children, ...htmlProps}: TextButtonProps, @@ -16,7 +15,7 @@ export const TextButton: React.FC = forwardRef - + {children} diff --git a/HDesign/src/components/Title/Title.style.ts b/HDesign/src/components/Title/Title.style.ts index 700128c02..ab347a214 100644 --- a/HDesign/src/components/Title/Title.style.ts +++ b/HDesign/src/components/Title/Title.style.ts @@ -12,28 +12,8 @@ export const titleContainerStyle = (theme: Theme) => padding: '1rem', }); -export const titleStyle = (theme: Theme) => - css({ - color: theme.colors.black, - }); - -export const descriptionStyle = (theme: Theme) => - css({ - color: theme.colors.darkGray, - }); - export const priceContainerStyle = css({ display: 'flex', justifyContent: 'space-between', alignItems: 'end', }); - -export const priceTitleStyle = (theme: Theme) => - css({ - color: theme.colors.gray, - }); - -export const priceStyle = (theme: Theme) => - css({ - color: theme.colors.black, - }); diff --git a/HDesign/src/components/Title/Title.tsx b/HDesign/src/components/Title/Title.tsx index ee7847a0e..759588b5e 100644 --- a/HDesign/src/components/Title/Title.tsx +++ b/HDesign/src/components/Title/Title.tsx @@ -1,13 +1,6 @@ /** @jsxImportSource @emotion/react */ import Text from '@components/Text/Text'; -import { - descriptionStyle, - priceContainerStyle, - priceStyle, - priceTitleStyle, - titleContainerStyle, - titleStyle, -} from '@components/Title/Title.style'; +import {priceContainerStyle, titleContainerStyle} from '@components/Title/Title.style'; import {TitleProps} from '@components/Title/Title.type'; import {useTheme} from '@theme/HDesignProvider'; @@ -16,20 +9,18 @@ export const Title: React.FC = ({title, description, price}: TitlePr const {theme} = useTheme(); return (
    - - {title} - + {title} {description && ( - + {description} )} {price !== undefined && (
    - + 전체 지출 금액 - {price.toLocaleString('ko-kr')}원 + {price.toLocaleString('ko-kr')}원
    )}
    diff --git a/client/package-lock.json b/client/package-lock.json index ef30d2408..e041d7df0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,8 +11,9 @@ "dependencies": { "@emotion/react": "^11.11.4", "@sentry/react": "^8.24.0", - "haengdong-design": "^0.1.60", + "haengdong-design": "^0.1.65", "react": "^18.3.1", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", "react-error-boundary": "^4.0.13", "react-router-dom": "^6.24.1" @@ -5086,6 +5087,14 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js-compat": { "version": "3.38.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz", @@ -7231,9 +7240,9 @@ } }, "node_modules/haengdong-design": { - "version": "0.1.60", - "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.60.tgz", - "integrity": "sha512-XZ8Dtsg9s3WAiQ3XCGNbSjMUOkH0yw1HYvjkmp/BgwErMhwVOH5QlpM9O7jsiSf9p08foS+K2w9Xqtg2pMWZFg==", + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.65.tgz", + "integrity": "sha512-zRLb6U2dbN9eBABl9e+7ggSdbAqy/4Bh863BZ0NiLM7ESTktrg8bkJYDUi0j42W4e+1owctA34eNc60FevlKZg==", "dependencies": { "@emotion/react": "^11.11.4", "@storybook/addon-webpack5-compiler-swc": "^1.0.5", @@ -9043,7 +9052,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9594,7 +9602,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -9738,6 +9745,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -11074,6 +11093,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/client/package.json b/client/package.json index cfcf40b9a..739d0df4c 100644 --- a/client/package.json +++ b/client/package.json @@ -51,8 +51,9 @@ "dependencies": { "@emotion/react": "^11.11.4", "@sentry/react": "^8.24.0", - "haengdong-design": "^0.1.60", + "haengdong-design": "^0.1.65", "react": "^18.3.1", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", "react-error-boundary": "^4.0.13", "react-router-dom": "^6.24.1" diff --git a/client/src/apis/request/member.ts b/client/src/apis/request/member.ts index 2552de857..4bd6e1833 100644 --- a/client/src/apis/request/member.ts +++ b/client/src/apis/request/member.ts @@ -2,7 +2,7 @@ import type {MemberType} from 'types/serviceType'; import {BASE_URL} from '@apis/baseUrl'; import {TEMP_PREFIX} from '@apis/tempPrefix'; -import {requestPost, requestDelete} from '@apis/fetcher'; +import {requestPost, requestDelete, requestGet, requestPut} from '@apis/fetcher'; import {WithEventId} from '@apis/withEventId.type'; type RequestPostMemberList = { @@ -31,3 +31,43 @@ export const requestDeleteMemberAction = async ({eventId, actionId}: WithEventId endpoint: `${TEMP_PREFIX}/${eventId}/member-actions/${actionId}`, }); }; + +type ResponseGetAllMemberList = { + memberNames: string[]; +}; + +export const requestGetAllMemberList = async ({eventId}: WithEventId) => { + return requestGet({ + endpoint: `${TEMP_PREFIX}/${eventId}/members`, + }); +}; + +export type MemberChange = { + before: string; + after: string; +}; + +type RequestPutAllMemberList = { + members: MemberChange[]; +}; + +export const requestPutAllMemberList = async ({eventId, members}: WithEventId) => { + await requestPut({ + baseUrl: BASE_URL.HD, + endpoint: `${TEMP_PREFIX}/${eventId}/members/nameChange`, + body: { + members, + }, + }); +}; + +type RequestDeleteAllMemberList = { + memberName: string; +}; + +export const requestDeleteAllMemberList = async ({eventId, memberName}: WithEventId) => { + await requestDelete({ + baseUrl: BASE_URL.HD, + endpoint: `${TEMP_PREFIX}/${eventId}/members/${memberName}`, + }); +}; diff --git a/client/src/components/InputAndDeleteButton/InputAndDeleteButton.style.ts b/client/src/components/InputAndDeleteButton/InputAndDeleteButton.style.ts deleted file mode 100644 index ff9f6cf6e..000000000 --- a/client/src/components/InputAndDeleteButton/InputAndDeleteButton.style.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {css} from '@emotion/react'; - -export const InputAndDeleteButtonContainer = () => - css({ - display: 'flex', - alignItems: 'center', - width: '100%', - gap: '1rem', - }); diff --git a/client/src/components/InputAndDeleteButton/InputAndDeleteButton.tsx b/client/src/components/InputAndDeleteButton/InputAndDeleteButton.tsx deleted file mode 100644 index 5ce491099..000000000 --- a/client/src/components/InputAndDeleteButton/InputAndDeleteButton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import {Icon, IconButton, LabelGroupInput} from 'haengdong-design'; - -import {InputAndDeleteButtonContainer} from './InputAndDeleteButton.style'; - -const InputAndDeleteButton = () => { - return ( -
    -
    - -
    - - - -
    - ); -}; - -export default InputAndDeleteButton; diff --git a/client/src/components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount.tsx b/client/src/components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount.tsx index 83e6c58f1..4799a92c2 100644 --- a/client/src/components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount.tsx +++ b/client/src/components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount.tsx @@ -1,7 +1,7 @@ import {SetAllMemberListModal, SetInitialMemberListModal, SetActionListModal} from '@components/Modal/index'; interface ModalBasedOnMemberCountProps { - memberNameList: string[]; + allMemberList: string[]; isOpenBottomSheet: boolean; isOpenAllMemberListButton: boolean; setOrder: React.Dispatch>; @@ -10,7 +10,7 @@ interface ModalBasedOnMemberCountProps { } const ModalBasedOnMemberCount = ({ - memberNameList, + allMemberList, isOpenBottomSheet, isOpenAllMemberListButton, setOrder, @@ -20,13 +20,14 @@ const ModalBasedOnMemberCount = ({ if (isOpenAllMemberListButton) { return ( ); } - switch (memberNameList.length) { + switch (allMemberList.length) { case 0: return ( diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts index 6162d55b5..7fad2e001 100644 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts +++ b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts @@ -1,21 +1,19 @@ import {css} from '@emotion/react'; -const container = () => - css({ - display: 'flex', - flexDirection: 'column', - gap: '1.5rem', - height: '100%', - }); +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 inputGroup = css({ + display: 'flex', + flexDirection: 'column', + gap: '1rem', + overflow: 'auto', + paddingBottom: '14rem', +}); const addMemberActionListModalContentStyle = { container, diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx index b88befbb5..8d84993ab 100644 --- a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx +++ b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx @@ -8,6 +8,8 @@ import validateMemberName from '@utils/validate/validateMemberName'; import useDynamicInput from '@hooks/useDynamicInput'; import style from './AddMemberActionListModalContent.style'; +import InMember from './InMember'; +import OutMember from './OutMember'; interface AddMemberActionListModalContentProps { inOutAction: MemberType; @@ -15,16 +17,8 @@ interface AddMemberActionListModalContentProps { } const AddMemberActionListModalContent = ({inOutAction, setIsOpenBottomSheet}: AddMemberActionListModalContentProps) => { - const { - inputList, - inputRefList, - handleInputChange, - deleteEmptyInputElementOnBlur, - getFilledInputList, - errorMessage, - canSubmit, - focusNextInputOnEnter, - } = useDynamicInput(validateMemberName); + const dynamicProps = useDynamicInput(validateMemberName); + const {inputList, getFilledInputList, errorMessage, canSubmit} = dynamicProps; const {updateMemberList} = useStepList(); @@ -36,21 +30,8 @@ const AddMemberActionListModalContent = ({inOutAction, setIsOpenBottomSheet}: Ad return (
    - {/* TODO: (@soha) Search로 변경하기 */} - {inputList.map(({value, index}) => ( - (inputRefList.current[index] = el)} - onChange={e => handleInputChange(index, e)} - onBlur={() => deleteEmptyInputElementOnBlur()} - onKeyDown={e => focusNextInputOnEnter(e, index)} - placeholder="이름" - /> - ))} + {inOutAction === 'IN' ? : }
    { + const {inputList, inputRefList, handleInputChange, deleteEmptyInputElementOnBlur, focusNextInputOnEnter} = + dynamicProps; + return inputList.map(({value, index}) => ( + (inputRefList.current[index] = el)} + onChange={e => handleInputChange(index, e)} + onBlur={() => deleteEmptyInputElementOnBlur()} + onKeyDown={e => focusNextInputOnEnter(e, index)} + placeholder="이름" + /> + )); +}; + +export default InMember; diff --git a/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx new file mode 100644 index 000000000..fa2ac61d1 --- /dev/null +++ b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/OutMember.tsx @@ -0,0 +1,49 @@ +import {LabelGroupInput, Search} from 'haengdong-design'; + +import {ReturnUseDynamicInput} from '@hooks/useDynamicInput'; +import useSearchInMemberList from '@hooks/useSearchInMemberList'; + +interface OutMemberProps { + dynamicProps: ReturnUseDynamicInput; +} + +const OutMember = ({dynamicProps}: OutMemberProps) => { + const { + inputList, + inputRefList, + deleteEmptyInputElementOnBlur, + focusNextInputOnEnter, + handleInputChange, + setInputValueTargetIndex, + } = dynamicProps; + const {currentInputIndex, filteredInMemberList, handleCurrentInputIndex, searchCurrentInMember, chooseMember} = + useSearchInMemberList(setInputValueTargetIndex); + + 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)} + onChange={e => validationAndSearchOnChange(index, e)} + onBlur={() => deleteEmptyInputElementOnBlur()} + onKeyDown={e => focusNextInputOnEnter(e, index)} + placeholder="이름" + /> + + )); +}; + +export default OutMember; diff --git a/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.style.ts b/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.style.ts index b703e43de..f54166c08 100644 --- a/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.style.ts +++ b/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.style.ts @@ -26,3 +26,11 @@ export const allMemberListModalLabelGroupInputStyle = () => 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 index 7eb4ab1fb..d09327ccc 100644 --- a/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.tsx +++ b/client/src/components/Modal/SetAllMemberListModal/SetAllMemberListModal.tsx @@ -1,21 +1,26 @@ -import {BottomSheet, Text, LabelGroupInput, FixedButton} from 'haengdong-design'; +import {BottomSheet, Text, LabelGroupInput, FixedButton, IconButton, Icon} from 'haengdong-design'; -import InputAndDeleteButton from '@components/InputAndDeleteButton/InputAndDeleteButton'; +import validateMemberName from '@utils/validate/validateMemberName'; + +import useSetAllMemberList from '@hooks/useSetAllMemberList'; 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) => { @@ -24,22 +29,60 @@ const SetAllMemberListModal = ({ setIsOpenBottomSheet(false); }; + const { + editedAllMemberList, + canSubmit, + errorMessage, + errorIndexList, + handleNameChange, + handleClickDeleteButton, + handlePutAllMemberList, + } = useSetAllMemberList({ + validateFunc: validateMemberName, + allMemberList, + handleCloseAllMemberListModal, + }); + return (
    전체 참여자 수정하기 {/* TODO: (@soha): 인원 텍스트 색 수정 필요 */} +<<<<<<< HEAD + +======= - 총 N명 +>>>>>>> fe-dev + 총 {allMemberList.length}명
    +<<<<<<< HEAD + + {editedAllMemberList.map((member, index) => ( +
    +
    + handleNameChange(index, e)} + /> +
    + handleClickDeleteButton(index)}> + + +
    +======= - + {allMemberList.map((member, index) => ( + +>>>>>>> fe-dev + ))}
    - +
    ); diff --git a/client/src/components/Modal/index.ts b/client/src/components/Modal/index.ts index 7a9d0eaa4..180063b03 100644 --- a/client/src/components/Modal/index.ts +++ b/client/src/components/Modal/index.ts @@ -1,3 +1,4 @@ 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/Toast/ToastProvider.tsx b/client/src/components/Toast/ToastProvider.tsx index 974361dd7..3107e37ac 100644 --- a/client/src/components/Toast/ToastProvider.tsx +++ b/client/src/components/Toast/ToastProvider.tsx @@ -41,6 +41,8 @@ const ToastProvider = ({children}: React.PropsWithChildren) => { isAlwaysOn: false, position: 'bottom', bottom: '6.25rem', + // TODO: (@soha&weadie) zIndex의 값 추후에 꼭!!! 수정 + style: {zIndex: '1000'}, }); clearError(DEFAULT_TIME); diff --git a/client/src/constants/rule.ts b/client/src/constants/rule.ts index d6d53cd55..4b78031b9 100644 --- a/client/src/constants/rule.ts +++ b/client/src/constants/rule.ts @@ -1,7 +1,7 @@ const RULE = { maxEventNameLength: 30, maxEventPasswordLength: 4, - maxMemberNameLength: 8, + maxMemberNameLength: 4, maxPrice: 10000000, }; diff --git a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.tsx b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.tsx index 1e3965820..3e68d5b99 100644 --- a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.tsx +++ b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.tsx @@ -1,8 +1,8 @@ import type {MemberAction} from 'types/serviceType'; import {useState} from 'react'; -import {useToast} from 'haengdong-design'; +import {useToast} from '@components/Toast/ToastProvider'; import useEventId from '@hooks/useEventId/useEventId'; import {requestDeleteMemberAction} from '@apis/request/member'; import {useStepList} from '@hooks/useStepList/useStepList'; diff --git a/client/src/hooks/useDynamicInput.tsx b/client/src/hooks/useDynamicInput.tsx index 8588c2c40..12fdd4319 100644 --- a/client/src/hooks/useDynamicInput.tsx +++ b/client/src/hooks/useDynamicInput.tsx @@ -7,7 +7,19 @@ type InputValue = { index: number; }; -const useDynamicInput = (validateFunc: (name: string) => ValidateResult) => { +export type ReturnUseDynamicInput = { + inputList: InputValue[]; + inputRefList: React.MutableRefObject<(HTMLInputElement | null)[]>; + handleInputChange: (index: number, event: React.ChangeEvent) => void; + deleteEmptyInputElementOnBlur: () => void; + errorMessage: string; + getFilledInputList: (list?: InputValue[]) => InputValue[]; + focusNextInputOnEnter: (e: React.KeyboardEvent, index: number) => void; + canSubmit: boolean; + setInputValueTargetIndex: (index: number, value: string) => void; +}; + +const useDynamicInput = (validateFunc: (name: string) => ValidateResult): ReturnUseDynamicInput => { const [inputList, setInputList] = useState([{value: '', index: 0}]); const inputRefList = useRef<(HTMLInputElement | null)[]>([]); const [errorMessage, setErrorMessage] = useState(''); @@ -98,6 +110,17 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult) => { } }; + const setInputValueTargetIndex = (index: number, value: string) => { + setInputList(prevInputs => { + const updatedInputList = [...prevInputs]; + const targetInput = findInputByIndex(index, updatedInputList); + + targetInput.value = value; + + return updatedInputList; + }); + }; + const focusNextInputOnEnter = (e: React.KeyboardEvent, index: number) => { if (e.nativeEvent.isComposing) return; @@ -132,6 +155,7 @@ const useDynamicInput = (validateFunc: (name: string) => ValidateResult) => { getFilledInputList, focusNextInputOnEnter, canSubmit, + setInputValueTargetIndex, // TODO: (@weadie) 네이밍 수정 }; }; diff --git a/client/src/hooks/useSearchInMemberList.ts b/client/src/hooks/useSearchInMemberList.ts new file mode 100644 index 000000000..bd1fe180e --- /dev/null +++ b/client/src/hooks/useSearchInMemberList.ts @@ -0,0 +1,74 @@ +import {useEffect, useState} from 'react'; + +import {requestGetCurrentInMemberList} from '@apis/request/member'; + +import {useFetch} from '@apis/useFetch'; + +import useEventId from './useEventId/useEventId'; + +export type ReturnUseSearchInMemberList = { + currentInputIndex: number; + handleCurrentInputIndex: (inputIndex: number) => void; + filteredInMemberList: string[]; + searchCurrentInMember: (event: React.ChangeEvent) => void; + chooseMember: (inputIndex: number, name: string) => void; +}; + +const useSearchInMemberList = ( + setInputValueTargetIndex: (index: number, value: string) => void, +): ReturnUseSearchInMemberList => { + const {eventId} = useEventId(); + + const {fetch} = useFetch(); + const [currentInputIndex, setCurrentInputIndex] = useState(-1); + + // 서버에서 가져온 전체 리스트 + const [currentInMemberList, setCurrentInMemberList] = useState>([]); + + // 검색된 리스트 (따로 둔 이유는 검색 후 클릭했을 때 리스트를 비워주어야하기 때문) + const [filteredInMemberList, setFilteredInMemberList] = useState>([]); + + useEffect(() => { + if (eventId === '') return; + + const getCurrentInMembers = async () => { + const currentInMemberListFromServer = await fetch(() => requestGetCurrentInMemberList(eventId)); + setCurrentInMemberList(currentInMemberListFromServer.members); + }; + + getCurrentInMembers(); + }, [eventId]); + + 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); + }; + + const chooseMember = (inputIndex: number, name: string) => { + setFilteredInMemberList([]); + setInputValueTargetIndex(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/useSetAllMemberList.tsx b/client/src/hooks/useSetAllMemberList.tsx new file mode 100644 index 000000000..51d50fa53 --- /dev/null +++ b/client/src/hooks/useSetAllMemberList.tsx @@ -0,0 +1,127 @@ +import {useEffect, useState} from 'react'; + +import {ValidateResult} from '@utils/validate/type'; +import {MemberChange, requestDeleteAllMemberList, requestPutAllMemberList} from '@apis/request/member'; + +import {useFetch} from '@apis/useFetch'; + +import useEventId from './useEventId/useEventId'; +import {useStepList} from './useStepList/useStepList'; + +interface UseSetAllMemberListProps { + validateFunc: (name: string) => ValidateResult; + allMemberList: string[]; + handleCloseAllMemberListModal: () => void; +} + +const useSetAllMemberList = ({ + validateFunc, + allMemberList, + handleCloseAllMemberListModal, +}: UseSetAllMemberListProps) => { + const [editedAllMemberList, setEditedAllMemberList] = useState(allMemberList); + const [errorMessage, setErrorMessage] = useState(''); + const [errorIndexList, setErrorIndexList] = useState([]); + const [canSubmit, setCanSubmit] = useState(false); + + const {refreshStepList} = useStepList(); + const {eventId} = useEventId(); + const {fetch} = useFetch(); + + useEffect(() => { + if (arraysEqual(editedAllMemberList, allMemberList)) { + setCanSubmit(false); + } else { + setCanSubmit(true); + } + }, [editedAllMemberList]); + + const arraysEqual = (arr1: string[], arr2: string[]) => { + if (arr1.length !== arr2.length) return false; + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) return false; + } + return true; + }; + + const handleNameChange = (index: number, event: React.ChangeEvent) => { + const {value} = event.target; + const {isValid, errorMessage: validationResultMessage} = validateFunc(value); + + if (isValid && value.length !== 0) { + setErrorMessage(''); + + setEditedAllMemberList(prev => { + const newList = [...prev]; + newList[index] = value; + return newList; + }); + + setErrorIndexList(prev => prev.filter(i => i !== index)); + + setCanSubmit(true); + } else if (value.length === 0) { + setErrorMessage(''); + + setEditedAllMemberList(prev => { + const newList = [...prev]; + newList[index] = value; + return newList; + }); + + changeErrorIndex(index); + } else { + setErrorMessage(validationResultMessage ?? ''); + + changeErrorIndex(index); + } + }; + + const handleClickDeleteButton = async (index: number) => { + const memberToDelete = editedAllMemberList[index]; + + await fetch(() => requestDeleteAllMemberList({eventId, memberName: memberToDelete})); + + setEditedAllMemberList(prev => [...prev.slice(0, index), ...prev.slice(index + 1)]); + + refreshStepList(); + }; + + const handlePutAllMemberList = async () => { + const editedMemberName: MemberChange[] = allMemberList + .map((originalName, index) => { + if (editedAllMemberList[index] !== originalName) { + return {before: originalName, after: editedAllMemberList[index]}; + } + return null; // 조건에 맞지 않으면 null을 반환 + }) + .filter(item => item !== null); // null인 항목을 필터링하여 제거 + + await fetch(() => requestPutAllMemberList({eventId, members: editedMemberName})); + + refreshStepList(); + + handleCloseAllMemberListModal(); + }; + + const changeErrorIndex = (index: number) => { + setErrorIndexList(prev => { + if (!prev.includes(index)) { + return [...prev, index]; + } + return prev; + }); + }; + + return { + editedAllMemberList, + canSubmit, + errorMessage, + errorIndexList, + handleNameChange, + handleClickDeleteButton, + handlePutAllMemberList, + }; +}; + +export default useSetAllMemberList; diff --git a/client/src/hooks/useStepList/useStepList.tsx b/client/src/hooks/useStepList/useStepList.tsx index 994399018..6ca12e22b 100644 --- a/client/src/hooks/useStepList/useStepList.tsx +++ b/client/src/hooks/useStepList/useStepList.tsx @@ -4,17 +4,17 @@ import {PropsWithChildren, createContext, useContext, useEffect, useState} from import useEventId from '@hooks/useEventId/useEventId'; import {requestPostBillList} from '@apis/request/bill'; -import {requestPostMemberList} from '@apis/request/member'; +import {requestGetAllMemberList, requestPostMemberList} from '@apis/request/member'; import {requestGetStepList} from '@apis/request/stepList'; import {useFetch} from '@apis/useFetch'; interface StepListContextProps { stepList: (BillStep | MemberStep)[]; + allMemberList: string[]; getTotalPrice: () => number; addBill: (billList: Bill[]) => Promise; updateMemberList: ({type, memberNameList}: {type: MemberType; memberNameList: string[]}) => Promise; - memberNameList: string[]; refreshStepList: () => Promise; } @@ -23,7 +23,7 @@ export const StepListContext = createContext(null); const StepListProvider = ({children}: PropsWithChildren) => { const {fetch} = useFetch(); const [stepList, setStepList] = useState<(BillStep | MemberStep)[]>([]); - const [memberNameList, setNameMemberList] = useState([]); + const [allMemberList, setAllMemberList] = useState([]); const {eventId} = useEventId(); @@ -38,10 +38,7 @@ const StepListProvider = ({children}: PropsWithChildren) => { const refreshStepList = async () => { const stepList = await fetch(() => requestGetStepList({eventId})); - if (stepList.length !== 0) { - setNameMemberList(stepList[stepList.length - 1].members); - } - + getAllMemberList(); setStepList(stepList); }; @@ -49,16 +46,18 @@ const StepListProvider = ({children}: PropsWithChildren) => { try { await fetch(() => requestPostMemberList({eventId, type, memberNameList})); - // TODO: (@weadie) 클라이언트 단에서 멤버 목록을 관리하기 위한 로직. 개선이 필요하다. - if (type === 'IN') setNameMemberList(prev => [...prev, ...memberNameList]); - if (type === 'OUT') setNameMemberList(prev => prev.filter(name => !memberNameList.includes(name))); - refreshStepList(); } catch (error) { alert(error); } }; + const getAllMemberList = async () => { + const allMembers = await requestGetAllMemberList({eventId}); + + setAllMemberList(allMembers.memberNames); + }; + const addBill = async (billList: Bill[]) => { // TODO: (@weadie) 에러 처리 await fetch(() => requestPostBillList({eventId, billList})); @@ -86,7 +85,7 @@ const StepListProvider = ({children}: PropsWithChildren) => { getTotalPrice, updateMemberList, stepList, - memberNameList, + allMemberList, refreshStepList, }} > diff --git a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx index 0f30c1f04..808bf4319 100644 --- a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx +++ b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx @@ -1,20 +1,23 @@ import {useEffect, useState} from 'react'; import {useLocation, useNavigate} from 'react-router-dom'; -import {FixedButton, MainLayout, Title, TopNav} from 'haengdong-design'; +import {Button, FixedButton, Input, MainLayout, Text, Title, TopNav} from 'haengdong-design'; +import {CopyToClipboard} from 'react-copy-to-clipboard'; +import {css} from '@emotion/react'; + +import {useToast} from '@components/Toast/ToastProvider'; import {ROUTER_URLS} from '@constants/routerUrls'; const CompleteCreateEventPage = () => { - const [url, setUrl] = useState(''); 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 ?? ''); }; @@ -22,14 +25,35 @@ const CompleteCreateEventPage = () => { getUrl(); }, []); + const {showToast} = useToast(); + return ( } /> - + <Title title="행사 개시" description="행사가 성공적으로 개시됐어요 :)" /> + <div css={css({display: 'flex', flexDirection: 'column', gap: '1rem', margin: '0 1rem'})}> + <Text textColor="gray">행사 링크를 통해서 지출 내역 공유와 참여자 관리가 가능해요.</Text> + <Text textColor="primary">관리를 위해서 행사 링크를 복사 후 보관해 주세요.</Text> + <Input value={`haengdong.pro${ROUTER_URLS.event}/${url}/home`} disabled /> + + <CopyToClipboard + text={`haengdong.pro${ROUTER_URLS.event}/${url}/home`} + onCopy={() => + showToast({ + showingTime: 3000, + message: '링크가 복사되었어요 :) \n링크를 절대 분실하지 마세요!', + type: 'confirm', + position: 'top', + top: '2rem', + }) + } + > + <Button size="large" variants="tertiary"> + 행사 링크 복사하기 + </Button> + </CopyToClipboard> + </div> + <FixedButton onClick={() => navigate(`${ROUTER_URLS.event}/${url}/admin`)}>관리 페이지로 이동</FixedButton> </MainLayout> ); diff --git a/client/src/pages/EventPage/AdminPage/AdminPage.tsx b/client/src/pages/EventPage/AdminPage/AdminPage.tsx index 0918aa1da..d7b7563dc 100644 --- a/client/src/pages/EventPage/AdminPage/AdminPage.tsx +++ b/client/src/pages/EventPage/AdminPage/AdminPage.tsx @@ -5,7 +5,7 @@ import StepList from '@components/StepList/StepList'; import {useStepList} from '@hooks/useStepList/useStepList'; import {requestGetEventName} from '@apis/request/event'; import useEventId from '@hooks/useEventId/useEventId'; -import ModalBasedOnMemberCount from '@components/Modal/ModalBasedOnMemberCount/ModalBasedOnMemberCount'; +import {ModalBasedOnMemberCount} from '@components/Modal/index'; import {receiptStyle, titleAndListButtonContainerStyle} from './AdminPage.style'; @@ -16,7 +16,7 @@ const AdminPage = () => { // TODO: (@weadie) eventName이 새로고침시 공간이 없다가 생겨나 레이아웃이 움직이는 문제 const [eventName, setEventName] = useState(' '); - const {getTotalPrice, memberNameList} = useStepList(); + const {getTotalPrice, allMemberList} = useStepList(); const {eventId} = useEventId(); // TODO: (@weadie) 아래 로직을 훅으로 분리합니다. @@ -45,10 +45,10 @@ const AdminPage = () => { description="“초기인원 설정하기” 버튼을 눌러서 행사 초기 인원을 설정해 주세요." price={getTotalPrice()} /> - {memberNameList.length !== 0 && ( + {allMemberList.length !== 0 && ( <ListButton prefix="전체 참여자" - suffix={`${memberNameList.length}명`} + suffix={`${allMemberList.length}명`} onClick={handleOpenAllMemberListButton} /> )} @@ -56,12 +56,12 @@ const AdminPage = () => { <section css={receiptStyle}> <StepList /> <FixedButton - children={memberNameList.length === 0 ? '초기인원 설정하기' : '행동 추가하기'} + children={allMemberList.length === 0 ? '초기인원 설정하기' : '행동 추가하기'} onClick={() => setIsOpenFixedBottomBottomSheet(prev => !prev)} /> {isOpenFixedButtonBottomSheet && ( <ModalBasedOnMemberCount - memberNameList={memberNameList} + allMemberList={allMemberList} setOrder={setOrder} setIsOpenBottomSheet={setIsOpenFixedBottomBottomSheet} isOpenBottomSheet={isOpenFixedButtonBottomSheet} diff --git a/client/src/utils/validate/validateMemberName.ts b/client/src/utils/validate/validateMemberName.ts index 0e7483358..06c7416dd 100644 --- a/client/src/utils/validate/validateMemberName.ts +++ b/client/src/utils/validate/validateMemberName.ts @@ -1,5 +1,5 @@ import REGEXP from '@constants/regExp'; -import ERROR_MESSAGE from '@constants/errorMessage'; +import {ERROR_MESSAGE} from '@constants/errorMessage'; import RULE from '@constants/rule'; import {ValidateResult} from './type';