Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
63 changes: 63 additions & 0 deletions src/app/chat/[roomId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use client';
import { useEffect, 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) => ({
id: index + 1,
text: '안녕하세요 멍선생입니다 계속 입력하면 어떻게 되나 봅시다',
time: '오후 11:24',
profileImage: DEFAULT_PROFILE_IMAGE,
nickName: '흰둥이',
userId: index % 3 === 0 ? 0 : 1,
}));

const myId = 0;

const ChatRoomPage = () => {
const [messages, setMessages] = useState(data);

const handleSubmit = (text: string) => {
const newMessage = {
id: Date.now(),
text,
profileImage: DEFAULT_PROFILE_IMAGE,
nickName: '흰둥이',
time: '지금',
userId: myId,
};

setMessages((prev) => [...prev, newMessage]);
};

const bottomRef = useRef<HTMLDivElement>(null);

// 마지막 메세지 ref로 스크롤 이동
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [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'>
{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} />
</div>
);
};

export default ChatRoomPage;
3 changes: 2 additions & 1 deletion src/app/message/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';

import { API } from '@/api';
import { Chat, FollowingList, FollowingNone, FollowingSearch } from '@/components/pages/message';
import { Chat } 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';
import { useIntersectionObserver } from '@/hooks/use-intersection-observer';
Expand Down
20 changes: 20 additions & 0 deletions src/components/pages/chat/chat-header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client';

import { useRouter } from 'next/navigation';

import { Icon } from '@/components/icon';

export const ChatHeader = () => {
const router = useRouter();
return (
<div className='bg-mono-white flex w-full items-center justify-between border-b border-gray-200 px-5 py-3'>
<Icon
id='chevron-left-2'
className='w-6 cursor-pointer text-gray-500'
onClick={() => router.back()}
/>
<span className='text-text-md-bold text-gray-800'>분당 보드게임 동아리</span>
<div className='w-6' />
</div>
);
};
54 changes: 54 additions & 0 deletions src/components/pages/chat/chat-input/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';
import { useRef, useState } from 'react';

import { Icon } from '@/components/icon';

interface IProps {
onSubmit: (text: string) => void;
}

export const ChatInput = ({ onSubmit }: IProps) => {
const [message, setMessage] = useState('');
const inputRef = useRef<HTMLTextAreaElement>(null);

// Enter 키로 전송, Shift + Enter 로 줄바꿈
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};

const handleSubmit = () => {
const changedMessage = message.trim();

if (!changedMessage) {
inputRef.current?.focus();
setMessage('');
return;
}

onSubmit(changedMessage);
setMessage('');
inputRef.current?.focus();
};

return (
<div className='flex w-full justify-center px-5'>
<div className='relative w-full max-w-110'>
<textarea
ref={inputRef}
className='bg-mono-white text-text-md-medium w-full resize-none rounded-2xl border border-gray-300 px-4 py-4 text-gray-800 placeholder:text-gray-500 focus:outline-none [&::-webkit-scrollbar]:hidden'
placeholder='메세지를 입력해주세요.'
rows={1}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button className='absolute top-4 right-5 h-6 w-6' aria-label='메세지 전송' type='button'>
<Icon id='send' className='cursor-pointer text-gray-500' onClick={handleSubmit} />
</button>
</div>
</div>
);
};
23 changes: 23 additions & 0 deletions src/components/pages/chat/chat-my-chat/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
interface IProps {
item: {
id: number;
nickName: string;
profileImage: string;
text: string;
time: string;
};
}

export const MyChat = ({ item }: IProps) => {
const { text, time } = item;
return (
<div className='mr-3 mb-5 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>
</div>
);
};
33 changes: 33 additions & 0 deletions src/components/pages/chat/chat-other-chat/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Image from 'next/image';

interface IProps {
item: {
id: number;
nickName: string;
profileImage: string;
text: string;
time: string;
};
}

export const OtherChat = ({ item }: IProps) => {
const { nickName, profileImage, text, time } = item;
return (
<div className='mb-5 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'>
<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>
<div className='text-text-2xs-regular flex items-end py-1 text-gray-500'>{time}</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';
import Image from 'next/image';
import { useRouter } from 'next/navigation';

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

import { cn } from '@/lib/utils';

const dummy = [
{
id: 1,
Expand Down Expand Up @@ -39,12 +39,17 @@ const dummy = [
];

export const Chat = () => {
const router = useRouter();
const handleClick = () => {
router.push('/chat/1');
};
Comment on lines +43 to +45
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 23, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

채팅방 ID를 동적으로 사용해야 합니다.

현재 모든 채팅 아이템을 클릭하면 /chat/1로 이동하도록 하드코딩되어 있습니다. 클릭한 아이템의 id를 사용하여 해당 채팅방으로 이동해야 합니다.

🔎 제안하는 수정 사항
-  const handleClick = () => {
-    router.push('/chat/1');
+  const handleClick = (id: number) => {
+    router.push(`/chat/${id}`);
  };

그리고 Line 52의 onClick도 수정이 필요합니다:

-          onClick={handleClick}
+          onClick={() => handleClick(item.id)}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/pages/chat/chat/index.tsx around lines 43-45 (and update the
onClick at line 52), the click handler is hardcoded to navigate to '/chat/1';
change it to accept the clicked chat item's id and navigate dynamically. Modify
handleClick to take an id parameter and call router.push(`/chat/${id}`) (or
router.push({ pathname: '/chat/[id]', query: { id } }) if using dynamic route
patterns), and update the onClick at line 52 to call handleClick(item.id)
(ensure item.id is typed/defined and handleClick's signature matches). Ensure
imports/types are adjusted if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

나중에 수정할 예정입니다 임시 테스트에요ㅜㅜ

Copy link
Contributor

Choose a reason for hiding this comment

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

@wooktori 네, 알겠습니다! 임시 테스트 코드라면 나중에 수정하셔도 괜찮습니다. 😊

나중에 놓치지 않도록 이 내용으로 후속 이슈를 생성해 드릴까요? 원하시면 말씀해 주세요!

return (
<ul className='flex flex-col'>
{dummy.map((item) => (
<li
key={item.id}
className='flex cursor-pointer items-center gap-3 bg-white p-5 transition hover:bg-gray-50'
onClick={handleClick}
>
{/* 프로필 이미지 */}
<div className='relative size-12 overflow-hidden rounded-full'>
Expand Down
5 changes: 5 additions & 0 deletions src/components/pages/chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { Chat } from './chat';
export { ChatHeader } from './chat-header';
export { ChatInput } from './chat-input';
export { MyChat } from './chat-my-chat';
export { OtherChat } from './chat-other-chat';
1 change: 0 additions & 1 deletion src/components/pages/message/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { Chat } from './chat';
export { FollowingCard } from './message-following-card';
export { FollowingList } from './message-following-list';
export { FollowingNone } from './message-following-none';
Expand Down