Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b15bf66
๐Ÿ› Fix: ์ฑ„ํŒ…ํ•˜๊ธฐ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก (#372)
jinhyo0 Apr 19, 2026
5205a40
โœจ Feat: ์ดˆ๊ธฐ ๋ Œ๋”๋ง์‹œ ์ตœ์ดˆ ์‚ฌ์šฉ์ž ์ฑ„ํŒ… ๋œจ๋„๋ก (#372)
jinhyo0 Apr 19, 2026
b8040c2
โœจ Feat: ์ฑ„ํŒ… ์‹œ๊ฐ„ ์ถ”๊ฐ€ (#372)
jinhyo0 Apr 19, 2026
92d67ea
๐Ÿ”€ Merge: feat/#372/chatting (#404)
jinhyo0 Apr 19, 2026
0cf893a
๐Ÿ’„ Design: ๋‚ ์งœ ํ…์ŠคํŠธ ํฌ๊ธฐ ์ˆ˜์ • (#406)
jinhyo0 May 16, 2026
d6cf526
๐Ÿ’„ Design: ์ฑ„ํŒ…์ฐฝ ํ•˜๋‹จ ์ž…๋ ฅ๋ฐ•์Šค pt ์ถ”๊ฐ€ (#406)
jinhyo0 May 16, 2026
a4fb7b4
๐Ÿ’„ Design: ์ •๋ ฌ ๋ฐ ์Šคํฌ๋กค ์ˆ˜์ • (#406)
jinhyo0 May 16, 2026
4a42cda
๐Ÿ› Fix: ํŒŒ์ผ ํฌ๊ธฐ ๋ฐ ๊ฐœ์ˆ˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ (#406)
jinhyo0 May 16, 2026
74a792a
๐Ÿ’„ Design: ๊ฐ„๊ฒฉ, ํฐํŠธ ํฌ๊ธฐ ๋“ฑ UI ์ˆ˜์ • (#406)
jinhyo0 May 16, 2026
a6b9076
๐Ÿ› Fix: ์‹œ๊ฐ„ ๋กœ์ง ์ˆ˜์ • (#406)
jinhyo0 May 16, 2026
64e5588
๐Ÿ› Fix: ์ฑ„ํŒ…๋ฐฉ ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์‹œ room_id undefined ์—๋Ÿฌ ์ˆ˜์ • (#406)
jinhyo0 May 16, 2026
45d405b
๐Ÿ’„ Design: ์ž…๋ ฅ์ฐฝ ์Šคํฌ๋กค ์žฌ์ˆ˜์ • (#406)
jinhyo0 May 16, 2026
1a657f6
๐Ÿ”€ Merge: fix/#406/chatting-qa (#407)
jinhyo0 May 16, 2026
d1ba412
๐Ÿ’„ Design: ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ ์ˆ˜์ •
phjlia2430 May 16, 2026
52694c1
Merge pull request #408 from PromptPlace/Feat/#377/Qadetail
phjlia2430 May 16, 2026
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
2 changes: 1 addition & 1 deletion src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const Footer = () => {
))}
<div
onClick={handleFollowChannel}
className="relative cursor-pointer py-[12px] custom-h4 text-gray700 max-lg:text-[16px] max-phone:text-[14px] max-lg:px-[0px]">
className="cursor-pointer py-[12px] custom-h4 text-gray700 max-lg:text-[16px] max-phone:text-[14px] max-lg:px-[0px]">
๊ณ ๊ฐ์„ผํ„ฐ
</div>
</nav>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ const Navbar = () => {
src={data?.data.has_unread_messages ? MessageIcon : MessageIcon}
alt="๋ฉ”์„ธ์ง€ ์•Œ๋ฆผ"
className="self-center max-phone:w-[16px] max-phone:h-[16px]"
// onClick={() => navigate('/chat')}
onClick={() => setIsMessageModalShow((prev) => !prev)}
onClick={() => navigate('/chat')}
// onClick={() => setIsMessageModalShow((prev) => !prev)}
/>
<img
src={data?.data.profile_image || UserIcon}
Expand Down
17 changes: 15 additions & 2 deletions src/pages/ChatPage/ChatPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import ChatList from './components/ChatList';
import ChattingRoom from './components/ChattingRoom';
import useGetInfiniteChatRooms from '@/hooks/queries/ChatPage/useGetInfiniteChatRooms';

const ChatPage = () => {
const [selectedRoomId, setSelectedRoomId] = useState<number | null>(null);
const isTablet = window.innerWidth < 1024;

const { data } = useGetInfiniteChatRooms({ limit: 20 }); // ์ฑ„ํŒ… ๋ชฉ๋ก ์กฐํšŒ

useEffect(() => {
const latestRoomId = data?.pages?.[0]?.data?.rooms?.[0]?.room_id;

if (latestRoomId) {
setSelectedRoomId(latestRoomId);
}
}, [data]);

return (
<div className="px-[102px] max-lg:px-[40px] max-phone:px-[20px] max-lg:bg-white max-lg:h-dvh">
{(!isTablet || selectedRoomId === null) && (
<h1 className="custom-h1 pt-[64px] mb-[56px] max-lg:pt-[32px] max-lg:mb-[20px]">๋ฉ”์‹œ์ง€</h1>
)}

<div className="lg:flex gap-[20px]">
{(!isTablet || selectedRoomId === null) && <ChatList setSelectedRoomId={setSelectedRoomId} />}
{(!isTablet || selectedRoomId === null) && (
<ChatList setSelectedRoomId={setSelectedRoomId} selectedRoomId={selectedRoomId} />
)}
{(!isTablet || selectedRoomId !== null) && <ChattingRoom selectedRoomId={selectedRoomId!} />}
</div>
</div>
Expand Down
19 changes: 17 additions & 2 deletions src/pages/ChatPage/components/ChatBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,36 @@ interface ChatBubbleProps {
files: Attachment[];
isMine: boolean;
popup?: boolean;
date: string;
showTime?: boolean;
}

const ChatBubble = ({ text, files, isMine, popup }: ChatBubbleProps) => {
const ChatBubble = ({ text, files, isMine, popup, date, showTime }: ChatBubbleProps) => {
const [preview, setPreview] = useState<string | null>(null);
const [selected, setSelected] = useState<Attachment | null>(null);
const hasFile = files.some((file) => file.type === 'FILE');

const formatTime = (date: string) => {
const d = new Date(date);

return new Intl.DateTimeFormat('ko-KR', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
}).format(d);
};

return (
<div className={clsx('flex', isMine ? 'justify-end' : 'justify-start')}>
<div className={clsx('flex gap-2', isMine ? 'justify-end' : 'justify-end flex-row-reverse')}>
{showTime && <div className={clsx('custom-body3 text-gray700 self-end')}>{formatTime(date)}</div>}

<div
className={clsx(
'px-[20px] py-[16px] custom-body1 max-w-[316px] w-fit',
isMine
? 'bg-primary text-white rounded-l-[32px] rounded-tr-[32px]'
: 'bg-background rounded-r-[32px] rounded-bl-[32px]',
hasFile && 'flex flex-col gap-[16px]',
)}>
{text}

Expand Down
4 changes: 3 additions & 1 deletion src/pages/ChatPage/components/ChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useNavigate } from 'react-router-dom';

interface ChatListProps {
setSelectedRoomId: (roomId: number) => void;
selectedRoomId: number | null;
}

type ActiveButton = {
Expand All @@ -21,7 +22,7 @@ const BUTTONS: ActiveButton[] = [
{ id: 3, label: '๊ณ ์ •๋œ ๋ฉ”์‹œ์ง€', filter: 'pinned' },
];

const ChatList = ({ setSelectedRoomId }: ChatListProps) => {
const ChatList = ({ setSelectedRoomId, selectedRoomId }: ChatListProps) => {
const [search, setSearch] = useState('');
const [activeButton, setActiveButton] = useState<ActiveButton>(BUTTONS[0]);
const isTablet = window.innerWidth < 1024;
Expand Down Expand Up @@ -81,6 +82,7 @@ const ChatList = ({ setSelectedRoomId }: ChatListProps) => {
last_message={list.last_message}
unread_count={list.unread_count}
is_pinned={list.is_pinned}
is_clicked={list.room_id === selectedRoomId}
onClick={() => {
if (isTablet) {
navigate(`/chat/${list.room_id}`);
Expand Down
9 changes: 7 additions & 2 deletions src/pages/ChatPage/components/ChatListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import formatDate from '@/utils/formatDate';
import Default from '@assets/icon-profile-image-default.svg';
import clsx from 'clsx';

interface ChatListItemProps {
partner: {
Expand All @@ -13,16 +14,20 @@ interface ChatListItemProps {
};
unread_count: number;
is_pinned: boolean;
is_clicked: boolean;
onClick: () => void;
}

const ChatListItem = ({ partner, last_message, unread_count, onClick }: ChatListItemProps) => {
const ChatListItem = ({ partner, last_message, unread_count, is_clicked, onClick }: ChatListItemProps) => {
const { month, day } = formatDate(last_message.sent_at);

return (
<div
onClick={onClick}
className="p-[16px] flex gap-[16px] items-center lg:max-w-[317px] w-full cursor-pointer hover:bg-background rounded-[8px]">
className={clsx(
'p-[16px] flex gap-[16px] items-start lg:max-w-[317px] w-full cursor-pointer hover:bg-background rounded-[8px]',
is_clicked && 'bg-background',
)}>
<div className="size-[48px] shrink-0">
<img
src={partner.profile_image_url || Default}
Expand Down
88 changes: 68 additions & 20 deletions src/pages/ChatPage/components/ChattingRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =
const [showMenu, setShowMenu] = useState<boolean>(false); // ๋ฉ”๋‰ด ํด๋ฆญ ์—ฌ๋ถ€
const [showDownload, setShowDownload] = useState<boolean>(false); // ๋‚ด๊ฐ€ ๋‹ค์šด๋ฐ›์€ ํ”„๋กฌํ”„ํŠธ
const [showDownloadAll, setShowDownloadAll] = useState<boolean>(false); // ๋‚ด๊ฐ€ ๋‹ค์šด๋ฐ›์€ ํ”„๋กฌํ”„ํŠธ ๋” ๋ณด๊ธฐ
const [fileError, setFileError] = useState<string>('');

const MAX_FILE_COUNT = 3;
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

const menuRef = useRef<HTMLDivElement | null>(null);
const isFirstLoad = useRef(true); // ์ฒ˜์Œ ์ž…์žฅํ–ˆ๋Š”์ง€
const prevHeightRef = useRef(0);
Expand Down Expand Up @@ -135,7 +140,7 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =
});
};

const handleEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
const handleEnter = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
handleSubmit();
}
Expand All @@ -157,11 +162,20 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files;
if (!selected) return;
if (files.length === 3) return;
if (previews.length === 3) return;

setFileError('');

const fileArr = Array.from(selected);

const countError = files.length + fileArr.length > MAX_FILE_COUNT;
const sizeError = fileArr.find((file) => file.size > MAX_FILE_SIZE);

if (countError || sizeError) {
setFileError('์ด๋ฏธ์ง€, ํŒŒ์ผ์€ ์ตœ๋Œ€ 3๊ฐœ, 10mb๊นŒ์ง€ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์–ด์š”!');
e.target.value = '';
return;
}

setFiles((prev) => [...prev, ...fileArr]);

const previewUrl = fileArr.map((file) => URL.createObjectURL(file));
Expand All @@ -176,6 +190,8 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =

setPreviews((prev) => prev.filter((_, i) => i !== idx));
setFiles((prev) => prev.filter((_, i) => i !== idx));

setFileError('');
};

const isNearBottom = () => {
Expand Down Expand Up @@ -428,7 +444,7 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =
) : (
<PinIcon onClick={() => mutatePatchPinChat(selectedRoomId)} className="cursor-pointer" />
)}
<DotsIcon className="cursor-pointer" onClick={() => setShowMenu((prev) => !prev)} />
<DotsIcon className="cursor-pointer w-[24px]" onClick={() => setShowMenu((prev) => !prev)} />

{/* ๋ฉ”๋‰ด */}
{showMenu && (
Expand All @@ -443,7 +459,7 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =
<section
ref={scrollRef}
onScroll={handleScroll}
className="flex flex-col gap-[20px] flex-1 min-h-0 overflow-auto">
className="flex flex-col gap-[20px] flex-1 min-h-0 overflow-auto pr-[16px]">
<div ref={ref} className="h-2 shrink-0"></div>

{/* ์‚ฌ์šฉ์ž ์ •๋ณด ๋ถ€๋ถ„ */}
Expand Down Expand Up @@ -487,31 +503,48 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =
{/* ๋‚ ์งœ */}
<div className="py-[16px] flex items-center">
<div className="w-full h-[1px] bg-gray400"></div>
<div className="px-[20px] text-gray400">{`${year}.${month}.${day}(${dayOfWeek})`}</div>
<div className="px-[20px] text-gray400 ">{`${year}.${month}.${day}(${dayOfWeek})`}</div>
<div className="w-full h-[1px] bg-gray400"></div>
</div>

{/* ๋ฉ”์‹œ์ง€ */}
{messages && (
<div className="flex flex-col gap-[8px] flex-1">
{messages.map((msg) => (
<ChatBubble
key={msg.message_id}
text={msg.content}
files={msg.attachments}
isMine={(msg.sender_id ?? msg?.sender?.user_id) === user.user_id}
popup={popup}
/>
))}
{messages.map((msg, idx) => {
const nextMessage = messages[idx + 1];

const isSameSender =
(msg.sender_id ?? msg?.sender?.user_id) ===
(nextMessage?.sender_id ?? nextMessage?.sender?.user_id);

const isSameMinute =
nextMessage &&
new Date(msg.sent_at).getHours() === new Date(nextMessage.sent_at).getHours() &&
new Date(msg.sent_at).getMinutes() === new Date(nextMessage.sent_at).getMinutes();

const showTime = !(isSameSender && isSameMinute);

return (
<ChatBubble
key={msg.message_id}
text={msg.content}
files={msg.attachments}
isMine={(msg.sender_id ?? msg?.sender?.user_id) === user.user_id}
popup={popup}
date={msg.sent_at}
showTime={showTime}
/>
);
})}
</div>
)}

{/* <div ref={bottomRef} className="h-2 bg-red-400 shrink-0"></div> */}
</section>

{/* ์ž…๋ ฅ์ฐฝ */}
<div className="flex flex-col w-full relative bg-background">
<div className="w-full px-[20px] py-[16px] rounded-[8px] flex gap-[20px] items-start">
<div className="flex flex-col w-full relative bg-white pt-[20px]">
<div className="w-full h-max px-[20px] py-[16px] rounded-[8px] flex gap-[20px] items-start bg-background">
<div className="flex gap-[8px]">
{/* ํŒŒ์ผ ์„ ํƒ */}
<label>
Expand All @@ -526,16 +559,31 @@ const ChattingRoom = ({ selectedRoomId, className, popup }: ChattingRoomProps) =
</label>
</div>

<div className="flex-1">
<div className="flex-1 flex flex-col gap-[4px] h-max">
{/* ์ฑ„ํŒ… ์ž…๋ ฅ */}
<input
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onInput={(e) => {
const target = e.currentTarget;

target.style.height = 'auto';
target.style.height = `${Math.min(target.scrollHeight, 102)}px`;

if (target.scrollHeight > 102) {
target.style.overflowY = 'auto';
} else {
target.style.overflowY = 'hidden';
}
}}
onKeyDown={handleEnter}
placeholder="๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
className="flex-1 cursor-pointer custom-body1 plcaeholder:font-['SCoreDream'] placeholder:custom-body1 placeholder:text-gray500"
rows={1}
className="w-full min-h-[24px] max-h-[102px] resize-none cursor-pointer custom-body1 placeholder:font-['SCoreDream'] placeholder:custom-body1 placeholder:text-gray500"
/>

{fileError && <div className="custom-button2 text-alert">{fileError}</div>}

<div className="flex gap-[4px] flex-wrap">
{previews.length > 0 &&
previews.map((item, idx) => (
Expand Down
16 changes: 11 additions & 5 deletions src/pages/ChatPage/components/PreviewItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const PreviewItem = ({ imageURL, fileSize, type, name, handleDeleteImage }: Prev
return (
<div
className={clsx(
'bg-white p-[8px] border border-gray300 rounded-[12px] w-full flex gap-[16px] items-center',
'bg-white p-[8px] border border-gray300 rounded-[12px] w-full flex gap-[8px] items-center',
type.startsWith('image/') ? 'max-w-[150px]' : 'max-w-[256px] h-[56px] min-w-0',
)}>
{/* ์ด๋ฏธ์ง€ */}
Expand All @@ -28,19 +28,25 @@ const PreviewItem = ({ imageURL, fileSize, type, name, handleDeleteImage }: Prev

{/* ํŒŒ์ผ */}
{!type.startsWith('image/') && (
<div className="flex gap-[10px] items-center flex-1 min-w-0">
<div className="flex gap-[4px] items-center flex-1 min-w-0">
<AttachIcon className="text-black size-[24px] shrink-0" />
<p className={clsx('custom-button2 truncate text-black max-w-fit')}>{name}</p>
<p
className={clsx(
'truncate text-black max-w-fit',
type.startsWith('image/') ? 'custom-button2' : 'custom-button1',
)}>
{name}
</p>
</div>
)}

{handleDeleteImage && (
<div className="flex items-center gap-[16px] shrink-0">
{/* ํŒŒ์ผ ๋ฐ ์ด๋ฏธ์ง€ ์„ ํƒ ์‹œ ํŒŒ์ผ ์‚ฌ์ด์ฆˆ */}
<p className="custom-button3">{fileSize}</p>
<p className="custom-button2">{fileSize}</p>

{/* ํŒŒ์ผ ๋ฐ ์ด๋ฏธ์ง€ ์‚ญ์ œ */}
<XIcon onClick={handleDeleteImage} className="cursor-pointer shrink-0 text-gray500" />
<XIcon onClick={handleDeleteImage} className="cursor-pointer shrink-0 text-gray500 size-[24px]" />
</div>
)}

Expand Down
9 changes: 8 additions & 1 deletion src/pages/ProfilePage/components/ProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ const ProfileCard = ({ mypage }: ProfileCardProps) => {
});
};

// ์ฑ„ํŒ…ํ•˜๊ธฐ ๋กœ๊ทธ์ธ ๊ด€๋ จ
const handleChatting = () => {
handleShowLoginModal(() => {
openChatRoom(member_id);
});
};

const normalizedFollowerList: FollowerWithStatus[] =
followerData?.data.map((f) => ({
...f,
Expand Down Expand Up @@ -215,7 +222,7 @@ const ProfileCard = ({ mypage }: ProfileCardProps) => {

{!isMyProfile && !isAdmin && (
<div className="flex gap-[20px]">
<ProfileButton text="๋ฌธ์˜ํ•˜๊ธฐ" type="chat" onClick={() => openChatRoom(member_id)} />
<ProfileButton text="๋ฌธ์˜ํ•˜๊ธฐ" type="chat" onClick={handleChatting} />
<ProfileButton
text={isFollow ? 'ํŒ”๋กœ์šฐ ์™„๋ฃŒ' : 'ํŒ”๋กœ์šฐ'}
type={isFollow ? 'check' : 'plus'}
Expand Down
Loading