Skip to content

Commit 3399564

Browse files
authored
Merge pull request #23 from SWMTheFirstTake/dev
오른쪽 사진 패널 기능 강화
2 parents 03e4413 + fb140ce commit 3399564

File tree

5 files changed

+162
-39
lines changed

5 files changed

+162
-39
lines changed

app/chat/page.tsx

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import ChatSubmit from '@/components/ChatSubmit';
88
import ChatArea from '@/components/ChatArea';
99
import { useChat } from '@/hooks/useChat';
1010
import { useChatRooms } from '@/hooks/useChatRoom';
11-
import ImageDetailPanel from '@/components/ImageDetailPanel';
11+
import ImagePanel from '@/components/ImagePanel';
1212

1313
export default function Chat() {
1414
const [inputValue, setInputValue] = useState<string>('');
1515
const [inputImage, setInputImage] = useState<MessageImage | undefined>(undefined);
1616
const [currentChatId, setCurrentChatId] = useState<number | null>(null);
1717
const [isAIResponding, setIsAIResponding] = useState<boolean>(false);
1818
const [hasUserSentMessage, setHasUserSentMessage] = useState<boolean>(false);
19-
const [selectedImage, setSelectedImage] = useState<MessageImage | null>(null);
19+
const [openTabs, setOpenTabs] = useState<MessageImage[]>([]);
20+
const [activeTabId, setActiveTabId] = useState<string | null>(null);
21+
const [isPanelOpen, setIsPanelOpen] = useState(false);
22+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
2023

2124
/** chat state 관리하는 hook */
2225
const { messages, examples, isLoading: isChatLoading, sendMessage, addMessageToCache } = useChat(currentChatId);
@@ -30,6 +33,12 @@ export default function Chat() {
3033
}
3134
}, [messages, hasUserSentMessage]);
3235

