From 332344ae037c4a208665fcba7829a6bfb0576fd6 Mon Sep 17 00:00:00 2001 From: Taewoo Park Date: Fri, 5 Dec 2025 16:13:24 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20chatListSocket=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=86=8C=EC=BC=93=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/chat/lib/useChatSocket.ts | 4 +- src/entities/chat/model/chatSocket.ts | 76 ++++++++ src/entities/chat/model/socket.ts | 202 +++++++++------------- src/features/chat/model/chatListSocket.ts | 73 ++++++++ 4 files changed, 235 insertions(+), 120 deletions(-) create mode 100644 src/entities/chat/model/chatSocket.ts create mode 100644 src/features/chat/model/chatListSocket.ts diff --git a/src/entities/chat/lib/useChatSocket.ts b/src/entities/chat/lib/useChatSocket.ts index 87e00b7e..2e6e14b5 100644 --- a/src/entities/chat/lib/useChatSocket.ts +++ b/src/entities/chat/lib/useChatSocket.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from "react"; -import { ChatSocket } from "../model/socket"; +import { ChatSocket } from "../model/chatSocket"; import { DealStatus, MessageProps } from "../model/types"; import { PostStatus } from "@/entities/post/model/types/post"; @@ -86,7 +86,7 @@ export const useChatSocket = ( if (chatId) connectSocket(); return () => { - socketRef.current?.leaveRoom(); + socketRef.current?.close(); socketRef.current = null; }; }, [chatId]); diff --git a/src/entities/chat/model/chatSocket.ts b/src/entities/chat/model/chatSocket.ts new file mode 100644 index 00000000..453bb842 --- /dev/null +++ b/src/entities/chat/model/chatSocket.ts @@ -0,0 +1,76 @@ +import { Socket, SocketEvents } from "./socket"; +import type { DealStatus, MessageProps } from "./types"; +import type { PostStatus } from "@/entities/post/model/types/post"; + +export interface ChatSocketEvents extends SocketEvents { + onMessage?: (message: MessageProps) => void; + onSystem?: (system: { type: string; message: string }) => void; + onDealUpdate?: (update: { + postStatus: PostStatus; + dealStatus: DealStatus; + message: string; + }) => void; +} + +export class ChatSocket extends Socket { + private chatId: number; + + constructor(chatId: number, events: ChatSocketEvents = {}) { + super(events); + this.chatId = chatId; + } + + protected getEndpointPath(): string { + return `/ws/chat/${this.chatId}`; + } + + protected getDebugName(): string { + return `ChatSocket (ID: ${this.chatId})`; + } + + protected getCloseCodeName(): string { + return "leave_room"; // 개별 채팅방 종료 이벤트 이름 + } + + // 개별 채팅방 고유의 메시지 처리 로직 구현 + protected handleMessage(event: MessageEvent): void { + try { + const data = JSON.parse(event.data); + + if (data.type === "deal_update") { + this.events.onDealUpdate?.(data); + return; + } + + if (["welcome", "system", "read"].includes(data.type)) { + this.events.onSystem?.(data); + return; + } + + if (data.messageId && data.content) { + // 메시지 수신 로직 + const msg: MessageProps = { + messageId: data.messageId, + type: data.type, + content: data.content, + isMine: false, + sendAt: data.createdAt, + isRead: false, + }; + this.events.onMessage?.(msg); + } + } catch (err) { + console.error("[ChatSocket] Message parse error:", err); + } + } + + public sendMessage(type: "text" | "image", content: string) { + if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { + console.warn("[ChatSocket] Not connected"); + return; + } + + const payload = { event: "send_message", type, content }; + this.socket.send(JSON.stringify(payload)); + } +} diff --git a/src/entities/chat/model/socket.ts b/src/entities/chat/model/socket.ts index bc4e1277..b81fadd8 100644 --- a/src/entities/chat/model/socket.ts +++ b/src/entities/chat/model/socket.ts @@ -1,166 +1,132 @@ -import { DealStatus, MessageProps } from "./types"; import { useAuthStore } from "@/features/auth/model/auth.store"; -import { PostStatus } from "@/entities/post/model/types/post"; import { AuthorizationError } from "@/shared/error/error"; import { handleError } from "@/shared/error/handleError"; -export interface ChatSocketEvents { +// 모든 소켓 이벤트가 상속받을 기본 인터페이스 +export interface SocketEvents { onOpen?: () => void; - onMessage?: (message: MessageProps) => void; - onSystem?: (system: { type: string; message: string }) => void; - onDealUpdate?: (update: { - postStatus: PostStatus; - dealStatus: DealStatus; - message: string; - }) => void; - onClose?: (code: number, reason?: string) => void; onError?: (event: Event) => void; + onClose?: (code: number, reason?: string) => void; } -export class ChatSocket { - private socket: WebSocket | null = null; - private chatId: number; - private events: ChatSocketEvents; +// 각 소켓 클래스가 구현해야 하는 이벤트 핸들러 +export abstract class Socket { + protected socket: WebSocket | null = null; + protected events: Events; - constructor(chatId: number, events: ChatSocketEvents = {}) { - this.chatId = chatId; + constructor(events: Events) { this.events = events; } - isOpen(): boolean { + // 서브클래스에서 구현해야 하는 추상 메서드 + protected abstract getEndpointPath(): string; + protected abstract handleMessage(event: MessageEvent): void; + protected abstract getDebugName(): string; + protected abstract getCloseCodeName(): string; + + public isOpen(): boolean { return this.socket !== null && this.socket.readyState === WebSocket.OPEN; } - connect(): Promise { + private getWsUrl(): string { + const { accessToken } = useAuthStore.getState(); + const path = this.getEndpointPath(); + const baseUrl = process.env.NEXT_PUBLIC_API_WS_URL || "ws://localhost:8000"; + + // 쿼리 파라미터로 accessToken을 추가하는 공통 로직 + return `${baseUrl}${path}?token=${accessToken}`; + } + + public connect(): Promise { return new Promise((resolve, reject) => { + const debugName = this.getDebugName(); + if (this.socket) { - console.warn("[Socket] Already connected"); + console.warn(`[${debugName}] Already connected`); resolve(); return; } - const { accessToken } = useAuthStore.getState(); - const wsUrl = `${ - process.env.NEXT_PUBLIC_API_WS_URL || "ws://localhost:8000" - }/ws/chat/${this.chatId}?token=${accessToken}`; - - this.socket = new WebSocket(wsUrl); + try { + this.socket = new WebSocket(this.getWsUrl()); + } catch (e) { + console.error(`[${debugName}] Failed to create WebSocket URL.`); + reject(e); + return; + } this.socket.onopen = () => { - console.log("[Socket] Connected"); + console.log(`[${debugName}] Connected`); this.events.onOpen?.(); resolve(); }; - this.socket.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - if (data.type === "deal_update") { - this.events.onDealUpdate?.({ - dealStatus: data.dealStatus, - postStatus: data.postStatus, - message: data.systemMessage, - }); - return; - } - - if (["welcome", "system", "read"].includes(data.type)) { - this.events.onSystem?.(data); - return; - } - - if (data.messageId && data.content) { - const msg: MessageProps = { - messageId: data.messageId, - type: data.type, - content: data.content, - isMine: false, - sendAt: data.createdAt, - isRead: false, - }; - this.events.onMessage?.(msg); - } - } catch (err) { - console.error("[Socket] Message parse error:", err); - } - }; - - this.socket.onclose = async (event) => { - if (event.code === 4001) { - console.warn("[Socket] Token expired (4001)"); - - const { logout, setAccessToken } = useAuthStore.getState(); - - try { - const refreshed = await fetch("/api/auth/refresh", { - method: "POST", - credentials: "include", - }); - - if (!refreshed.ok) { - logout(); + this.socket.onmessage = (event) => this.handleMessage(event); - throw new AuthorizationError( - "세션이 만료되었습니다.\n다시 로그인 해주세요.", - ); - } - - const { accessToken: newToken } = await refreshed.json(); - setAccessToken(newToken); - - this.reconnect(); - } catch (err) { - handleError(err); - logout(); - return; - } - } - - console.warn("[Socket] Closed:", event.code); - this.events.onClose?.(event.code, event.reason); - this.socket = null; - }; + this.socket.onclose = (event) => this.handleClose(event); this.socket.onerror = (err) => { - console.error("[Socket] Error:", err); + console.error(`[${debugName}] Error:`, err); this.events.onError?.(err); reject(err); }; }); } - private reconnect() { - console.log("[Socket] Reconnecting after refresh…"); - this.socket = null; - this.connect(); - } + // 토큰 만료 시 재연결 로직 (모든 소켓에 공통) + protected async handleClose(event: CloseEvent) { + const debugName = this.getDebugName(); + + if (event.code === 4001) { + console.warn(`[${debugName}] Token expired (4001). Attempting refresh.`); + const { logout, setAccessToken } = useAuthStore.getState(); + + try { + const refreshed = await fetch("/api/auth/refresh", { + method: "POST", + credentials: "include", + }); + + if (!refreshed.ok) { + logout(); + throw new AuthorizationError( + "세션이 만료되었습니다.\\n다시 로그인 해주세요.", + ); + } - sendMessage(type: "text" | "image", content: string) { - if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { - console.warn("[Socket] Not connected"); - return; + const { accessToken: newToken } = await refreshed.json(); + setAccessToken(newToken); + this.reconnect(); // 갱신 성공 시 재연결 + } catch (err) { + handleError(err); + logout(); + return; + } } - const payload = { - event: "send_message", - type, - content, - }; + console.warn(`[${debugName}] Closed:`, event.code); + this.events.onClose?.(event.code, event.reason); + this.socket = null; + } - this.socket.send(JSON.stringify(payload)); + protected reconnect() { + console.log(`[${this.getDebugName()}] Reconnecting after refresh…`); + this.socket = null; + this.connect(); } - leaveRoom() { + public close() { if (!this.socket) return; - const state = this.socket.readyState; - - if (state === WebSocket.OPEN) { - this.socket.send(JSON.stringify({ event: "leave_room" })); - this.socket.close(1000, "User left"); + // 서브클래스에서 정의한 종료 이벤트 이름 사용 + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify({ event: this.getCloseCodeName() })); + this.socket.close(1000, `User left ${this.getDebugName()}`); } else { - this.socket.close(1000, `User left skipped - ${state.toString()}`); + this.socket.close( + 1000, + `User left skipped - ${this.socket.readyState.toString()}`, + ); } this.socket = null; diff --git a/src/features/chat/model/chatListSocket.ts b/src/features/chat/model/chatListSocket.ts new file mode 100644 index 00000000..63165d79 --- /dev/null +++ b/src/features/chat/model/chatListSocket.ts @@ -0,0 +1,73 @@ +// src/features/chat/model/ChatListSocket.ts + +import { Socket, SocketEvents } from "./socket"; +import type { Chat, MessageProps } from "./types"; + +// ChatListSocket 고유의 이벤트 확장 +interface ChatListUpdatePayload { + chatId: number; + lastMessage: MessageProps; +} + +export interface ChatListSocketEvents extends SocketEvents { + onChatCreated?: (chat: Chat) => void; + onChatListUpdate?: (update: ChatListUpdatePayload) => void; + onSystem?: (system: { type: string; message: string }) => void; +} + +export class ChatListSocket extends Socket { + constructor(events: ChatListSocketEvents = {}) { + super(events); + } + + protected getEndpointPath(): string { + return "/ws/chat-list"; + } + + protected getDebugName(): string { + return "ChatListSocket"; + } + + protected getCloseCodeName(): string { + return "leave_chat_list"; // 채팅 목록 종료 이벤트 이름 + } + + // 💡 SocketBase의 connect()를 오버라이드하여 연결 성공 후 join_chat_list 이벤트 전송 + public override connect(): Promise { + return super.connect().then(() => { + if (this.socket?.readyState === WebSocket.OPEN) { + // 상위 connect()가 성공하면 join 이벤트 전송 + this.socket.send(JSON.stringify({ event: "join_chat_list" })); + } + }); + } + + // 💡 채팅 목록 고유의 메시지 처리 로직 구현 (서버 명세 기반) + protected handleMessage(event: MessageEvent): void { + try { + const data = JSON.parse(event.data); + + switch (data.event) { + case "chat_created": + // 새로운 채팅방 생성 알림 + this.events.onChatCreated?.(data); + break; + case "chat_list_update": + // 기존 채팅방의 메시지 업데이트 알림 + this.events.onChatListUpdate?.(data); + break; + case "system_message": + // 서버 시스템 알림 + this.events.onSystem?.(data); + break; + case "error": + // 서버에서 명시적으로 에러를 보낼 경우 + console.error(`[${this.getDebugName()}] Server Error:`, data); + this.events.onError?.(new Error(data.message) as unknown as Event); // Event 타입으로 변환 필요 + break; + } + } catch (err) { + console.error(`[${this.getDebugName()}] Message parse error:`, err); + } + } +} From 552448381fc3f06f6b0cadbc1c5c6bcf0e003cb7 Mon Sep 17 00:00:00 2001 From: Taewoo Park Date: Fri, 5 Dec 2025 16:15:06 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20chatListSocket=EC=9D=84=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20=EC=B1=84=ED=8C=85=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/chat/lib/useChatListSocket.ts | 63 ++++++++++++++++++++++ src/features/chat/ui/ChatList.tsx | 45 +++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/features/chat/lib/useChatListSocket.ts diff --git a/src/features/chat/lib/useChatListSocket.ts b/src/features/chat/lib/useChatListSocket.ts new file mode 100644 index 00000000..0e71a9de --- /dev/null +++ b/src/features/chat/lib/useChatListSocket.ts @@ -0,0 +1,63 @@ +// src/features/chat/hooks/useChatListSocket.ts + +import { useEffect, useRef } from "react"; +import { ChatListSocket } from "../model/chatListSocket"; +import { handleError } from "@/shared/error/handleError"; +import { Chat, MessageProps } from "@/entities/chat/model/types"; +interface UseChatListSocketProps { + onChatCreated: (chat: Chat) => void; + onChatListUpdate: (update: { + chatId: number; + lastMessage: MessageProps; + }) => void; +} + +export const useChatListSocket = ({ + onChatCreated, + onChatListUpdate, +}: UseChatListSocketProps) => { + const socketRef = useRef(null); + + const connectListSocket = async () => { + if (socketRef.current?.isOpen()) return; + + if (!socketRef.current) { + socketRef.current = new ChatListSocket({ + onOpen: () => { + console.log("[useChatListSocket] List Socket Ready."); + }, + onChatCreated: onChatCreated, + onChatListUpdate: onChatListUpdate, + onClose: (code) => { + console.log(`[useChatListSocket] Closed: ${code}`); + }, + }); + } + + try { + await socketRef.current.connect(); + } catch (error) { + // 초기 연결 시 발생하는 인증/네트워크 에러 처리 + console.error("[useChatListSocket] Initial connection failed:", error); + onConnectionError?.(error); + } + }; + + useEffect(() => { + // 컴포넌트 마운트 시 소켓 연결 시도 + connectListSocket(); + + return () => { + // 컴포넌트 언마운트 시 소켓 연결 해제 + socketRef.current?.close(); + socketRef.current = null; + }; + }, []); // 💡 의존성 배열을 비워 마운트/언마운트 시점에만 실행되도록 보장 + + const isConnected = socketRef.current?.isOpen(); + + return { + isConnected, + // 필요하다면 수동 연결/해제 함수도 반환 가능 + }; +}; diff --git a/src/features/chat/ui/ChatList.tsx b/src/features/chat/ui/ChatList.tsx index 437c6b1b..39e19288 100644 --- a/src/features/chat/ui/ChatList.tsx +++ b/src/features/chat/ui/ChatList.tsx @@ -2,10 +2,11 @@ import ChatItem from "@/entities/chat/ui/ChatItem"; import { fetchChatList } from "../model/chat.api"; -import type { Chat } from "@/entities/chat/model/types"; -import { useQuery } from "@tanstack/react-query"; +import type { Chat, MessageProps } from "@/entities/chat/model/types"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { DealStatus } from "@/entities/chat/model/types"; import { handleError } from "@/shared/error/handleError"; +import { useChatListSocket } from "../lib/useChatListSocket"; interface ChatListProps { onSelect: (info: { @@ -18,7 +19,9 @@ interface ChatListProps { } const ChatList = ({ onSelect, tab = "all" }: ChatListProps) => { + const queryClient = useQueryClient(); const role = tab === "all" ? undefined : tab === "buyer" ? "buyer" : "seller"; + const queryKey = ["chats", role]; const { data: chats = [], @@ -30,6 +33,44 @@ const ChatList = ({ onSelect, tab = "all" }: ChatListProps) => { queryFn: () => fetchChatList(role), }); + const handleChatCreated = (newChat: Chat) => { + const roles = [newChat.role, undefined]; + roles.forEach((updatedRole) => { + queryClient.setQueryData(["chats", updatedRole], (oldChats) => { + if (!oldChats) return [newChat]; + return [newChat, ...oldChats]; + }); + }); + }; + + const handleChatListUpdated = (update: { + chatId: number; + lastMessage: MessageProps; + }) => { + const roles = [undefined, "buyer", "seller"]; + roles.forEach((updatedRole) => { + const queryKey = ["chats", updatedRole]; + queryClient.setQueryData(queryKey, (oldChats: Chat[] | undefined) => { + if (!oldChats) return undefined; + + const updatedChatIndex = oldChats.findIndex( + (chat) => chat.chatId === update.chatId, + ); + if (updatedChatIndex === -1) return oldChats; + + const updatedChat: Chat = { + ...oldChats[updatedChatIndex], + lastMessage: { + ...update.lastMessage, + }, + }; + const remainChats = oldChats.filter( + (_, index) => index !== updatedChatIndex, + ); + return [updatedChat, ...remainChats]; + }); + }); + }; if (isError) { handleError(error); } From 8d08e8e674bb1778164a4a523b18de4ebf704763 Mon Sep 17 00:00:00 2001 From: Taewoo Park Date: Mon, 8 Dec 2025 17:12:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=86=8C=EC=BC=93=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/chat/lib/useChatListSocket.ts | 8 ++--- src/features/chat/model/chatListSocket.ts | 34 +++++----------------- src/features/chat/ui/ChatList.tsx | 5 ++++ 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/features/chat/lib/useChatListSocket.ts b/src/features/chat/lib/useChatListSocket.ts index 0e71a9de..0bbe2131 100644 --- a/src/features/chat/lib/useChatListSocket.ts +++ b/src/features/chat/lib/useChatListSocket.ts @@ -37,27 +37,23 @@ export const useChatListSocket = ({ try { await socketRef.current.connect(); } catch (error) { - // 초기 연결 시 발생하는 인증/네트워크 에러 처리 console.error("[useChatListSocket] Initial connection failed:", error); - onConnectionError?.(error); + handleError(error); } }; useEffect(() => { - // 컴포넌트 마운트 시 소켓 연결 시도 connectListSocket(); return () => { - // 컴포넌트 언마운트 시 소켓 연결 해제 socketRef.current?.close(); socketRef.current = null; }; - }, []); // 💡 의존성 배열을 비워 마운트/언마운트 시점에만 실행되도록 보장 + }, []); const isConnected = socketRef.current?.isOpen(); return { isConnected, - // 필요하다면 수동 연결/해제 함수도 반환 가능 }; }; diff --git a/src/features/chat/model/chatListSocket.ts b/src/features/chat/model/chatListSocket.ts index 63165d79..5c974899 100644 --- a/src/features/chat/model/chatListSocket.ts +++ b/src/features/chat/model/chatListSocket.ts @@ -1,9 +1,6 @@ -// src/features/chat/model/ChatListSocket.ts +import { Socket, SocketEvents } from "../../../entities/chat/model/socket"; +import type { Chat, MessageProps } from "../../../entities/chat/model/types"; -import { Socket, SocketEvents } from "./socket"; -import type { Chat, MessageProps } from "./types"; - -// ChatListSocket 고유의 이벤트 확장 interface ChatListUpdatePayload { chatId: number; lastMessage: MessageProps; @@ -32,39 +29,22 @@ export class ChatListSocket extends Socket { return "leave_chat_list"; // 채팅 목록 종료 이벤트 이름 } - // 💡 SocketBase의 connect()를 오버라이드하여 연결 성공 후 join_chat_list 이벤트 전송 - public override connect(): Promise { - return super.connect().then(() => { - if (this.socket?.readyState === WebSocket.OPEN) { - // 상위 connect()가 성공하면 join 이벤트 전송 - this.socket.send(JSON.stringify({ event: "join_chat_list" })); - } - }); - } - - // 💡 채팅 목록 고유의 메시지 처리 로직 구현 (서버 명세 기반) protected handleMessage(event: MessageEvent): void { try { const data = JSON.parse(event.data); - + console.log( + `event : ${JSON.stringify(event)}, data : ${JSON.stringify(data)} 호출`, + ); switch (data.event) { case "chat_created": - // 새로운 채팅방 생성 알림 - this.events.onChatCreated?.(data); + this.events.onChatCreated?.(data.payload); break; case "chat_list_update": - // 기존 채팅방의 메시지 업데이트 알림 - this.events.onChatListUpdate?.(data); + this.events.onChatListUpdate?.(data.payload); break; case "system_message": - // 서버 시스템 알림 this.events.onSystem?.(data); break; - case "error": - // 서버에서 명시적으로 에러를 보낼 경우 - console.error(`[${this.getDebugName()}] Server Error:`, data); - this.events.onError?.(new Error(data.message) as unknown as Event); // Event 타입으로 변환 필요 - break; } } catch (err) { console.error(`[${this.getDebugName()}] Message parse error:`, err); diff --git a/src/features/chat/ui/ChatList.tsx b/src/features/chat/ui/ChatList.tsx index 39e19288..5972a9a6 100644 --- a/src/features/chat/ui/ChatList.tsx +++ b/src/features/chat/ui/ChatList.tsx @@ -71,6 +71,11 @@ const ChatList = ({ onSelect, tab = "all" }: ChatListProps) => { }); }); }; + + useChatListSocket({ + onChatCreated: handleChatCreated, + onChatListUpdate: handleChatListUpdated, + }); if (isError) { handleError(error); } From 0d2b750a100bc901be6b80bd6d5e9d6c907da138 Mon Sep 17 00:00:00 2001 From: Taewoo Park Date: Mon, 8 Dec 2025 17:46:24 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20next=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33db738b..6b23f486 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "cookie": "^1.0.2", "date-fns": "^4.1.0", "mock-socket": "^9.3.1", - "next": "15.5.3", + "next": "^16.0.7", "react": "19.1.0", "react-dom": "19.1.0", "swiper": "^12.0.2",