Skip to content

Commit b51643c

Browse files
authored
refactor: 밸런스 게임 서브 태그 모달 ui 및 로직 수정 (#310)
* refactor: 밸런스 게임 서브태그 모달 ui 수정 * feat: 스페이스 바로 서브 태그 블록이 생성되도록 로직 구현 * feat: 서브 태그를 10글자 이상 입력 시 에러 메세지가 제시되도록 구현 * refactor: 서브 태그가 바로 업데이트되도록 수정 및 서브 태그가 여러 개로 제시되도록 수정 * feat: 인풋 바깥 영역 클릭 시 인풋 내의 텍스트가 태그 블록으로 처리되도록 구현 * refactor: handleKeyUp 함수 간결화 * refactor: 서브 태그 블록에 key 값 추가 * refactor: MAIN_2 색상 이름을 SECONDARY로 수정 * refactor: 서브 태그 값 업데이트 useEffect 문의 의존성 배열 수정
1 parent b2be48d commit b51643c

7 files changed

Lines changed: 156 additions & 26 deletions

File tree

src/components/mobile/molecules/GameTagModal/GameTagModal.style.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const contentWrapper = css({
66
display: 'flex',
77
flexDirection: 'column',
88
alignItems: 'center',
9-
gap: '20px',
9+
gap: '16px',
1010
});
1111

1212
export const textBox = css({
@@ -24,15 +24,29 @@ export const tagTextStyling = css(typo.Mobile.Text.SemiBold_14, {
2424
color: color.GY[1],
2525
});
2626

27+
export const subTagTextStyling = css(typo.Mobile.Main.Regular_12, {
28+
color: color.GY[2],
29+
});
30+
2731
export const markStyling = css(typo.Mobile.Text.SemiBold_14, {
2832
color: color.MAIN,
2933
});
3034

35+
export const errorMessageStyling = css(typo.Mobile.Main.Regular_12, {
36+
color: color.RED,
37+
});
38+
3139
export const tagWrapper = css({
3240
width: '100%',
3341
display: 'flex',
3442
flexDirection: 'column',
35-
gap: '6px',
43+
gap: '8px',
44+
});
45+
46+
export const tagBottomWrapper = css({
47+
width: '100%',
48+
display: 'flex',
49+
flexDirection: 'column',
3650
});
3751

3852
export const buttonWrapper = css({
@@ -46,6 +60,42 @@ export const buttonStyling = css(typo.Mobile.Text.SemiBold_14, {
4660
borderRadius: '6px',
4761
});
4862

63+
export const inputWrapper = css({
64+
display: 'flex',
65+
flexDirection: 'column',
66+
height: '67px',
67+
gap: '4px',
68+
});
69+
70+
export const subTagChipStyling = css(typo.Mobile.Text.SemiBold_12, {
71+
display: 'flex',
72+
alignItems: 'center',
73+
padding: '5px 9px 5px 12px',
74+
gap: '5px',
75+
borderRadius: '6px',
76+
outline: `1px solid ${color.SECONDARY}`,
77+
backgroundColor: color.WT_VIOLET,
78+
color: color.MAIN,
79+
});
80+
81+
export const subTagButtonStyling = css({
82+
all: 'unset',
83+
display: 'flex',
84+
color: color.MAIN,
85+
fontSize: '12px',
86+
cursor: 'pointer',
87+
});
88+
89+
export const subTagWrapper = (isTagMax: boolean) =>
90+
css({
91+
display: 'flex',
92+
flexWrap: 'wrap',
93+
width: '100%',
94+
gap: '5px',
95+
paddingTop: '3px',
96+
paddingBottom: isTagMax ? '17px' : '5px',
97+
});
98+
4999
export const inputStyling = css(typo.Mobile.Text.Medium_12, {
50100
fontSize: '14px',
51101
width: '295px',

src/components/mobile/molecules/GameTagModal/GameTagModal.tsx

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
import React from 'react';
1+
import React, { useEffect, useRef, useState } from 'react';
22
import { BalanceGame } from '@/types/game';
33
import { MobileCheckIcon } from '@/assets';
44
import { TAG_OPTIONS } from '@/constants/game';
55
import Modal from '@/components/mobile/atoms/Modal/Modal';
66
import Button from '@/components/mobile/atoms/Button/Button';
77
import Divider from '@/components/atoms/Divider/Divider';
88
import { validateGameTag } from '@/hooks/game/validateBalanceGameForm';
9+
import { createArrayFromCommaString } from '@/utils/array';
10+
import useOutsideClick from '@/hooks/common/useOutsideClick';
911
import * as S from './GameTagModal.style';
1012

1113
interface GameTagModalProps {
1214
form: BalanceGame;
1315
isOpen?: boolean;
1416
onClose?: () => void;
1517
setMainTagValue: (name: string, tag: string) => void;
16-
setSubTagValue: (e: React.ChangeEvent<HTMLInputElement>) => void;
18+
setSubTagValue: (name: string, tag: string) => void;
1719
submitGame: () => void;
1820
}
1921

@@ -25,12 +27,62 @@ const GameTagModal = ({
2527
setSubTagValue,
2628
submitGame,
2729
}: GameTagModalProps) => {
30+
const inputRef = useRef<HTMLInputElement>(null);
31+
2832
const currentMainTag: string = form.mainTag;
33+
const [subTagArray] = useState(() => createArrayFromCommaString(form.subTag));
34+
const [currentSubTag, setCurrentSubTag] = useState<string[]>(subTagArray);
35+
36+
const [inputValue, setInputValue] = useState<string>('');
37+
const [inputError, setInputError] = useState<boolean>(false);
38+
39+
useEffect(() => {
40+
const subTagList = inputValue
41+
? [...currentSubTag, inputValue]
42+
: currentSubTag;
43+
setSubTagValue('subTag', subTagList.join(','));
44+
}, [currentSubTag, inputValue, setSubTagValue]);
45+
46+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
47+
const { value } = e.target;
48+
49+
if (value.length > 10) return;
50+
51+
setInputValue(value);
52+
setInputError(false);
53+
};
54+
55+
const handleSpaceAction = () => {
56+
if (!inputValue.trim()) return;
57+
58+
setCurrentSubTag((prev) => [...prev, inputValue.trim()]);
59+
setInputValue('');
60+
setInputError(false);
61+
};
62+
useOutsideClick(inputRef, handleSpaceAction);
63+
64+
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
65+
if (!inputValue) {
66+
setInputError(false);
67+
return;
68+
}
69+
70+
if (e.code === 'Space') {
71+
e.preventDefault();
72+
handleSpaceAction();
73+
}
74+
75+
setInputError(inputValue.length >= 10);
76+
};
2977

3078
const handleMainTag = (tag: string) => {
3179
setMainTagValue('mainTag', tag);
3280
};
3381

82+
const handleDeleteSubTag = (idx: number) => {
83+
setCurrentSubTag((prev) => prev.filter((_, i) => i !== idx));
84+
};
85+
3486
const handleTagSubmit = () => {
3587
if (!currentMainTag) return;
3688

@@ -66,28 +118,54 @@ const GameTagModal = ({
66118
))}
67119
</div>
68120
</div>
69-
<div css={S.tagWrapper}>
121+
<div css={S.tagBottomWrapper}>
70122
<div css={S.textBox}>
71123
<span css={S.tagTextStyling}>서브태그</span>
124+
<span css={S.subTagTextStyling}>(최대 3개)</span>
125+
</div>
126+
<div css={S.subTagWrapper(currentSubTag.length === 3)}>
127+
{currentSubTag.map((tag, idx) => (
128+
<div css={S.subTagChipStyling} key={tag}>
129+
<span>#{tag}</span>
130+
<button
131+
type="button"
132+
css={S.subTagButtonStyling}
133+
onClick={() => handleDeleteSubTag(idx)}
134+
>
135+
136+
</button>
137+
</div>
138+
))}
72139
</div>
73-
<input
74-
name="subTag"
75-
css={S.inputStyling}
76-
placeholder="ex. 너무어려운밸런스게임, 선택장애, 이상형"
77-
value={form.subTag}
78-
onChange={setSubTagValue}
79-
/>
140+
{currentSubTag.length !== 3 && (
141+
<div css={S.inputWrapper}>
142+
<input
143+
type="text"
144+
ref={inputRef}
145+
css={S.inputStyling}
146+
value={inputValue}
147+
placeholder="ex. 연애, 데이트, 데이트취향"
148+
onChange={handleInputChange}
149+
onKeyUp={handleKeyUp}
150+
/>
151+
{inputError && (
152+
<span css={S.errorMessageStyling}>
153+
서브태그 1개 당 최대 10자까지 입력 가능
154+
</span>
155+
)}
156+
</div>
157+
)}
158+
<Button
159+
size="large"
160+
variant="roundPrimary"
161+
onClick={handleTagSubmit}
162+
disabled={!currentMainTag}
163+
active={!!currentMainTag}
164+
css={S.customButtonStyle(!currentMainTag)}
165+
>
166+
등록하기
167+
</Button>
80168
</div>
81-
<Button
82-
size="large"
83-
variant="roundPrimary"
84-
onClick={handleTagSubmit}
85-
disabled={!currentMainTag}
86-
active={!!currentMainTag}
87-
css={S.customButtonStyle(!currentMainTag)}
88-
>
89-
등록하기
90-
</Button>
91169
</div>
92170
</Modal>
93171
);

src/components/mobile/organisms/BalanceGameCreateSection/BalanceGameCreateSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const BalanceGameCreateSection = ({
6565
isOpen={tagModalOpen}
6666
onClose={() => setTagModalOpen(false)}
6767
setMainTagValue={setEach}
68-
setSubTagValue={onChange}
68+
setSubTagValue={setEach}
6969
submitGame={handleBalanceGame}
7070
/>
7171
)}

src/components/mobile/organisms/BalanceGameSection/BalanceGameSection.style.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const descriptionStyling = css(typo.Mobile.Main.Regular_12, {
7878

7979
export const subTagWrapper = css({
8080
display: 'flex',
81+
flexWrap: 'wrap',
8182
width: '100%',
8283
marginBottom: '50px',
8384
gap: '8px',

src/stories/mobile/molecules/GameTagModal.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import type { Meta, StoryObj } from '@storybook/react';
77
const defaultGameOptions = createInitialGameStages(10);
88
const exampleGame: BalanceGame = {
99
title: 'title',
10-
mainTag: 'mainTag',
11-
subTag: 'subTag',
10+
mainTag: '커플',
11+
subTag: '커플커플커플커플커플',
1212
games: defaultGameOptions,
1313
};
1414

src/styles/color.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const color = {
22
MAIN: '#7782FF',
3+
SECONDARY: '#9DB7FF',
34
BK: '#181818',
45
GY: {
56
1: '#8C8C8C',

src/utils/array.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ export const createRangeArray = (currentPage: number, maxPage: number) => {
1111
};
1212

1313
export const createArrayFromCommaString = (str: string): string[] => {
14-
return str.split(',');
14+
return str ? str.split(',') : [];
1515
};

0 commit comments

Comments
 (0)