From 3e87d44d196bf5b9009d361ba086727263385505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 18 Mar 2026 22:56:43 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=B0=8F=20=EC=83=81=EC=84=B8=20=EB=A6=AC?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- src/components/layout/Header/headerConfig.ts | 4 ++ .../Application/components/AccountInfo.tsx | 2 +- .../Club/ClubDetail/components/ClubIntro.tsx | 2 +- .../Club/ClubDetail/components/ClubMember.tsx | 67 +++++++++++++------ .../ClubDetail/components/ClubRecruitment.tsx | 6 +- src/pages/Club/ClubDetail/index.tsx | 43 ++++++------ .../Club/ClubList/components/ClubCard.tsx | 16 ++--- .../Club/ClubList/components/SearchBar.tsx | 28 +++++--- src/pages/Club/ClubList/index.tsx | 12 ++-- src/pages/Club/ClubSearch/index.tsx | 7 +- 11 files changed, 115 insertions(+), 74 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6dc7e6d..a701b87 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -98,10 +98,10 @@ function App() { } /> } /> - } /> }> + } /> } /> } /> } /> diff --git a/src/components/layout/Header/headerConfig.ts b/src/components/layout/Header/headerConfig.ts index a0b8983..82a1f18 100644 --- a/src/components/layout/Header/headerConfig.ts +++ b/src/components/layout/Header/headerConfig.ts @@ -5,6 +5,10 @@ export const HEADER_CONFIGS: HeaderConfig[] = [ type: 'none', match: (pathname) => pathname === '/', }, + { + type: 'default', + match: (pathname) => /^\/clubs\/\d+$/.test(pathname), + }, { type: 'profile', match: (pathname) => pathname === '/mypage', diff --git a/src/pages/Club/Application/components/AccountInfo.tsx b/src/pages/Club/Application/components/AccountInfo.tsx index 8f47ee6..a5bf346 100644 --- a/src/pages/Club/Application/components/AccountInfo.tsx +++ b/src/pages/Club/Application/components/AccountInfo.tsx @@ -41,7 +41,7 @@ function AccountInfoCard({ accountInfo }: AccountInfoCardProps) { showToast('복사에 실패했습니다'); } }} - className="bg-primary text-indigo-0 flex items-center justify-center gap-1.5 rounded-sm py-3 text-xs font-medium disabled:opacity-50" + className="bg-primary-500 text-indigo-0 flex items-center justify-center gap-1.5 rounded-sm py-3 text-xs font-medium disabled:opacity-50" > 계좌번호 복사하기 diff --git a/src/pages/Club/ClubDetail/components/ClubIntro.tsx b/src/pages/Club/ClubDetail/components/ClubIntro.tsx index 45db858..9ab1329 100644 --- a/src/pages/Club/ClubDetail/components/ClubIntro.tsx +++ b/src/pages/Club/ClubDetail/components/ClubIntro.tsx @@ -61,7 +61,7 @@ function ClubIntro({ clubDetail }: ClubIntroProps) { type="button" onClick={handleInquireClick} disabled={isCreatingChatRoom} - className="bg-primary text-body3 flex items-center justify-center gap-1 rounded-sm py-3 text-white" + className="bg-primary-500 text-body3 flex items-center justify-center gap-1 rounded-sm py-3 text-white" > {isCreatingChatRoom ? '이동 중...' : '문의하기'} diff --git a/src/pages/Club/ClubDetail/components/ClubMember.tsx b/src/pages/Club/ClubDetail/components/ClubMember.tsx index 7813bff..12c5aba 100644 --- a/src/pages/Club/ClubDetail/components/ClubMember.tsx +++ b/src/pages/Club/ClubDetail/components/ClubMember.tsx @@ -1,49 +1,78 @@ +import clsx from 'clsx'; import { useParams } from 'react-router-dom'; import type { ClubMember, PositionType } from '@/apis/club/entity'; -import Card from '@/components/common/Card'; import { useGetClubMembers } from '../hooks/useGetClubMembers'; const POSITION_LABELS: Record = { PRESIDENT: '회장', VICE_PRESIDENT: '부회장', - MANAGER: '관리자', + MANAGER: '임원진', MEMBER: '일반 회원', }; +const POSITION_BADGE_STYLES: Partial> = { + PRESIDENT: 'bg-[#69BFDF]', + VICE_PRESIDENT: 'bg-[#F6DE8C]', + MANAGER: 'bg-[#FFB8B8]', +}; + +function ClubMemberAvatar({ imageUrl, name }: Pick) { + if (imageUrl) { + return ( + {`${name} + ); + } + + return
; +} + const ClubMemberCard = (clubMember: ClubMember) => { return ( - - Member Avatar -
-
-
{clubMember.name}
+
+ +
+
+
{clubMember.name}
{clubMember.position !== 'MEMBER' && ( -
+
{POSITION_LABELS[clubMember.position]}
)}
-
{clubMember.studentNumber}
+
{clubMember.studentNumber}
- +
); }; -function ClubMemberTab() { +interface ClubMemberTabProps { + memberCount: number; +} + +function ClubMemberTab({ memberCount }: ClubMemberTabProps) { const { clubId } = useParams(); const { data: clubMembers } = useGetClubMembers(Number(clubId)); - const totalMembers = clubMembers?.clubMembers.length; + const members = clubMembers?.clubMembers ?? []; + const totalMembers = clubMembers?.clubMembers.length ?? memberCount; + return ( - <> -
- 총 {totalMembers}명의 동아리인원 -
-
- {clubMembers?.clubMembers.map((member) => ( +
+
{totalMembers}명
+
+ {members.map((member) => ( ))}
- +
); } diff --git a/src/pages/Club/ClubDetail/components/ClubRecruitment.tsx b/src/pages/Club/ClubDetail/components/ClubRecruitment.tsx index bcfeada..86a8fe4 100644 --- a/src/pages/Club/ClubDetail/components/ClubRecruitment.tsx +++ b/src/pages/Club/ClubDetail/components/ClubRecruitment.tsx @@ -58,7 +58,7 @@ function ClubRecruitment({ clubId, isMember, presidentUserId }: ClubRecruitProps hasQuestions ? ( 지원하기 @@ -66,7 +66,7 @@ function ClubRecruitment({ clubId, isMember, presidentUserId }: ClubRecruitProps @@ -101,7 +101,7 @@ function ClubRecruitment({ clubId, isMember, presidentUserId }: ClubRecruitProps type="button" onClick={handleApply} disabled={isPending} - className="bg-primary text-h3 w-full rounded-lg py-3.5 text-center text-white disabled:opacity-50" + className="bg-primary-500 text-h3 w-full rounded-lg py-3.5 text-center text-white disabled:opacity-50" > 지원하기 diff --git a/src/pages/Club/ClubDetail/index.tsx b/src/pages/Club/ClubDetail/index.tsx index cfd7fa9..50b478d 100644 --- a/src/pages/Club/ClubDetail/index.tsx +++ b/src/pages/Club/ClubDetail/index.tsx @@ -1,6 +1,6 @@ import { Activity, useEffect } from 'react'; import clsx from 'clsx'; -import { useParams, useSearchParams, useLocation } from 'react-router-dom'; +import { useLocation, useParams, useSearchParams } from 'react-router-dom'; import useScrollToTop from '@/utils/hooks/useScrollToTop'; import ClubAccount from './components/ClubAccount'; import ClubIntro from './components/ClubIntro'; @@ -24,9 +24,10 @@ function ClubDetail() { }, [location.state]); const [searchParams, setSearchParams] = useSearchParams(); - const currentTab = searchParams.get('tab') || 'intro'; + const requestedTab = searchParams.get('tab'); + const clubIdNumber = Number(clubId); - const { data: clubDetail } = useGetClubDetail(Number(clubId)); + const { data: clubDetail } = useGetClubDetail(clubIdNumber); const handleTabClick = (tab: TabType) => { setSearchParams({ tab }, { replace: true }); @@ -44,32 +45,34 @@ function ClubDetail() { ]; const visibleTabs = tabs.filter((tab) => tab.show); + const currentTab = visibleTabs.some((tab) => tab.key === requestedTab) + ? (requestedTab as TabType) + : (visibleTabs.find((tab) => tab.key === 'intro')?.key ?? visibleTabs[0]?.key ?? 'intro'); return ( - <> -
-
+
+
+
{clubDetail.name} -
-
{clubDetail.name}
-
{clubDetail.categoryName}
-
{clubDetail.description}
+
+
{clubDetail.name}
+
{clubDetail.categoryName}
+
{clubDetail.description}
-
+
{visibleTabs.map((tab) => (
-
+
{clubDetail.recruitment.status !== 'CLOSED' && ( @@ -92,7 +95,7 @@ function ClubDetail() { {clubDetail.isMember && ( - + )} {(clubDetail.isMember || clubDetail.isApplied) && ( @@ -101,7 +104,7 @@ function ClubDetail() { )}
- +
); } diff --git a/src/pages/Club/ClubList/components/ClubCard.tsx b/src/pages/Club/ClubList/components/ClubCard.tsx index 6ffcb48..57beb58 100644 --- a/src/pages/Club/ClubList/components/ClubCard.tsx +++ b/src/pages/Club/ClubList/components/ClubCard.tsx @@ -33,7 +33,7 @@ function ClubCard({ club }: ClubCardProps) { const clubTag: ClubTag | null = (() => { if (club.isPendingApproval) { return { - label: '승인대기중', + label: '승인 대기중', bgColor: '#FEF3C7', textColor: '#B45309', }; @@ -62,24 +62,24 @@ function ClubCard({ club }: ClubCardProps) { - {club.name} + {club.name}
-
-
{club.name}
-
{club.categoryName}
+
+
{club.name}
+
{club.categoryName}
{clubTag && (
- + {clubTag.label}
)} diff --git a/src/pages/Club/ClubList/components/SearchBar.tsx b/src/pages/Club/ClubList/components/SearchBar.tsx index 275f752..56947a1 100644 --- a/src/pages/Club/ClubList/components/SearchBar.tsx +++ b/src/pages/Club/ClubList/components/SearchBar.tsx @@ -2,6 +2,8 @@ import { type FormEvent } from 'react'; import { Link } from 'react-router-dom'; import SearchIcon from '@/assets/svg/search.svg'; +const SEARCH_PLACEHOLDER = '동아리 이름으로 검색'; + interface SearchBarProps { isButton?: boolean; value?: string; @@ -11,19 +13,23 @@ interface SearchBarProps { } function SearchBar({ isButton, value, onChange, onSubmit, autoFocus }: SearchBarProps) { - const wrapperClass = 'fixed left-0 right-0 bg-white px-3 py-2 shadow-[0_2px_2px_0_rgba(0,0,0,0.04)] z-10'; + const wrapperClass = + 'fixed top-11 left-0 right-0 z-20 rounded-b-2xl bg-white px-3 py-2 shadow-[0_0_20px_0_rgba(0,0,0,0.03)]'; const content = ( -
- - onChange(e.target.value) : undefined} - autoFocus={autoFocus} - /> +
+ + {isButton && !onChange ? ( +
{SEARCH_PLACEHOLDER}
+ ) : ( + onChange(e.target.value) : undefined} + autoFocus={autoFocus} + /> + )}
); diff --git a/src/pages/Club/ClubList/index.tsx b/src/pages/Club/ClubList/index.tsx index f3f6a7c..0d0e07f 100644 --- a/src/pages/Club/ClubList/index.tsx +++ b/src/pages/Club/ClubList/index.tsx @@ -19,12 +19,12 @@ function ClubList() { const allClubs = data?.pages.flatMap((page) => page.clubs) ?? []; return ( -
+ <> -
-
- 총 {totalCount}개의 동아리 -
+
+ +
+
총 {totalCount}개의 동아리
{allClubs.map((club) => ( @@ -34,7 +34,7 @@ function ClubList() { {hasNextPage &&
}
-
+ ); } diff --git a/src/pages/Club/ClubSearch/index.tsx b/src/pages/Club/ClubSearch/index.tsx index 97001c0..33e3137 100644 --- a/src/pages/Club/ClubSearch/index.tsx +++ b/src/pages/Club/ClubSearch/index.tsx @@ -44,15 +44,14 @@ function ClubSearch() { return ( <> +
-
+
{!debouncedQuery ? (
검색어를 입력해서 동아리를 검색해보세요.
) : ( <> -
- 총 {totalCount}개의 동아리 -
+
총 {totalCount}개의 동아리
{allClubs.map((club) => ( From a41f5b577634e8fafb9d9186c4b6d20d3bfcbf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 18 Mar 2026 22:57:10 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=B0=8F=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=EB=A6=AC?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/svg/chat-send-arrow.svg | 6 + .../layout/Header/components/ChatHeader.tsx | 30 ++- src/components/layout/Header/routeTitles.ts | 4 - src/pages/Chat/ChatRoom.tsx | 184 ++++++++++-------- src/pages/Chat/index.tsx | 157 +++++++++------ 5 files changed, 213 insertions(+), 168 deletions(-) create mode 100644 src/assets/svg/chat-send-arrow.svg diff --git a/src/assets/svg/chat-send-arrow.svg b/src/assets/svg/chat-send-arrow.svg new file mode 100644 index 0000000..929e885 --- /dev/null +++ b/src/assets/svg/chat-send-arrow.svg @@ -0,0 +1,6 @@ + + + diff --git a/src/components/layout/Header/components/ChatHeader.tsx b/src/components/layout/Header/components/ChatHeader.tsx index 3c045f4..2cba476 100644 --- a/src/components/layout/Header/components/ChatHeader.tsx +++ b/src/components/layout/Header/components/ChatHeader.tsx @@ -21,26 +21,20 @@ function ChatHeader() { return ( <> -
- +
+
+ - {chatRoom?.roomName ?? ''} + {chatRoom?.roomName ?? ''} +
- + {isGroup && ( + + )}
pathname.startsWith('/clubs/search'), title: '동아리 검색', }, - { - match: (pathname) => pathname === '/chats', - title: '채팅방', - }, { match: (pathname) => pathname === '/council', title: '총동아리연합회', diff --git a/src/pages/Chat/ChatRoom.tsx b/src/pages/Chat/ChatRoom.tsx index f8a7602..6472098 100644 --- a/src/pages/Chat/ChatRoom.tsx +++ b/src/pages/Chat/ChatRoom.tsx @@ -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 ( +
+
+ {formattedUnreadCount && ( + {formattedUnreadCount} + )} + {formattedTime} + +
+ +
+
+
+ ); + } + + return ( +
+
+ {showSenderName &&
{message.senderName}
} + +
+
+ +
+ {formattedTime} +
+
+
+ ); +} + 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 ( -
+
@@ -106,91 +169,42 @@ function ChatRoom() { const isSameSender = prevMessage?.senderId === message.senderId && !showDateHeader; return ( -
+
{showDateHeader && ( -
- +
+ {formatDate(message.createdAt)}
)} -
- {!message.isMine && ( -
- {isGroup && !isSameSender && ( -
{message.senderName}
- )} - -
- {isGroup && ( -
- {!isSameSender ? ( -
- {message.senderName?.[0]} -
- ) : ( -
- )} -
- )} - -
- -
- -
- {message.unreadCount > 0 && ( - {message.unreadCount} - )} - {formatTime(message.createdAt)} -
-
-
- )} - - {/* ===== RIGHT (내 메시지) ===== */} - {message.isMine && ( -
-
- -
- -
- {message.unreadCount > 0 && ( - {message.unreadCount} - )} - {formatTime(message.createdAt)} -
-
- )} -
+
); })}
-
-