36+
useEffect(() => {
37+
if (isSidebarOpen) {
38+
setIsPanelOpen(false);
39+
}
40+
}, [isSidebarOpen]);
41+
3342
const sendMsg = useCallback(
3443
(inputValue: string, inputImage?: MessageImage) => {
3544
const userMessage: Message = {
@@ -89,6 +98,32 @@ export default function Chat() {
8998
setInputImage(undefined);
9099
};
91100

101+
const handleOpenTab = (newImageData: MessageImage) => {
102+
const isAlreadyOpen = openTabs.some((tab) => tab.src === newImageData.src);
103+
if (!isAlreadyOpen) {
104+
setOpenTabs((prevTabs) => [...prevTabs, newImageData]);
105+
}
106+
setActiveTabId(newImageData.src);
107+
setIsPanelOpen(true); // 이미지 클릭 시 패널 열기
108+
};
109+
110+
// 3. 탭을 닫는 함수
111+
const handleCloseTab = (tabSrcToClose: string) => {
112+
const remainingTabs = openTabs.filter((tab) => tab.src !== tabSrcToClose);
113+
setOpenTabs(remainingTabs);
114+
if (activeTabId === tabSrcToClose) {
115+
if (remainingTabs.length > 0) {
116+
setActiveTabId(remainingTabs[remainingTabs.length - 1].src);
117+
} else {
118+
setActiveTabId(null);
119+
}
120+
}
121+
};
122+
123+
const handleTogglePanel = () => {
124+
setIsPanelOpen(!isPanelOpen);
125+
};
126+
92127
return (
93128
<div className="flex min-h-screen w-full relative">
94129
<div className="hidden lg:block">
@@ -106,6 +141,9 @@ export default function Chat() {
106141
onChatSelect={handleChatSelect}
107142
onNewChat={handleNewChat}
108143
chatRooms={chatRooms}
144+
isSidebarOpen={isSidebarOpen}
145+
setIsSidebarOpen={setIsSidebarOpen}
146+
setIsPanelOpen={setIsPanelOpen}
109147
/>
110148
<div className="flex-1 min-h-0">
111149
<ChatArea
@@ -115,7 +153,7 @@ export default function Chat() {
115153
isAIResponding={isAIResponding && hasUserSentMessage}
116154
examples={examples}
117155
onExampleSelect={handleExampleSelect}
118-
setSelectedImage={setSelectedImage}
156+
setSelectedImage={handleOpenTab}
119157
/>
120158
</div>
121159
<ChatSubmit
@@ -127,18 +165,14 @@ export default function Chat() {
127165
/>
128166
</div>
129167
</SidebarInset>
130-
<div className="flex h-[100vh] overflow-hidden">
131-
<div className="hidden lg:block h-full">
132-
<div
133-
className={`h-full ${selectedImage ? 'w-96' : 'w-0'} bg-beige border-l border-gray-200 transition-all duration-500 ease-in-out
134-
${selectedImage ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0 pointer-events-none'}
135-
`}
136-
style={{ minWidth: 0 }}
137-
>
138-
{selectedImage && <ImageDetailPanel imageData={selectedImage} onClose={() => setSelectedImage(null)} />}
139-
</div>
140-
</div>
141-
</div>
168+
<ImagePanel
169+
isOpen={isPanelOpen}
170+
onToggle={handleTogglePanel}
171+
openTabs={openTabs}
172+
activeTabId={activeTabId}
173+
onTabSelect={setActiveTabId}
174+
onTabClose={handleCloseTab}
175+
/>
142176
</div>
143177
);
144178
}

src/components/ChatArea.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function ChatArea({
2020
isAIResponding?: boolean;
2121
examples: string[];
2222
onExampleSelect?: (text: string) => void;
23-
setSelectedImage: (img: MessageImage | null) => void;
23+
setSelectedImage: Function;
2424
}) {
2525
const scrollAreaRef = useRef<HTMLDivElement>(null);
2626

src/components/ChatHeader.tsx

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,53 @@ interface AppSidebarProps {
66
currentChatId: number | null;
77
onChatSelect: (chatId: number) => void;
88
onNewChat: () => void;
9+
isSidebarOpen?: boolean;
10+
setIsSidebarOpen: (open: boolean) => void;
11+
setIsPanelOpen?: (open: boolean) => void;
912
}
1013

11-
export default function ChatHeader({ chatRooms, currentChatId, onChatSelect, onNewChat }: AppSidebarProps) {
14+
export default function ChatHeader({
15+
chatRooms,
16+
currentChatId,
17+
onChatSelect,
18+
onNewChat,
19+
isSidebarOpen,
20+
setIsSidebarOpen,
21+
}: AppSidebarProps) {
1222
return (
1323
<div className="flex items-center justify-between px-4 py-6 mx-16 md:mx-auto w-[calc(100vw-68px)] lg:w-full bg-white">
1424
<div className="flex items-center gap-2">
15-
{/* 모바일: 햄버거 버튼 + 체크박스 해킹 */}
16-
<input type="checkbox" id="sidebar-mobile-toggle" className="hidden peer" />
17-
<label htmlFor="sidebar-mobile-toggle" className="fixed top-4 left-4 z-50 lg:hidden p-2 cursor-pointer">
25+
{/* 모바일: 햄버거 버튼 */}
26+
<button className="fixed top-4 left-4 z-50 lg:hidden p-2 cursor-pointer" onClick={() => setIsSidebarOpen(true)}>
1827
<Menu className="w-6 h-6 text-blue" />
19-
</label>
20-
{/* 모바일 오버레이 */}
21-
<label
22-
htmlFor="sidebar-mobile-toggle"
23-
className="fixed inset-0 z-40 bg-black opacity-40 transition-opacity duration-200 hidden peer-checked:block lg:hidden"
24-
/>
25-
{/* 모바일 사이드바 */}
26-
<aside className="fixed top-0 left-0 h-full w-[80%] max-w-full z-50 bg-beige shadow-lg transform transition-transform duration-300 -translate-x-full peer-checked:translate-x-0 lg:hidden">
27-
<SidebarContent
28-
chatRooms={chatRooms}
29-
currentChatId={currentChatId}
30-
onChatSelect={onChatSelect}
31-
onNewChat={onNewChat}
32-
className="py-6"
33-
/>
34-
</aside>
28+
</button>
29+
{/* 모바일 오버레이 및 사이드바 */}
30+
{isSidebarOpen && (
31+
<>
32+
<div
33+
className="fixed inset-0 z-[500] bg-black opacity-40 transition-opacity duration-200 lg:hidden"
34+
onClick={() => setIsSidebarOpen(false)}
35+
/>
36+
<aside className="fixed top-0 left-0 h-full w-[80%] max-w-full z-[500] bg-beige shadow-lg transform transition-transform duration-300 lg:hidden">
37+
<div className="flex items-center w-full h-20">
38+
<button
39+
type="button"
40+
className="p-6 cursor-pointer flex items-center justify-center"
41+
onClick={() => setIsSidebarOpen(false)}
42+
>
43+
<Menu className="w-8 h-8 text-blue" />
44+
</button>
45+
</div>
46+
<SidebarContent
47+
chatRooms={chatRooms}
48+
currentChatId={currentChatId}
49+
onChatSelect={onChatSelect}
50+
onNewChat={onNewChat}
51+
className="py-6"
52+
/>
53+
</aside>
54+
</>
55+
)}
3556
<span className="font-bold text-3xl text-blue">The First Take {currentChatId?.toString() || ''}</span>
3657
</div>
3758
<div className="w-9 h-9 rounded-full bg-gray-200 flex items-center justify-center text-gray-500 font-bold text-lg">

src/components/ImageDetailPanel.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ export default function ImageDetailPanel({
2121
{/* 헤더 */}
2222
<div className="flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700">
2323
<h2 className="text-lg font-semibold text-gray-800 dark:text-white">패션 아이템 상세 정보</h2>
24-
<button onClick={onClose} className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors">
25-
<X className="w-4 h-4 text-gray-500 dark:text-gray-400" />
26-
</button>
2724
</div>
2825

2926
{/* 내용 */}

src/components/ImagePanel.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// components/ImagePanel.tsx
2+
3+
import Image from 'next/image';
4+
import ImageDetailPanel from '@/components/ImageDetailPanel';
5+
import { ChevronLeft } from 'lucide-react';
6+
7+
export default function ImagePanel({
8+
isOpen,
9+
onToggle, // 패널을 여닫는 함수
10+
openTabs,
11+
activeTabId,
12+
onTabSelect,
13+
onTabClose,
14+
}: {
15+
isOpen: boolean;
16+
onToggle: () => void;
17+
openTabs: any[];
18+
activeTabId: string | null;
19+
onTabSelect: (id: string) => void;
20+
onTabClose: (id: string) => void;
21+
}) {
22+
const activeTabData = openTabs.find((tab) => tab.src === activeTabId);
23+
24+
return (
25+
<div
26+
className={`fixed top-0 right-0 h-full bg-beige border-l border-gray-200 shadow-xl z-40
27+
flex flex-row transition-transform duration-500 ease-in-out
28+
${isOpen ? 'translate-x-0' : 'translate-x-full'}`}
29+
>
30+
<button
31+
onClick={onToggle}
32+
className="absolute top-36 -translate-y-1/2 left-0 h-36 -translate-x-full z-20 bg-beige p-2 rounded-l-lg border border-gray-200 hover:bg-gray-100 transition-colors"
33+
title={isOpen ? '패널 닫기' : '패널 열기'}
34+
>
35+
<ChevronLeft size={20} className={`transition-transform duration-300 ${isOpen ? 'rotate-180' : 'rotate-0'}`} />
36+
</button>
37+
{/* 왼쪽: 세로 탭 바 (1/3) */}
38+
<div className="h-full w-28 border-r border-gray-200 bg-beige flex flex-col items-center gap-2 overflow-y-auto p-2">
39+
{openTabs.map((tab) => (
40+
<button
41+
key={tab.src}
42+
onClick={() => onTabSelect(tab.src)}
43+
className={`relative overflow-hidden flex-shrink-0
44+
transition-all duration-200
45+
${activeTabId === tab.src ? 'ring-2 ring-blue-500 ring-offset-2' : 'hover:opacity-80'}`}
46+
>
47+
<Image src={tab.src} alt={tab.name} width={64} height={64} className="h-32 w-28 object-cover" />
48+
<button
49+
onClick={(e) => {
50+
e.stopPropagation();
51+
onTabClose(tab.src);
52+
}}
53+
className="absolute top-0 right-0 m-1 w-5 h-5 bg-black/50 text-white text-xs rounded-full flex items-center justify-center hover:bg-red-500"
54+
title="탭 닫기"
55+
>
56+
×
57+
</button>
58+
</button>
59+
))}
60+
</div>
61+
{/* 오른쪽: 상세 이미지 영역 (2/3) */}
62+
<div className="flex-1 w-96 h-full overflow-y-auto">
63+
{activeTabData ? (
64+
<ImageDetailPanel imageData={activeTabData} onClose={() => onTabClose(activeTabData.src)} />
65+
) : (
66+
<div className="p-8 text-center text-gray-500">선택된 이미지가 없습니다.</div>
67+
)}
68+
</div>
69+
</div>
70+
);
71+
}

0 commit comments

Comments
 (0)