diff --git a/app/routes/rooms/components/ChatRoomHeader.tsx b/app/components/common/NavigateHeader.tsx similarity index 68% rename from app/routes/rooms/components/ChatRoomHeader.tsx rename to app/components/common/NavigateHeader.tsx index 5ae4ec5..1a24e20 100644 --- a/app/routes/rooms/components/ChatRoomHeader.tsx +++ b/app/components/common/NavigateHeader.tsx @@ -1,14 +1,11 @@ - type Props = { - title: string; - subtitle: string; - subTitleClass?: string; - onBack?: () => void; + title: string; // 텍스트 + onBack?: () => void; //onBack={() => history.back()} 뒤로가기 }; -export default function ChatRoomHeader({ title, subtitle, subTitleClass, onBack }: Props) { +export default function NavigationHeader({ title, onBack }: Props) { return ( -
+
{/* 오른쪽 여백 맞추기 */} diff --git a/app/data/chat-room.ts b/app/data/chat-room.ts index da0f241..5518ac9 100644 --- a/app/data/chat-room.ts +++ b/app/data/chat-room.ts @@ -1,4 +1,4 @@ -import { type ChatRoom } from "../routes/chat/types/ChatRoom"; +import { type ChatRoom } from "../routes/chat/ChatList"; export const rooms: ChatRoom[] = [ { @@ -7,7 +7,6 @@ export const rooms: ChatRoom[] = [ lastMessage: "안녕하세요! 제안 확인 부탁드립니다.안녕하세요 제안 확인 부탁드립니다 안녕하세요 제안 확인 부탁드립니다 안녕하세요 제안 확인 부탁드립니다", updatedAt: new Date().toISOString(), unreadCount: 2, - status: "matching", logoUrl: "", type: "sent", isCollaborating: true, @@ -18,11 +17,8 @@ export const rooms: ChatRoom[] = [ lastMessage: "검토 중입니다!", updatedAt: "2025-01-05T10:00:00", unreadCount: 0, - status: "reviewing", logoUrl: "", - type: "received", + type: "sent", isCollaborating: false, }, ]; - -// TODO: API 연결 전 임시 더미 데이터 \ No newline at end of file diff --git a/app/hooks/useHideHeader.ts b/app/hooks/useHideHeader.ts new file mode 100644 index 0000000..2a28ba7 --- /dev/null +++ b/app/hooks/useHideHeader.ts @@ -0,0 +1,16 @@ +import { useContext, useLayoutEffect } from "react"; +import { LayoutContext } from "../routes/layout-context"; + +export function useHideHeader(hide: boolean) { + const layout = useContext(LayoutContext); + + useLayoutEffect(() => { + if (!layout) return; + + layout.setHideHeader(hide); + + return () => { + layout.setHideHeader(false); + }; + }, [hide, layout]); +} diff --git a/app/routes.ts b/app/routes.ts index a62de7f..c2b4536 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -52,7 +52,15 @@ export default [ route(":chatId", "routes/rooms/$chatId.tsx"), ]), - route("mypage", "routes/mypage/route.tsx"), + route("mypage", "routes/mypage/route.tsx", [ + index("routes/mypage/mypage/route.tsx"), + route("profileCard", "routes/mypage/profileCard/route.tsx"), + route("edit", "routes/mypage/edit/route.tsx"), + route("likes", "routes/mypage/likes/route.tsx"), + route("privacy", "routes/mypage/privacy/route.tsx"), + route("terms", "routes/mypage/terms/route.tsx") + ]), + route("brand", "routes/brand/route.tsx"), ]), diff --git a/app/routes/chat/ChatList.tsx b/app/routes/chat/ChatList.tsx index c4904e8..a280c59 100644 --- a/app/routes/chat/ChatList.tsx +++ b/app/routes/chat/ChatList.tsx @@ -7,7 +7,7 @@ export type ChatRoom = { lastMessage: string; updatedAt: string; unreadCount: number; - logoUrl: string; + logoUrl: string; type: "sent" | "received"; isCollaborating: boolean; }; @@ -99,4 +99,4 @@ export function ChatListItem({ room }: { room: ChatRoom }) { ); } -export default ChatList; +export default ChatList; \ No newline at end of file diff --git a/app/routes/chat/chat-content.tsx b/app/routes/chat/chat-content.tsx index addff41..5d61ccf 100644 --- a/app/routes/chat/chat-content.tsx +++ b/app/routes/chat/chat-content.tsx @@ -1,7 +1,7 @@ import { useState, useMemo } from "react"; import { SORT_LABEL, type SortOption } from "./components/SortingSheetConstant"; import { rooms } from "../../data/chat-room"; -import ChatListHeader from "./components/ChatListHeader"; +import { ChatListHeader } from "./components/ChatListHeader"; import SortFilterSheet from "./components/SortingSheet"; import ChatList from "./ChatList"; import { EmptyChatState } from "./components/EmptyState"; @@ -9,14 +9,14 @@ import { useHideBottomTab } from "../../hooks/useHideBottomTab"; function ChatPage() { const [activeTab, setActiveTab] = useState<"sent" | "received">("sent"); // 보낸 제안 / 받은 제안 탭 - const [isSortOpen, setIsSortOpen] = useState(false); // 정렬 바텀시트 - const [sort, setSort] = useState("latest"); // 현재 선택된 정렬 옵션 + const [isSortOpen, setIsSortOpen] = useState(false); // 정렬 바텀시트 + const [sort, setSort] = useState("latest"); // 최신순 / 협업중만 const [pendingSort, setPendingSort] = useState(sort); // 바텀시트에서 고른 값 // 바텀탭 숨기기 (바텀시트 열렸을 때) useHideBottomTab(isSortOpen); - // 받은제안/보낸제안 필터 + // 받은/보낸 필터 const filteredRooms = useMemo(() => { return rooms.filter((room) => room.type === activeTab); }, [activeTab]); @@ -39,18 +39,17 @@ function ChatPage() { }, [filteredRooms, sort]); const openSortSheet = () => { - setPendingSort(sort); // 열 때 현재 적용값으로 + setPendingSort(sort); setIsSortOpen(true); }; const applySort = () => { - setSort(pendingSort); // 기준 적용 + setSort(pendingSort); setIsSortOpen(false); }; return (
-
- {sortedRooms.length === 0 ? ( - - ) : ( - - )} + {sortedRooms.length === 0 ? : }
- {rooms.map((room) => ( - - ))} -
- ); -} - -export function ChatListItem({ room }: { room: ChatRoom }) { - const statusLabel = - room.status === "matching" ? "매칭" : room.status === "reviewing" ? "검토 중" : "거절"; - - // 뱃지 톤: 매칭=보라, 검토중=연보라, 거절=그레이 - const statusClass = - room.status === "matching" - ? "bg-[#E6E6F3] text-[#6666E5]" - : room.status === "reviewing" - ? "bg-[#EBEEFB] text-[#A7B8FC]" - : "bg-[#F3F3F3] text-text-gray3"; - - const { dateText, timeText } = formatKoreanDateTime(room.updatedAt); - - const navigate = useNavigate(); - - return ( - - ); -} - -export default ChatList; \ No newline at end of file diff --git a/app/routes/chat/components/ChatListHeader.tsx b/app/routes/chat/components/ChatListHeader.tsx index 6ed7032..79ab83f 100644 --- a/app/routes/chat/components/ChatListHeader.tsx +++ b/app/routes/chat/components/ChatListHeader.tsx @@ -1,5 +1,5 @@ import searchIcon from "../../../assets/search2.svg"; -import closeIcon from "../../../assets/cancel.svg"; +import closeIcon from "../../../assets/cancel.svg"; import { useState } from "react"; export function ChatListHeader({ @@ -11,7 +11,7 @@ export function ChatListHeader({ sortLabel: string; onClickSort: () => void; sortOpen: boolean; - + }) { const [query, setQuery] = useState(""); @@ -22,7 +22,7 @@ export function ChatListHeader({ {/* 검색 입력창 */}
search - + setQuery(e.target.value)} /> - - - -
-
- - {/* list */} -
- - - - - - - - - -
-
약관
- - - - -
- - - setOpenLogout(true)} py={16} gap={6} /> - - - setOpenWithdraw(true)} muted={true} py={16} gap={6} /> -
- - {/* gate modal */} - {openGate ? ( - setOpenGate(false)} - onGoTest={onGoMatchingTest} - /> - ) : null} - - {/* logout modal */} - {openLogout ? ( - setOpenLogout(false)} - onPrimary={() => { - setOpenLogout(false); - onLogout(); - }} - /> - ) : null} - - {/* withdraw modal */} - {openWithdraw ? ( - setOpenWithdraw(false)} - onPrimary={() => { - setOpenWithdraw(false); - onWithdraw(); - }} - /> - ) : null} - - - ); -} - -function MenuButton({ - title, label, onClick, muted, py, gap = 6, -}: { title?: string; label: string; onClick: () => void; muted?: boolean; py?: number; gap?: number; }) { - return ( - - ); -} - -function Divider() { - return
; -} - -function GateModal({ - onClose, - onGoTest, -}: { - onClose: () => void; - onGoTest: () => void; -}) { - return ( -
- {/* dim */} -
- - {/* modal */} -
- - -
-
- ! -
- -
- 매칭 검사를 -
- 먼저 진행해주세요 -
- - -
-
-
- ); -} \ No newline at end of file diff --git a/app/routes/mypage/edit.tsx b/app/routes/mypage/edit/edit-content.tsx similarity index 100% rename from app/routes/mypage/edit.tsx rename to app/routes/mypage/edit/edit-content.tsx diff --git a/app/routes/mypage/edit/route.tsx b/app/routes/mypage/edit/route.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/mypage/inquiry.tsx b/app/routes/mypage/inquiry.tsx deleted file mode 100644 index f31c248..0000000 --- a/app/routes/mypage/inquiry.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function MyPageInquiry() { - return
Hello "/mypage/inquiry"!
-} diff --git a/app/routes/mypage/likes.tsx b/app/routes/mypage/likes/likes-content.tsx similarity index 100% rename from app/routes/mypage/likes.tsx rename to app/routes/mypage/likes/likes-content.tsx diff --git a/app/routes/mypage/likes/route.tsx b/app/routes/mypage/likes/route.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/mypage/mypage-content.tsx b/app/routes/mypage/mypage-content.tsx deleted file mode 100644 index e4acb9b..0000000 --- a/app/routes/mypage/mypage-content.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useNavigate } from "react-router"; -import MyPageHome from "./MyPageHome"; -import { useAuthStore } from "../../stores/auth-store"; - -export default function MyPageContent() { - const navigate = useNavigate(); - const me = useAuthStore((s) => s.me); - - const hasMatchingTest = Boolean(me?.matchingTestDone); - - return ( - navigate({ to: "/matching/test/step1" })} - onOpenProfileCard={() => navigate({ to: "/mypage/profileCard" })} - onOpenLikes={() => navigate({ to: "/mypage/likes" })} - onOpenEditProfile={() => navigate({ to: "/mypage/edit" })} - onOpenNotifications={() => navigate({ to: "/mypage/notifications" })} - onOpenInquiry={() => navigate({ to: "/mypage/inquiry" })} - onOpenTerms={() => navigate({ to: "/mypage/terms" })} // policy/terms - onOpenPrivacy={() => navigate({ to: "/mypage/privacy" })} // policy/privacy - onLogout={() => { - useAuthStore.getState().logout?.(); - navigate({ to: "/auth/login" }); - }} - onWithdraw={() => navigate({ to: "/mypage/withdraw" })} - /> - ); -} diff --git a/app/routes/mypage/mypage/MyPageHome.tsx b/app/routes/mypage/mypage/MyPageHome.tsx new file mode 100644 index 0000000..e096e16 --- /dev/null +++ b/app/routes/mypage/mypage/MyPageHome.tsx @@ -0,0 +1,299 @@ +import { useState } from "react"; +import ConfirmModal from "../components/ConfirmModal"; + +type Props = { + // 서버/스토어에서 내려오는 값이라고 가정 + hasMatchingTest: boolean; + user: { + name: string; + roleText?: string; // "홍길동" 같은 라벨 + email: string; + avatarUrl: string; + }; + onGoMatchingTest: () => void; + onOpenProfileCard: () => void; + onOpenLikes: () => void; + onOpenEditProfile: () => void; + onOpenNotifications: () => void; + onOpenInquiry: () => void; + onOpenTerms: () => void; + onOpenPrivacy: () => void; + onLogout: () => void; + onWithdraw: () => void; +}; + +export default function MyPageHome({ + hasMatchingTest, + user, + onGoMatchingTest, + onOpenProfileCard, + onOpenLikes, + onOpenEditProfile, + onOpenNotifications, + onOpenInquiry, + onOpenTerms, + onOpenPrivacy, + onLogout, + onWithdraw, +}: Props) { + const [openGate, setOpenGate] = useState(!hasMatchingTest); + const [openLogout, setOpenLogout] = useState(false); + const [openWithdraw, setOpenWithdraw] = useState(false); + + //const actionsDisabled = useMemo(() => !hasMatchingTest, [hasMatchingTest]); 매칭검사 안했을 시 + + return ( +
+ {/* profile card */} +
+
+
+ {user.avatarUrl ? ( + avatar + ) : ( +
+ logo +
+ )} +
+ +
+
+
+ {user.name} +
+ {user.roleText ? ( +
{user.roleText}
+ ) : null} +
+
{user.email}
+
+
+ + {/* top buttons */} +
+ + + +
+
+ +
+ + {/* list */} +
+ + + + + + + + + +
+
약관
+ + + + +
+ + + setOpenLogout(true)} py={16} /> + + + setOpenWithdraw(true)} muted={true} py={16} /> +
+ + {/* gate modal */} + {openGate ? ( + setOpenGate(false)} + onGoTest={onGoMatchingTest} + /> + ) : null} + + {/* logout modal */} + {openLogout ? ( + setOpenLogout(false)} + onPrimary={() => { + setOpenLogout(false); + onLogout(); // MyPageContent에서 로그인 페이지 이동 처리 + }} + /> + ) : null} + + {/* withdraw modal */} + {openWithdraw ? ( + setOpenWithdraw(false)} + onPrimary={() => { + setOpenWithdraw(false); + onWithdraw(); // 탈퇴 페이지 이동 or API 호출로 연결 + }} + /> + ) : null} + +
+ ); +} + +function MenuButton({ + title, label, onClick, muted, py, +}: { + title?: string; + label: string; + onClick: () => void; + muted?: boolean; + py?: number; +}) { + return ( +
+ {title && ( +
+ {title} +
+ )} + + {/* label */} +
+ +
+
+ ); +} + + +function Divider() { + return
; +} + +function GateModal({ + onClose, + onGoTest, +}: { + onClose: () => void; + onGoTest: () => void; +}) { + return ( +
+ {/* dim */} +
+ + {/* modal */} +
+
+ +
+ +
+
+ ! +
+ +
+ 매칭 검사를 +
+ 먼저 진행해주세요 +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/routes/mypage/mypage/mypage-content.tsx b/app/routes/mypage/mypage/mypage-content.tsx new file mode 100644 index 0000000..73b31c8 --- /dev/null +++ b/app/routes/mypage/mypage/mypage-content.tsx @@ -0,0 +1,35 @@ +import { useNavigate } from "react-router"; +import MyPageHome from "./MyPageHome"; +import { useAuthStore } from "../../../stores/auth-store"; + +export default function MyPageContent() { + const navigate = useNavigate(); + const me = useAuthStore((s) => s.me); + + const hasMatchingTest = Boolean(me?.matchingTestDone); + + return ( + navigate("/matching/test/step1")} + onOpenProfileCard={() => navigate( "/mypage/profileCard")} + onOpenLikes={() => navigate( "/mypage/likes")} + onOpenEditProfile={() => navigate("/mypage/edit")} + onOpenNotifications={() => navigate("/mypage/notifications")} + onOpenInquiry={() => navigate("/mypage/inquiry")} + onOpenTerms={() => navigate("/mypage/terms")} // policy/terms + onOpenPrivacy={() => navigate("/mypage/privacy")} // policy/privacy + onLogout={() => { + useAuthStore.getState().logout?.(); + navigate("/auth/login"); + }} + onWithdraw={() => navigate("/mypage/withdraw")} + /> + ); +} diff --git a/app/routes/mypage/mypage/route.tsx b/app/routes/mypage/mypage/route.tsx new file mode 100644 index 0000000..c18a6cb --- /dev/null +++ b/app/routes/mypage/mypage/route.tsx @@ -0,0 +1,5 @@ +import MyPageContent from "./mypage-content"; + +export default function ProfileCardLayout() { + return ; +} \ No newline at end of file diff --git a/app/routes/mypage/notifications.tsx b/app/routes/mypage/notifivation/notifications-content.tsx similarity index 100% rename from app/routes/mypage/notifications.tsx rename to app/routes/mypage/notifivation/notifications-content.tsx diff --git a/app/routes/mypage/notifivation/route.tsx b/app/routes/mypage/notifivation/route.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/mypage/privacy.tsx b/app/routes/mypage/privacy/privacy-content.tsx similarity index 100% rename from app/routes/mypage/privacy.tsx rename to app/routes/mypage/privacy/privacy-content.tsx diff --git a/app/routes/mypage/privacy/route.tsx b/app/routes/mypage/privacy/route.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/mypage/profileCard.tsx b/app/routes/mypage/profileCard.tsx deleted file mode 100644 index d9059f5..0000000 --- a/app/routes/mypage/profileCard.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function MyPageProfileCard() { - return
Hello "/mypage/profileCard"!
-} diff --git a/app/routes/mypage/profileCard/profileCard-content.tsx b/app/routes/mypage/profileCard/profileCard-content.tsx new file mode 100644 index 0000000..6eb0611 --- /dev/null +++ b/app/routes/mypage/profileCard/profileCard-content.tsx @@ -0,0 +1,204 @@ +import React from "react"; +import NavigationHeader from "../../../components/common/NavigateHeader"; + +type Campaign = { + type: "보낸 제안" | "지원"; + title: string; + date: string; +}; + +export default function ProfileCard() { + const campaigns: Campaign[] = [ + { type: "보낸 제안", title: "비플레인 - ‘글로우업’ 선크림 신제품 홍보…", date: "01/24/25 완료" }, + { type: "보낸 제안", title: "라운드랩 - ‘글로우업’ 크림 신제품 홍보…", date: "01/15/25 완료" }, + { type: "지원", title: "이즈토리 - 비타크림 신제품 체험단 모집", date: "12/15/24 완료" }, + ]; + + return ( +
+
+ {/* header */} +
+ history.back()} + /> +
+ +
+ {/* profile summary */} +
+
+ {/* 이미지 자리 */} +
+
+
+
+
+ +
+
비비
+
여성 22세
+
+ 관심분야: 뷰티, 패션 +
+
+
+
+ +
+ + {/* content */} +
+ {/* SNS */} +
+
+
+ + + +
+
www.instagram.com/vivi
+ +
+
+ + {/* Matching */} +
+
+
+
+ 비비 님은 +
+
+ OO한 크리에이터 입니다. +
+ OO한 브랜드와 잘 어울려요. +
+
+
+
+
+
+
+ + {/* Traits */} +
+ › + + } + > +
+ + +
+
+ + {/* Campaigns */} +
+ ˅ + + } + > +
+ {campaigns.map((c, idx) => ( +
+
+ {c.type} + {c.title} +
+ {c.date} +
+ ))} + + {/* pagination mock */} +
+ + + + + + +
+
+
+
+
+
+ ); +} + +function Section({ + title, + right, + children, +}: { + title: string; + right?: React.ReactNode; + children: React.ReactNode; +}) { + return ( +
+
+
{title}
+ {right} +
+ {children} +
+ ); +} + +function TraitCard({ + badge, + icon, + lines, +}: { + badge: string; + icon: string; + lines: string[]; +}) { + return ( +
+
+ {badge} +
+ +
+ {icon} +
+ +
+ {lines.map((t, i) => ( +
{t}
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/app/routes/mypage/profileCard/route.tsx b/app/routes/mypage/profileCard/route.tsx new file mode 100644 index 0000000..027660f --- /dev/null +++ b/app/routes/mypage/profileCard/route.tsx @@ -0,0 +1,5 @@ +import ProfileCard from "./profileCard-content"; + +export default function ProfileCardLayout() { + return ; +} \ No newline at end of file diff --git a/app/routes/mypage/route.tsx b/app/routes/mypage/route.tsx index d57ff26..e203410 100644 --- a/app/routes/mypage/route.tsx +++ b/app/routes/mypage/route.tsx @@ -1,5 +1,9 @@ -import MyPage from "./mypage-content"; +import { Outlet } from "react-router"; -export default function MyPageRoute() { - return ; +export default function MyPageLayout() { + return ( +
+ +
+ ) } diff --git a/app/routes/mypage/terms/route.tsx b/app/routes/mypage/terms/route.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/mypage/terms.tsx b/app/routes/mypage/terms/terms-content.tsx similarity index 100% rename from app/routes/mypage/terms.tsx rename to app/routes/mypage/terms/terms-content.tsx diff --git a/app/routes/mypage/withdraw.tsx b/app/routes/mypage/withdraw.tsx deleted file mode 100644 index d63ccf4..0000000 --- a/app/routes/mypage/withdraw.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function MyPageWithdraw() { - return
Hello "/mypage/withdraw"!
-} diff --git a/app/routes/rooms/chatting-room.tsx b/app/routes/rooms/chatting-room.tsx index 379070e..1f33170 100644 --- a/app/routes/rooms/chatting-room.tsx +++ b/app/routes/rooms/chatting-room.tsx @@ -1,11 +1,14 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import ChatRoomHeader from "./components/ChatRoomHeader"; +import NavigationHeader from "../../components/common/NavigateHeader"; import { type ChatMessage } from "./components/Bubbles/TextMessageTypes"; import ChatComposer from "./components/ChatComposer"; import AttachmentSheet, { type AttachmentAction } from "./components/AttachmentSheet"; import useKeyboardOffset from "../../hooks/KeyboardOffset"; import MessageRenderer from "./components/MessageRender"; import { formatKoreanDateTime } from "../../utils/dateTime"; +import CollaborationSummaryBar from "./components/CollaborationBar"; +import { useHideBottomTab } from "../../hooks/useHideBottomTab"; +import { useHideHeader } from "../../hooks/useHideHeader"; type Props = { chatId: string; @@ -40,7 +43,7 @@ export default function ChattingRoom( {chatId} : Props ) { campaignName: "글로우 쿠션 신제품 론칭 리뷰", campaignContent: "안녕하세요 크리에이터 비비 입니다! 이번에 글로우에서 신제품 쿠션이 출시되어 리뷰를 진행하고자 합니다. 자연스러운 커버력과 촉촉한 사용감이 특징인 이번 제품은 봄철 메이크업에 딱 맞는 아이템입니다. 리뷰를 통해 많은 분들께 소개하고 싶습니다. 긍정적인 검토 부탁드립니다!", time: "26.01.22\n00:10", - status: "sent", + status: "failed", price: 500000, orderId: "ORD1234567890", }, @@ -65,7 +68,7 @@ export default function ChattingRoom( {chatId} : Props ) { ext: "png", time: "26.01.22\n10:11", avatarSize: 38, - avartarSrc: "/brand.png", + avatarSrc: "/brand.png", }, { id: "file1", @@ -89,28 +92,34 @@ export default function ChattingRoom( {chatId} : Props ) { }, [chatId]); const partnerName = "민주"; // TODO: API/route loader에서 받아오기 - const hashtags = "#청정자극 #저자극 #심플한 감성"; // TODO: 프로필/카드에서 받아오기 - const matchStatus: "MATCHED" | "REJECTED" | "REVIEWING" = "REVIEWING"; // TODO: 서버에서 받아오기 - const partnerAvatarUrl = "" -; // TODO: 프로필/카드에서 받아오기 + const partnerAvatarUrl = ""; // TODO: 프로필/카드에서 받아오기 + + const isCollaborating = messages.some((m) => m.type === "MATCHED_CAMPAIGN"); + // 임시 로직: MATCHED_CAMPAIGN 메시지가 있으면 협업중이라고 가정 + // 실제로는 chatRoom.isCollaborating + + // 협업 요약바에 넣을 데이터(임시: 매칭 캠페인 메시지에서 뽑음) + const matchedCampaignMessage = useMemo(() => { + return messages.find((m) => m.type === "MATCHED_CAMPAIGN"); + }, [messages]); // 대화 시작 여부 - const hasStartedChat = messages.some((m) => m.side === "me" || m.side === "other"); + //const hasStartedChat = messages.some((m) => m.side === "me" || m.side === "other"); // 전송 중 상태 //const [isSending, setIsSending] = useState(false); - const statusLabelMap = { - MATCHED: "매칭", - REJECTED: "거절", - REVIEWING: "검토중", - } as const; + const collabTitle = matchedCampaignMessage?.campaignName ?? ""; + const collabSubtitle = matchedCampaignMessage ? "일주일 챌린지" : ""; + const collabThumb = partnerAvatarUrl; // 콜라보 상품 이미지. 임시로 브랜드 프로필로 설정 - const subtitleClass = hasStartedChat ? "text-[#6666E5]" : "text-[#5B5D6B]"; - const subtitleText = hasStartedChat ? statusLabelMap[matchStatus] : hashtags; + const summaryBarHeight = isCollaborating ? 64 : 0; const listRef = useRef(null); const inputRef = useRef(null); + useHideBottomTab(true); + useHideHeader(true); + const actions: AttachmentAction[] = useMemo( () => [ { key: "suggest", label: "재 제안", icon: "refresh" }, @@ -147,6 +156,7 @@ export default function ChattingRoom( {chatId} : Props ) { const trimmed = text.trim(); if (!trimmed) return; + //일반 메시지 전송 setMessages((prev) => [ ...prev, { @@ -155,9 +165,7 @@ export default function ChattingRoom( {chatId} : Props ) { content: trimmed, time: `${dateText}\n${timeText}`, type: "TEXT", - status: "sent", // 수정필요 - campaignName: "", - campaignContent: "", + status: "sent", }, ]); setText(""); // 입력창 비우기 @@ -165,19 +173,35 @@ export default function ChattingRoom( {chatId} : Props ) { requestAnimationFrame(() => inputRef.current?.focus()); // 한 프레임 뒤 focus 복귀 }; + /*const handleDeleteMessage = (id: string) => { + setMessages((prev) => prev.filter((m) => m.id !== id)); + }; + + const handleRetryMessage = (id: string) => { + setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, status: "sending" } : m))); + // TODO: API 재전송 후 성공/실패에 따라 status 갱신 + };*/ + return ( -
- + history.back()} /> + {isCollaborating && ( + + )} + {/* 메시지 영역 */}
diff --git a/app/routes/rooms/components/AttachmentSheet.tsx b/app/routes/rooms/components/AttachmentSheet.tsx index 7cf9421..167ba73 100644 --- a/app/routes/rooms/components/AttachmentSheet.tsx +++ b/app/routes/rooms/components/AttachmentSheet.tsx @@ -76,7 +76,7 @@ export default function AttachmentSheet({ open, actions, onClose, onAction, heig > -
{a.label}
+
{a.label}
))}
diff --git a/app/routes/rooms/components/Bubbles/MatchingMessage.tsx b/app/routes/rooms/components/Bubbles/MatchingMessage.tsx index 76b6aa9..36c8338 100644 --- a/app/routes/rooms/components/Bubbles/MatchingMessage.tsx +++ b/app/routes/rooms/components/Bubbles/MatchingMessage.tsx @@ -1,13 +1,18 @@ import { type ChatMessage } from "./TextMessageTypes"; +import { useNavigate } from "react-router"; +import MessageMeta from "./MessageStatus"; type Props = { message: ChatMessage; }; +//onRetry: (id: string) => void; +//onDelete: (id: string) => void; -export default function MatchedCampaignMessage({ message }: Props) { +export default function MatchedCampaignMessage({ message, }: Props) { const timeText = message.time ?? ""; const avatarSize = 38; const avatarSrc = undefined; + const navigate = useNavigate(); return (
@@ -32,7 +37,7 @@ export default function MatchedCampaignMessage({ message }: Props) {
{/* bubble + time */} -
+
@@ -46,7 +51,7 @@ export default function MatchedCampaignMessage({ message }: Props) {
- {timeText ? ( -
- {timeText} -
- ) : null} + {}}//Todo: onRetry + onDelete={() => {}}// Todo: onDelete + />
diff --git a/app/routes/rooms/components/Bubbles/MessageStatus.tsx b/app/routes/rooms/components/Bubbles/MessageStatus.tsx new file mode 100644 index 0000000..faae493 --- /dev/null +++ b/app/routes/rooms/components/Bubbles/MessageStatus.tsx @@ -0,0 +1,83 @@ +import { type ChatMessage } from "./TextMessageTypes"; + +type Props = { + message: ChatMessage; + timeText: string; + onRetry: (messageId: string) => void; + onDelete: (messageId: string) => void; +}; + +function MessageStatus({ + onRetry, + onDelete, +}: { + onRetry: () => void; + onDelete: () => void; +}) { + return ( +
+ + +
+ + +
+ ); +} + +export default function MessageMeta({ message, timeText, onRetry, onDelete }: Props) { + if (message.status === "failed") { + return ( + onRetry(message.id)} + onDelete={() => onDelete(message.id)} + /> + ); + } + + if (message.status === "sent") { + return timeText ? ( +
+ {timeText} +
+ ) : null; + } + + // sending이면 아무것도 안 보임 + return null; +} \ No newline at end of file diff --git a/app/routes/rooms/components/Bubbles/ProposalMessage.tsx b/app/routes/rooms/components/Bubbles/ProposalMessage.tsx index b175e74..333dc96 100644 --- a/app/routes/rooms/components/Bubbles/ProposalMessage.tsx +++ b/app/routes/rooms/components/Bubbles/ProposalMessage.tsx @@ -1,4 +1,5 @@ import { type ChatMessage } from "./TextMessageTypes"; +import { useNavigate } from "react-router"; type Props = { message: ChatMessage; @@ -9,6 +10,7 @@ export default function ProposalMessage({ message }: Props) { const timeText = message.time ?? ""; const isLeft = message.side === "other" || message.side === "system"; const avatarSrc = undefined; + const navigate = useNavigate(); if (isMe) return (
@@ -36,7 +38,7 @@ export default function ProposalMessage({ message }: Props) {