diff --git a/src/api/chatApi.js b/src/api/chatApi.js index c243390..1fc2e39 100644 --- a/src/api/chatApi.js +++ b/src/api/chatApi.js @@ -20,6 +20,11 @@ export async function getUserChatRooms(type = "ALL") { throw new Error(result.message || "채팅방 정보를 찾을 수 없습니다."); } + if (response.status === 500) { + // 서버 에러 + throw new Error(result.message || "서버 에러가 발생했습니다. 잠시 후 다시 시도해주세요."); + } + throw new Error(result.message || `채팅방 목록 조회 실패 (${response.status})`); } @@ -46,5 +51,10 @@ export async function getChatRoomMessages(roomId) { throw new Error(result.message || "존재하지 않는 채팅방입니다."); } + if (response.status === 500) { + // 서버 에러 + throw new Error(result.message || "서버 에러가 발생했습니다. 잠시 후 다시 시도해주세요."); + } + throw new Error(result.message || `채팅 내역 조회 실패 (${response.status})`); } diff --git a/src/api/clothesApi.js b/src/api/clothesApi.js index fd38297..a686334 100644 --- a/src/api/clothesApi.js +++ b/src/api/clothesApi.js @@ -386,11 +386,6 @@ export async function filterClothes(keyword = "", styles = [], priceRangeCodes = const response = await apiRequestGet(url); const result = await response.json(); - isSuccess: result.isSuccess, - message: result.message, - fullResult: result, - clothesListLength: result.result?.clothesList?.length || 0 - }); if (response.status === 200 && result.isSuccess === true) { return result.result; // { clothesList, listSize, totalPage, totalElements, isFirst, isLast } @@ -416,12 +411,24 @@ export async function filterClothes(keyword = "", styles = [], priceRangeCodes = export async function deleteClothes(clothesId) { const url = `${API_BASE_URL}/api/clothes/${clothesId}`; const body = JSON.stringify({ clothesId }); - + let response = await apiRequestDelete(url, { body, }); - const result = await response.json(); + // 403 에러 처리 (응답 본문이 비어있을 수 있음) + if (response.status === 403) { + throw new Error('삭제 권한이 없는 사용자입니다.'); + } + + // 응답 본문이 비어있을 수 있으므로 안전하게 파싱 + let result; + const text = await response.text(); + try { + result = text ? JSON.parse(text) : {}; + } catch { + result = {}; + } if (result.code === 'MEMBER4001') { const store = getStoreInstance()?.(); @@ -456,7 +463,19 @@ export async function deleteClothes(clothesId) { body, }); - const retryResult = await response.json(); + // 403 에러 처리 + if (response.status === 403) { + throw new Error('삭제 권한이 없는 사용자입니다.'); + } + + // 응답 본문이 비어있을 수 있으므로 안전하게 파싱 + let retryResult; + const retryText = await response.text(); + try { + retryResult = retryText ? JSON.parse(retryText) : {}; + } catch { + retryResult = {}; + } if (response.status === 200 && retryResult.isSuccess === true) { return retryResult.result; diff --git a/src/main.jsx b/src/main.jsx index 7ed8a5f..4248dbf 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import { HashRouter } from "react-router-dom"; +import { BrowserRouter } from "react-router-dom"; import { Provider } from "react-redux"; import App from "./App.jsx"; import "./styles/global.css"; @@ -13,10 +13,10 @@ setStoreGetter(() => store); ReactDOM.createRoot(document.getElementById('root')).render( - + - + ); diff --git a/src/pages/Chat/ChatRoomPage.jsx b/src/pages/Chat/ChatRoomPage.jsx index 6d6abc7..61faed8 100644 --- a/src/pages/Chat/ChatRoomPage.jsx +++ b/src/pages/Chat/ChatRoomPage.jsx @@ -10,6 +10,7 @@ import { useRequireAuth } from "../../hooks/useRequireAuth.js"; import { getChatRoomMessages } from "../../api/chatApi.js"; import { getMyPageInfo } from "../../api/memberApi.js"; import { payOrder, acceptOrder, cancelOrder } from "../../api/orderApi.js"; +import { formatChatTime } from "../../utils/formatters.js"; import toast from "react-hot-toast"; function ChatRoomPage() { @@ -51,52 +52,53 @@ function ChatRoomPage() { } }, [isAuthenticated]); - // 채팅방 내역 조회 - useEffect(() => { + // 채팅방 내역 조회 함수 + const fetchChatMessages = async () => { if (!roomId || !isAuthenticated) return; - const fetchChatMessages = async () => { - try { - setIsLoadingMessages(true); - const data = await getChatRoomMessages(Number(roomId)); - setChatData(data); - - // 메시지를 senderId와 현재 사용자 ID를 비교하여 타입 변환 - if (currentUserId && data.messages) { - const formattedMessages = data.messages.map((msg) => ({ - id: msg.messageId, - type: msg.senderId === currentUserId ? "user" : "partner", - text: msg.content, - timestamp: msg.sendTime, - })); - setMessages(formattedMessages); - } else if (data.messages) { - // currentUserId가 아직 로드되지 않았으면 일단 메시지만 저장 - setMessages(data.messages.map((msg) => ({ - id: msg.messageId, - type: "partner", // 임시로 partner로 설정 - text: msg.content, - timestamp: msg.sendTime, - }))); - } - } catch (error) { - console.error("채팅 내역 조회 실패:", error); - const errorMessage = error.message || "채팅 내역을 불러오지 못했습니다."; - toast.error(errorMessage); - - // 404 에러인 경우 채팅 페이지로 이동 - if (errorMessage.includes("존재하지 않는") || errorMessage.includes("404")) { - navigate("/chat"); - return; - } - - // 나머지 에러는 토스트 메시지만 표시하고 채팅 페이지로 이동 + try { + setIsLoadingMessages(true); + const data = await getChatRoomMessages(Number(roomId)); + setChatData(data); + + // 메시지를 senderId와 현재 사용자 ID를 비교하여 타입 변환 + if (currentUserId && data.messages) { + const formattedMessages = data.messages.map((msg) => ({ + id: msg.messageId, + type: msg.senderId === currentUserId ? "user" : "partner", + text: msg.content, + timestamp: msg.sendTime, + })); + setMessages(formattedMessages); + } else if (data.messages) { + // currentUserId가 아직 로드되지 않았으면 일단 메시지만 저장 + setMessages(data.messages.map((msg) => ({ + id: msg.messageId, + type: "partner", // 임시로 partner로 설정 + text: msg.content, + timestamp: msg.sendTime, + }))); + } + } catch (error) { + console.error("채팅 내역 조회 실패:", error); + const errorMessage = error.message || "채팅 내역을 불러오지 못했습니다."; + toast.error(errorMessage); + + // 404 에러인 경우 채팅 페이지로 이동 + if (errorMessage.includes("존재하지 않는") || errorMessage.includes("404")) { navigate("/chat"); - } finally { - setIsLoadingMessages(false); + return; } - }; - + + // 나머지 에러는 토스트 메시지만 표시하고 채팅 페이지로 이동 + navigate("/chat"); + } finally { + setIsLoadingMessages(false); + } + }; + + // 채팅방 내역 조회 + useEffect(() => { fetchChatMessages(); }, [roomId, isAuthenticated, currentUserId, navigate]); @@ -194,21 +196,21 @@ function ChatRoomPage() { // tradeStatus 가져오기 const tradeStatus = chatData?.clothesInfo?.tradeStatus; + // orderStatus 가져오기 (chatData에서도 확인) + const currentOrderStatus = chatData?.orderStatus || orderStatus; - // tradeStatus에 따른 입력창 비활성화 - // BUYER이고 REQUESTED 상태일 때, 또는 SELLER이고 REQUESTED 상태일 때 비활성화 - const isInputDisabled = - (isBuyer && tradeStatus === "REQUESTED") || - (isSeller && tradeStatus === "REQUESTED") || - orderStatus === "REQUESTED"; + // orderStatus에 따른 입력창 비활성화 + // orderStatus가 REQUESTED일 때 비활성화 + const isInputDisabled = currentOrderStatus === "REQUESTED"; // 구매자 버튼 상태 (BUYER일 때만) const getBuyerButtonState = () => { if (!isBuyer) return null; - const isPaid = chatData?.clothesInfo?.isPaid; + // isPaid 확인 (여러 경로에서 확인) + const isPaid = chatData?.clothesInfo?.isPaid || chatData?.isPaid; - switch (tradeStatus) { + switch (currentOrderStatus) { case "REQUESTED": return { disabled: true, @@ -217,7 +219,7 @@ function ChatRoomPage() { cursor: "cursor-not-allowed", }; case "MATCHED": - // MATCHED 상태에서 isPaid가 true면 비활성화 + // MATCHED 상태에서 isPaid가 true면 비활성화하고 --main-color 사용 if (isPaid) { return { disabled: true, @@ -323,16 +325,9 @@ function ChatRoomPage() { try { const paymentResult = await payOrder(orderId, usedPoints); - // 결제 성공 시 chatData 업데이트 + // 결제 성공 시 채팅방 데이터 다시 가져오기 if (paymentResult.isPaid) { - setChatData((prev) => ({ - ...prev, - clothesInfo: { - ...prev.clothesInfo, - isPaid: true, - }, - })); - + await fetchChatMessages(); toast.success("결제가 완료되었습니다."); } } catch (error) { @@ -353,16 +348,9 @@ function ChatRoomPage() { try { const acceptResult = await acceptOrder(orderId); - // 수락 성공 시 chatData 업데이트 (status를 MATCHED로 변경) + // 수락 성공 시 채팅방 데이터 다시 가져오기 if (acceptResult.status === "MATCHED") { - setChatData((prev) => ({ - ...prev, - clothesInfo: { - ...prev.clothesInfo, - tradeStatus: "MATCHED", - }, - })); - + await fetchChatMessages(); toast.success("구매 요청을 수락했습니다. 이제 대화할 수 있습니다."); } } catch (error) { @@ -467,8 +455,8 @@ function ChatRoomPage() { > 결제하기 - {/* 구매 취소 버튼 (REQUESTED, MATCHED, SELLING 상태일 때만 표시) */} - {tradeStatus && (tradeStatus === "REQUESTED" || tradeStatus === "MATCHED" || tradeStatus === "SELLING") && ( + {/* 구매 취소 버튼 (REQUESTED, MATCHED 상태일 때만 표시) */} + {currentOrderStatus && (currentOrderStatus === "REQUESTED" || currentOrderStatus === "MATCHED") && ( )} - {/* 물품 전달 완료 버튼 (매칭확정 상태이고 결제 완료된 경우만 표시) */} - {order.status === "MATCHED" && order.isPaid && ( + {/* 물품 전달 완료 버튼 (판매중, 판매완료 상태가 아닐 때만 표시) */} + {order.status !== "ON_SALE" && order.status !== "COMPLETED" && ( diff --git a/src/pages/category/CategoryResultPage.jsx b/src/pages/category/CategoryResultPage.jsx index 23d8332..62a85e7 100644 --- a/src/pages/category/CategoryResultPage.jsx +++ b/src/pages/category/CategoryResultPage.jsx @@ -1,6 +1,7 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { useRequireAuth } from "../../hooks/useRequireAuth.js"; +import { MdArrowBackIosNew } from "react-icons/md"; import { getClothesByCategory } from "../../api/clothesApi.js"; function CategoryResultPage() { @@ -154,9 +155,9 @@ function CategoryResultPage() {
diff --git a/src/utils/formatters.js b/src/utils/formatters.js index 9871810..5d6a68e 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -48,3 +48,39 @@ export function formatPrice(price) { return numPrice.toLocaleString("ko-KR"); } +/** + * 채팅 메시지 시간 포맷팅 유틸리티 + * 시간을 "오후 2:30" 형식으로 변환 (한국 시간대) + * @param {string|Date|null|undefined} dateString - 포맷팅할 날짜 문자열 또는 Date 객체 + * @returns {string} 포맷팅된 시간 문자열 (예: "오후 2:30") + */ +export function formatChatTime(dateString) { + if (!dateString) return ""; + + let date; + if (typeof dateString === 'string') { + date = new Date(dateString); + } else if (dateString instanceof Date) { + date = dateString; + } else { + return ""; + } + + // 유효하지 않은 날짜 체크 + if (isNaN(date.getTime())) { + return ""; + } + + // 한국 시간대로 변환 (Asia/Seoul, UTC+9) + // toLocaleString으로 한국 시간대의 시간 정보를 가져옴 + const options = { + timeZone: "Asia/Seoul", + hour: "numeric", + minute: "2-digit", + hour12: true + }; + + // "오전 1:46" 또는 "오후 2:30" 형식으로 자동 변환됨 + return date.toLocaleString("ko-KR", options); +} +