diff --git a/src/components/mobile/molecules/GameTagModal/GameTagModal.style.ts b/src/components/mobile/molecules/GameTagModal/GameTagModal.style.ts index 7741aae8..6f3767a1 100644 --- a/src/components/mobile/molecules/GameTagModal/GameTagModal.style.ts +++ b/src/components/mobile/molecules/GameTagModal/GameTagModal.style.ts @@ -6,7 +6,7 @@ export const contentWrapper = css({ display: 'flex', flexDirection: 'column', alignItems: 'center', - gap: '20px', + gap: '16px', }); export const textBox = css({ @@ -24,15 +24,29 @@ export const tagTextStyling = css(typo.Mobile.Text.SemiBold_14, { color: color.GY[1], }); +export const subTagTextStyling = css(typo.Mobile.Main.Regular_12, { + color: color.GY[2], +}); + export const markStyling = css(typo.Mobile.Text.SemiBold_14, { color: color.MAIN, }); +export const errorMessageStyling = css(typo.Mobile.Main.Regular_12, { + color: color.RED, +}); + export const tagWrapper = css({ width: '100%', display: 'flex', flexDirection: 'column', - gap: '6px', + gap: '8px', +}); + +export const tagBottomWrapper = css({ + width: '100%', + display: 'flex', + flexDirection: 'column', }); export const buttonWrapper = css({ @@ -46,6 +60,42 @@ export const buttonStyling = css(typo.Mobile.Text.SemiBold_14, { borderRadius: '6px', }); +export const inputWrapper = css({ + display: 'flex', + flexDirection: 'column', + height: '67px', + gap: '4px', +}); + +export const subTagChipStyling = css(typo.Mobile.Text.SemiBold_12, { + display: 'flex', + alignItems: 'center', + padding: '5px 9px 5px 12px', + gap: '5px', + borderRadius: '6px', + outline: `1px solid ${color.SECONDARY}`, + backgroundColor: color.WT_VIOLET, + color: color.MAIN, +}); + +export const subTagButtonStyling = css({ + all: 'unset', + display: 'flex', + color: color.MAIN, + fontSize: '12px', + cursor: 'pointer', +}); + +export const subTagWrapper = (isTagMax: boolean) => + css({ + display: 'flex', + flexWrap: 'wrap', + width: '100%', + gap: '5px', + paddingTop: '3px', + paddingBottom: isTagMax ? '17px' : '5px', + }); + export const inputStyling = css(typo.Mobile.Text.Medium_12, { fontSize: '14px', width: '295px', diff --git a/src/components/mobile/molecules/GameTagModal/GameTagModal.tsx b/src/components/mobile/molecules/GameTagModal/GameTagModal.tsx index e60aa62b..9678af5e 100644 --- a/src/components/mobile/molecules/GameTagModal/GameTagModal.tsx +++ b/src/components/mobile/molecules/GameTagModal/GameTagModal.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { BalanceGame } from '@/types/game'; import { MobileCheckIcon } from '@/assets'; import { TAG_OPTIONS } from '@/constants/game'; @@ -6,6 +6,8 @@ import Modal from '@/components/mobile/atoms/Modal/Modal'; import Button from '@/components/mobile/atoms/Button/Button'; import Divider from '@/components/atoms/Divider/Divider'; import { validateGameTag } from '@/hooks/game/validateBalanceGameForm'; +import { createArrayFromCommaString } from '@/utils/array'; +import useOutsideClick from '@/hooks/common/useOutsideClick'; import * as S from './GameTagModal.style'; interface GameTagModalProps { @@ -13,7 +15,7 @@ interface GameTagModalProps { isOpen?: boolean; onClose?: () => void; setMainTagValue: (name: string, tag: string) => void; - setSubTagValue: (e: React.ChangeEvent) => void; + setSubTagValue: (name: string, tag: string) => void; submitGame: () => void; } @@ -25,12 +27,62 @@ const GameTagModal = ({ setSubTagValue, submitGame, }: GameTagModalProps) => { + const inputRef = useRef(null); + const currentMainTag: string = form.mainTag; + const [subTagArray] = useState(() => createArrayFromCommaString(form.subTag)); + const [currentSubTag, setCurrentSubTag] = useState(subTagArray); + + const [inputValue, setInputValue] = useState(''); + const [inputError, setInputError] = useState(false); + + useEffect(() => { + const subTagList = inputValue + ? [...currentSubTag, inputValue] + : currentSubTag; + setSubTagValue('subTag', subTagList.join(',')); + }, [currentSubTag, inputValue, setSubTagValue]); + + const handleInputChange = (e: React.ChangeEvent) => { + const { value } = e.target; + + if (value.length > 10) return; + + setInputValue(value); + setInputError(false); + }; + + const handleSpaceAction = () => { + if (!inputValue.trim()) return; + + setCurrentSubTag((prev) => [...prev, inputValue.trim()]); + setInputValue(''); + setInputError(false); + }; + useOutsideClick(inputRef, handleSpaceAction); + + const handleKeyUp = (e: React.KeyboardEvent) => { + if (!inputValue) { + setInputError(false); + return; + } + + if (e.code === 'Space') { + e.preventDefault(); + handleSpaceAction(); + } + + setInputError(inputValue.length >= 10); + }; const handleMainTag = (tag: string) => { setMainTagValue('mainTag', tag); }; + const handleDeleteSubTag = (idx: number) => { + setCurrentSubTag((prev) => prev.filter((_, i) => i !== idx)); + }; + const handleTagSubmit = () => { if (!currentMainTag) return; @@ -66,28 +118,54 @@ const GameTagModal = ({ ))} -
+
서브태그 + (최대 3개) +
+
+ {currentSubTag.map((tag, idx) => ( +
+ #{tag} + +
+ ))}
- + {currentSubTag.length !== 3 && ( +
+ + {inputError && ( + + 서브태그 1개 당 최대 10자까지 입력 가능 + + )} +
+ )} +
-
); diff --git a/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx b/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx index 580d817c..66095843 100644 --- a/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx +++ b/src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx @@ -65,7 +65,7 @@ const BalanceGameCreateSection = ({ isOpen={tagModalOpen} onClose={() => setTagModalOpen(false)} setMainTagValue={setEach} - setSubTagValue={onChange} + setSubTagValue={setEach} submitGame={handleBalanceGame} /> )} diff --git a/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts b/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts index b53bae11..4b0e5e0b 100644 --- a/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts +++ b/src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts @@ -78,6 +78,7 @@ export const descriptionStyling = css(typo.Mobile.Main.Regular_12, { export const subTagWrapper = css({ display: 'flex', + flexWrap: 'wrap', width: '100%', marginBottom: '50px', gap: '8px', diff --git a/src/stories/mobile/molecules/GameTagModal.stories.tsx b/src/stories/mobile/molecules/GameTagModal.stories.tsx index 3f167186..0c9ce13e 100644 --- a/src/stories/mobile/molecules/GameTagModal.stories.tsx +++ b/src/stories/mobile/molecules/GameTagModal.stories.tsx @@ -7,8 +7,8 @@ import type { Meta, StoryObj } from '@storybook/react'; const defaultGameOptions = createInitialGameStages(10); const exampleGame: BalanceGame = { title: 'title', - mainTag: 'mainTag', - subTag: 'subTag', + mainTag: '커플', + subTag: '커플커플커플커플커플', games: defaultGameOptions, }; diff --git a/src/styles/color.ts b/src/styles/color.ts index 2553aa05..a0e261d1 100644 --- a/src/styles/color.ts +++ b/src/styles/color.ts @@ -1,5 +1,6 @@ const color = { MAIN: '#7782FF', + SECONDARY: '#9DB7FF', BK: '#181818', GY: { 1: '#8C8C8C', diff --git a/src/utils/array.ts b/src/utils/array.ts index cec126f5..bfd22762 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -11,5 +11,5 @@ export const createRangeArray = (currentPage: number, maxPage: number) => { }; export const createArrayFromCommaString = (str: string): string[] => { - return str.split(','); + return str ? str.split(',') : []; };