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
38 changes: 28 additions & 10 deletions src/app/chat/[roomId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { useLayoutEffect, useRef, useState } from 'react';

import { DEFAULT_PROFILE_IMAGE } from 'constants/default-images';

import { ChatHeader, ChatInput, MyChat, OtherChat } from '@/components/pages/chat';

// 임시 데이터
const data = Array.from({ length: 30 }, (_, index) => ({
let data = Array.from({ length: 30 }, (_, index) => ({
id: index + 1,
text: '안녕하세요 멍선생입니다 계속 입력하면 어떻게 되나 봅시다',
text: '안녕하세요 멍선생입니다 \n 계속 입력하면 어떻게 되나 봅시다',
time: '오후 11:24',
profileImage: DEFAULT_PROFILE_IMAGE,
nickName: '흰둥이',
userId: index % 3 === 0 ? 0 : 1,
}));

data = [
...data,
{
id: 31,
text: '어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마어마하게 긴 메세지',
time: '오후 11:25',
profileImage: DEFAULT_PROFILE_IMAGE,
nickName: '흰둥이',
userId: 0,
},
];
const myId = 0;

const ChatRoomPage = () => {
Expand All @@ -33,26 +43,34 @@ const ChatRoomPage = () => {
setMessages((prev) => [...prev, newMessage]);
};

const bottomRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);

// 마지막(가장 최근) 메세지 ref로 스크롤 이동
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) return;

// 마지막 메세지 ref로 스크롤 이동
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
// 다음 프레임에서 스크롤 실행
requestAnimationFrame(() => {
container.scrollTop = container.scrollHeight;
});
}, [messages]);

return (
<div className='flex h-[calc(100vh-112px)] flex-col'>
<ChatHeader />

<div className='scrollbar-thin ml-4 flex-1 overflow-y-auto pt-4'>
<div
ref={containerRef}
className='scrollbar-thin ml-4 flex flex-1 flex-col gap-4 overflow-y-auto py-4'
>
{messages.map((item) =>
item.userId === myId ? (
<MyChat key={item.id} item={item} />
) : (
<OtherChat key={item.id} item={item} />
),
)}
<div ref={bottomRef} />
</div>

<ChatInput onSubmit={handleSubmit} />
Expand Down
4 changes: 2 additions & 2 deletions src/app/message/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';

import { API } from '@/api';
import { Chat } from '@/components/pages/chat';
import { ChatList } from '@/components/pages/chat';
import { FollowingList, FollowingNone, FollowingSearch } from '@/components/pages/message';
import { TabNavigation } from '@/components/shared';
import { useInfiniteScroll } from '@/hooks/use-group/use-group-infinite-list';
Expand Down Expand Up @@ -66,7 +66,7 @@ export default function FollowingPage() {
<div className='min-h-screen bg-[#F1F5F9]'>
<TabNavigation basePath='/message' defaultValue='chat' tabs={SOCIAL_TABS} />

{tab === 'chat' && <Chat />}
{tab === 'chat' && <ChatList />}
{tab === 'following' && (
<>
<FollowingSearch userId={userId} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const dummy = [
},
];

export const Chat = () => {
export const ChatList = () => {
const router = useRouter();
const handleClick = () => {
router.push('/chat/1');
Expand Down
38 changes: 38 additions & 0 deletions src/components/pages/chat/chat-long-text/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { Icon } from '@/components/icon';
import { useModal } from '@/components/ui';
import { useLongText } from '@/hooks/use-chat/use-chat-longText';

import { LongTextModal } from '../chat-modal';

interface Props {
text: string;
className?: string;
}

export const ExpandableText = ({ text, className }: Props) => {
const { open } = useModal();
const { textRef, isLongText } = useLongText(text);

return (
<>
<span
ref={textRef}
className={`line-clamp-20 block whitespace-pre-line text-gray-800 ${className}`}
>
{text}
</span>

{isLongText && (
<button
className='text-text-xs-medium mt-2 flex items-center'
onClick={() => open(<LongTextModal text={text} />)}
>
<span className='text-text-xs-regular text-gray-500'>전체보기 </span>
<Icon id='chevron-right-1' className='w-4 text-gray-600' />
</button>
)}
</>
);
};
13 changes: 13 additions & 0 deletions src/components/pages/chat/chat-modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ModalContent, ModalTitle } from '@/components/ui';

export const LongTextModal = ({ text }: { text: string }) => (
<ModalContent className='mx-4 w-full max-w-[311px]'>
<div className='mb-4'>
<ModalTitle>메세지 전체보기</ModalTitle>
</div>

<div className='scrollbar-thin max-h-[70vh] overflow-y-auto px-1'>
<div className='whitespace-pre-line text-gray-800'>{text}</div>
</div>
</ModalContent>
);
15 changes: 9 additions & 6 deletions src/components/pages/chat/chat-my-chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ExpandableText } from '../chat-long-text';

interface IProps {
item: {
id: number;
nickName: string;
profileImage: string;
text: string;
Expand All @@ -10,13 +11,15 @@ interface IProps {

export const MyChat = ({ item }: IProps) => {
const { text, time } = item;

return (
<div className='mr-3 mb-5 flex justify-end'>
<div className='mr-3 flex justify-end'>
<div className='text-text-2xs-regular flex items-end py-1 text-gray-500'>{time}</div>
<div className='ml-1.5 flex flex-col'>
<span className='bg-mint-200 mt-1 max-w-60 rounded-tl-2xl rounded-tr-sm rounded-br-2xl rounded-bl-2xl px-4 py-3 text-gray-800'>
{text}
</span>

<div className='ml-1.5'>
<div className='bg-mint-200 mt-1 max-w-60 rounded-tl-2xl rounded-tr-sm rounded-br-2xl rounded-bl-2xl px-4 py-3'>
<ExpandableText text={text} />
</div>
</div>
</div>
);
Expand Down
17 changes: 11 additions & 6 deletions src/components/pages/chat/chat-other-chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Image from 'next/image';

import { ExpandableText } from '../chat-long-text';

interface IProps {
item: {
id: number;
nickName: string;
profileImage: string;
text: string;
Expand All @@ -12,21 +13,25 @@ interface IProps {

export const OtherChat = ({ item }: IProps) => {
const { nickName, profileImage, text, time } = item;

return (
<div className='mb-5 flex'>
<div className='flex'>
<Image
width={40}
className='mr-3 size-10 rounded-full object-cover'
alt='프로필 이미지'
height={40}
src={profileImage}
/>
<div className='mr-1.5 flex flex-col'>

<div className='mr-1.5 max-w-60'>
<span className='text-text-xs-medium text-gray-800'>{nickName}</span>
<span className='bg-mono-white mt-1 max-w-60 rounded-tl-sm rounded-tr-2xl rounded-br-2xl rounded-bl-2xl px-4 py-3 text-gray-800'>
{text}
</span>

<div className='bg-mono-white mt-1 rounded-tl-sm rounded-tr-2xl rounded-br-2xl rounded-bl-2xl px-4 py-3'>
<ExpandableText text={text} />
</div>
</div>

<div className='text-text-2xs-regular flex items-end py-1 text-gray-500'>{time}</div>
</div>
);
Expand Down
4 changes: 3 additions & 1 deletion src/components/pages/chat/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { Chat } from './chat';
export { ChatHeader } from './chat-header';
export { ChatInput } from './chat-input';
export { ChatList } from './chat-list';
export { ExpandableText } from './chat-long-text';
export { LongTextModal } from './chat-modal';
export { MyChat } from './chat-my-chat';
export { OtherChat } from './chat-other-chat';
40 changes: 40 additions & 0 deletions src/hooks/use-chat/use-chat-longText/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useLayoutEffect, useRef, useState } from 'react';

export const useLongText = (text: string, maxLines = 20) => {
const textRef = useRef<HTMLSpanElement>(null);
const [isLongText, setIsLongText] = useState(false);

// 20줄이 넘어가면 longText로 판단.
useLayoutEffect(() => {
if (!textRef.current) return;

const el = textRef.current;

// 클론 생성
const clone = el.cloneNode(true) as HTMLSpanElement;

// 오프스크린 스타일 적용
clone.style.position = 'absolute';
clone.style.visibility = 'hidden';
clone.style.pointerEvents = 'none';
clone.style.display = 'block';
clone.style.webkitLineClamp = 'unset';
clone.style.overflow = 'visible';

clone.style.width = `${el.offsetWidth}px`;

// DOM에 추가
document.body.appendChild(clone);

// 측정
const lineHeight = parseFloat(getComputedStyle(clone).lineHeight);
const fullHeight = clone.scrollHeight;

setIsLongText(fullHeight > lineHeight * maxLines);

// 정리
document.body.removeChild(clone);
}, [text, maxLines]);

return { textRef, isLongText };
};
7 changes: 7 additions & 0 deletions src/styles/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@
.scrollbar-thin::-webkit-scrollbar-thumb {
@apply rounded-lg bg-gray-400;
}

.line-clamp-20 {
display: -webkit-box;
-webkit-line-clamp: 20;
-webkit-box-orient: vertical;
overflow: hidden;
}