-
Notifications
You must be signed in to change notification settings - Fork 0
[refactor] 동아리 목록, 상세 및 채팅 페이지 리디자인 반영 #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "188-refactor-\uB3D9\uC544\uB9AC-\uBAA9\uB85D-\uC0C1\uC138-\uBC0F-\uCC44\uD305-\uD398\uC774\uC9C0-\uB9AC\uB514\uC790\uC778-\uBC18\uC601"
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,10 @@ | ||||||||||||||||||||||||
| import { useEffect, useRef, useState } from 'react'; | ||||||||||||||||||||||||
| import clsx from 'clsx'; | ||||||||||||||||||||||||
| import { useParams } from 'react-router-dom'; | ||||||||||||||||||||||||
| import PaperPlaneIcon from '@/assets/svg/paper-plane.svg'; | ||||||||||||||||||||||||
| import type { ChatMessage } from '@/apis/chat/entity'; | ||||||||||||||||||||||||
| import SendArrowIcon from '@/assets/svg/chat-send-arrow.svg'; | ||||||||||||||||||||||||
| import LinkifiedText from '@/components/common/LinkifiedText'; | ||||||||||||||||||||||||
| import useKeyboardHeight from '@/utils/hooks/useViewportHeight'; | ||||||||||||||||||||||||
| import { cn } from '@/utils/ts/cn'; | ||||||||||||||||||||||||
| import useChat from './hooks/useChat'; | ||||||||||||||||||||||||
| import useChatRoomScroll from './hooks/useChatRoomScroll'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -23,6 +24,58 @@ const formatTime = (dateString: string) => { | |||||||||||||||||||||||
| return `${hour}:${minute}`; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| interface ChatMessageRowProps { | ||||||||||||||||||||||||
| isGroup: boolean; | ||||||||||||||||||||||||
| isSameSender: boolean; | ||||||||||||||||||||||||
| message: ChatMessage; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function ChatMessageRow({ isGroup, isSameSender, message }: ChatMessageRowProps) { | ||||||||||||||||||||||||
| const showSenderName = isGroup && !message.isMine && !isSameSender; | ||||||||||||||||||||||||
| const formattedTime = formatTime(message.createdAt); | ||||||||||||||||||||||||
| const formattedUnreadCount = message.unreadCount > 0 ? String(message.unreadCount) : null; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (message.isMine) { | ||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||
| <div className="flex justify-end px-6 py-2"> | ||||||||||||||||||||||||
| <div className="flex max-w-full items-end gap-2"> | ||||||||||||||||||||||||
| {formattedUnreadCount && ( | ||||||||||||||||||||||||
| <span className="text-primary-500 text-[10px] leading-[1.6] font-medium">{formattedUnreadCount}</span> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
| <span className="text-[10px] leading-[1.6] font-medium text-indigo-100">{formattedTime}</span> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div className="bg-primary-500/80 text-sub4 max-w-[78%] rounded-2xl px-3 py-2 text-white shadow-[0_0_3px_rgba(0,0,0,0.15)]"> | ||||||||||||||||||||||||
| <LinkifiedText | ||||||||||||||||||||||||
| text={message.content} | ||||||||||||||||||||||||
| className="wrap-anywhere whitespace-pre-wrap" | ||||||||||||||||||||||||
| linkClassName="text-white underline" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||
| <div className="px-6 py-2"> | ||||||||||||||||||||||||
| <div className="max-w-full"> | ||||||||||||||||||||||||
| {showSenderName && <div className="text-sub4 text-text-400 mb-1 px-3">{message.senderName}</div>} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div className="flex items-end gap-2"> | ||||||||||||||||||||||||
| <div className="bg-indigo-5 text-sub4 max-w-[78%] rounded-2xl px-3 py-2 text-black"> | ||||||||||||||||||||||||
| <LinkifiedText | ||||||||||||||||||||||||
| text={message.content} | ||||||||||||||||||||||||
| className="wrap-anywhere whitespace-pre-wrap" | ||||||||||||||||||||||||
| linkClassName="text-primary-500 underline" | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| <span className="shrink-0 text-[10px] leading-[1.6] font-medium text-indigo-100">{formattedTime}</span> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| function ChatRoom() { | ||||||||||||||||||||||||
| const { chatRoomId } = useParams(); | ||||||||||||||||||||||||
| const { sendMessage, chatMessages, fetchNextPage, hasNextPage, isFetchingNextPage, chatRoomList, isSendingMessage } = | ||||||||||||||||||||||||
|
|
@@ -47,6 +100,16 @@ function ChatRoom() { | |||||||||||||||||||||||
| const isGroup = currentRoom?.chatType === 'GROUP'; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const sortedMessages = [...chatMessages].reverse(); | ||||||||||||||||||||||||
| const isSubmitDisabled = isSendingMessage || !value.trim(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const resetTextareaHeight = () => { | ||||||||||||||||||||||||
| if (!textareaRef.current) return; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| textareaRef.current.style.height = 'auto'; | ||||||||||||||||||||||||
| const baseHeight = baseTextareaHeightRef.current || textareaRef.current.scrollHeight; | ||||||||||||||||||||||||
| baseTextareaHeightRef.current = baseHeight; | ||||||||||||||||||||||||
| textareaRef.current.style.height = `${baseHeight}px`; | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| const handleSubmit = (e: React.FormEvent) => { | ||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||
|
|
@@ -61,8 +124,6 @@ function ChatRoom() { | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| setValue(''); | ||||||||||||||||||||||||
| if (textareaRef.current) { | ||||||||||||||||||||||||
| const baseHeight = baseTextareaHeightRef.current || textareaRef.current.scrollHeight; | ||||||||||||||||||||||||
| textareaRef.current.style.height = `${baseHeight}px`; | ||||||||||||||||||||||||
| textareaRef.current.focus(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| scrollToBottom(); | ||||||||||||||||||||||||
|
|
@@ -81,18 +142,20 @@ function ChatRoom() { | |||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||
| if (!textareaRef.current) return; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| textareaRef.current.style.height = 'auto'; | ||||||||||||||||||||||||
| baseTextareaHeightRef.current = textareaRef.current.scrollHeight; | ||||||||||||||||||||||||
| textareaRef.current.style.height = `${baseTextareaHeightRef.current}px`; | ||||||||||||||||||||||||
| resetTextareaHeight(); | ||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||
| if (!value) { | ||||||||||||||||||||||||
| resetTextareaHeight(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }, [value]); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||
| <div className="bg-indigo-0 flex min-h-0 flex-1 flex-col"> | ||||||||||||||||||||||||
| <div className="flex min-h-0 flex-1 flex-col bg-white"> | ||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||
| ref={scrollContainerRef} | ||||||||||||||||||||||||
| className="bg-indigo-0 min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain pb-4" | ||||||||||||||||||||||||
| className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain py-3" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
|
Comment on lines
156
to
159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메시지 목록을 live region으로 노출해주세요. 현재 컨테이너는 동적으로 추가되는 메시지를 보조기기가 채팅 로그로 인식할 근거가 없습니다. 채팅처럼 순차적으로 항목이 추가되는 영역은 💡 제안 <div
ref={scrollContainerRef}
+ role="log"
+ aria-label="채팅 메시지"
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain py-3"
>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| <div ref={topRef} /> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -106,91 +169,42 @@ function ChatRoom() { | |||||||||||||||||||||||
| const isSameSender = prevMessage?.senderId === message.senderId && !showDateHeader; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||
| key={message.messageId} | ||||||||||||||||||||||||
| className={clsx('w-full min-w-0 px-6', showDateHeader ? 'mt-4' : isSameSender ? 'mt-1' : 'mt-3')} | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| <div key={message.messageId} className="w-full min-w-0"> | ||||||||||||||||||||||||
| {showDateHeader && ( | ||||||||||||||||||||||||
| <div className="flex justify-center py-3"> | ||||||||||||||||||||||||
| <span className="bg-indigo-25 text-primary rounded-full px-3 py-1 text-xs"> | ||||||||||||||||||||||||
| <div className={cn('flex justify-center px-6', index === 0 ? 'pb-2' : 'pt-4 pb-2')}> | ||||||||||||||||||||||||
| <span className="text-text-400 text-sub4 rounded-2xl bg-white px-3 py-1"> | ||||||||||||||||||||||||
| {formatDate(message.createdAt)} | ||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div className={clsx('flex w-full min-w-0 items-end', message.isMine ? 'justify-end' : 'justify-start')}> | ||||||||||||||||||||||||
| {!message.isMine && ( | ||||||||||||||||||||||||
| <div className="flex max-w-full min-w-0 flex-col"> | ||||||||||||||||||||||||
| {isGroup && !isSameSender && ( | ||||||||||||||||||||||||
| <div className="text-body3 mb-1 pl-10 text-indigo-400">{message.senderName}</div> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div className="flex min-w-0 items-start gap-2"> | ||||||||||||||||||||||||
| {isGroup && ( | ||||||||||||||||||||||||
| <div className="w-8 shrink-0"> | ||||||||||||||||||||||||
| {!isSameSender ? ( | ||||||||||||||||||||||||
| <div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-300 text-xs"> | ||||||||||||||||||||||||
| {message.senderName?.[0]} | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||
| <div className="h-8 w-8" /> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div className="bg-info-100 text-body1 rounded-lg px-3 py-2 wrap-anywhere whitespace-pre-wrap"> | ||||||||||||||||||||||||
| <LinkifiedText text={message.content} /> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div className="flex shrink-0 flex-col items-start gap-0.5 self-end"> | ||||||||||||||||||||||||
| {message.unreadCount > 0 && ( | ||||||||||||||||||||||||
| <span className="text-cap2 text-info-600 font-medium">{message.unreadCount}</span> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
| <span className="text-cap2 text-indigo-300">{formatTime(message.createdAt)}</span> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| {/* ===== RIGHT (내 메시지) ===== */} | ||||||||||||||||||||||||
| {message.isMine && ( | ||||||||||||||||||||||||
| <div className="flex max-w-full min-w-0 flex-row-reverse items-end gap-2"> | ||||||||||||||||||||||||
| <div className="bg-indigo-5 text-body1 rounded-lg px-3 py-2 wrap-anywhere whitespace-pre-wrap"> | ||||||||||||||||||||||||
| <LinkifiedText text={message.content} /> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <div className="flex shrink-0 flex-col items-end self-end"> | ||||||||||||||||||||||||
| {message.unreadCount > 0 && ( | ||||||||||||||||||||||||
| <span className="text-cap2 text-info-600 font-medium">{message.unreadCount}</span> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
| <span className="text-cap2 text-indigo-300">{formatTime(message.createdAt)}</span> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| <ChatMessageRow isGroup={isGroup} isSameSender={isSameSender} message={message} /> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| })} | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <form onSubmit={handleSubmit} className="bg-indigo-25 flex min-w-0 shrink-0 items-end gap-2 px-5 py-2"> | ||||||||||||||||||||||||
| <textarea | ||||||||||||||||||||||||
| ref={textareaRef} | ||||||||||||||||||||||||
| value={value} | ||||||||||||||||||||||||
| onChange={handleInputChange} | ||||||||||||||||||||||||
| className="bg-indigo-0 max-h-32 min-w-0 flex-1 resize-none overflow-x-hidden rounded-sm px-3 py-2 text-sm wrap-anywhere whitespace-pre-wrap text-indigo-700 placeholder:text-indigo-500" | ||||||||||||||||||||||||
| rows={1} | ||||||||||||||||||||||||
| placeholder="메세지 보내기" | ||||||||||||||||||||||||
| maxLength={1000} | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||
| type="submit" | ||||||||||||||||||||||||
| disabled={isSendingMessage || !value.trim()} | ||||||||||||||||||||||||
| className="bg-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-sm disabled:opacity-50" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| <PaperPlaneIcon className="text-indigo-0" /> | ||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||
| <form onSubmit={handleSubmit} className="shrink-0 bg-white px-5 pt-3 pb-[calc(22px+var(--sab))]"> | ||||||||||||||||||||||||
| <div className="bg-text-100 flex min-w-0 items-end gap-3 rounded-[30px] px-4 py-3"> | ||||||||||||||||||||||||
| <textarea | ||||||||||||||||||||||||
| ref={textareaRef} | ||||||||||||||||||||||||
| value={value} | ||||||||||||||||||||||||
| onChange={handleInputChange} | ||||||||||||||||||||||||
| aria-label="메시지 입력" | ||||||||||||||||||||||||
| className="text-text-600 text-sub4 placeholder:text-text-300 max-h-32 min-h-6 min-w-0 flex-1 resize-none overflow-x-hidden bg-transparent py-1 wrap-anywhere whitespace-pre-wrap outline-none" | ||||||||||||||||||||||||
| rows={1} | ||||||||||||||||||||||||
| maxLength={1000} | ||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||
| type="submit" | ||||||||||||||||||||||||
| aria-label="메시지 전송" | ||||||||||||||||||||||||
| disabled={isSubmitDisabled} | ||||||||||||||||||||||||
| className="text-text-600 disabled:text-text-300 flex size-6 shrink-0 items-center justify-center" | ||||||||||||||||||||||||
| > | ||||||||||||||||||||||||
| <SendArrowIcon className="size-6" /> | ||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClubDetail는 전용 Layout 배경을 분리해주세요.
ClubDetail는src/pages/Club/ClubDetail/index.tsx에서 루트 배경을bg-indigo-5로 두고 있지만, 실제 페이지 배경과 헤더 padding 영역은Layout의<main>이 담당합니다. 지금처럼 공용<Layout />아래에 두면 상세 상단/overscroll 구간은 기본 배경색이 남아서 리디자인과 다른 띠가 보일 수 있습니다.clubs/:clubId만contentClassName="bg-indigo-5"를 주는 Layout으로 분리하는 편이 안전합니다.💡 제안
As per coding guidelines
src/pages/**/*.tsx: PassshowBottomNav(bottom tab display) andcontentClassName(background color, etc.) props to Layout component.🤖 Prompt for AI Agents