Skip to content

Commit 9caf791

Browse files
authored
Merge pull request codeit-maso#113 from codeit-maso/feature/chan
✨ feat: 이모지 리액션 옵티미스틱 UI 적용 및 드래그 방지
2 parents dbdd343 + d719722 commit 9caf791

File tree

3 files changed

+72
-11
lines changed

3 files changed

+72
-11
lines changed

src/components/FontSelect/FontSelect.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useRef, useId } from 'react';
1+
import { useRef, useId } from 'react';
22
import useDetectClose from '../../hooks/useDetectClose';
33
import styles from './FontSelect.module.scss';
44

src/components/recipient/HeaderService/HeaderService.jsx

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export default function HeaderService({ recipient }) {
99
const [reactions, setReactions] = useState([]);
1010
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
1111
const [showAllEmojisDropdown, setShowAllEmojisDropdown] = useState(false);
12+
const pendingEmojisRef = useRef([]);
13+
const [pendingEmojis, setPendingEmojis] = useState([]);
1214
const emojiPickerRef = useRef(null);
1315
const emojiMoreRef = useRef(null);
1416
const [isLargeScreen, setIsLargeScreen] = useState(true);
@@ -81,16 +83,45 @@ export default function HeaderService({ recipient }) {
8183

8284
const handleAddReaction = async (emoji) => {
8385
if (!recipient?.id) return;
86+
if (pendingEmojisRef.current.includes(emoji)) return;
87+
88+
pendingEmojisRef.current = [...pendingEmojisRef.current, emoji];
89+
setPendingEmojis(pendingEmojisRef.current);
90+
91+
setReactions((prev) => {
92+
const found = prev.find((r) => r.emoji === emoji);
93+
if (found) {
94+
return prev.map((r) =>
95+
r.emoji === emoji ? { ...r, count: r.count + 1 } : r,
96+
);
97+
} else {
98+
return [...prev, { id: `optimistic-${emoji}`, emoji, count: 1 }];
99+
}
100+
});
84101

85102
try {
86103
await addReaction(recipient.id, emoji);
87-
88104
const updatedReactions = await fetchReactions(recipient.id);
89105
setReactions(updatedReactions);
106+
} catch (error) {
107+
console.error(error);
108+
setReactions((prev) => {
109+
const found = prev.find((r) => r.emoji === emoji);
110+
if (found && found.count === 1 && found.id?.startsWith('optimistic-')) {
111+
return prev.filter((r) => r.emoji !== emoji);
112+
} else {
113+
return prev.map((r) =>
114+
r.emoji === emoji ? { ...r, count: r.count - 1 } : r,
115+
);
116+
}
117+
});
118+
} finally {
119+
pendingEmojisRef.current = pendingEmojisRef.current.filter(
120+
(e) => e !== emoji,
121+
);
122+
setPendingEmojis(pendingEmojisRef.current);
90123
setShowEmojiPicker(false);
91124
setShowAllEmojisDropdown(false);
92-
} catch (error) {
93-
console.error('이모지 추가 실패:', error);
94125
}
95126
};
96127

@@ -148,13 +179,19 @@ export default function HeaderService({ recipient }) {
148179
<div className={styles['header-service__emojis']}>
149180
{topReactions.length > 0 ? (
150181
topReactions.map((reaction) => (
151-
<div
182+
<button
152183
key={reaction.id}
153184
className={styles['header-service__emoji-item']}
154185
onClick={() => handleAddReaction(reaction.emoji)}
186+
disabled={pendingEmojis.includes(reaction.emoji)}
155187
>
156-
{reaction.emoji} {reaction.count}
157-
</div>
188+
<span className={styles['header-service__emoji']}>
189+
{reaction.emoji}
190+
</span>{' '}
191+
<span className={styles['header-service__count']}>
192+
{reaction.count}
193+
</span>
194+
</button>
158195
))
159196
) : (
160197
<span>아직 리액션이 없어요</span>
@@ -188,15 +225,21 @@ export default function HeaderService({ recipient }) {
188225
.sort((a, b) => b.count - a.count)
189226
.slice(0, isLargeScreen ? 8 : 6)
190227
.map((reaction) => (
191-
<div
228+
<button
192229
key={reaction.id}
193230
className={
194231
styles['header-service__emoji-dropdown-item']
195232
}
196233
onClick={() => handleAddReaction(reaction.emoji)}
234+
disabled={pendingEmojis.includes(reaction.emoji)}
197235
>
198-
{reaction.emoji} {reaction.count}
199-
</div>
236+
<span className={styles['header-service__emoji']}>
237+
{reaction.emoji}
238+
</span>{' '}
239+
<span className={styles['header-service__count']}>
240+
{reaction.count}
241+
</span>
242+
</button>
200243
))
201244
) : (
202245
<span>아직 리액션이 없어요</span>

src/components/recipient/HeaderService/HeaderService.module.scss

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@
232232
transition:
233233
transform 0.2s ease,
234234
background-color 0.2s ease;
235+
user-select: none;
236+
-webkit-user-select: none;
235237

236238
&:hover {
237239
background-color: #5d5d5d;
@@ -252,6 +254,12 @@
252254
padding: 2px 6px;
253255
font-size: 13px;
254256
}
257+
258+
&:disabled {
259+
opacity: 0.5;
260+
cursor: not-allowed;
261+
pointer-events: none;
262+
}
255263
}
256264

257265
&__emoji-more-container {
@@ -296,13 +304,15 @@
296304
}
297305

298306
@media (max-width: 767px) {
307+
right: -24px;
299308
width: 240px;
300309
min-width: auto;
301310
box-sizing: border-box;
302311
}
303312

304313
@media (max-width: 420px) {
305-
width: 220px;
314+
right: -12px;
315+
width: 200px;
306316
padding: 8px;
307317
top: 35px;
308318
}
@@ -347,6 +357,8 @@
347357
transform 0.2s ease,
348358
background-color 0.2s ease;
349359
text-align: center;
360+
user-select: none;
361+
-webkit-user-select: none;
350362

351363
&:hover {
352364
background-color: #5d5d5d;
@@ -366,6 +378,12 @@
366378
padding: 2px 6px;
367379
font-size: 13px;
368380
}
381+
382+
&:disabled {
383+
opacity: 0.5;
384+
cursor: not-allowed;
385+
pointer-events: none;
386+
}
369387
}
370388

371389
&__add-button {

0 commit comments

Comments
 (0)