Skip to content

Commit d977031

Browse files
authored
Merge pull request #58 from prgrms-aibe-devcourse/refactor/50-chat-typing-indicator-fix
[Refactor] 타이핑 인디케이터 버그 수정 및 메시지 구조 개선
2 parents c6b0cf1 + bca6032 commit d977031

10 files changed

Lines changed: 565 additions & 456 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ Thumbs.db
3737

3838
# Git fetch metadata accidentally created in the workspace root
3939
/FETCH_HEAD
40+
41+
# 로컬 도구 설정
42+
.claude/

src/app/components/ChannelPanel.tsx

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Hash, MessageSquare, Send, Bookmark, Share2, MoreVertical, X, Paperclip, Smile, UserPlus, FileUp, Image as ImageIcon, Link2 } from "lucide-react";
22
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
33
import { createFileMessageAttachment, createLinkMessageAttachment, createLinkMessageAttachmentFromText, messageAttachmentGroups, messageAttachmentTypeLabels, type MessageAttachment, type MessageAttachmentType } from "./messageAttachments";
4-
import { TypingIndicator } from "./TypingIndicator";
54
import { EmojiPicker } from "./EmojiPicker";
65
import { MessageReactions, toggleMessageReaction, type MessageReaction } from "./MessageReactions";
76
import { MessageAttachmentCard } from "./MessageAttachmentCard";
7+
import { TypingIndicatorBar } from "./TypingIndicatorBar";
88

99
interface Thread {
1010
id: number;
@@ -91,6 +91,19 @@ function saveThreads(storageKey: string, threads: Thread[]) {
9191
}
9292
}
9393

94+
const currentUserDisplayName = "김재준";
95+
const currentUserAvatar = currentUserDisplayName.charAt(0);
96+
const selfUserNames = new Set(["나", "me", "you", "jean", "jeaju", currentUserDisplayName]);
97+
98+
function isSelfUser(user?: string) {
99+
return selfUserNames.has((user ?? "").trim().toLowerCase());
100+
}
101+
102+
function getDisplayUserName(user?: string) {
103+
const trimmed = (user ?? "").trim();
104+
return isSelfUser(trimmed) ? currentUserDisplayName : trimmed;
105+
}
106+
94107
export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCounts = {}, onOpenThread, onOpenInvite, onToggleReaction }: ChannelPanelProps) {
95108
const channelStorageId = channelId ?? repoId ?? "general";
96109
const channelStorageKey = `${CHANNEL_THREADS_KEY_PREFIX}:${channelStorageId}`;
@@ -145,11 +158,15 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
145158
const scrollContainer = scrollContainerRef.current;
146159
if (!scrollContainer) return;
147160

148-
scrollContainer.scrollTo({
149-
top: scrollContainer.scrollHeight,
150-
behavior: "smooth"
161+
const frameId = window.requestAnimationFrame(() => {
162+
scrollContainer.scrollTo({
163+
top: scrollContainer.scrollHeight,
164+
behavior: "smooth"
165+
});
151166
});
152-
}, [threads.length, responderTyping]);
167+
168+
return () => window.cancelAnimationFrame(frameId);
169+
}, [threads.length, responderTyping, messageText]);
153170

