Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/assets/svg/add_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 6 additions & 3 deletions src/components/layout/Header/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ function ChatHeader() {

return (
<>
<header className="fixed top-0 right-0 left-0 z-30 flex h-11 items-center bg-white px-4 py-2">
<header className="fixed top-0 right-0 left-0 z-30 flex h-13 items-center bg-white px-4 py-2">
<div className="flex min-w-0 flex-1 items-center gap-3">
<button type="button" aria-label="뒤로가기" onClick={smartBack} className="shrink-0">
<ChevronLeftIcon />
</button>

<span className="truncate text-[18px] leading-5 font-normal text-indigo-700">{chatRoom?.roomName ?? ''}</span>
<div className="flex min-w-0 items-center gap-1">
<span className="truncate leading-5 font-bold text-indigo-700">{chatRoom?.roomName ?? ''}</span>
{isGroup && <span className="text-text-700 text-[13px] leading-5">{clubMembers.length}</span>}
</div>
</div>

<button type="button" aria-label="채팅방 정보 열기" onClick={openSidebar} className="ml-3 shrink-0">
<button type="button" aria-label="채팅방 정보 열기" onClick={openSidebar} className="shrink-0">
<HamburgerIcon />
</button>
</header>
Expand Down
25 changes: 25 additions & 0 deletions src/components/layout/Header/components/ChatListHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import AddCircle from '@/assets/svg/add_circle.svg';
import ChevronLeftIcon from '@/assets/svg/chevron-left.svg';
import Search from '@/assets/svg/search.svg';
import { useSmartBack } from '@/utils/hooks/useSmartBack';

export default function ChatListHeader() {
Comment thread
ParkSungju01 marked this conversation as resolved.
const smartBack = useSmartBack();

return (
<header className="fixed top-0 right-0 left-0 z-30 flex h-13 items-center justify-center rounded-b-3xl bg-white px-4 py-2">
<button type="button" aria-label="뒤로가기" onClick={smartBack} className="shrink-0">
<ChevronLeftIcon />
</button>
<span className="px-2 py-4 font-semibold">채팅방</span>
<div className="ml-auto flex items-center gap-2">
<button type="button" aria-label="검색" className="shrink-0">
<Search />
</button>
<button type="button" aria-label="채팅방 추가" className="shrink-0">
<AddCircle />
</button>
</div>
</header>
);
}
4 changes: 4 additions & 0 deletions src/components/layout/Header/headerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const HEADER_CONFIGS: HeaderConfig[] = [
type: 'info',
match: (pathname) => pathname === '/home' || pathname === '/timer' || pathname === '/council',
},
{
type: 'chatList',
match: (pathname) => pathname === '/chats',
},
{
type: 'chat',
match: (pathname) => /^\/chats\/\d+$/.test(pathname),
Expand Down
3 changes: 3 additions & 0 deletions src/components/layout/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useLocation, useNavigate } from 'react-router-dom';
import ChatHeader from './components/ChatHeader';
import ChatListHeader from './components/ChatListHeader';
import DefaultHeader from './components/DefaultHeader';
import InfoHeader from './components/InfoHeader';
import ManagerHeader from './components/ManagerHeader';
Expand All @@ -18,6 +19,7 @@ function Header() {
const HEADER_RENDERERS: Record<HeaderType, HeaderRenderer> = {
profile: () => <ProfileHeader />,
info: () => <InfoHeader />,
chatList: () => <ChatListHeader />,
chat: () => <ChatHeader />,
none: () => null,
notification: ({ title }) => <SubpageHeader title={title} />,
Expand All @@ -26,6 +28,7 @@ function Header() {
normal: ({ title }) => <DefaultHeader title={title} showBackButton={false} />,
full: ({ title }) => <DefaultHeader title={title} showNotificationBell={true} />,
signup: ({ title, onBack }) => <DefaultHeader title={title} onBack={onBack} />,
council: ({ title }) => <DefaultHeader title={title} />,
default: ({ title }) => <DefaultHeader title={title} />,
manager: ({ title }) => <ManagerHeader fallbackTitle={title} />,
};
Expand Down
4 changes: 3 additions & 1 deletion src/components/layout/Header/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export type HeaderType =
| 'full'
| 'signup'
| 'schedule'
| 'manager';
| 'council'
| 'manager'
| 'chatList';

export interface HeaderConfig {
type: HeaderType;
Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function Layout({ showBottomNav = false, contentClassName }: Layo
const { pathname } = useLocation();
const { contentPaddingClassName, hasHeader } = getHeaderPresentation(pathname);
const isChatRoomPage = pathname.startsWith('/chats/') && pathname !== '/chats';
const mainBackgroundClassName = pathname === '/chats' ? 'bg-white' : 'bg-background';
const mainBackgroundClassName = 'bg-background';
const { bottomNavRef, bottomOverlayInset, handleLayoutElement, layoutElement, mainRef } =
useLayoutElements(showBottomNav);
const layoutElements = useMemo(
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Chat/ChatRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function ChatMessageRow({ isGroup, isSameSender, message }: ChatMessageRowProps)
)}
<span className="shrink-0 text-[10px] leading-[1.6] font-medium text-indigo-100">{formattedTime}</span>

<div className="bg-primary-500/80 text-sub4 max-w-[78%] min-w-0 rounded-2xl px-3 py-2 text-white shadow-[0_0_3px_rgba(0,0,0,0.15)]">
<div className="bg-primary-500 text-sub4 max-w-[78%] min-w-0 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"
Expand Down
58 changes: 31 additions & 27 deletions src/pages/Chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { Advertisement } from '@/apis/advertisement/entity';
import type { Room } from '@/apis/chat/entity';
import BellOffIcon from '@/assets/svg/bell-off.svg';
import PersonIcon from '@/assets/svg/person.svg';
import BottomOverlaySpacer from '@/components/layout/BottomOverlaySpacer';
import { useAdvertisementInterval } from '@/utils/hooks/useAdvertisementInterval';
import { useAdvertisements } from '@/utils/hooks/useAdvertisements';
import useChat from './hooks/useChat';
Expand Down Expand Up @@ -91,8 +90,12 @@ function ChatRoomListItem({ room, itemRef }: ChatRoomListItemProps) {

{hasUnreadMessage && (
<span className="shrink-0">
<span aria-hidden="true" className="bg-primary-500 block size-2 rounded-full" />
<span className="sr-only">{`읽지 않은 메시지 ${room.unreadCount}개`}</span>
<span
aria-hidden="true"
className="bg-primary-500 flex h-4 min-w-5 items-center justify-center rounded-full px-1 py-0.5 text-[10px] text-white"
>
{room.unreadCount > 300 ? '300+' : room.unreadCount}
</span>
Comment on lines +93 to +98
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

읽지 않은 메시지 수를 스크린리더에도 노출해주세요.

Line 97에 aria-hidden="true"만 있어 시각 외 사용자에게 unread 정보가 전달되지 않습니다. 숨김 텍스트(sr-only)를 함께 두는 쪽이 안전합니다.

예시 수정
           {hasUnreadMessage && (
             <span className="shrink-0">
+              <span className="sr-only">읽지 않은 메시지 {room.unreadCount > 300 ? '300+' : room.unreadCount}개</span>
               <span
                 aria-hidden="true"
                 className="bg-primary-500 flex h-4 min-w-5 items-center justify-center rounded-full px-1 py-0.5 text-[10px] text-white"
               >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/Chat/index.tsx` around lines 96 - 101, The visible unread badge
currently has aria-hidden="true" so screen readers can't see the count; keep the
visual span (the element rendering {room.unreadCount > 300 ? '300+' :
room.unreadCount}) for sighted users but add a sibling hidden text span
(className="sr-only") that announces the unread count (e.g. `{room.unreadCount >
300 ? '300+ unread messages' : `${room.unreadCount} unread messages`}`) so
assistive tech receives the information; keep aria-hidden on the decorative
badge span if desired and place the sr-only span next to it (referencing the
span that renders room.unreadCount).

</span>
)}
</div>
Expand Down Expand Up @@ -193,30 +196,31 @@ function ChatListPage() {
}

return (
<div className="flex min-h-full flex-col bg-white py-3">
{rooms.map((room, index) => {
const shouldRenderAdvertisement =
chatRoomSlotsPerAdvertisement !== null && (index + 1) % chatRoomSlotsPerAdvertisement === 0;
const advertisement = shouldRenderAdvertisement
? advertisements[Math.floor(index / chatRoomSlotsPerAdvertisement)]
: undefined;

return (
<Fragment key={room.roomId}>
<ChatRoomListItem
room={room}
itemRef={index === 0 ? firstChatRoomItemRef : index === 1 ? secondChatRoomItemRef : undefined}
/>
{advertisement && (
<ChatAdvertisementListItem advertisement={advertisement} onClick={trackAdvertisementClick} />
)}
{!advertisement && shouldRenderAdvertisement && isLoadingAdvertisements && (
<ChatAdvertisementListItemSkeleton />
)}
</Fragment>
);
})}
<BottomOverlaySpacer gap={24} />
<div className="flex min-h-full min-w-full flex-col overflow-y-auto bg-gray-100 px-5 py-[23px]">
<div className="h-full [&>*:first-child]:rounded-t-2xl [&>*:last-child]:rounded-b-lg">
{rooms.map((room, index) => {
const shouldRenderAdvertisement =
chatRoomSlotsPerAdvertisement !== null && (index + 1) % chatRoomSlotsPerAdvertisement === 0;
const advertisement = shouldRenderAdvertisement
? advertisements[Math.floor(index / chatRoomSlotsPerAdvertisement)]
: undefined;

return (
<Fragment key={room.roomId}>
<ChatRoomListItem
room={room}
itemRef={index === 0 ? firstChatRoomItemRef : index === 1 ? secondChatRoomItemRef : undefined}
/>
{advertisement && (
<ChatAdvertisementListItem advertisement={advertisement} onClick={trackAdvertisementClick} />
)}
{!advertisement && shouldRenderAdvertisement && isLoadingAdvertisements && (
<ChatAdvertisementListItemSkeleton />
)}
</Fragment>
);
})}
</div>
</div>
);
}
Expand Down
Loading