Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 15 additions & 22 deletions app/routes/chat/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,42 @@
import { formatKoreanDateTime } from "../../utils/dateTime";
import { useNavigate } from "react-router";
import { type ChatRoomCard } from "./api/chat";

export type ChatRoom = {
id: string;
brandName: string;
lastMessage: string;
updatedAt: string;
unreadCount: number;
logoUrl: string;
type: "sent" | "received";
isCollaborating: boolean;
};
export function ChatList({ rooms }: { rooms: ChatRoomCard[] }) {

export function ChatList({ rooms }: { rooms: ChatRoom[] }) {
const safeRooms = Array.isArray(rooms) ? rooms : [];

return (
<div className="flex flex-col gap-[10px]">
{rooms.map((room) => (
<ChatListItem key={room.id} room={room} />
{safeRooms.map((room) => (
<ChatListItem key={room.roomId} room={room} />
))}
</div>
);
}

export function ChatListItem({ room }: { room: ChatRoom }) {
export function ChatListItem({ room }: { room: ChatRoomCard }) {

const { dateText, timeText } = formatKoreanDateTime(room.updatedAt);
const { dateText, timeText } = formatKoreanDateTime(room.lastMessageAt);
const navigate = useNavigate();

return (
<button
type="button"
className=" w-full max-w-[420px] rounded-[10px] bg-white px-4 py-[14px] flex items-start gap-[14px] text-left active:bg-[#F2F2F5]"
onClick={() => navigate(`/rooms/${room.id}`)}
onClick={() => navigate(`/rooms/${room.roomId}`)}
>
{/* 왼쪽 로고 */}
<div className="w-[43px] h-[43px] rounded-[10px] bg-white border border-[#E6E6F3] flex items-center justify-center overflow-hidden shrink-0">
{room.logoUrl ? (
{room.opponentProfileImageUrl ? (
<img
src={room.logoUrl}
alt={`${room.brandName} 로고`}
src={room.opponentProfileImageUrl}
alt={`${room.opponentName} 로고`}
className="w-full h-full object-contain"
/>
) : (
<span className="text-callout3 text-text-gray3">
{room.brandName.slice(0, 2)}
{room.opponentName.slice(0, 2)}
</span>
)}
</div>
Expand All @@ -53,7 +46,7 @@ export function ChatListItem({ room }: { room: ChatRoom }) {
{/* 1줄: 브랜드명 + 상태 뱃지 */}
<div className="flex items-center gap-2">
<div className="text-title1 text-text-black font-Pretendard truncate">
{room.brandName}
{room.opponentName}
</div>

{room.isCollaborating && (
Expand All @@ -74,7 +67,7 @@ export function ChatListItem({ room }: { room: ChatRoom }) {

{/* 2줄 미리보기 */}
<div className="text-[12px] mt-2.5 text-Medium text-text-gray3 line-clamp-2">
{room.lastMessage}
{room.lastMessagePreview}
</div>
</div>

Expand Down
48 changes: 48 additions & 0 deletions app/routes/chat/api/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { axiosInstance } from "../../../api/axios";

//채팅 리스트 조회

export type ChatRoomListStatus = "LATEST" | "COLLABORATING";

export interface ChatRoomListResponse {
totalUnreadCount: number;
rooms: ChatRoomCard[];
nextCursor: string | null;
hasNext: boolean;
}

export interface ChatRoomCard {
roomId: number;
opponentUserId: number;
opponentName: string;
opponentProfileImageUrl: string | null;
isCollaborating: boolean;
lastMessagePreview: string;
lastMessageType: "TEXT" | "IMAGE" | "FILE" | "SYSTEM";
lastMessageAt: string; // ISO-8601
unreadCount: number;
}

type GetChatRoomsParams = {
status?: ChatRoomListStatus; // 기본값 LATEST
cursor?: string;
size?: number;
search?: string;
};

export async function getChatRooms(params: GetChatRoomsParams): Promise<ChatRoomListResponse> {
const { status, cursor, size = 20, search } = params;

const res = await axiosInstance.get<ChatRoomListResponse>("/api/v1/chat/rooms", {
params: {
status,
cursor,
size,
search: search && search.trim().length > 0 ? search.trim() : undefined,
},
});

return res.data;
}


54 changes: 29 additions & 25 deletions app/routes/chat/chat-content.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,44 @@
import { useState, useMemo } from "react";
import { useState } from "react";
import { SORT_LABEL, type SortOption } from "./components/SortingSheetConstant";
import { rooms } from "../../data/chat-room";
import { useEffect } from "react";
import { ChatListHeader } from "./components/ChatListHeader";
import SortFilterSheet from "./components/SortingSheet";
import ChatList from "./ChatList";
import { EmptyChatState } from "./components/EmptyState";
import { useHideBottomTab } from "../../hooks/useHideBottomTab";
import { getChatRooms, type ChatRoomCard } from "./api/chat";

function ChatPage() {
const [activeTab, setActiveTab] = useState<"sent" | "received">("sent"); // 보낸 제안 / 받은 제안 탭
const [isSortOpen, setIsSortOpen] = useState(false); // 정렬 바텀시트
const [sort, setSort] = useState<SortOption>("latest"); // 최신순 / 협업중만
const [pendingSort, setPendingSort] = useState<SortOption>(sort); // 바텀시트에서 고른 값

// 바텀탭 숨기기 (바텀시트 열렸을 때)
useHideBottomTab(isSortOpen);
const [rooms, setRooms] = useState<ChatRoomCard[]>([]);
const [loading, setLoading] = useState(false);

// 받은/보낸 필터
const filteredRooms = useMemo(() => {
return rooms.filter((room) => room.type === activeTab);
}, [activeTab]);
// 바텀탭 숨기기
useHideBottomTab(isSortOpen);

// 정렬 + (필요시) 협업중 필터
const sortedRooms = useMemo(() => {
let filtered = filteredRooms;
const fetchRooms = async () => {
setLoading(true);
try {
const data = await getChatRooms({
status: sort === "collaborating" ? "COLLABORATING" : "LATEST",
});
console.log("[chat] response:", data);

// 협업중만 보기
if (sort === "collaborating") {
filtered = filteredRooms.filter((room) => room.isCollaborating);
setRooms(Array.isArray(data.rooms) ? data.rooms : []);
} catch (e) {
console.error("[chat] fetchRooms error:", e);
setRooms([]);
} finally {
setLoading(false);
}
};

const copy = [...filtered];
copy.sort((a, b) => {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
});

return copy;
}, [filteredRooms, sort]);
useEffect(() => {
fetchRooms();
}, [sort]);

Check warning on line 41 in app/routes/chat/chat-content.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'fetchRooms'. Either include it or remove the dependency array

Check warning on line 41 in app/routes/chat/chat-content.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'fetchRooms'. Either include it or remove the dependency array

const openSortSheet = () => {
setPendingSort(sort);
Expand All @@ -52,14 +54,16 @@
<div className="min-h-screen bg-gradient-to-b from-[#F6F6FF] via-[#F3F3FA] to-[#E8E8FB]">
<main className="p-4 pb-16">
<ChatListHeader
activeTab={activeTab}
setActiveTab={setActiveTab}
sortLabel={SORT_LABEL[sort]}
onClickSort={openSortSheet}
sortOpen={isSortOpen}
/>

{sortedRooms.length === 0 ? <EmptyChatState /> : <ChatList rooms={sortedRooms} />}
{!loading && rooms.length === 0 ? (
<EmptyChatState />
) : (
<ChatList rooms={rooms} />
)}
</main>

<SortFilterSheet
Expand Down
3 changes: 0 additions & 3 deletions app/routes/chat/components/ChatListHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ export function ChatListHeader({
sortLabel,
onClickSort,
}: {
activeTab: "sent" | "received";
setActiveTab: (tab: "sent" | "received") => void;
sortLabel: string;
onClickSort: () => void;
sortOpen: boolean;

}) {
const [query, setQuery] = useState("");

Expand Down
4 changes: 2 additions & 2 deletions app/routes/chat/components/SortingSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export function SortFilterSheet({
/>

{/* 시트 */}
<div className="fixed left-1/2 -translate-x-1/2 top-[235px] w-full max-w-[375px] h-[530px] bg-white rounded-t-[12px] pt-[20px] px-4 flex flex-col">
<div className="w-full max-w-[375px] h-[70px] fixed left-1/2 -translate-x-1/2">
<div className="fixed left-1/2 -translate-x-1/2 top-[235px] w-full max-w-[430px] h-[530px] bg-white rounded-t-[12px] pt-[20px] px-4 flex flex-col">
<div className="w-full max-w-[430px] h-[70px] fixed left-1/2 -translate-x-1/2">
<div className="px-4 text-Medium text-text-black mb-3">정렬 필터</div>

<div className="bg-[#F3F4F8] px-4 py-3">
Expand Down
2 changes: 1 addition & 1 deletion app/routes/mypage/components/profileCard/traitData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const TRAITS: Trait[] = [
},
{
id: "content",
badge: "향수 특성",
badge: "콘텐츠 특성",
icon: (className = "") => (
<img src={contentIcon} alt="content" className={className} />
),
Expand Down
7 changes: 6 additions & 1 deletion app/routes/rooms/$chatId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@ import ChattingRoom from "./chatting-room";

export default function ChatRoomRoute() {
const { chatId } = useParams();
return <ChattingRoom chatId={chatId!} />;

const roomId = Number(chatId);
if (!chatId || Number.isNaN(roomId)) {
return null;
}
return <ChattingRoom roomId={roomId} />;
}
14 changes: 14 additions & 0 deletions app/routes/rooms/api/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { axiosInstance } from "../../../api/axios";

export async function uploadAttachment(file: File) {
const formData = new FormData();
formData.append("file", file);

const res = await axiosInstance.post("/api/v1/attachments", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});

return res.data;
}
Loading
Loading