154171
const triggerResponderTyping = () => {
155172
if (responderTypingTimerRef.current) {
@@ -170,15 +187,12 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
170187
const canSendMessage = messageText.trim().length > 0 || selectedAttachments.length > 0;
171188
const composerTyping = messageText.trim().length > 0;
172189
const typingLabel = responderTyping
173-
? "CodeDock AI가 답변을 정리 중입니다"
190+
? composerTyping
191+
? `CodeDock AI, ${currentUserDisplayName} 입력 중입니다`
192+
: "CodeDock AI가 답변을 정리 중입니다"
174193
: composerTyping
175194
? "내가 입력 중입니다"
176195
: "";
177-
const typingNote = responderTyping
178-
? "채널 맥락을 확인하고 다음 메시지를 준비합니다."
179-
: composerTyping
180-
? "팀원에게 입력 중 상태로 표시됩니다."
181-
: "";
182196

183197
const handleAttachmentToggle = (attachment: MessageAttachment) => {
184198
setSelectedAttachments((prev) =>
@@ -253,8 +267,8 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
253267

254268
const nextThread: Thread = {
255269
id: Date.now(),
256-
user: '나',
257-
avatar: '나',
270+
user: currentUserDisplayName,
271+
avatar: currentUserAvatar,
258272
message: trimmedMessage || `${outgoingAttachments.length}개 항목을 공유합니다.`,
259273
time: '방금',
260274
replies: 0,
@@ -319,30 +333,51 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
319333
<div className="grid gap-4">
320334
{threads.map((thread) => {
321335
const displayedReplyCount = replyCounts[thread.id] ?? thread.replies;
336+
const isOwnThread = isSelfUser(thread.user);
322337

323338
return (
324339
<div
325340
key={thread.id}
326341
className="rounded-xl overflow-hidden relative group"
327342
style={{
328-
background: 'rgba(5, 11, 20, 0.6)',
329-
border: '1px solid rgba(32, 227, 255, 0.14)'
343+
width: '100%',
344+
background: isOwnThread ? 'rgba(32, 227, 255, 0.075)' : 'rgba(5, 11, 20, 0.54)',
345+
border: isOwnThread ? '1px solid rgba(32, 227, 255, 0.18)' : '1px solid rgba(32, 227, 255, 0.14)',
346+
borderRadius: '12px',
347+
boxShadow: 'none'
330348
}}
331349
onMouseEnter={() => setHoveredMessageId(thread.id)}
332350
onMouseLeave={() => setHoveredMessageId(null)}
333351
>
334352
<div className="w-full px-5 py-4">
335353
<div className="flex items-start gap-3">
336-
<span style={{ fontSize: '28px', lineHeight: 1 }}>{thread.avatar}</span>
354+
<span className="grid h-10 w-10 flex-shrink-0 place-items-center rounded-full" style={{
355+
background: isOwnThread ? 'rgba(32, 227, 255, 0.16)' : 'rgba(32, 227, 255, 0.12)',
356+
border: isOwnThread ? '1px solid rgba(32, 227, 255, 0.30)' : '1px solid rgba(32, 227, 255, 0.22)',
357+
color: 'var(--neon-cyan)',
358+
fontSize: thread.avatar.length > 2 ? '18px' : '13px',
359+
fontWeight: 950,
360+
lineHeight: 1
361+
}}>{isOwnThread ? currentUserAvatar : thread.avatar}</span>
337362
<div className="flex-1 min-w-0">
338363
<div className="flex items-center gap-2 mb-1">
339364
<span className="tracking-tight" style={{
340365
fontSize: '13px',
341366
fontWeight: 900,
342-
color: 'var(--matrix-green)'
367+
color: isOwnThread ? 'var(--neon-cyan)' : 'var(--matrix-green)'
343368
}}>
344-
{thread.user}
369+
{isOwnThread ? getDisplayUserName(thread.user) : thread.user}
345370
</span>
371+
{isOwnThread && (
372+
<span className="rounded px-1.5 py-0.5 tracking-tight" style={{
373+
background: 'rgba(32, 227, 255, 0.12)',
374+
color: 'var(--neon-cyan)',
375+
fontSize: '10px',
376+
fontWeight: 950
377+
}}>
378+
내 메시지
379+
</span>
380+
)}
346381
<span className="tracking-tight" style={{
347382
fontSize: '11px',
348383
fontWeight: 700,
@@ -484,13 +519,6 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
484519
</div>
485520
);
486521
})}
487-
{typingLabel && (
488-
<TypingIndicator
489-
label={typingLabel}
490-
note={typingNote}
491-
avatar={responderTyping ? "AI" : "나"}
492-
/>
493-
)}
494522
</div>
495523
</div>
496524

@@ -693,20 +721,22 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
693721
onChange={(event) => handleLocalFilesSelected(event, "image")}
694722
/>
695723

696-
<div className="flex items-center gap-2 px-4 py-3 rounded-xl" style={{
724+
<TypingIndicatorBar label={typingLabel} />
725+
726+
<div className="relative flex items-center gap-2 px-4 py-3 rounded-xl" style={{
697727
background: 'rgba(5, 11, 20, 0.6)',
698728
border: '1px solid rgba(32, 227, 255, 0.14)'
699729
}}>
700-
<input
701-
type="text"
702-
value={messageText}
703-
onChange={(event) => setMessageText(event.target.value)}
704-
onKeyDown={handleMessageKeyDown}
705-
placeholder={`#${channelLabel}에 메시지 보내기`}
706-
className="min-w-0 flex-1 bg-transparent border-0 outline-none tracking-tight"
707-
style={{ color: 'var(--white)', fontSize: '14px', fontWeight: 700 }}
708-
/>
709-
<div className="flex shrink-0 items-center gap-1">
730+
<input
731+
type="text"
732+
value={messageText}
733+
onChange={(event) => setMessageText(event.target.value)}
734+
onKeyDown={handleMessageKeyDown}
735+
placeholder={`#${channelLabel}에 메시지 보내기`}
736+
className="min-w-0 flex-1 bg-transparent border-0 outline-none tracking-tight"
737+
style={{ color: 'var(--white)', fontSize: '14px', fontWeight: 700 }}
738+
/>
739+
<div className="flex shrink-0 items-center gap-1">
710740
<button
711741
onClick={() => togglePanel('attachment')}
712742
className="w-9 h-9 rounded-lg border-0 flex items-center justify-center transition-all cursor-pointer"

0 commit comments

Comments
 (0)