diff --git a/.github/workflows/frontend-pull-request.yml b/.github/workflows/frontend-pull-request.yml index c9efc26cc..238a9a0d1 100644 --- a/.github/workflows/frontend-pull-request.yml +++ b/.github/workflows/frontend-pull-request.yml @@ -9,7 +9,7 @@ on: jobs: test: - runs-on: ubuntu-latest log + runs-on: ubuntu-latest defaults: run: diff --git a/HDesign/package-lock.json b/HDesign/package-lock.json index 58d306b1a..bbf0d12a9 100644 --- a/HDesign/package-lock.json +++ b/HDesign/package-lock.json @@ -1,12 +1,12 @@ { "name": "haengdong-design", - "version": "0.1.36", + "version": "0.1.52", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "haengdong-design", - "version": "0.1.36", + "version": "0.1.52", "license": "ISC", "dependencies": { "@emotion/react": "^11.11.4", diff --git a/HDesign/package.json b/HDesign/package.json index a10d83b7b..f1bc9c8e7 100644 --- a/HDesign/package.json +++ b/HDesign/package.json @@ -1,6 +1,6 @@ { "name": "haengdong-design", - "version": "0.1.36", + "version": "0.1.52", "description": "", "main": "./dist/index.js", "module": "./dist/index.js", diff --git a/HDesign/src/components/Input/Input.stories.tsx b/HDesign/src/components/Input/Input.stories.tsx index 0f5187cb3..97365fa9d 100644 --- a/HDesign/src/components/Input/Input.stories.tsx +++ b/HDesign/src/components/Input/Input.stories.tsx @@ -1,26 +1,20 @@ import type {Meta, StoryObj} from '@storybook/react'; +import React, {useEffect, useState} from 'react'; + import Input from '@components/Input/Input'; const meta = { title: 'Components/Input', component: Input, tags: ['autodocs'], - parameters: { - // layout: 'centered', - }, argTypes: { - value: { - description: '', - control: {type: 'text'}, - }, inputType: { // TODO: (@cookie) 스토리북 라디오버튼 보이도록 설정해야 함 control: {type: 'radio'}, }, }, args: { - disabled: false, placeholder: 'placeholder', }, } satisfies Meta; @@ -29,4 +23,23 @@ export default meta; type Story = StoryObj; -export const Playground: Story = {}; +export const Playground: Story = { + render: ({...args}) => { + const [value, setValue] = useState(''); + const [isError, setIsError] = useState(false); + const handleChange = (event: React.ChangeEvent) => { + if (event.target.value.length < 4) { + setValue(event.target.value); + setIsError(false); + } else { + event.target.value = value; + setIsError(true); + } + }; + const handleBlur = () => { + console.log('blur'); + }; + + return handleChange(e)} isError={isError} onBlur={handleBlur} {...args} />; + }, +}; diff --git a/HDesign/src/components/Input/Input.style.ts b/HDesign/src/components/Input/Input.style.ts index 285908bab..cb7471afc 100644 --- a/HDesign/src/components/Input/Input.style.ts +++ b/HDesign/src/components/Input/Input.style.ts @@ -4,7 +4,7 @@ import {Theme} from '@theme/theme.type'; import {InputType} from './Input.type'; -const inputBoxBackgroundColorByInputType = (theme: Theme, inputType: InputType = 'input') => { +const getBackgroundColorStyle = (theme: Theme, inputType: InputType = 'input') => { switch (inputType) { case 'input': return theme.colors.lightGrayContainer; @@ -17,6 +17,9 @@ const inputBoxBackgroundColorByInputType = (theme: Theme, inputType: InputType = } }; +const getBorderStyle = (isFocus: boolean, theme: Theme, isError?: boolean) => + isError ? `0 0 0 1px ${theme.colors.error} inset` : isFocus ? `0 0 0 1px ${theme.colors.primary} inset` : 'none'; + export const inputBoxStyle = ( theme: Theme, inputType: InputType = 'input', @@ -26,13 +29,13 @@ export const inputBoxStyle = ( css({ display: 'flex', justifyContent: 'space-between', - + gap: '1rem', padding: '0.75rem 1rem', borderRadius: '1rem', - backgroundColor: inputBoxBackgroundColorByInputType(theme, inputType), + backgroundColor: getBackgroundColorStyle(theme, inputType), boxSizing: 'border-box', - outline: isFocus ? `1px solid ${theme.colors.primary}` : isError ? `1px solid ${theme.colors.error}` : 'none', + boxShadow: getBorderStyle(isFocus, theme, isError), }); export const inputStyle = (theme: Theme) => diff --git a/HDesign/src/components/Input/Input.tsx b/HDesign/src/components/Input/Input.tsx index 81f0f1c93..cebdfb5a7 100644 --- a/HDesign/src/components/Input/Input.tsx +++ b/HDesign/src/components/Input/Input.tsx @@ -1,23 +1,28 @@ /** @jsxImportSource @emotion/react */ -import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react'; +import React, {forwardRef, useImperativeHandle, useRef} from 'react'; -import IconButton from '@components/IconButton/IconButton'; -import {InputProps} from '@components/Input/Input.type'; -import {inputBoxStyle, inputStyle} from '@components/Input/Input.style'; -import {useInput} from '@components/Input/useInput'; +import {useTheme} from '@/theme/HDesignProvider'; -import {useTheme} from '@theme/HDesignProvider'; +import IconButton from '../IconButton/IconButton'; + +import {useInput} from './useInput'; +import {InputProps} from './Input.type'; +import {inputBoxStyle, inputStyle} from './Input.style'; export const Input: React.FC = forwardRef(function Input( - {value: propsValue, onChange, inputType, isError, ...htmlProps}: InputProps, + {value: propsValue, onChange, onFocus, onBlur, inputType, isError, placeholder, ...htmlProps}: InputProps, ref, ) { const {theme} = useTheme(); - const inputRef = useRef(null); - useImperativeHandle(ref, () => inputRef.current!); - - const {value, hasFocus, handleChange, handleClickDelete, toggleFocus} = useInput({propsValue, onChange, inputRef}); + const inputRef = useRef(null); + const {value, handleChange, hasFocus, handleClickDelete, handleBlur, handleFocus, handleKeyDown} = useInput({ + propsValue, + onChange, + onBlur, + onFocus, + inputRef, + }); return (
@@ -26,8 +31,10 @@ export const Input: React.FC = forwardRef {value && hasFocus && } diff --git a/HDesign/src/components/Input/useInput.ts b/HDesign/src/components/Input/useInput.ts index 5b37d275a..46a195032 100644 --- a/HDesign/src/components/Input/useInput.ts +++ b/HDesign/src/components/Input/useInput.ts @@ -3,39 +3,66 @@ import {RefObject, useEffect, useState} from 'react'; interface UseInputProps { propsValue: T; onChange?: (e: React.ChangeEvent) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; inputRef: RefObject; + autoFocus?: boolean; } -export const useInput = ({propsValue, onChange, inputRef}: UseInputProps) => { - const [value, setValue] = useState(propsValue || ''); - const [hasFocus, setHasFocus] = useState(false); +export const useInput = ({propsValue, onChange, onBlur, onFocus, inputRef, autoFocus}: UseInputProps) => { + const [value, setValue] = useState(propsValue); + const [hasFocus, setHasFocus] = useState(inputRef.current === document.activeElement); useEffect(() => { - setValue(propsValue || ''); - }, [propsValue]); + setHasFocus(inputRef.current === document.activeElement); + }, []); - const handleClickDelete = () => { - setValue(''); - - if (inputRef.current) { - inputRef.current.focus(); - } + useEffect(() => { + setValue(propsValue); + }, [value]); + const handleClickDelete = (event: React.MouseEvent) => { + event.preventDefault(); + setValue('' as T); if (onChange) { onChange({target: {value: ''}} as React.ChangeEvent); } + if (inputRef.current) { + inputRef.current.focus(); + } }; const handleChange = (e: React.ChangeEvent) => { - setValue(e.target.value); + setValue(e.target.value as T); if (onChange) { onChange(e); } }; - const toggleFocus = () => { - setHasFocus(!hasFocus); + const handleBlur = (e: React.FocusEvent) => { + setHasFocus(false); + if (onBlur) { + onBlur(e); + } + }; + + const handleFocus = (e: React.FocusEvent) => { + setHasFocus(true); + if (onFocus) { + onFocus(e); + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.nativeEvent.isComposing) return; + + if (event.key === 'Enter' || event.key === 'Escape') { + setHasFocus(false); + if (inputRef.current) { + inputRef.current.blur(); + } + } }; - return {value, hasFocus, handleChange, handleClickDelete, toggleFocus}; + return {value, handleChange, hasFocus, handleClickDelete, handleBlur, handleFocus, handleKeyDown}; }; diff --git a/HDesign/src/components/LabelGroupInput/Element.tsx b/HDesign/src/components/LabelGroupInput/Element.tsx new file mode 100644 index 000000000..1043aa313 --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/Element.tsx @@ -0,0 +1,62 @@ +/** @jsxImportSource @emotion/react */ + +import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from 'react'; + +import Input from '../Input/Input'; + +import {ElementProps} from './Element.type'; +import {useGroupInputContext} from './GroupInputContext'; + +const Element: React.FC = forwardRef(function Element( + {elementKey, value: propsValue, onChange, onBlur, onFocus, isError, ...htmlProps}: ElementProps, + + ref, +) { + useImperativeHandle(ref, () => inputRef.current!); + const inputRef = useRef(null); + const {setHasAnyFocus, values, setValues, hasAnyErrors, setHasAnyErrors} = useGroupInputContext(); + + useEffect(() => { + setValues({...values, [elementKey]: `${propsValue}`}); + }, [propsValue]); + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + setValues({...values, [elementKey]: newValue}); + if (onChange) { + onChange(e); + } + }; + + useEffect(() => { + setHasAnyErrors({...hasAnyErrors, [elementKey]: isError ?? false}); + }, [isError]); + + const handleBlur = (e: React.FocusEvent) => { + setHasAnyFocus(false); + if (onBlur) { + onBlur(e); + } + }; + + const handleFocus = (e: React.FocusEvent) => { + setHasAnyFocus(true); + if (onFocus) { + onFocus(e); + } + }; + + return ( + + ); +}); + +export default Element; diff --git a/HDesign/src/components/LabelGroupInput/Element.type.ts b/HDesign/src/components/LabelGroupInput/Element.type.ts new file mode 100644 index 000000000..2ec2ef9ed --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/Element.type.ts @@ -0,0 +1,10 @@ +export interface ElementStyleProps {} + +export interface ElementCustomProps { + elementKey: string; + isError?: boolean; +} + +export type ElementOptionProps = ElementStyleProps & ElementCustomProps; + +export type ElementProps = React.ComponentProps<'input'> & ElementOptionProps; diff --git a/HDesign/src/components/LabelGroupInput/GroupInputContext.tsx b/HDesign/src/components/LabelGroupInput/GroupInputContext.tsx new file mode 100644 index 000000000..a564ef94f --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/GroupInputContext.tsx @@ -0,0 +1,32 @@ +import React, {createContext, PropsWithChildren, useContext, useState} from 'react'; + +interface GroupInputContextProps { + hasAnyFocus: boolean; + setHasAnyFocus: React.Dispatch>; + values: {[key: string]: string}; + setValues: React.Dispatch>; + hasAnyErrors: {[key: string]: boolean}; + setHasAnyErrors: React.Dispatch>; +} + +const GroupInputContext = createContext(undefined); + +export const useGroupInputContext = () => { + const context = useContext(GroupInputContext); + if (!context) { + throw new Error('useGroupInputContext must be used within an GroupInputProvider'); + } + return context; +}; + +export const GroupInputProvider: React.FC = ({children}: React.PropsWithChildren) => { + const [hasAnyFocus, setHasAnyFocus] = useState(false); + const [values, setValues] = useState<{[key: string]: string}>({}); + const [hasAnyErrors, setHasAnyErrors] = useState<{[key: string]: boolean}>({}); + + return ( + + {children} + + ); +}; diff --git a/HDesign/src/components/LabelGroupInput/LabelGroupInput.stories.tsx b/HDesign/src/components/LabelGroupInput/LabelGroupInput.stories.tsx new file mode 100644 index 000000000..b3f48b452 --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/LabelGroupInput.stories.tsx @@ -0,0 +1,73 @@ +/** @jsxImportSource @emotion/react */ +import type {Meta, StoryObj} from '@storybook/react'; + +import {useState} from 'react'; + +import LabelGroupInput from '@components/LabelGroupInput/LabelGroupInput'; + +const meta = { + title: 'Components/LabelGroupInput', + component: LabelGroupInput, + tags: ['autodocs'], + parameters: { + // layout: 'centered', + }, + argTypes: { + labelText: { + description: 'label에 들어갈 텍스트를 작성', + control: {type: 'text'}, + }, + errorText: { + description: 'error에 들어갈 텍스트를 작성', + control: {type: 'text'}, + }, + }, + args: { + labelText: '지출내역 / 금액', + errorText: 'error가 발생했을 때 나타납니다!', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + render: ({...args}) => { + const [name, setName] = useState(''); + const [price, setPrice] = useState(''); + const [isError, setIsError] = useState(false); + const handleChangeName = (event: React.ChangeEvent) => { + if (event.target.value.length < 4) { + setName(event.target.value); + setIsError(false); + } else { + event.target.value = name; + setIsError(true); + } + }; + const handleChangePrice = (event: React.ChangeEvent) => { + setPrice(event.target.value); + }; + return ( + + handleChangeName(e)} + onBlur={() => console.log('!!!')} + isError={isError} + /> + console.log('!!!')} + isError={false} + /> + + ); + }, +}; diff --git a/HDesign/src/components/LabelGroupInput/LabelGroupInput.style.ts b/HDesign/src/components/LabelGroupInput/LabelGroupInput.style.ts new file mode 100644 index 000000000..99075fe1d --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/LabelGroupInput.style.ts @@ -0,0 +1,37 @@ +import {css} from '@emotion/react'; + +import {Theme} from '@/theme/theme.type'; + +export const labelInputStyle = css({ + display: 'flex', + flexDirection: 'column', + gap: '0.375rem', +}); + +export const labelGroupStyle = css({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + + paddingInline: '0.5rem', + marginBottom: '0.375rem', +}); + +export const inputGroupStyle = css({ + display: 'flex', + flexDirection: 'column', + gap: '0.5rem', +}); + +export const labelTextStyle = (theme: Theme) => + css({ + height: '1.125rem', + color: theme.colors.gray, + }); + +export const errorTextStyle = (theme: Theme) => + css({ + height: '1.125rem', + color: theme.colors.onErrorContainer, + }); diff --git a/HDesign/src/components/LabelGroupInput/LabelGroupInput.tsx b/HDesign/src/components/LabelGroupInput/LabelGroupInput.tsx new file mode 100644 index 000000000..e115bc71a --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/LabelGroupInput.tsx @@ -0,0 +1,44 @@ +/** @jsxImportSource @emotion/react */ + +import Text from '@components/Text/Text'; +import {useTheme} from '@/theme/HDesignProvider'; + +import Flex from '../Flex/Flex'; + +import {LabelGroupInputProps} from './LabelGroupInput.type'; +import {errorTextStyle, labelTextStyle} from './LabelGroupInput.style'; +import Element from './Element'; +import {GroupInputProvider, useGroupInputContext} from './GroupInputContext'; + +const LabelGroupInput: React.FC = ({labelText, errorText, children}: LabelGroupInputProps) => { + const {theme} = useTheme(); + const {hasAnyFocus, values, hasAnyErrors} = useGroupInputContext(); + + return ( + + + + {(hasAnyFocus || !Object.values(values).every(value => value === '')) && labelText} + + {errorText && ( + + {!Object.values(hasAnyErrors).every(error => !error) && errorText} + + )} + + + {children} + + + ); +}; + +const LabelGroupInputContainer = (props: LabelGroupInputProps) => ( + + + +); + +LabelGroupInputContainer.Element = Element; + +export default LabelGroupInputContainer; diff --git a/HDesign/src/components/LabelGroupInput/LabelGroupInput.type.ts b/HDesign/src/components/LabelGroupInput/LabelGroupInput.type.ts new file mode 100644 index 000000000..cfa938457 --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/LabelGroupInput.type.ts @@ -0,0 +1,10 @@ +export interface LabelGroupInputStyleProps {} + +export interface LabelGroupInputCustomProps { + labelText: string; + errorText?: string; +} + +export type LabelGroupInputOptionProps = LabelGroupInputStyleProps & LabelGroupInputCustomProps; + +export type LabelGroupInputProps = React.ComponentProps<'input'> & LabelGroupInputOptionProps; diff --git a/HDesign/src/components/LabelGroupInput/index.ts b/HDesign/src/components/LabelGroupInput/index.ts new file mode 100644 index 000000000..c31f35eaf --- /dev/null +++ b/HDesign/src/components/LabelGroupInput/index.ts @@ -0,0 +1,3 @@ +import LabelGroupInputContainer from './LabelGroupInput'; + +export {LabelGroupInputContainer as LabelGroupInput}; diff --git a/HDesign/src/components/LabelInput/LabelInput.stories.tsx b/HDesign/src/components/LabelInput/LabelInput.stories.tsx index 63ebcea37..853c0260a 100644 --- a/HDesign/src/components/LabelInput/LabelInput.stories.tsx +++ b/HDesign/src/components/LabelInput/LabelInput.stories.tsx @@ -1,8 +1,9 @@ /** @jsxImportSource @emotion/react */ import type {Meta, StoryObj} from '@storybook/react'; +import {useEffect, useState} from 'react'; + import LabelInput from '@components/LabelInput/LabelInput'; -import Input from '@components/Input/Input'; const meta = { title: 'Components/LabelInput', @@ -16,15 +17,19 @@ const meta = { description: 'label에 들어갈 텍스트를 작성', control: {type: 'text'}, }, + isError: { + description: '', + control: {type: 'boolean'}, + }, errorText: { description: 'error에 들어갈 텍스트를 작성', control: {type: 'text'}, }, }, args: { - labelText: 'label 내용', + // value: '', + labelText: '이름', errorText: 'error가 발생했을 때 나타납니다!', - children: , }, } satisfies Meta; @@ -32,4 +37,19 @@ export default meta; type Story = StoryObj; -export const Playground: Story = {}; +export const Playground: Story = { + render: ({...args}) => { + const [value, setValue] = useState(''); + const [isError, setIsError] = useState(false); + const handleChange = (event: React.ChangeEvent) => { + if (event.target.value.length < 4) { + setValue(event.target.value); + setIsError(false); + } else { + event.target.value = value; + setIsError(true); + } + }; + return handleChange(e)} isError={isError} {...args} />; + }, +}; diff --git a/HDesign/src/components/LabelInput/LabelInput.style.ts b/HDesign/src/components/LabelInput/LabelInput.style.ts index 208d6e56c..5d5eaed8f 100644 --- a/HDesign/src/components/LabelInput/LabelInput.style.ts +++ b/HDesign/src/components/LabelInput/LabelInput.style.ts @@ -2,23 +2,14 @@ import {css} from '@emotion/react'; import {Theme} from '@/theme/theme.type'; -export const labelGroupStyle = () => - css({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - - paddingInline: '0.5rem', - marginBottom: '0.375rem', - }); - export const labelTextStyle = (theme: Theme) => css({ + height: '1.125rem', color: theme.colors.gray, }); export const errorTextStyle = (theme: Theme) => css({ + height: '1.125rem', color: theme.colors.onErrorContainer, }); diff --git a/HDesign/src/components/LabelInput/LabelInput.tsx b/HDesign/src/components/LabelInput/LabelInput.tsx index 97cc9bcd1..b7759b361 100644 --- a/HDesign/src/components/LabelInput/LabelInput.tsx +++ b/HDesign/src/components/LabelInput/LabelInput.tsx @@ -1,30 +1,41 @@ /** @jsxImportSource @emotion/react */ +import {forwardRef, useImperativeHandle, useRef} from 'react'; + import Text from '@components/Text/Text'; import {useTheme} from '@/theme/HDesignProvider'; +import Input from '../Input/Input'; +import Flex from '../Flex/Flex'; + +import {errorTextStyle, labelTextStyle} from './LabelInput.style'; +import {useLabelInput} from './useLabelInput'; import {LabelInputProps} from './LabelInput.type'; -import {errorTextStyle, labelGroupStyle, labelTextStyle} from './LabelInput.style'; -const LabelInput = ({labelText, errorText, children}: LabelInputProps) => { +const LabelInput: React.FC = forwardRef(function LabelInput( + {labelText, errorText, isError, ...htmlProps}: LabelInputProps, + ref, +) { + useImperativeHandle(ref, () => inputRef.current!); const {theme} = useTheme(); + const inputRef = useRef(null); + const {hasFocus} = useLabelInput({inputRef}); + return ( - <> -
- - {errorText && ( - - {errorText} - - )} -
- {children} - + + + + {(hasFocus || htmlProps.value) && labelText} + + + {isError && errorText} + + + + + + ); -}; +}); export default LabelInput; diff --git a/HDesign/src/components/LabelInput/LabelInput.type.ts b/HDesign/src/components/LabelInput/LabelInput.type.ts index 539a10da1..ae0d0c5f6 100644 --- a/HDesign/src/components/LabelInput/LabelInput.type.ts +++ b/HDesign/src/components/LabelInput/LabelInput.type.ts @@ -1,13 +1,11 @@ -import {InputProps} from '../Input/Input.type'; - export interface LabelInputStyleProps {} -export interface ButtonCustomProps { +export interface LabelInputCustomProps { labelText: string; errorText?: string; - children?: React.ReactElement[] | React.ReactElement; + isError?: boolean; } -export type LabelInputOptionProps = LabelInputStyleProps & ButtonCustomProps; +export type LabelInputOptionProps = LabelInputCustomProps & LabelInputCustomProps; -export type LabelInputProps = React.ComponentProps<'div'> & LabelInputOptionProps; +export type LabelInputProps = React.ComponentProps<'input'> & LabelInputOptionProps; diff --git a/HDesign/src/components/LabelInput/useLabelInput.ts b/HDesign/src/components/LabelInput/useLabelInput.ts new file mode 100644 index 000000000..ed64fd8bc --- /dev/null +++ b/HDesign/src/components/LabelInput/useLabelInput.ts @@ -0,0 +1,21 @@ +import {RefObject, useEffect, useState} from 'react'; + +interface UseLabelInput { + inputRef: RefObject; +} + +export const useLabelInput = ({inputRef}: UseLabelInput) => { + const [hasFocus, setHasFocus] = useState(inputRef.current === document.activeElement); + + useEffect(() => { + inputRef.current?.addEventListener('focus', () => setHasFocus(true)); + inputRef.current?.addEventListener('blur', () => setHasFocus(false)); + + return () => { + inputRef.current?.removeEventListener('focus', () => setHasFocus(true)); + inputRef.current?.removeEventListener('blur', () => setHasFocus(false)); + }; + }, []); + + return {hasFocus}; +}; diff --git a/HDesign/src/index.tsx b/HDesign/src/index.tsx index f75b5f640..f6449f0bb 100644 --- a/HDesign/src/index.tsx +++ b/HDesign/src/index.tsx @@ -8,6 +8,8 @@ import Flex from '@components/Flex/Flex'; import IconButton from '@components/IconButton/IconButton'; import InOutItem from '@components/InOutItem/InOutItem'; import Input from '@components/Input/Input'; +import LabelInput from '@components/LabelInput/LabelInput'; +import LabelGroupInput from '@components/LabelGroupInput/LabelGroupInput'; import Search from '@components/Search/Search'; import StepItem from '@components/StepItem/StepItem'; import Switch from '@components/Switch/Switch'; @@ -15,8 +17,8 @@ import Tabs from '@/components/Tabs/Tabs'; import Text from '@components/Text/Text'; import TextButton from '@components/TextButton/TextButton'; import Title from '@components/Title/Title'; -import TopNav from '@components/TopNav/TopNav'; import Toast from '@components/Toast/Toast'; +import TopNav from '@components/TopNav/TopNav'; import {ToastProvider, useToast} from '@components/Toast/ToastProvider'; import {MainLayout} from '@layouts/MainLayout'; @@ -39,6 +41,8 @@ export { IconButton, InOutItem, Input, + LabelInput, + LabelGroupInput, Search, StepItem, Switch, @@ -50,8 +54,8 @@ export { TopNav, MainLayout, ContentLayout, - HDesignProvider, + Toast, ToastProvider, useToast, - Toast, + HDesignProvider, }; diff --git a/client/package-lock.json b/client/package-lock.json index e1cd34770..d35cb264b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.11.4", "@types/dotenv-webpack": "^7.0.7", "dotenv-webpack": "^8.1.0", - "haengdong-design": "^0.1.35", + "haengdong-design": "^0.1.51", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.1" @@ -400,9 +400,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", - "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -411,12 +414,12 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.0.tgz", - "integrity": "sha512-dG0aApncVQwAUJa8tP1VHTnmU67BeIQvKafd3raEx315H54FfkZSz3B/TT+33ZQAjatGJA79gZqTtqL5QZUKXw==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", + "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", "dependencies": { "@babel/helper-plugin-utils": "^7.24.8", - "@babel/traverse": "^7.25.0" + "@babel/traverse": "^7.25.3" }, "engines": { "node": ">=6.9.0" @@ -1578,15 +1581,15 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.2.tgz", - "integrity": "sha512-Y2Vkwy3ITW4id9c6KXshVV/x5yCGK7VdJmKkzOzNsDZMojRKfSA/033rRbLqlRozmhRXCejxWHLSJOg/wUHfzw==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", + "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", "dependencies": { "@babel/compat-data": "^7.25.2", "@babel/helper-compilation-targets": "^7.25.2", "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.0", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", @@ -1753,13 +1756,13 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.2.tgz", - "integrity": "sha512-s4/r+a7xTnny2O6FcZzqgT6nE4/GHEdcqj4qAeglbUOh0TeglEfmNJFAd/OLoVtGd6ZhAO8GCVvCNUO5t/VJVQ==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.0", + "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/types": "^7.25.2", "debug": "^4.3.1", @@ -2286,9 +2289,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", - "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.0.tgz", + "integrity": "sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==", "engines": { "node": ">=14.0.0" } @@ -2549,9 +2552,9 @@ } }, "node_modules/@swc/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.3.tgz", - "integrity": "sha512-HHAlbXjWI6Kl9JmmUW1LSygT1YbblXgj2UvvDzMkTBPRzYMhW6xchxdO8HbtMPtFYRt/EQq9u1z7j4ttRSrFsA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.5.tgz", + "integrity": "sha512-qKK0/Ta4qvxs/ok3XyYVPT7OBenwRn1sSINf1cKQTBHPqr7U/uB4k2GTl6JgEs8H4PiJrMTNWfMLTucIoVSfAg==", "hasInstallScript": true, "dependencies": { "@swc/counter": "^0.1.3", @@ -2565,16 +2568,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.3", - "@swc/core-darwin-x64": "1.7.3", - "@swc/core-linux-arm-gnueabihf": "1.7.3", - "@swc/core-linux-arm64-gnu": "1.7.3", - "@swc/core-linux-arm64-musl": "1.7.3", - "@swc/core-linux-x64-gnu": "1.7.3", - "@swc/core-linux-x64-musl": "1.7.3", - "@swc/core-win32-arm64-msvc": "1.7.3", - "@swc/core-win32-ia32-msvc": "1.7.3", - "@swc/core-win32-x64-msvc": "1.7.3" + "@swc/core-darwin-arm64": "1.7.5", + "@swc/core-darwin-x64": "1.7.5", + "@swc/core-linux-arm-gnueabihf": "1.7.5", + "@swc/core-linux-arm64-gnu": "1.7.5", + "@swc/core-linux-arm64-musl": "1.7.5", + "@swc/core-linux-x64-gnu": "1.7.5", + "@swc/core-linux-x64-musl": "1.7.5", + "@swc/core-win32-arm64-msvc": "1.7.5", + "@swc/core-win32-ia32-msvc": "1.7.5", + "@swc/core-win32-x64-msvc": "1.7.5" }, "peerDependencies": { "@swc/helpers": "*" @@ -2586,9 +2589,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.3.tgz", - "integrity": "sha512-CTkHa6MJdov9t41vuV2kmQIMu+Q19LrEHGIR/UiJYH06SC/sOu35ZZH8DyfLp9ZoaCn21gwgWd61ixOGQlwzTw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.5.tgz", + "integrity": "sha512-Y+bvW9C4/u26DskMbtQKT4FU6QQenaDYkKDi028vDIKAa7v1NZqYG9wmhD/Ih7n5EUy2uJ5I5EWD7WaoLzT6PA==", "cpu": [ "arm64" ], @@ -2601,9 +2604,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.3.tgz", - "integrity": "sha512-mun623y6rCoZ2EFIYfIRqXYRFufJOopoYSJcxYhZUrfTpAvQ1zLngjQpWCUU1krggXR2U0PQj+ls0DfXUTraNg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.5.tgz", + "integrity": "sha512-AuIbDlcaAhYS6mtF4UqvXgrLeAfXZbVf4pgtgShPbutF80VbCQiIB55zOFz5aZdCpsBVuCWcBq0zLneK+VQKkQ==", "cpu": [ "x64" ], @@ -2616,9 +2619,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.3.tgz", - "integrity": "sha512-4Jz4UcIcvZNMp9qoHbBx35bo3rjt8hpYLPqnR4FFq6gkAsJIMFC56UhRZwdEQoDuYiOFMBnnrsg31Fyo6YQypA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.5.tgz", + "integrity": "sha512-99uBPHITRqgGwCXAjHY94VaV3Z40+D2NQNgR1t6xQpO8ZnevI6YSzX6GVZfBnV7+7oisiGkrVEwfIRRa+1s8FA==", "cpu": [ "arm" ], @@ -2631,9 +2634,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.3.tgz", - "integrity": "sha512-p+U/M/oqV7HC4erQ5TVWHhJU1984QD+wQBPxslAYq751bOQGm0R/mXK42GjugqjnR6yYrAiwKKbpq4iWVXNePA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.5.tgz", + "integrity": "sha512-xHL3Erlz+OGGCG4h6K2HWiR56H5UYMuBWWPbbUufi2bJpfhuKQy/X3vWffwL8ZVfJmCUwr4/G91GHcm32uYzRg==", "cpu": [ "arm64" ], @@ -2646,9 +2649,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.3.tgz", - "integrity": "sha512-s6VzyaJwaRGTi2mz2h6Ywxfmgpkc69IxhuMzl+sl34plH0V0RgnZDm14HoCGIKIzRk4+a2EcBV1ZLAfWmPACQg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.5.tgz", + "integrity": "sha512-5ArGdqvFMszNHdi4a67vopeYq8d1K+FuTWDrblHrAvZFhAyv+GQz2PnKqYOgl0sWmQxsNPfNwBFtxACpUO3Jzg==", "cpu": [ "arm64" ], @@ -2661,9 +2664,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.3.tgz", - "integrity": "sha512-IrFY48C356Z2dU2pjYg080yvMXzmSV3Lmm/Wna4cfcB1nkVLjWsuYwwRAk9CY7E19c+q8N1sMNggubAUDYoX2g==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.5.tgz", + "integrity": "sha512-mSVVV/PFzCGtI1nVQQyx34NwCMgSurF6ZX/me8pUAX054vsE/pSFL66xN+kQOe/1Z/LOd4UmXFkZ/EzOSnYcSg==", "cpu": [ "x64" ], @@ -2676,9 +2679,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.3.tgz", - "integrity": "sha512-qoLgxBlBnnyUEDu5vmRQqX90h9jldU1JXI96e6eh2d1gJyKRA0oSK7xXmTzorv1fGHiHulv9qiJOUG+g6uzJWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.5.tgz", + "integrity": "sha512-09hY3ZKMUORXVunESKS9yuP78+gQbr759GKHo8wyCdtAx8lCZdEjfI5NtC7/1VqwfeE32/U6u+5MBTVhZTt0AA==", "cpu": [ "x64" ], @@ -2691,9 +2694,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.3.tgz", - "integrity": "sha512-OAd7jVVJ7nb0Ev80VAa1aeK+FldPeC4eZ35H4Qn6EICzIz0iqJo2T33qLKkSZiZEBKSoF4KcwrqYfkjLOp5qWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.5.tgz", + "integrity": "sha512-B/UDtPI3RlYRFW42xQxOpl6kI/9LtkD7No+XeRIKQTPe15EP2o+rUlv7CmKljVBXgJ8KmaQbZlaEh1YP+QZEEQ==", "cpu": [ "arm64" ], @@ -2706,9 +2709,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.3.tgz", - "integrity": "sha512-31+Le1NyfSnILFV9+AhxfFOG0DK0272MNhbIlbcv4w/iqpjkhaOnNQnLsYJD1Ow7lTX1MtIZzTjOhRlzSviRWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.5.tgz", + "integrity": "sha512-BgLesVGmIY6Nub/sURqtSRvWYcbCE/ACfuZB3bZHVKD6nsZJJuOpdB8oC41fZPyc8yZUzL3XTBIifkT2RP+w9w==", "cpu": [ "ia32" ], @@ -2721,9 +2724,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.3.tgz", - "integrity": "sha512-jVQPbYrwcuueI4QB0fHC29SVrkFOBcfIspYDlgSoHnEz6tmLMqUy+txZUypY/ZH/KaK0HEY74JkzgbRC1S6LFQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.5.tgz", + "integrity": "sha512-CnF557tidLfQRPczcqDJ8x+LBQYsFa0Ra6w2+YU1iFUboaI2jJVuqt3vEChu80y6JiRIBAaaV2L/GawDJh1dIQ==", "cpu": [ "x64" ], @@ -2890,11 +2893,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", - "integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", "dependencies": { - "undici-types": "~6.11.1" + "undici-types": "~6.13.0" } }, "node_modules/@types/node-forge": { @@ -3933,9 +3936,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -3951,9 +3954,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, "bin": { @@ -4041,9 +4044,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001645", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz", - "integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==", + "version": "1.0.30001646", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz", + "integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==", "funding": [ { "type": "opencollective", @@ -6332,9 +6335,9 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, "node_modules/globals": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", - "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, "engines": { "node": ">=18" @@ -6403,9 +6406,9 @@ "dev": true }, "node_modules/haengdong-design": { - "version": "0.1.45", - "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.45.tgz", - "integrity": "sha512-s3Xf7xRPWHfcFF4tiG470eHa1+iaBgLNrRKXaTYpmfJTO5vDV7g+zEboq0jno1zTrD0bS7QTPRE+4A5R9OlbDg==", + "version": "0.1.52", + "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.52.tgz", + "integrity": "sha512-BLbPvCm/CnuixH9kLbqPC2l7GLJQfxhjf/hRCTpfadUybSiMkYA1HOSXCBT4QY4CP5PsfzZC0L7llRCoDSNAyQ==", "dependencies": { "@emotion/react": "^11.11.4", "@storybook/addon-webpack5-compiler-swc": "^1.0.5", @@ -8501,6 +8504,112 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", + "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8699,11 +8808,11 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-router": { - "version": "6.25.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", - "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.0.tgz", + "integrity": "sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==", "dependencies": { - "@remix-run/router": "1.18.0" + "@remix-run/router": "1.19.0" }, "engines": { "node": ">=14.0.0" @@ -8713,12 +8822,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.25.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", - "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.0.tgz", + "integrity": "sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==", "dependencies": { - "@remix-run/router": "1.18.0", - "react-router": "6.25.1" + "@remix-run/router": "1.19.0", + "react-router": "6.26.0" }, "engines": { "node": ">=14.0.0" @@ -8980,9 +9089,9 @@ } }, "node_modules/rimraf": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.9.tgz", - "integrity": "sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", "dev": true, "dependencies": { "glob": "^10.3.7" @@ -8990,9 +9099,6 @@ "bin": { "rimraf": "dist/esm/bin.mjs" }, - "engines": { - "node": "14 >=14.20 || 16 >=16.20 || >=18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -10315,9 +10421,9 @@ } }, "node_modules/undici-types": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", - "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==" + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -10625,9 +10731,9 @@ "dev": true }, "node_modules/webpack-dev-middleware/node_modules/memfs": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.0.tgz", - "integrity": "sha512-+6kz90/YQoZuHvg3rn1CGPMZfEMaU5xe7xIavZMNiom2RNesiI8S37p9O9n+PlIUnUgretjLdM6HnqpZYl3X2g==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz", + "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==", "dev": true, "dependencies": { "@jsonjoy.com/json-pack": "^1.0.3", diff --git a/client/package.json b/client/package.json index 804a122d6..49de616ce 100644 --- a/client/package.json +++ b/client/package.json @@ -45,7 +45,7 @@ "@emotion/react": "^11.11.4", "@types/dotenv-webpack": "^7.0.7", "dotenv-webpack": "^8.1.0", - "haengdong-design": "^0.1.35", + "haengdong-design": "^0.1.51", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.1" diff --git a/client/src/components/Modal/Modal.style.ts b/client/src/components/Modal/Modal.style.ts deleted file mode 100644 index 727d0482f..000000000 --- a/client/src/components/Modal/Modal.style.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {css} from '@emotion/react'; - -export const modalBodyStyle = css` - z-index: 100; - background-color: white; - position: fixed; - bottom: 0; - left: 0; - right: 0; - padding: 20px; - border-radius: 20px 20px 0 0; - box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); -`; - -export const modalBackdropStyle = css` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - z-index: 99; -`; diff --git a/client/src/components/Modal/SetActionModalContent/SetPurchase.style.ts b/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.style.ts similarity index 61% rename from client/src/components/Modal/SetActionModalContent/SetPurchase.style.ts rename to client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.style.ts index cc852da28..80c770362 100644 --- a/client/src/components/Modal/SetActionModalContent/SetPurchase.style.ts +++ b/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.style.ts @@ -1,11 +1,11 @@ import {css} from '@emotion/react'; -export const setPurchaseStyle = () => +const container = () => css({ height: '100%', }); -export const setPurchaseInputContainerStyle = () => +const inputContainer = () => css({ display: 'flex', height: '100%', @@ -15,9 +15,17 @@ export const setPurchaseInputContainerStyle = () => paddingBottom: '14rem', }); -export const setPurchaseInputStyle = () => +export const input = () => css({ display: 'flex', flexDirection: 'column', gap: '0.5rem', }); + +const addBillActionListStyle = { + container, + inputContainer, + input, +}; + +export default addBillActionListStyle; diff --git a/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.tsx b/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.tsx new file mode 100644 index 000000000..873fe8118 --- /dev/null +++ b/client/src/components/Modal/SetActionModal/AddBillActionListModalContent/AddBillActionListModalContent.tsx @@ -0,0 +1,74 @@ +import {FixedButton, LabelGroupInput} from 'haengdong-design'; + +import {useStepList} from '@hooks/useStepList/useStepList'; +import validatePurchase from '@utils/validate/validatePurchase'; + +import useDynamicBillActionInput from '@hooks/useDynamicBillActionInput'; + +import style from './AddBillActionListModalContent.style'; + +interface SetPurchaseProps { + setOpenBottomSheet: React.Dispatch>; + setOrder: React.Dispatch>; +} + +const AddBillActionListModalContent = ({setOpenBottomSheet, setOrder}: SetPurchaseProps) => { + const { + inputPairList, + inputRefList, + handleInputChange, + getFilledInputPairList, + deleteEmptyInputPairElementOnBlur, + focusNextInputOnEnter, + } = useDynamicBillActionInput(validatePurchase); + const {addBill} = useStepList(); + + const handleSetPurchaseSubmit = () => { + setOrder(prev => prev + 1); + + // TODO: (@weadie) 요청 실패시 오류 핸들 필요 + addBill(getFilledInputPairList().map(({title, price}) => ({title, price: Number(price)}))); // TODO: (@weadie) DTO같은게 다이내믹에 필요할까? + setOpenBottomSheet(false); + }; + + return ( +
+
+ + {inputPairList.map(({index, title, price}) => ( +
+ handleInputChange(index, 'title', e)} + onKeyDown={e => focusNextInputOnEnter(e, index, 'title')} + onBlur={() => deleteEmptyInputPairElementOnBlur()} // TODO: (@weadie) 이 블러프롭이 내부적으로 index를 넘기고 있기 때문에 화살표 함수로 써야만하내요.. + placeholder="지출 내역" + ref={el => (inputRefList.current[index * 2] = el)} + /> + handleInputChange(index, 'price', e)} + onKeyDown={e => focusNextInputOnEnter(e, index, 'price')} + onBlur={() => deleteEmptyInputPairElementOnBlur()} + placeholder="금액" + ref={el => (inputRefList.current[index * 2 + 1] = el)} + /> +
+ ))} +
+
+ +
+ ); +}; + +export default AddBillActionListModalContent; diff --git a/client/src/components/Modal/SetActionModalContent/UpdateParticipants.style.ts b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts similarity index 60% rename from client/src/components/Modal/SetActionModalContent/UpdateParticipants.style.ts rename to client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts index 087e25a85..6162d55b5 100644 --- a/client/src/components/Modal/SetActionModalContent/UpdateParticipants.style.ts +++ b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.style.ts @@ -1,6 +1,6 @@ import {css} from '@emotion/react'; -export const updateParticipantsStyle = () => +const container = () => css({ display: 'flex', flexDirection: 'column', @@ -8,7 +8,7 @@ export const updateParticipantsStyle = () => height: '100%', }); -export const updateParticipantsInputStyle = () => +const inputGroup = () => css({ display: 'flex', flexDirection: 'column', @@ -16,3 +16,10 @@ export const updateParticipantsInputStyle = () => 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 new file mode 100644 index 000000000..59894493c --- /dev/null +++ b/client/src/components/Modal/SetActionModal/AddMemberActionListModalContent/AddMemberActionListModalContent.tsx @@ -0,0 +1,65 @@ +import {FixedButton, LabelGroupInput} from 'haengdong-design'; + +import {useStepList} from '@hooks/useStepList/useStepList'; +import validateMemberName from '@utils/validate/validateMemberName'; +import {MemberType} from 'types/serviceType'; + +import useDynamicInput from '@hooks/useDynamicInput'; + +import style from './AddMemberActionListModalContent.style'; + +interface UpdateMembersProps { + inOutAction: MemberType; + setOpenBottomSheet: React.Dispatch>; +} + +const AddMemberActionListModalContent = ({inOutAction, setOpenBottomSheet}: UpdateMembersProps) => { + const { + inputList, + inputRefList, + handleInputChange, + deleteEmptyInputElementOnBlur, + getFilledInputList, + errorMessage, + canSubmit, + focusNextInputOnEnter, + } = useDynamicInput(validateMemberName); + + const {updateMemberList} = useStepList(); + + const handleUpdateMemberListSubmit = () => { + updateMemberList({memberNameList: getFilledInputList().map(({value}) => value), type: inOutAction}); + setOpenBottomSheet(false); + }; + + 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="이름" + /> + ))} + +
+ +
+ ); +}; + +export default AddMemberActionListModalContent; diff --git a/client/src/components/Modal/SetActionModalContent/SetActionModalContent.style.ts b/client/src/components/Modal/SetActionModal/SetActionListModal.style.ts similarity index 58% rename from client/src/components/Modal/SetActionModalContent/SetActionModalContent.style.ts rename to client/src/components/Modal/SetActionModal/SetActionListModal.style.ts index 560f672d5..ca6703309 100644 --- a/client/src/components/Modal/SetActionModalContent/SetActionModalContent.style.ts +++ b/client/src/components/Modal/SetActionModal/SetActionListModal.style.ts @@ -1,6 +1,6 @@ import {css} from '@emotion/react'; -export const setActionModalContentStyle = css({ +export const container = css({ display: 'flex', flexDirection: 'column', width: '100%', @@ -9,8 +9,12 @@ export const setActionModalContentStyle = css({ gap: '1.5rem', }); -export const setActionModalContentSwitchContainerStyle = css({ +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/SetActionModalContent/SetActionModalContent.tsx b/client/src/components/Modal/SetActionModal/SetActionListModal.tsx similarity index 72% rename from client/src/components/Modal/SetActionModalContent/SetActionModalContent.tsx rename to client/src/components/Modal/SetActionModal/SetActionListModal.tsx index b3ccd64c9..f33597334 100644 --- a/client/src/components/Modal/SetActionModalContent/SetActionModalContent.tsx +++ b/client/src/components/Modal/SetActionModal/SetActionListModal.tsx @@ -1,9 +1,9 @@ import {useState} from 'react'; import {BottomSheet, Switch} from 'haengdong-design'; -import SetPurchase from './SetPurchase'; -import UpdateParticipants from './UpdateParticipants'; -import {setActionModalContentStyle, setActionModalContentSwitchContainerStyle} from './SetActionModalContent.style'; +import SetPurchase from './AddBillActionListModalContent/AddBillActionListModalContent'; +import AddMemberActionListModalContent from './AddMemberActionListModalContent/AddMemberActionListModalContent'; +import style from './SetActionListModal.style'; export type ActionType = '지출' | '인원'; @@ -13,7 +13,7 @@ interface SetActionModalContentProps { setOrder: React.Dispatch>; } -const SetActionModalContent = ({openBottomSheet, setOpenBottomSheet, setOrder}: SetActionModalContentProps) => { +const SetActionListModal = ({openBottomSheet, setOpenBottomSheet, setOrder}: SetActionModalContentProps) => { const [action, setAction] = useState('지출'); const [inOutAction, setInOutAction] = useState('탈주'); @@ -27,8 +27,8 @@ const SetActionModalContent = ({openBottomSheet, setOpenBottomSheet, setOrder}: return ( setOpenBottomSheet(false)}> -
-
+
+
{action === '인원' && ( @@ -37,7 +37,7 @@ const SetActionModalContent = ({openBottomSheet, setOpenBottomSheet, setOrder}: {action === '지출' && } {action === '인원' && ( - @@ -47,4 +47,4 @@ const SetActionModalContent = ({openBottomSheet, setOpenBottomSheet, setOrder}: ); }; -export default SetActionModalContent; +export default SetActionListModal; diff --git a/client/src/components/Modal/SetActionModal/index.ts b/client/src/components/Modal/SetActionModal/index.ts new file mode 100644 index 000000000..f689cb596 --- /dev/null +++ b/client/src/components/Modal/SetActionModal/index.ts @@ -0,0 +1 @@ +export {default as SetActionListModal} from './SetActionListModal'; diff --git a/client/src/components/Modal/SetActionModalContent/SetPurchase.tsx b/client/src/components/Modal/SetActionModalContent/SetPurchase.tsx deleted file mode 100644 index a83a697a7..000000000 --- a/client/src/components/Modal/SetActionModalContent/SetPurchase.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import {Input, FixedButton} from 'haengdong-design'; - -import {useStepList} from '@hooks/useStepList/useStepList'; - -import useDynamicInputPairs from '@hooks/useDynamicInputPairs'; - -import {setPurchaseInputStyle, setPurchaseStyle, setPurchaseInputContainerStyle} from './SetPurchase.style'; - -interface SetPurchaseProps { - setOpenBottomSheet: React.Dispatch>; - setOrder: React.Dispatch>; -} - -const SetPurchase = ({setOpenBottomSheet, setOrder}: SetPurchaseProps) => { - const {inputPairs, inputRefs, handleInputChange, handleInputBlur, getNonEmptyInputPairs} = useDynamicInputPairs(); - const {addBill} = useStepList(); - - const handleSetPurchaseSubmit = () => { - setOrder(prev => prev + 1); - - // TODO: (@weadie) 요청 실패시 오류 핸들 필요 - addBill(getNonEmptyInputPairs()); - setOpenBottomSheet(false); - }; - - return ( -
-
- {inputPairs.map((pair, index) => ( -
- handleInputChange(index, 'title', e.target.value)} - onBlur={() => handleInputBlur(index)} - placeholder="지출 내역" - ref={el => (inputRefs.current[index * 2] = el)} - /> - handleInputChange(index, 'price', e.target.value)} - onBlur={() => handleInputBlur(index)} - placeholder="금액" - ref={el => (inputRefs.current[index * 2 + 1] = el)} - /> -
- ))} -
- -
- ); -}; - -export default SetPurchase; diff --git a/client/src/components/Modal/SetActionModalContent/UpdateParticipants.tsx b/client/src/components/Modal/SetActionModalContent/UpdateParticipants.tsx deleted file mode 100644 index 5adc3cd43..000000000 --- a/client/src/components/Modal/SetActionModalContent/UpdateParticipants.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import {Input, FixedButton} from 'haengdong-design'; - -import {useStepList} from '@hooks/useStepList/useStepList'; - -import useDynamicInput from '@hooks/useDynamicAdditionalInput'; - -import {updateParticipantsInputStyle, updateParticipantsStyle} from './UpdateParticipants.style'; - -interface UpdateParticipantsProps { - inOutAction: MemberType; - setOpenBottomSheet: React.Dispatch>; -} - -const UpdateParticipants = ({inOutAction, setOpenBottomSheet}: UpdateParticipantsProps) => { - const {inputs, inputRefs, handleInputChange, handleInputBlur, getNonEmptyInputs} = useDynamicInput(); - const {updateMemberList} = useStepList(); - - const handleUpdateParticipantsSubmit = () => { - updateMemberList({memberNameList: getNonEmptyInputs(), type: inOutAction}); - setOpenBottomSheet(false); - }; - - return ( -
-
- {/* TODO: (@soha) Search로 변경하기 */} - {inputs.map((name, index) => ( - (inputRefs.current[index] = el)} - onChange={e => handleInputChange(index, e.target.value)} - onBlur={() => handleInputBlur(index)} - /> - ))} -
- -
- ); -}; - -export default UpdateParticipants; diff --git a/client/src/components/Modal/SetActionModalContent/index.ts b/client/src/components/Modal/SetActionModalContent/index.ts deleted file mode 100644 index c7514135c..000000000 --- a/client/src/components/Modal/SetActionModalContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as SetActionModalContent} from './SetActionModalContent'; diff --git a/client/src/components/Modal/SetInitialParticipants/SetInitialParticipants.style.ts b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.style.ts similarity index 73% rename from client/src/components/Modal/SetInitialParticipants/SetInitialParticipants.style.ts rename to client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.style.ts index f0f5bf996..ed835682c 100644 --- a/client/src/components/Modal/SetInitialParticipants/SetInitialParticipants.style.ts +++ b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.style.ts @@ -1,6 +1,6 @@ import {css} from '@emotion/react'; -export const setInitialParticipantsStyle = () => +export const setInitialMemberListModalStyle = () => css({ display: 'flex', flexDirection: 'column', @@ -10,7 +10,7 @@ export const setInitialParticipantsStyle = () => padding: '0 1.5rem', }); -export const setInitialParticipantsInputGroupStyle = () => +export const setInitialMemberListModalInputGroupStyle = () => css({ display: 'flex', flexDirection: 'column', diff --git a/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx new file mode 100644 index 000000000..4546e0540 --- /dev/null +++ b/client/src/components/Modal/SetInitialMemberListModal/SetInitialMemberListModal.tsx @@ -0,0 +1,62 @@ +import {Text, BottomSheet, FixedButton, LabelGroupInput} from 'haengdong-design'; + +import {useStepList} from '@hooks/useStepList/useStepList'; +import validateMemberName from '@utils/validate/validateMemberName'; + +import useDynamicInput from '@hooks/useDynamicInput'; + +import { + setInitialMemberListModalInputGroupStyle, + setInitialMemberListModalStyle, +} from './SetInitialMemberListModal.style'; + +interface SetInitialMemberListProps { + openBottomSheet: boolean; + setOpenBottomSheet: React.Dispatch>; +} + +const SetInitialMemberListModal = ({openBottomSheet, setOpenBottomSheet}: SetInitialMemberListProps) => { + const { + inputList, + inputRefList, + handleInputChange, + deleteEmptyInputElementOnBlur, + getFilledInputList, + errorMessage, + canSubmit, + focusNextInputOnEnter, + } = useDynamicInput(validateMemberName); + const {updateMemberList} = useStepList(); + + const handleSubmit = () => { + updateMemberList({memberNameList: getFilledInputList().map(({value}) => value), type: 'IN'}); + setOpenBottomSheet(false); + }; + + return ( + setOpenBottomSheet(false)}> +
+ 초기 인원 설정하기 +
+ + {inputList.map(({value, index}) => ( + (inputRefList.current[index] = el)} + onChange={e => handleInputChange(index, e)} + onBlur={() => deleteEmptyInputElementOnBlur()} + onKeyDown={e => focusNextInputOnEnter(e, index)} + /> + ))} + +
+
+ +
+ ); +}; + +export default SetInitialMemberListModal; diff --git a/client/src/components/Modal/SetInitialMemberListModal/index.ts b/client/src/components/Modal/SetInitialMemberListModal/index.ts new file mode 100644 index 000000000..18098e4f6 --- /dev/null +++ b/client/src/components/Modal/SetInitialMemberListModal/index.ts @@ -0,0 +1 @@ +export {default as SetInitialMemberListModal} from './SetInitialMemberListModal'; diff --git a/client/src/components/Modal/SetInitialParticipants/SetInitialParticipants.tsx b/client/src/components/Modal/SetInitialParticipants/SetInitialParticipants.tsx deleted file mode 100644 index e5dda7c96..000000000 --- a/client/src/components/Modal/SetInitialParticipants/SetInitialParticipants.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import {Text, Input, BottomSheet, FixedButton} from 'haengdong-design'; - -import {useStepList} from '@hooks/useStepList/useStepList'; - -import useDynamicInput from '@hooks/useDynamicAdditionalInput'; - -import {setInitialParticipantsInputGroupStyle, setInitialParticipantsStyle} from './SetInitialParticipants.style'; - -interface SetInitialParticipantsProps { - openBottomSheet: boolean; - setOpenBottomSheet: React.Dispatch>; -} - -const SetInitialParticipants = ({openBottomSheet, setOpenBottomSheet}: SetInitialParticipantsProps) => { - const {inputs, inputRefs, handleInputChange, handleInputBlur, getNonEmptyInputs} = useDynamicInput(); - const {updateMemberList} = useStepList(); - - const handleSubmit = () => { - updateMemberList({memberNameList: getNonEmptyInputs(), type: 'IN'}); - setOpenBottomSheet(false); - }; - - return ( - setOpenBottomSheet(false)}> -
- 초기 인원 설정하기 -
- {inputs.map((participant, index) => ( - (inputRefs.current[index] = el)} - onChange={e => handleInputChange(index, e.target.value)} - onBlur={() => handleInputBlur(index)} - /> - ))} -
-
- -
- ); -}; - -export default SetInitialParticipants; diff --git a/client/src/components/Modal/SetInitialParticipants/index.ts b/client/src/components/Modal/SetInitialParticipants/index.ts deleted file mode 100644 index c41b6a206..000000000 --- a/client/src/components/Modal/SetInitialParticipants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as SetInitialParticipants} from './SetInitialParticipants'; diff --git a/client/src/components/Modal/index.ts b/client/src/components/Modal/index.ts index e5d5264cd..36ccd8b2e 100644 --- a/client/src/components/Modal/index.ts +++ b/client/src/components/Modal/index.ts @@ -1,2 +1,2 @@ -export {default as SetInitialParticipants} from './SetInitialParticipants/SetInitialParticipants'; -export {default as SetActionModalContent} from './SetActionModalContent/SetActionModalContent'; +export {default as SetInitialMemberListModal} from './SetInitialMemberListModal/SetInitialMemberListModal'; +export {default as SetActionListModal} from './SetActionModal/SetActionListModal'; diff --git a/client/src/constants/errorMessage.ts b/client/src/constants/errorMessage.ts new file mode 100644 index 000000000..e37220f7f --- /dev/null +++ b/client/src/constants/errorMessage.ts @@ -0,0 +1,8 @@ +const ERROR_MESSAGE = { + eventName: '행사 이름은 30자 이하만 가능해요', + memberName: '참여자 이름은 8자 이하의 한글, 영어만 가능해요', + purchasePrice: '10,000,000원 이하의 숫자만 입력이 가능해요', + purchaseTitle: '지출 이름은 30자 이하의 한글, 영어, 숫자만 가능해요', +}; + +export default ERROR_MESSAGE; diff --git a/client/src/constants/regExp.ts b/client/src/constants/regExp.ts new file mode 100644 index 000000000..674be7ba2 --- /dev/null +++ b/client/src/constants/regExp.ts @@ -0,0 +1,6 @@ +const REGEXP = { + memberName: /^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z\s]*$/, + purchaseTitle: /^[ㄱ-ㅎㅏ-ㅣ가-힣a-zA-Z0-9\s]*$/, +}; + +export default REGEXP; diff --git a/client/src/constants/rule.ts b/client/src/constants/rule.ts new file mode 100644 index 000000000..0001cb73a --- /dev/null +++ b/client/src/constants/rule.ts @@ -0,0 +1,7 @@ +const RULE = { + maxEventNameLength: 30, + maxMemberNameLength: 8, + maxPrice: 10000000, +}; + +export default RULE; diff --git a/client/src/hooks/useDynamicAdditionalInput.tsx b/client/src/hooks/useDynamicAdditionalInput.tsx deleted file mode 100644 index 6bd592254..000000000 --- a/client/src/hooks/useDynamicAdditionalInput.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import {useEffect, useRef, useState} from 'react'; - -const useDynamicInput = () => { - const [inputs, setInputs] = useState(['']); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - // TODO: (@soha) 입력이 완료되고 중간에 값을 모두 지웠을 경우 Input이 없애지도록 수정하기 - const handleInputChange = (index: number, value: string) => { - const newInputs = [...inputs]; - newInputs[index] = value; - setInputs(newInputs); - }; - - const handleInputBlur = (index: number) => { - if (inputs[index].trim() === '') { - setInputs(prev => { - const newInputs = [...prev]; - newInputs[index] = ''; - return newInputs; - }); - } else if (inputs[index].trim() !== '' && index === inputs.length - 1) { - setInputs(prev => { - const newInputs = [...prev, '']; - newInputs[index] = inputs[index].trim(); - return newInputs; - }); - } - }; - - const getNonEmptyInputs = () => { - return inputs.filter(input => input.trim() !== ''); - }; - - useEffect(() => { - if (inputRefs.current.length > 0) { - const lastInput = inputRefs.current[inputRefs.current.length - 1]; - if (lastInput) { - lastInput.scrollIntoView({behavior: 'smooth', block: 'center'}); - } - } - }, [inputs]); - - return { - inputs, - inputRefs, - handleInputChange, - handleInputBlur, - getNonEmptyInputs, - }; -}; - -export default useDynamicInput; diff --git a/client/src/hooks/useDynamicBillActionInput.tsx b/client/src/hooks/useDynamicBillActionInput.tsx new file mode 100644 index 000000000..f81e0b114 --- /dev/null +++ b/client/src/hooks/useDynamicBillActionInput.tsx @@ -0,0 +1,142 @@ +import {useEffect, useRef, useState} from 'react'; + +import {ValidateResult} from '@utils/validate/type'; +import {Bill} from 'types/serviceType'; + +type InputPair = Omit & { + price: string; + index: number; +}; + +type BillInputType = 'title' | 'price'; + +// TODO: (@weadie) 지나치게 도메인에 묶여있는 인풋. 절대 다른 페어인풋으로 재사용할 수 없다. +const useDynamicBillActionInput = (validateFunc: (inputPair: Bill) => ValidateResult) => { + const [inputPairList, setInputPairList] = useState([{title: '', price: '', index: 0}]); + const inputRefList = useRef<(HTMLInputElement | null)[]>([]); + const [errorMessage, setErrorMessage] = useState(''); + const [canSubmit, setCanSubmit] = useState(false); + + useEffect(() => { + if (inputRefList.current.length > 0) { + const lastInputPair = inputRefList.current.slice(-2); + lastInputPair.forEach(ref => ref?.scrollIntoView({behavior: 'smooth', block: 'center'})); + } + }, [inputPairList]); + + const handleInputChange = (index: number, field: BillInputType, event: React.ChangeEvent) => { + const {value} = event.target; + const targetInputPair = findInputPairByIndex(index); + const {isValid: isValidInput, errorMessage: validationResultMessage} = validateFunc({ + ...targetInputPair, + price: 0, // price가 input에서 0을 초기값으로 갖지않도록 타입을 수정했기 때문에 0을 명시적으로 넘겨줍니다. + [field]: value, + }); + + const {title, price} = targetInputPair; + + // TODO: (@weadie) 가독성이 안좋다는 리뷰. 함수로 분리 + if (isLastInputPairFilled({index, field, value})) { + setErrorMessage(''); + setInputPairList(prevInputPairList => { + const updatedInputPairList = [...prevInputPairList]; + const targetInputPair = findInputPairByIndex(index, updatedInputPairList); + + targetInputPair[field] = value; + + // 새로운 인덱스를 inputs 배열 길이를 기준으로 설정 + const newIndex = updatedInputPairList[updatedInputPairList.length - 1].index + 1; + const finalInputs = [...updatedInputPairList, {index: newIndex, title: '', price: ''}]; + + return finalInputs; + }); + } else if (isValidInput || value.length === 0) { + setErrorMessage(''); + setInputPairList(prevInputPairList => { + const updatedInputPairList = [...prevInputPairList]; + const targetInputPair = findInputPairByIndex(index, updatedInputPairList); + + targetInputPair[field] = value; + + return updatedInputPairList; + }); + } else { + const targetInput = findInputPairByIndex(index); + + event.target.value = targetInput[field]; + + setErrorMessage(validationResultMessage ?? ''); + } + + handleCanSubmit(); + }; + + const deleteEmptyInputPairElementOnBlur = () => { + // 이름, 금액 2개중 최소 하나 이상 값을 가지고 있는 inputPair 배열 + const filledMinInputPairList = inputPairList.filter(({title, price}) => title !== '' || price !== ''); + + // 0쌍, 1쌍 input이 값이 있는 상태에서 두 쌍의 값을 모두 x버튼으로 제거해도 입력 쌍이 2개 남아있는 문제를 위해 조건문을 추가했습니다. + if (filledMinInputPairList.length === 0 && inputPairList.length > 1) { + setInputPairList([{index: 0, title: '', price: ''}]); + return; + } + + if (filledMinInputPairList.length === 0) return; + + if (filledMinInputPairList.length !== inputPairList.length) { + // 이름, 금액 2개중 하나라도 값이 있다면 지우지 않습니다. + setInputPairList(prevInputPairList => { + const filledInputPairList = prevInputPairList.filter(({title, price}) => title !== '' || price !== ''); + + const newIndex = filledInputPairList[filledInputPairList.length - 1].index + 1; + return [...filledInputPairList, {index: newIndex, title: '', price: ''}]; + }); + } + }; + + const handleCanSubmit = () => { + setCanSubmit(inputPairList.length > 0 && getFilledInputPairList().length > 0); + }; + + const focusNextInputOnEnter = (e: React.KeyboardEvent, index: number, field: BillInputType) => { + if (e.nativeEvent.isComposing) return; + + if (e.key === 'Enter') { + // 2개(제목, 가격)를 쌍으로 index를 관리하고 있으므로 input element 정확히 특정하기 위한 개별 input element key 값을 계산합니다. + const exactInputIndex = index * 2 + (field === 'title' ? 0 : 1); + + inputRefList.current[exactInputIndex + 1]?.focus(); + } + }; + + // 아래부터는 이 훅에서 재사용되는 함수입니다. + + // list 인자를 넘겨주면 그 인자로 찾고, 없다면 InputPairList state를 사용합니다. + const findInputPairByIndex = (index: number, list?: InputPair[]) => { + return (list ?? inputPairList).filter(input => input.index === index)[0]; + }; + + // list 인자를 넘겨주면 그 인자로 찾고, 없다면 InputPairList state를 사용합니다. + const getFilledInputPairList = (list?: InputPair[]) => { + return (list ?? inputPairList).filter(({title, price}) => title !== '' && price !== ''); + }; + + const isLastInputPairFilled = ({index, value}: {index: number; field: BillInputType; value: string}) => { + const lastInputIndex = inputPairList[inputPairList.length - 1].index; + + return value !== '' && index === lastInputIndex; + }; + + return { + inputPairList, + getFilledInputPairList, + inputRefList, + handleInputChange, + deleteEmptyInputPairElementOnBlur, + errorMessage, + canSubmit, + focusNextInputOnEnter, + }; +}; + +export default useDynamicBillActionInput; diff --git a/client/src/hooks/useDynamicInput.tsx b/client/src/hooks/useDynamicInput.tsx new file mode 100644 index 000000000..8588c2c40 --- /dev/null +++ b/client/src/hooks/useDynamicInput.tsx @@ -0,0 +1,139 @@ +import {useEffect, useRef, useState} from 'react'; + +import {ValidateResult} from '@utils/validate/type'; + +type InputValue = { + value: string; + index: number; +}; + +const useDynamicInput = (validateFunc: (name: string) => ValidateResult) => { + const [inputList, setInputList] = useState([{value: '', index: 0}]); + const inputRefList = useRef<(HTMLInputElement | null)[]>([]); + const [errorMessage, setErrorMessage] = useState(''); + const [canSubmit, setCanSubmit] = useState(false); + + 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; + const {isValid: isValidInput, errorMessage: validationResultMessage} = validateFunc(value); + + // TODO: (@weadie) 가독성이 안좋다는 리뷰. 함수 분리필요 + if (isLastInputFilled(index, value)) { + // 마지막 인풋이 한 자라도 채워진다면 새로운 인풋을 생성해 간편한 다음 입력을 유도합니다. + + setErrorMessage(''); + setInputList(prevInputs => { + const updatedInputList = [...prevInputs]; + const targetInput = findInputByIndex(index, updatedInputList); + + targetInput.value = value; + + // 새로운 인덱스를 inputs 배열 길이를 기준으로 설정 + const newIndex = updatedInputList[updatedInputList.length - 1].index + 1; + + return [...updatedInputList, {index: newIndex, value: ''}]; + }); + } else if (isValidInput || value.length === 0) { + // 인풋이 비어있다면 새로운 인풋을 생성하지 않습니다. + + setErrorMessage(''); + setInputList(prevInputs => { + const updatedInputList = [...prevInputs]; + const targetInput = findInputByIndex(index, updatedInputList); + + targetInput.value = value; + + return updatedInputList; + }); + } else { + // 유효성 검사에 실패한 입력입니다. 이전 입력으로 복구하고 에러 메세지를 세팅합니다. + + // index에 해당하는 아이템을 찾습니다. + const targetInput = findInputByIndex(index); + + // 오류가 난 값말고 기존의 값을 사용합니다. + event.target.value = targetInput.value; + + setErrorMessage(validationResultMessage ?? ''); + } + + handleCanSubmit(); + }; + + // 현재까지 입력된 값들로 submit을 할 수 있는지 여부를 핸들합니다. + const handleCanSubmit = () => { + setCanSubmit(inputList.length > 0 && getFilledInputList().length > 0); + }; + + 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 focusNextInputOnEnter = (e: React.KeyboardEvent, index: number) => { + if (e.nativeEvent.isComposing) return; + + if (e.key === 'Enter') { + inputRefList.current[index + 1]?.focus(); + } + }; + // 아래부터는 이 훅에서 재사용되는 함수입니다. + + // list 인자를 넘겨주면 그 인자로 찾고, 없다면 inputList state를 사용합니다. + const findInputByIndex = (index: number, list?: InputValue[]) => { + return (list ?? inputList).filter(input => input.index === index)[0]; + }; + + // list 인자를 넘겨주면 그 인자로 찾고, 없다면 inputList state를 사용합니다. + const getFilledInputList = (list?: InputValue[]) => { + return (list ?? inputList).filter(({value}) => value !== ''); + }; + + const isLastInputFilled = (index: number, value: string) => { + const lastInputIndex = inputList[inputList.length - 1].index; + + return value !== '' && index === lastInputIndex; + }; + + return { + inputList, + inputRefList, + handleInputChange, + deleteEmptyInputElementOnBlur, + errorMessage, + getFilledInputList, + focusNextInputOnEnter, + canSubmit, + // TODO: (@weadie) 네이밍 수정 + }; +}; + +export default useDynamicInput; diff --git a/client/src/hooks/useDynamicInputPairs.tsx b/client/src/hooks/useDynamicInputPairs.tsx deleted file mode 100644 index b6f86b000..000000000 --- a/client/src/hooks/useDynamicInputPairs.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import {useEffect, useRef, useState} from 'react'; - -const useDynamicInputPairs = () => { - const [inputPairs, setInputPairs] = useState([{title: '', price: 0}]); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - const handleInputChange = (index: number, field: 'title' | 'price', value: string) => { - const newInputPairs = [...inputPairs]; - newInputPairs[index] = { - ...newInputPairs[index], - [field]: field === 'price' ? parseFloat(value) : value, - }; - setInputPairs(newInputPairs); - }; - - const handleInputBlur = (index: number) => { - const currentPair = inputPairs[index]; - if (currentPair.title.trim() === '' && currentPair.price === 0) { - setInputPairs(prev => prev.filter((_, i) => i !== index)); - } else if (currentPair.title.trim() !== '' && currentPair.price !== 0 && index === inputPairs.length - 1) { - setInputPairs(prev => [...prev, {title: '', price: 0}]); - } - }; - - const getNonEmptyInputPairs = () => { - return inputPairs.filter(currentPair => currentPair.title.trim() !== '' && currentPair.price !== 0); - }; - - useEffect(() => { - if (inputRefs.current.length > 0) { - const lastInputPair = inputRefs.current.slice(-2); - lastInputPair.forEach(ref => ref?.scrollIntoView({behavior: 'smooth', block: 'center'})); - } - }, [inputPairs]); - - return { - inputPairs, - getNonEmptyInputPairs, - inputRefs, - handleInputChange, - handleInputBlur, - }; -}; - -export default useDynamicInputPairs; diff --git a/client/src/pages/Create/Name.tsx b/client/src/pages/Create/Name.tsx deleted file mode 100644 index c16870430..000000000 --- a/client/src/pages/Create/Name.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import {useState} from 'react'; -import {useNavigate} from 'react-router-dom'; -import {FixedButton, Input, MainLayout, Title, TopNav, Back} from 'haengdong-design'; - -import {requestCreateNewEvent} from '@apis/request/event'; - -import {ROUTER_URLS} from '@constants/routerUrls'; - -const CreateEvent = () => { - const [eventName, setEventName] = useState(''); - const navigate = useNavigate(); - - const submitEventName = async (event: React.FormEvent) => { - event.preventDefault(); - - const response = await requestCreateNewEvent({eventName}); - - if (response) { - const {eventId} = response; - navigate(`${ROUTER_URLS.eventCreateComplete}?${new URLSearchParams({eventId})}`); - } else { - // TODO: (@weadie) - alert('오류님'); - } - }; - - return ( - - - - - - <form onSubmit={submitEventName} style={{padding: '0 1rem'}}> - <Input - value={eventName} - onChange={event => setEventName(event.target.value)} - onBlur={() => setEventName(eventName.trim())} - placeholder="ex) 행동대장 야유회" - /> - <FixedButton disabled={!eventName.length}>행동 개시!</FixedButton> - </form> - </MainLayout> - ); -}; - -export default CreateEvent; diff --git a/client/src/pages/Create/index.ts b/client/src/pages/Create/index.ts deleted file mode 100644 index 5ce37a93e..000000000 --- a/client/src/pages/Create/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {default as CreateNamePage} from './Name'; -export {default as CreateCompletePage} from './Complete'; diff --git a/client/src/pages/Create/Complete.tsx b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx similarity index 93% rename from client/src/pages/Create/Complete.tsx rename to client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx index 786979607..0f30c1f04 100644 --- a/client/src/pages/Create/Complete.tsx +++ b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx @@ -4,7 +4,7 @@ import {FixedButton, MainLayout, Title, TopNav} from 'haengdong-design'; import {ROUTER_URLS} from '@constants/routerUrls'; -const CompleteCreateEvent = () => { +const CompleteCreateEventPage = () => { const [url, setUrl] = useState(''); const navigate = useNavigate(); const location = useLocation(); @@ -35,4 +35,4 @@ const CompleteCreateEvent = () => { ); }; -export default CompleteCreateEvent; +export default CompleteCreateEventPage; diff --git a/client/src/pages/CreateEventPage/SetEventNamePage.tsx b/client/src/pages/CreateEventPage/SetEventNamePage.tsx new file mode 100644 index 000000000..4da51dd98 --- /dev/null +++ b/client/src/pages/CreateEventPage/SetEventNamePage.tsx @@ -0,0 +1,67 @@ +import {useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {FixedButton, MainLayout, LabelInput, Input, Title, TopNav, Back} from 'haengdong-design'; + +import {requestCreateNewEvent} from '@apis/request/event'; +import validateEventName from '@utils/validate/validateEventName'; + +import {ROUTER_URLS} from '@constants/routerUrls'; + +const SetEventNamePage = () => { + const [eventName, setEventName] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [canSubmit, setCanSubmit] = useState(false); + const navigate = useNavigate(); + + const submitEventName = async (event: React.FormEvent<HTMLFormElement>) => { + event.preventDefault(); + + const response = await requestCreateNewEvent({eventName}); + + if (response) { + const {eventId} = response; + navigate(`${ROUTER_URLS.eventCreateComplete}?${new URLSearchParams({eventId})}`); + } else { + // TODO: (@weadie) + alert('오류님'); + } + }; + + const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { + const newValue = event.target.value; + const validation = validateEventName(newValue); + + setCanSubmit(newValue.length !== 0); + + if (validation.isValid) { + setEventName(newValue); + setErrorMessage(''); + } else { + event.target.value = eventName; + setErrorMessage(validation.errorMessage ?? ''); + } + }; + return ( + <MainLayout> + <TopNav> + <Back /> + </TopNav> + <Title title="행사 이름 입력" description="시작할 행사 이름을 입력해 주세요." /> + <form onSubmit={submitEventName} style={{padding: '0 1rem'}}> + <LabelInput + labelText="행사 이름" + errorText={errorMessage} + value={eventName} + type="text" + placeholder="행사 이름" + onChange={e => handleChange(e)} + isError={!!errorMessage} + autoFocus + ></LabelInput> + <FixedButton disabled={!canSubmit}>행동 개시!</FixedButton> + </form> + </MainLayout> + ); +}; + +export default SetEventNamePage; diff --git a/client/src/pages/CreateEventPage/index.ts b/client/src/pages/CreateEventPage/index.ts new file mode 100644 index 000000000..9b66c3ba5 --- /dev/null +++ b/client/src/pages/CreateEventPage/index.ts @@ -0,0 +1,2 @@ +export {default as SetEventNamePage} from './SetEventNamePage'; +export {default as CompleteCreateEventPage} from './CompleteCreateEventPage'; diff --git a/client/src/pages/Event/Admin/index.ts b/client/src/pages/Event/Admin/index.ts deleted file mode 100644 index 9799a7485..000000000 --- a/client/src/pages/Event/Admin/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as AdminPage} from './Admin'; diff --git a/client/src/pages/Event/Home/index.ts b/client/src/pages/Event/Home/index.ts deleted file mode 100644 index 75e27b8a3..000000000 --- a/client/src/pages/Event/Home/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as HomePage} from './Home'; diff --git a/client/src/pages/Event/index.ts b/client/src/pages/Event/index.ts deleted file mode 100644 index af2aa805d..000000000 --- a/client/src/pages/Event/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as EventPage} from './EventLayout'; diff --git a/client/src/pages/Event/Admin/Admin.style.ts b/client/src/pages/EventPage/AdminPage/AdminPage.style.ts similarity index 100% rename from client/src/pages/Event/Admin/Admin.style.ts rename to client/src/pages/EventPage/AdminPage/AdminPage.style.ts diff --git a/client/src/pages/Event/Admin/Admin.tsx b/client/src/pages/EventPage/AdminPage/AdminPage.tsx similarity index 78% rename from client/src/pages/Event/Admin/Admin.tsx rename to client/src/pages/EventPage/AdminPage/AdminPage.tsx index bda03da55..2c4ccd5a9 100644 --- a/client/src/pages/Event/Admin/Admin.tsx +++ b/client/src/pages/EventPage/AdminPage/AdminPage.tsx @@ -6,35 +6,30 @@ import {useStepList} from '@hooks/useStepList/useStepList'; import {requestGetEventName} from '@apis/request/event'; import useEventId from '@hooks/useEventId/useEventId'; -import {SetActionModalContent, SetInitialParticipants} from '@components/Modal'; +import {SetActionListModal, SetInitialMemberListModal} from '@components/Modal'; -import {ReceiptStyle} from './Admin.style'; +import {ReceiptStyle} from './AdminPage.style'; -export type PurchaseInformation = { - title: string; - price: number; -}; - -export type ParticipantType = { - name: string; - type: InOutType; -}; - -interface ModalRenderingProps { +interface ModalBasedOnMemberCountProps { memberNameList: string[]; openBottomSheet: boolean; setOrder: React.Dispatch<React.SetStateAction<number>>; setOpenBottomSheet: React.Dispatch<React.SetStateAction<boolean>>; } -const ModalRendering = ({memberNameList, openBottomSheet, setOrder, setOpenBottomSheet}: ModalRenderingProps) => { +const ModalBasedOnMemberCount = ({ + memberNameList, + openBottomSheet, + setOrder, + setOpenBottomSheet, +}: ModalBasedOnMemberCountProps) => { switch (memberNameList.length) { case 0: - return <SetInitialParticipants setOpenBottomSheet={setOpenBottomSheet} openBottomSheet={openBottomSheet} />; + return <SetInitialMemberListModal setOpenBottomSheet={setOpenBottomSheet} openBottomSheet={openBottomSheet} />; default: return ( - <SetActionModalContent + <SetActionListModal setOrder={setOrder} setOpenBottomSheet={setOpenBottomSheet} openBottomSheet={openBottomSheet} @@ -43,7 +38,7 @@ const ModalRendering = ({memberNameList, openBottomSheet, setOrder, setOpenBotto } }; -const Admin = () => { +const AdminPage = () => { const [openBottomSheet, setOpenBottomSheet] = useState(false); const [order, setOrder] = useState<number>(1); @@ -82,7 +77,7 @@ const Admin = () => { onClick={() => setOpenBottomSheet(prev => !prev)} /> {openBottomSheet && ( - <ModalRendering + <ModalBasedOnMemberCount memberNameList={memberNameList} setOrder={setOrder} setOpenBottomSheet={setOpenBottomSheet} @@ -94,4 +89,4 @@ const Admin = () => { ); }; -export default Admin; +export default AdminPage; diff --git a/client/src/pages/EventPage/AdminPage/index.ts b/client/src/pages/EventPage/AdminPage/index.ts new file mode 100644 index 000000000..b80c0bb2f --- /dev/null +++ b/client/src/pages/EventPage/AdminPage/index.ts @@ -0,0 +1 @@ +export {default as AdminPage} from './AdminPage'; diff --git a/client/src/pages/Event/EventLayout.tsx b/client/src/pages/EventPage/EvenPageLayout.tsx similarity index 88% rename from client/src/pages/Event/EventLayout.tsx rename to client/src/pages/EventPage/EvenPageLayout.tsx index 2b2b439de..3f68b4ba1 100644 --- a/client/src/pages/Event/EventLayout.tsx +++ b/client/src/pages/EventPage/EvenPageLayout.tsx @@ -5,7 +5,7 @@ import StepListProvider from '@hooks/useStepList/useStepList'; import useNavSwitch from '@hooks/useNavSwitch'; -const EventLayout = () => { +const EventPageLayout = () => { const {nav, paths, onChange} = useNavSwitch(); return ( @@ -20,4 +20,4 @@ const EventLayout = () => { ); }; -export default EventLayout; +export default EventPageLayout; diff --git a/client/src/pages/Event/Home/Home.tsx b/client/src/pages/EventPage/HomePage/HomePage.tsx similarity index 95% rename from client/src/pages/Event/Home/Home.tsx rename to client/src/pages/EventPage/HomePage/HomePage.tsx index 8ff195cb8..19227d404 100644 --- a/client/src/pages/Event/Home/Home.tsx +++ b/client/src/pages/EventPage/HomePage/HomePage.tsx @@ -7,7 +7,7 @@ import {useStepList} from '@hooks/useStepList/useStepList'; import useEventId from '@hooks/useEventId/useEventId'; import {requestGetEventName} from '@apis/request/event'; -const HomeContent = () => { +const HomePage = () => { const {getTotalPrice} = useStepList(); const {eventId} = useEventId(); @@ -37,4 +37,4 @@ const HomeContent = () => { ); }; -export default HomeContent; +export default HomePage; diff --git a/client/src/pages/EventPage/HomePage/index.ts b/client/src/pages/EventPage/HomePage/index.ts new file mode 100644 index 000000000..aa0bf2b3f --- /dev/null +++ b/client/src/pages/EventPage/HomePage/index.ts @@ -0,0 +1 @@ +export {default as HomePage} from './HomePage'; diff --git a/client/src/pages/EventPage/index.ts b/client/src/pages/EventPage/index.ts new file mode 100644 index 000000000..c560632da --- /dev/null +++ b/client/src/pages/EventPage/index.ts @@ -0,0 +1 @@ +export {default as EventPage} from './EvenPageLayout'; diff --git a/client/src/pages/Main/index.ts b/client/src/pages/Main/index.ts deleted file mode 100644 index c3cce532e..000000000 --- a/client/src/pages/Main/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {default as MainPage} from './Main'; diff --git a/client/src/pages/Main/Main.tsx b/client/src/pages/MainPage/MainPage.tsx similarity index 70% rename from client/src/pages/Main/Main.tsx rename to client/src/pages/MainPage/MainPage.tsx index 2a032b5cf..b216e50c6 100644 --- a/client/src/pages/Main/Main.tsx +++ b/client/src/pages/MainPage/MainPage.tsx @@ -3,13 +3,11 @@ import {FixedButton, MainLayout, Title, TopNav} from 'haengdong-design'; import {ROUTER_URLS} from '@constants/routerUrls'; -const Main = () => { +const MainPage = () => { const navigate = useNavigate(); return ( <MainLayout> - {/* <TopNav navType="back" /> */} - <Title title="행동대장" description="랜딩페이지입니다. 뿌뿌 잠깐만 테스트해볼게요.." /> <TopNav children={<></>} /> <Title title="행동대장" description="랜딩페이지입니다." /> <FixedButton onClick={() => navigate(ROUTER_URLS.eventCreateName)}>행사 생성하기</FixedButton> @@ -17,4 +15,4 @@ const Main = () => { ); }; -export default Main; +export default MainPage; diff --git a/client/src/pages/MainPage/index.ts b/client/src/pages/MainPage/index.ts new file mode 100644 index 000000000..017fff307 --- /dev/null +++ b/client/src/pages/MainPage/index.ts @@ -0,0 +1 @@ +export {default as MainPage} from './MainPage'; diff --git a/client/src/router.tsx b/client/src/router.tsx index 70f7a1e71..75777310a 100644 --- a/client/src/router.tsx +++ b/client/src/router.tsx @@ -1,11 +1,11 @@ import {createBrowserRouter} from 'react-router-dom'; -import {AdminPage} from '@pages/Event/Admin'; -import {HomePage} from '@pages/Event/Home'; +import {AdminPage} from '@pages/EventPage/AdminPage'; +import {HomePage} from '@pages/EventPage/HomePage'; -import {MainPage} from '@pages/Main'; -import {CreateNamePage, CreateCompletePage} from '@pages/Create'; -import {EventPage} from '@pages/Event'; +import {CompleteCreateEventPage, SetEventNamePage} from '@pages/CreateEventPage'; +import {MainPage} from '@pages/MainPage'; +import {EventPage} from '@pages/EventPage'; import {ROUTER_URLS} from '@constants/routerUrls'; @@ -23,11 +23,11 @@ const router = createBrowserRouter([ }, { path: ROUTER_URLS.eventCreateName, - element: <CreateNamePage />, + element: <SetEventNamePage />, }, { path: ROUTER_URLS.eventCreateComplete, - element: <CreateCompletePage />, + element: <CompleteCreateEventPage />, }, { path: ROUTER_URLS.event, diff --git a/client/src/types/serviceType.ts b/client/src/types/serviceType.ts new file mode 100644 index 000000000..9b6fbe06f --- /dev/null +++ b/client/src/types/serviceType.ts @@ -0,0 +1,54 @@ +export type MemberType = 'IN' | 'OUT'; + +export type InOutType = '늦참' | '탈주'; + +export type MemberReport = { + name: string; + price: number; +}; + +export type Bill = { + 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[]; +}; + +// TODO: (@weadie) 준 데이터 형식에서 steps를 빼내 flat하게 사용중. 일관성있게 하는게 좋긴 하나 사용시 번거로움이 있을 거라고 판단. +export type StepList = { + steps: (MemberStep | BillStep)[]; +}; + +export type Action = { + actionId: number; + name: string; + price: number | null; + sequence: number; +}; + +export type BillAction = Omit<Action, 'price'> & { + price: number; +}; + +export type MemberAction = Omit<Action, 'price'> & { + price: null; +}; + +export type Member = { + name: string; + status: MemberType; +}; diff --git a/client/src/utils/validate/type.ts b/client/src/utils/validate/type.ts new file mode 100644 index 000000000..1c994c1f8 --- /dev/null +++ b/client/src/utils/validate/type.ts @@ -0,0 +1,4 @@ +export interface ValidateResult { + isValid: boolean; + errorMessage?: string; +} diff --git a/client/src/utils/validate/validateEventName.ts b/client/src/utils/validate/validateEventName.ts new file mode 100644 index 000000000..b390f12b9 --- /dev/null +++ b/client/src/utils/validate/validateEventName.ts @@ -0,0 +1,13 @@ +import ERROR_MESSAGE from '@constants/errorMessage'; +import RULE from '@constants/rule'; + +import {ValidateResult} from './type'; + +const validateEventName = (name: string): ValidateResult => { + if (name.length > RULE.maxEventNameLength) { + return {isValid: false, errorMessage: ERROR_MESSAGE.eventName}; + } + return {isValid: true}; +}; + +export default validateEventName; diff --git a/client/src/utils/validate/validateMemberName.ts b/client/src/utils/validate/validateMemberName.ts new file mode 100644 index 000000000..0e7483358 --- /dev/null +++ b/client/src/utils/validate/validateMemberName.ts @@ -0,0 +1,25 @@ +import REGEXP from '@constants/regExp'; +import ERROR_MESSAGE from '@constants/errorMessage'; +import RULE from '@constants/rule'; + +import {ValidateResult} from './type'; + +const validateMemberName = (name: string): ValidateResult => { + const validateOnlyString = () => { + if (!REGEXP.memberName.test(name)) return false; + return true; + }; + + const validateLength = () => { + if (name.length > RULE.maxMemberNameLength || name.length < 1) return false; + return true; + }; + + if (validateOnlyString() && validateLength()) { + return {isValid: true}; + } + + return {isValid: false, errorMessage: ERROR_MESSAGE.memberName}; +}; + +export default validateMemberName; diff --git a/client/src/utils/validate/validatePurchase.ts b/client/src/utils/validate/validatePurchase.ts new file mode 100644 index 000000000..026cfb496 --- /dev/null +++ b/client/src/utils/validate/validatePurchase.ts @@ -0,0 +1,34 @@ +import ERROR_MESSAGE from '@constants/errorMessage'; +import RULE from '@constants/rule'; +import REGEXP from '@constants/regExp'; + +import {ValidateResult} from './type'; + +const validatePurchase = (inputPair: Bill): ValidateResult => { + const {title, price} = inputPair; + let errorMessage; + + const validatePrice = () => { + if (price > RULE.maxPrice) { + errorMessage = ERROR_MESSAGE.purchasePrice; + return false; + } + return true; + }; + + const validateTitle = () => { + if (REGEXP.purchaseTitle.test(title)) { + errorMessage = ERROR_MESSAGE.purchaseTitle; + return false; + } + return true; + }; + + if (validatePrice() && validateTitle()) { + return {isValid: true}; + } + + return {isValid: true, errorMessage: ''}; +}; + +export default validatePurchase;