Skip to content
4 changes: 2 additions & 2 deletions src/frontend/apps/web/app/(main)/[stockSlug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function generateMetadata({ params }) {

export default function StockDetailsPage({ params }) {
const { stockSlug } = params;
// console.log(1, params);
// console.log(1, stockSlug);

return (
<div className="flex py-6 px-[30px] h-full min-w-0 min-h-0">
Expand All @@ -32,7 +32,7 @@ export default function StockDetailsPage({ params }) {
</div>

<div className="pl-2 basis-[55%] flex-shrink-0 min-w-0">
<ChatContainer />
<ChatContainer stockSlug={stockSlug} />
</div>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions src/frontend/apps/web/src/features/chat/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as ChatContainer } from './ui/chat-container';
export { default as ThreadContainer } from './ui/thread-panel';
61 changes: 43 additions & 18 deletions src/frontend/apps/web/src/features/chat/ui/avatarlist.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
'use client';

import { useState } from 'react';
import { Avatar, AvatarImage, AvatarFallback } from '@workspace/ui/components';

import type { ChatContentWithAvatarsProps } from './chat-content';

const AvatarList = ({ avatarUrls }: ChatContentWithAvatarsProps) => {
const AvatarList = ({
avatarUrls,
setIsThreadOpen,
}: ChatContentWithAvatarsProps) => {
if (!avatarUrls) {
avatarUrls = [
'https://github.com/shadcn.png',
Expand All @@ -19,24 +25,43 @@ const AvatarList = ({ avatarUrls }: ChatContentWithAvatarsProps) => {
const displayedAvatars = avatarUrls.slice(0, 5);
const remainingCount = avatarUrls.length - displayedAvatars.length;

return (
<div className="px-1 py-1 mb-0.5 flex gap-1 items-center">
{displayedAvatars.map((url, index) => (
<Avatar
variant="square"
size="sm"
key={index}
className={index === 4 && remainingCount > 0 ? 'relative' : ''}
>
<AvatarImage src={url} />
<AvatarFallback>profile</AvatarFallback>
const [isHovered, setIsHovered] = useState(false);

{index === 4 && remainingCount > 0 && (
<div className="absolute top-0 right-0 w-6 h-6 bg-black-100 flex items-center justify-center text-white text-xs">+{remainingCount}</div>
)}
</Avatar>
))}
{avatarUrls.length > 0 && <div className="ml-1 text-sm text-thread cursor-pointer">{avatarUrls.length}개의 댓글</div>}
return (
<div
className="px-1 py-1 pr-40 mb-0.5 flex gap-1 items-center justify-between hover:shadow-md cursor-pointer"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => setIsThreadOpen(true)}
>
<div className="flex items-center gap-1">
{displayedAvatars.map((url, index) => (
<Avatar
variant="square"
size="sm"
key={index}
className={index === 4 && remainingCount > 0 ? 'relative' : ''}
>
<AvatarImage src={url} />
<AvatarFallback>profile</AvatarFallback>
{index === 4 && remainingCount > 0 && (
<div className="absolute top-0 right-0 w-6 h-6 bg-black-100 flex items-center justify-center text-white text-xs">
+{remainingCount}
</div>
)}
</Avatar>
))}
{avatarUrls.length > 0 && (
<div className="ml-1 text-sm text-thread">
{avatarUrls.length}개의 댓글
</div>
)}
</div>
<div
className={`ml-2 text-sm transition-all duration-300 ease-in-out ${isHovered ? 'opacity-100' : 'opacity-0'}`}
>
스레드 보기
</div>
</div>
);
};
Expand Down
7 changes: 4 additions & 3 deletions src/frontend/apps/web/src/features/chat/ui/chat-container.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { SidebarInset, SidebarProvider } from '@workspace/ui/components';
import { SidebarContainer } from '@/src/shared/components/sidebar';

import ChatHeader from './chat-header';
import ChatSection from './chat-section';
import { SidebarContainer } from '@/src/shared/components/sidebar';

const ChatContainer = () => {
const ChatContainer = ({ stockSlug }: { stockSlug: string }) => {
return (
<SidebarProvider className="flex w-full h-full min-w-0 min-h-0 border rounded-md overflow-hidden">
<SidebarContainer />
<SidebarInset className="flex flex-col min-w-0 min-h-0 w-full h-full">
<ChatHeader />
<ChatHeader stockSlug={stockSlug} />
<ChatSection />
</SidebarInset>
</SidebarProvider>
Expand Down
32 changes: 25 additions & 7 deletions src/frontend/apps/web/src/features/chat/ui/chat-content.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import ContentText from './content-text';
import ContentAvatar from './content-avatar';

import { MessageSquareText } from 'lucide-react';

export type ChatContentProps = {
type?: 'default' | 'live';
};

export type ChatContentWithAvatarsProps = ChatContentProps & {
avatarUrls?: string[];
setIsThreadOpen: (value: boolean) => void;
};

const ChatContent = ({ type = 'default', avatarUrls }: ChatContentWithAvatarsProps) => {
const ChatContent = ({
type = 'live',
avatarUrls,
setIsThreadOpen,
}: ChatContentWithAvatarsProps) => {
const backgroundColor = type === 'live' ? 'bg-live' : 'bg-white';

const hoverColor = type === 'default' ? 'hover:bg-chatboxHover' : '';

return (
<div className={`flex w-full h-auto ${backgroundColor} pb-2 pl-5 pt-5 pr-6 gap-4 group relative ${hoverColor} transition-all duration-300`}>
<div
className={`flex w-full h-auto ${backgroundColor} pb-2 pl-5 pt-5 pr-6 gap-4 group relative ${hoverColor} transition-all duration-300`}
>
<ContentAvatar type={type} />
<ContentText
type={type}
avatarUrls={avatarUrls}
/>
<div className="flex w-full items-start justify-between">
<ContentText
type={type}
avatarUrls={avatarUrls}
setIsThreadOpen={setIsThreadOpen}
/>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-in-out">
<MessageSquareText
size="15"
className="cursor-pointer hover:text-gray-600"
onClick={() => setIsThreadOpen(true)}
/>
</div>
</div>
</div>
);
};
Expand Down
24 changes: 18 additions & 6 deletions src/frontend/apps/web/src/features/chat/ui/chat-header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { SidebarTrigger } from '@workspace/ui/components';
import { Button, SidebarTrigger } from '@workspace/ui/components';
import { Headset } from 'lucide-react';
import Header from './header';

const ChatHeader = () => {
const ChatHeader = ({ stockSlug }: { stockSlug: string }) => {
return (
<header className="h-[54px] w-full flex items-center justify-between px-2.5 py-[13px] bg-slate-300">
<SidebarTrigger />
123
</header>
<Header>
<div className="flex items-center gap-2">
<SidebarTrigger />
<span className="font-semibold">{stockSlug}</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="default"
size="sm"
>
<Headset size={20} />
</Button>
</div>
</Header>
);
};

Expand Down
29 changes: 24 additions & 5 deletions src/frontend/apps/web/src/features/chat/ui/chat-section.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
'use client';

import { useState } from 'react';

import ChatContent from './chat-content';
import ChatTextarea from './chat-textarea';
import ThreadPanel from './thread-panel';

const ChatSection = () => {
const [isThreadOpen, setIsThreadOpen] = useState(false);

return (
<div className="flex flex-1 flex-col w-full h-full">
<div className="flex flex-1 flex-col w-full h-full overflow-y-auto">
<ChatContent />
<div className="relative flex flex-1 h-full">
{/* Main Chat Area */}
<div className="flex flex-col w-full h-full">
<div className="flex flex-1 flex-col w-full h-full overflow-y-auto">
<ChatContent setIsThreadOpen={setIsThreadOpen} />
</div>
<div className="p-4">
<ChatTextarea />
</div>
</div>
<div className="p-4">
<ChatTextarea />

{/* Thread Panel Overlay */}
<div
className={`absolute top-0 right-0 h-full w-full bg-white shadow-lg transform transition-transform duration-300 ease-in-out ${
isThreadOpen ? 'translate-x-0' : 'translate-x-full'
} w-96 z-50`}
>
{isThreadOpen && <ThreadPanel onClose={() => setIsThreadOpen(false)} />}
</div>
</div>
);
Expand Down
13 changes: 11 additions & 2 deletions src/frontend/apps/web/src/features/chat/ui/content-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Badge } from '@workspace/ui/components';
import type { ChatContentWithAvatarsProps } from './chat-content';
import AvatarList from './avatarlist';

const ContentText = ({ type, avatarUrls }: ChatContentWithAvatarsProps) => {
const ContentText = ({
type,
avatarUrls,
setIsThreadOpen,
}: ChatContentWithAvatarsProps) => {
return (
<div className="flex flex-col ">
<div className="flex flex-col gap-2">
Expand All @@ -21,7 +25,12 @@ const ContentText = ({ type, avatarUrls }: ChatContentWithAvatarsProps) => {
</div>
<div className="text-base">안녕하세요</div>
</div>
<AvatarList avatarUrls={avatarUrls} />
<div onClick={() => setIsThreadOpen(true)}>
<AvatarList
avatarUrls={avatarUrls}
setIsThreadOpen={setIsThreadOpen}
/>
</div>
</div>
);
};
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/apps/web/src/features/chat/ui/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Header = ({ children }: { children: React.ReactNode }) => {
return (
<header className="h-[54px] w-full flex items-center justify-between px-4 bg-white border-b border-gray-200">
{children}
</header>
);
};

export default Header;
16 changes: 16 additions & 0 deletions src/frontend/apps/web/src/features/chat/ui/thread-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Header from './header';

import { X } from 'lucide-react';

const ThreadHeader = ({ onClose }: { onClose: (value: boolean) => void }) => {
return (
<Header>
<span className="font-semibold text-gray-800">스레드</span>
<button onClick={() => onClose(false)}>
<X size={20} />
</button>
</Header>
);
};

export default ThreadHeader;
12 changes: 12 additions & 0 deletions src/frontend/apps/web/src/features/chat/ui/thread-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ThreadHeader from './thread-header';
import ThreadSection from './thread-section';

const ThreadPanel = ({ onClose }: { onClose: (value: boolean) => void }) => {
return (
<div className="z-50 maw-w-[906px] w-full h-full flex flex-col min-w-0 min-h-0">
<ThreadHeader onClose={onClose} /> <ThreadSection />
</div>
);
};

export default ThreadPanel;
14 changes: 14 additions & 0 deletions src/frontend/apps/web/src/features/chat/ui/thread-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ChatTextArea from './chat-textarea';

const ThreadSection = () => {
return (
<div className="flex flex-col w-full h-full">
<div className="flex flex-1 flex-col w-full h-full overflow-y-auto"></div>
<div className="p-4">
<ChatTextArea />
</div>
</div>
);
};

export default ThreadSection;
Loading