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:border-ghost hover:border 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 translate-x-0' : 'opacity-0 translate-x-2'}`}
Copy link
Collaborator

Choose a reason for hiding this comment

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

애니메이션이 조금 어색한것 같아요. translate가 없이 글자가 안보이다가 보이는 애니메이션만 주는건 어떠신가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

저는 오히려 더 빼니까 딱딱해지는 느낌이 드는데 너무 어색하신가요?

>
스레드 보기
</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
17 changes: 12 additions & 5 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,18 @@
import { 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">
<Headset size={20} />
</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