diff --git a/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts b/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts
index 0a252b183..5135715c9 100644
--- a/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts
+++ b/src/components/Articles/LostItemChatPage/components/DeleteModal/hooks/useBlockLostItemChatroom.ts
@@ -1,7 +1,7 @@
import { useRouter } from 'next/router';
import { isKoinError, sendClientError } from '@bcsdlab/koin';
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { postBlockLostItemChatroom } from 'api/articles';
+import { articleMutations } from 'api/articles/mutations';
import ROUTES from 'static/routes';
import useTokenState from 'utils/hooks/state/useTokenState';
import showToast from 'utils/ts/showToast';
@@ -10,12 +10,12 @@ const useDeleteLostItemChatroom = () => {
const token = useTokenState();
const queryClient = useQueryClient();
const router = useRouter();
+ const mutation = articleMutations.blockLostItemChatroom(queryClient, token);
const { mutate } = useMutation({
- mutationFn: ({ articleId, chatroomId }: { articleId: number; chatroomId: number }) =>
- postBlockLostItemChatroom(token, articleId, chatroomId),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['chatroom'] });
+ ...mutation,
+ onSuccess: async (...args) => {
+ await mutation.onSuccess?.(...args);
showToast('success', '채팅방이 차단되었습니다.');
router.push(ROUTES.LostItemChat());
},
diff --git a/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts b/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts
new file mode 100644
index 000000000..9dab73860
--- /dev/null
+++ b/src/components/Articles/LostItemChatPage/hooks/useChatPolling.ts
@@ -0,0 +1,197 @@
+import { useCallback, useEffect, useRef } from 'react';
+import { isKoinError, sendClientError } from '@bcsdlab/koin';
+import {
+ keepPreviousData,
+ skipToken,
+ useMutation,
+ useQuery,
+ useQueryClient,
+ useSuspenseQuery,
+} from '@tanstack/react-query';
+import { postLeaveLostItemChatroomV2, postLostItemChatroomMessageV2 } from 'api/articles';
+import { articleQueries, articleQueryKeys } from 'api/articles/queries';
+import { getCachedMessages, cacheMessages, clearChatroomCache } from 'utils/db/chatDB';
+import showToast from 'utils/ts/showToast';
+
+const POLLING_INTERVAL_MS = 10_000;
+
+interface UseChatPollingOptions {
+ token: string;
+ articleId: number | string | null;
+ chatroomId: number | string | null;
+ isOnline?: boolean;
+}
+
+const useChatPolling = ({ token, articleId, chatroomId, isOnline = true }: UseChatPollingOptions) => {
+ const queryClient = useQueryClient();
+
+ const { data: chatroomList } = useSuspenseQuery({
+ ...articleQueries.lostItemChatroomList(token),
+ staleTime: isOnline ? 0 : Infinity,
+ refetchInterval: isOnline ? POLLING_INTERVAL_MS : false,
+ refetchIntervalInBackground: false,
+ });
+
+ const defaultChatroomId = chatroomId ?? chatroomList?.[0]?.chat_room_id ?? null;
+ const defaultArticleId = articleId ?? chatroomList?.[0]?.article_id ?? null;
+
+ const numericArticleId = defaultArticleId != null ? Number(defaultArticleId) : null;
+ const numericChatroomId = defaultChatroomId != null ? Number(defaultChatroomId) : null;
+
+ // indexedDB 진입
+ useEffect(() => {
+ if (numericArticleId == null || numericChatroomId == null) return;
+
+ const queryKey = articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId);
+ const existing = queryClient.getQueryData(queryKey);
+ if (existing) return;
+
+ getCachedMessages(numericArticleId, numericChatroomId).then((cached) => {
+ if (cached && cached.length > 0) {
+ queryClient.setQueryData(queryKey, cached);
+ }
+ });
+ }, [queryClient, numericArticleId, numericChatroomId, defaultArticleId, defaultChatroomId]);
+
+ const { data: chatroomDetail } = useQuery({
+ ...(defaultArticleId && defaultChatroomId && isOnline
+ ? articleQueries.lostItemChatroomDetail(token, Number(defaultArticleId), Number(defaultChatroomId))
+ : {
+ queryKey: articleQueryKeys.lostItemChatroomDetail(defaultArticleId, defaultChatroomId),
+ queryFn: skipToken,
+ }),
+ placeholderData: keepPreviousData,
+ });
+
+ const { data: messages } = useQuery({
+ ...(defaultArticleId && defaultChatroomId && isOnline
+ ? articleQueries.lostItemChatroomMessages(token, Number(defaultArticleId), Number(defaultChatroomId))
+ : {
+ queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId),
+ queryFn: skipToken,
+ }),
+ placeholderData: keepPreviousData,
+ refetchInterval: isOnline && defaultArticleId && defaultChatroomId ? POLLING_INTERVAL_MS : false,
+ refetchIntervalInBackground: false,
+ });
+
+ // 폴링 응답 도착 시 indexedDB 캐시
+ useEffect(() => {
+ if (messages && messages.length > 0 && numericArticleId != null && numericChatroomId != null) {
+ cacheMessages(numericArticleId, numericChatroomId, messages);
+ }
+ }, [messages, numericArticleId, numericChatroomId]);
+
+ const { mutate: sendMessage } = useMutation({
+ mutationFn: ({ content, isImage = false }: { content: string; isImage?: boolean }) => {
+ if (defaultArticleId == null || defaultChatroomId == null) {
+ return Promise.reject(new Error('채팅방 정보가 없습니다.'));
+ }
+
+ return postLostItemChatroomMessageV2(token, Number(defaultArticleId), Number(defaultChatroomId), {
+ content,
+ is_image: isImage,
+ });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId),
+ });
+ },
+ onError: (error) => {
+ if (isKoinError(error)) {
+ showToast('error', error.message || '메시지 전송을 실패하였습니다');
+ } else {
+ showToast('error', '메시지 전송을 실패하였습니다');
+ sendClientError(error);
+ }
+ },
+ });
+
+ const { mutateAsync: leaveChatroom } = useMutation({
+ mutationFn: () => {
+ if (defaultArticleId == null || defaultChatroomId == null) {
+ return Promise.reject(new Error('채팅방 정보가 없습니다.'));
+ }
+
+ return postLeaveLostItemChatroomV2(token, Number(defaultArticleId), Number(defaultChatroomId));
+ },
+ onSuccess: () => {
+ if (numericArticleId != null && numericChatroomId != null) {
+ clearChatroomCache(numericArticleId, numericChatroomId);
+ }
+ queryClient.invalidateQueries({
+ queryKey: articleQueryKeys.lostItemChatroomList,
+ });
+ },
+ onError: (error) => {
+ if (isKoinError(error)) {
+ showToast('error', error.message || '채팅방 퇴장을 실패하였습니다');
+ } else {
+ showToast('error', '채팅방 퇴장을 실패하였습니다');
+ sendClientError(error);
+ }
+ },
+ });
+
+ const leaveRoom = useCallback(
+ (aId: number, cId: number) => {
+ postLeaveLostItemChatroomV2(token, aId, cId).catch((error) => {
+ if (isKoinError(error)) {
+ showToast('error', error.message || '채팅방 퇴장을 실패하였습니다');
+ } else {
+ showToast('error', '채팅방 퇴장을 실패하였습니다');
+ sendClientError(error);
+ }
+ });
+ },
+ [token],
+ );
+
+ const prevRoomRef = useRef<{ articleId: number; chatroomId: number } | null>(null);
+
+ useEffect(() => {
+ if (numericArticleId != null && numericChatroomId != null) {
+ if (
+ prevRoomRef.current &&
+ (prevRoomRef.current.articleId !== numericArticleId || prevRoomRef.current.chatroomId !== numericChatroomId)
+ ) {
+ leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId);
+ }
+ prevRoomRef.current = { articleId: numericArticleId, chatroomId: numericChatroomId };
+ }
+
+ return () => {
+ if (prevRoomRef.current) {
+ leaveRoom(prevRoomRef.current.articleId, prevRoomRef.current.chatroomId);
+ prevRoomRef.current = null;
+ }
+ };
+ }, [numericArticleId, numericChatroomId, leaveRoom]);
+
+ const invalidateChatroomList = useCallback(() => {
+ queryClient.invalidateQueries({
+ queryKey: articleQueryKeys.lostItemChatroomList,
+ });
+ }, [queryClient]);
+
+ const invalidateMessages = useCallback(() => {
+ queryClient.invalidateQueries({
+ queryKey: articleQueryKeys.lostItemChatroomMessages(defaultArticleId, defaultChatroomId),
+ });
+ }, [queryClient, defaultArticleId, defaultChatroomId]);
+
+ return {
+ chatroomList,
+ chatroomDetail,
+ messages,
+ defaultChatroomId,
+ defaultArticleId,
+ sendMessage,
+ leaveChatroom,
+ invalidateChatroomList,
+ invalidateMessages,
+ };
+};
+
+export default useChatPolling;
diff --git a/src/components/Articles/LostItemChatPage/hooks/useChatroomQuery.ts b/src/components/Articles/LostItemChatPage/hooks/useChatroomQuery.ts
deleted file mode 100644
index 732ebce75..000000000
--- a/src/components/Articles/LostItemChatPage/hooks/useChatroomQuery.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useCallback } from 'react';
-import { keepPreviousData, skipToken, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
-import { getLostItemChatroomDetail, getLostItemChatroomDetailMessages, getLostItemChatroomList } from 'api/articles';
-
-const useChatroomQuery = (token: string, articleId: number | string | null, chatroomId: number | string | null) => {
- const queryClient = useQueryClient();
-
- const { data: chatroomList } = useSuspenseQuery({
- queryKey: ['chatroom', 'lost-item', 'list'],
- queryFn: () => getLostItemChatroomList(token),
- });
-
- const defaultChatroomId = chatroomId ?? chatroomList?.[0]?.chat_room_id ?? null;
- const defaultArticleId = articleId ?? chatroomList?.[0]?.article_id ?? null;
-
- const { data: chatroomDetail } = useQuery({
- queryKey: ['chatroom', 'lost-item', 'detail', defaultArticleId, defaultChatroomId],
- queryFn:
- defaultArticleId && defaultChatroomId
- ? () => getLostItemChatroomDetail(token, Number(defaultArticleId), Number(defaultChatroomId))
- : skipToken,
- placeholderData: keepPreviousData,
- });
-
- const { data: messages } = useQuery({
- queryKey: ['chatroom', 'lost-item', 'messages', defaultArticleId, defaultChatroomId],
- queryFn:
- defaultArticleId && defaultChatroomId
- ? () => getLostItemChatroomDetailMessages(token, Number(defaultArticleId), Number(defaultChatroomId))
- : skipToken,
- placeholderData: keepPreviousData,
- });
-
- const invalidateChatroomList = useCallback(() => {
- queryClient.invalidateQueries({
- queryKey: ['chatroom', 'lost-item', 'list'],
- });
- }, [queryClient]);
-
- return {
- chatroomList,
- chatroomDetail,
- messages,
- defaultChatroomId,
- defaultArticleId,
- invalidateChatroomList,
- };
-};
-
-export default useChatroomQuery;
diff --git a/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx b/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx
index 54eee14bc..de8dc7767 100644
--- a/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx
+++ b/src/components/Articles/LostItemDetailPage/components/DeleteModal/index.tsx
@@ -20,7 +20,7 @@ export default function DeleteModal({ articleId, closeDeleteModal }: DeleteModal
const router = useRouter();
const { logFindUserDeleteConfirmClick } = useArticlesLogger();
const { mutate: deleteArticle } = useDeleteLostItemArticle({
- onSuccess: () => router.replace(ROUTES.Articles()),
+ onSuccess: () => router.replace(ROUTES.LostItems()),
});
useEscapeKeyDown({ onEscape: closeDeleteModal });
diff --git a/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx b/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx
index 1ce0129d3..817997313 100644
--- a/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx
+++ b/src/components/Articles/LostItemDetailPage/components/DisplayImage/index.tsx
@@ -1,15 +1,15 @@
import { useState } from 'react';
import Image from 'next/image';
import { cn } from '@bcsdlab/utils';
+import { LostItemImageDTO } from 'api/articles/entity';
import ChevronLeft from 'assets/svg/Articles/chevron-left-circle.svg';
import ChevronRight from 'assets/svg/Articles/chevron-right-circle.svg';
import SelectedDotIcon from 'assets/svg/Articles/ellipse-blue.svg';
import NotSelectedDotIcon from 'assets/svg/Articles/ellipse-grey.svg';
-import { ArticleImage } from 'static/articles';
import styles from './DisplayImage.module.scss';
interface DisplayImageProps {
- images: ArticleImage[];
+ images: LostItemImageDTO[];
}
export default function DisplayImage({ images }: DisplayImageProps) {
@@ -24,7 +24,7 @@ export default function DisplayImage({ images }: DisplayImageProps) {
{images.length > 0 && (
-
+