From 6544f89cfc80edb9b5db17ed7420a951fb8983a9 Mon Sep 17 00:00:00 2001 From: dasosann Date: Thu, 14 May 2026 00:32:29 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/chat-list/_components/ScreenChatList.tsx | 274 +++++++++++++++ app/chat-list/page.tsx | 33 ++ .../[chatid]/_components/ScreenChatRoom.tsx | 326 ++++++++++++++++++ app/chat/[chatid]/page.tsx | 11 + app/main/_components/FloatingButton.tsx | 9 +- hooks/useChatRooms.ts | 33 ++ 6 files changed, 682 insertions(+), 4 deletions(-) create mode 100644 app/chat-list/_components/ScreenChatList.tsx create mode 100644 app/chat-list/page.tsx create mode 100644 app/chat/[chatid]/_components/ScreenChatRoom.tsx create mode 100644 app/chat/[chatid]/page.tsx create mode 100644 hooks/useChatRooms.ts diff --git a/app/chat-list/_components/ScreenChatList.tsx b/app/chat-list/_components/ScreenChatList.tsx new file mode 100644 index 0000000..b2ba6dd --- /dev/null +++ b/app/chat-list/_components/ScreenChatList.tsx @@ -0,0 +1,274 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { BackButton } from "@/components/ui/BackButton"; +import { useChatRooms } from "@/hooks/useChatRooms"; +import { useMemo } from "react"; + +type ChatListItem = { + id: string; + roomId: string; + name: string; + detail: string; + preview: string; + time: string; + unread?: boolean; + avatar: string; +}; + +const inboxItems: ChatListItem[] = [ + { + id: "chat-1", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "22세, 정보통신전자공학부", + preview: + "안녕하세요! 오늘 시간 괜찮으세요? 혹시 축제 끝나고 같이 산책하면서 이야기 나눌 수 있을까요?", + time: "방금", + unread: true, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-2", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "22세, 정보통신전자공학부", + preview: + "혹시 축제 끝나고 커피 한 잔 하실래요? 요즘 좋아하는 음악이나 취미도 궁금해요!", + time: "방금", + unread: false, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-3", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "22세, 정보통신전자공학부", + preview: + "오늘 매칭 너무 반가웠어요 :) 혹시 시간 괜찮으시면 잠깐 만나서 인사하고 싶어요!", + time: "방금", + unread: true, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-4", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "23세, 컴퓨터공학과", + preview: "지금 어디에 계세요?", + time: "1분 전", + unread: false, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-5", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "21세, 심리학과", + preview: "공연 같이 보실래요?", + time: "3분 전", + unread: false, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-6", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "24세, 경영학과", + preview: "인사 늦어서 미안해요!", + time: "5분 전", + unread: true, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-7", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "22세, 전자공학과", + preview: "혹시 좋아하는 음악 장르 있어요?", + time: "8분 전", + unread: false, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-8", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "23세, 디자인학과", + preview: "지금 부스 앞에서 기다리고 있어요.", + time: "12분 전", + unread: false, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-9", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "21세, 사회학과", + preview: "요즘 관심사 뭐예요?", + time: "18분 전", + unread: false, + avatar: "/profile/default-profile.svg", + }, + { + id: "chat-10", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "24세, 기계공학과", + preview: "저도 그 동아리 관심 있어요!", + time: "25분 전", + unread: true, + avatar: "/profile/default-profile.svg", + }, +]; + +const pickedItems: ChatListItem[] = [ + { + id: "picked-1", + roomId: "6969e61b866d67f1c3b68106", + name: "username", + detail: "22세, 정보통신전자공학부", + preview: "아직 연락 안한 분들께 용기내어 연락해봐요!", + time: "방금", + unread: false, + avatar: "/profile/default-profile.svg", + }, +]; + +function ChatListRow({ item }: { item: ChatListItem }) { + return ( + +
+ {item.name} +
+ +
+
+ + {item.name} + + + {item.detail} + +
+ +
+ + {item.preview} + + + + {item.time} + +
+
+ + {/* */} diff --git a/app/main/_components/FooterButtonList.tsx b/app/main/_components/FooterButtonList.tsx index 8b81adf..7f0f057 100644 --- a/app/main/_components/FooterButtonList.tsx +++ b/app/main/_components/FooterButtonList.tsx @@ -1,8 +1,9 @@ -import React from "react"; -import { useRouter } from "next/navigation"; import Link from "next/link"; +import { useParticipantsCount } from "@/hooks/useParticipantsCount"; const MatchingButton = () => { + const { data: participantsCount } = useParticipantsCount(); + return ( { /> - 현재 740명 참여중이에요! + 현재{" "} + + {participantsCount?.data || 0}명 + {" "} + 참여중이에요! ); diff --git a/components/common/ChatSocketInitializer.tsx b/components/common/ChatSocketInitializer.tsx new file mode 100644 index 0000000..a9b7885 --- /dev/null +++ b/components/common/ChatSocketInitializer.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useEffect } from "react"; +import { useChatSocketStore } from "@/stores/chat-socket-store"; + +export default function ChatSocketInitializer() { + const connect = useChatSocketStore((state) => state.connect); + const disconnect = useChatSocketStore((state) => state.disconnect); + + useEffect(() => { + connect(); + return () => { + // 앱이 종료되거나 새로고침될 때 연결 해제 + // 사실 Root Layout에 두면 탭을 닫기 전까지는 유지됨 + disconnect(); + }; + }, [connect, disconnect]); + + return null; +} diff --git a/hooks/useChatMessages.ts b/hooks/useChatMessages.ts new file mode 100644 index 0000000..d71a5dd --- /dev/null +++ b/hooks/useChatMessages.ts @@ -0,0 +1,37 @@ +import { api } from "@/lib/axios"; +import { useQuery } from "@tanstack/react-query"; + +export type ChatMessagePayload = { + id: string; + roomId: string; + senderId: number; + content: string; + type: "TALK" | "ENTER" | "LEAVE" | "READ"; + createdAt: string; + readCount: number; +}; + +export type ChatMessagesResponse = { + code: string; + status: number; + message: string; + data: ChatMessagePayload[]; +}; + +export const fetchChatMessages = async ( + roomId: string, +): Promise => { + const { data } = await api.get( + `/api/chat/rooms/${roomId}/messages`, + ); + return data; +}; + +export const useChatMessages = (roomId: string) => { + return useQuery({ + queryKey: ["chatMessages", roomId], + queryFn: () => fetchChatMessages(roomId), + enabled: !!roomId, + staleTime: 1000 * 60, // 1분간 캐시 유지 + }); +}; diff --git a/hooks/useChatRoomSocket.ts b/hooks/useChatRoomSocket.ts new file mode 100644 index 0000000..d4e6c99 --- /dev/null +++ b/hooks/useChatRoomSocket.ts @@ -0,0 +1,96 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { useChatSocketStore } from "@/stores/chat-socket-store"; +import { IMessage } from "@stomp/stompjs"; + +export type ChatMessagePayload = { + id: string; + roomId: string; + senderId: number; + content: string; + type: "TALK" | "ENTER" | "LEAVE" | "READ"; + createdAt: string; + readCount: number; +}; + +export function useChatRoomSocket(roomId: string) { + const { client, status } = useChatSocketStore(); + const [messages, setMessages] = useState([]); + + const handleMessage = useCallback( + (message: IMessage) => { + try { + const payload: ChatMessagePayload = JSON.parse(message.body); + console.log(`[ChatRoom:${roomId}] New event received:`, payload); + + // TALK일 경우에만 메시지 리스트에 추가 (READ는 다른 로직 처리 가능) + if (payload.type === "TALK") { + setMessages((prev) => [...prev, payload]); + } + } catch (error) { + console.error("[ChatRoom] Failed to parse message:", error); + } + }, + [roomId], + ); + + const sendReadReceipt = useCallback(() => { + if (!client || !client.connected) return; + + client.publish({ + destination: "/app/chat/message", + body: JSON.stringify({ + roomId, + content: "", + type: "READ", + }), + }); + console.log(`[ChatRoom:${roomId}] Read receipt sent`); + }, [client, roomId]); + + useEffect(() => { + if (!client || status !== "connected" || !roomId) return; + + const destination = `/topic/chat.room.${roomId}`; + console.log(`[ChatRoom:${roomId}] Subscribing to ${destination}...`); + const subscription = client.subscribe(destination, handleMessage); + + // 구독 후 바로 읽음 처리 신호 전송 + sendReadReceipt(); + + return () => { + console.log(`[ChatRoom:${roomId}] Unsubscribing...`); + subscription.unsubscribe(); + }; + }, [client, status, roomId, handleMessage, sendReadReceipt]); + + const sendMessage = useCallback( + (content: string) => { + if (!client || !client.connected) { + console.error("[ChatRoom] Client not connected"); + return; + } + + const payload = { + roomId, + content, + type: "TALK", + }; + + client.publish({ + destination: "/app/chat/message", + body: JSON.stringify(payload), + }); + }, + [client, roomId], + ); + + return { + messages, + setMessages, + sendMessage, + sendReadReceipt, + isConnected: status === "connected", + }; +} diff --git a/hooks/useMatchingHistory.ts b/hooks/useMatchingHistory.ts index c52022a..3779297 100644 --- a/hooks/useMatchingHistory.ts +++ b/hooks/useMatchingHistory.ts @@ -3,7 +3,6 @@ import { Gender, MBTI, SocialType, - Hobby, ContactFrequency, } from "@/lib/types/profile"; import { diff --git a/hooks/useRequestStatus.ts b/hooks/useRequestStatus.ts index c1b703d..d0a8128 100644 --- a/hooks/useRequestStatus.ts +++ b/hooks/useRequestStatus.ts @@ -1,6 +1,6 @@ import { api } from "@/lib/axios"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import React, { useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { AxiosError } from "axios"; export interface RequestStatusResponse { diff --git a/stores/chat-socket-store.ts b/stores/chat-socket-store.ts new file mode 100644 index 0000000..201eef4 --- /dev/null +++ b/stores/chat-socket-store.ts @@ -0,0 +1,70 @@ +"use client"; + +import { create } from "zustand"; +import { Client } from "@stomp/stompjs"; +import SockJS from "sockjs-client"; + +type ConnectionStatus = "connected" | "disconnected" | "reconnecting"; + +interface ChatSocketStore { + client: Client | null; + status: ConnectionStatus; + connect: () => void; + disconnect: () => void; +} + +export const useChatSocketStore = create((set, get) => ({ + client: null, + status: "disconnected", + + connect: () => { + // 이미 연결 중이거나 연결된 경우 중복 실행 방지 + if (get().client?.active) return; + + const API_URL = process.env.NEXT_PUBLIC_API_URL; + if (!API_URL) { + console.error("[ChatSTOMP] NEXT_PUBLIC_API_URL is not defined"); + return; + } + + const wsUrl = `${API_URL}/ws/chat`; + + const client = new Client({ + webSocketFactory: () => new SockJS(wsUrl) as unknown as WebSocket, + reconnectDelay: 5000, + heartbeatIncoming: 10000, + heartbeatOutgoing: 10000, + + onConnect: () => { + console.log("✅ [ChatSTOMP] Connected (Zustand)"); + set({ status: "connected" }); + }, + + onDisconnect: () => { + console.log("🔌 [ChatSTOMP] Disconnected (Zustand)"); + set({ status: "disconnected" }); + }, + + onStompError: (frame) => { + console.error("❌ [ChatSTOMP] Error:", frame.headers?.message || frame); + set({ status: "disconnected" }); + }, + + onWebSocketClose: () => { + console.log("🔄 [ChatSTOMP] WebSocket closed"); + set({ status: "reconnecting" }); + }, + }); + + client.activate(); + set({ client }); + }, + + disconnect: () => { + const { client } = get(); + if (client) { + client.deactivate(); + set({ client: null, status: "disconnected" }); + } + }, +})); From 189006956f5d7f8233c9d15f690be5e42a44b017 Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 15 May 2026 13:42:27 +0900 Subject: [PATCH 03/12] =?UTF-8?q?fix=20build=EC=97=90=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[chatid]/_components/ScreenChatRoom.tsx | 21 +++++++++++-------- app/chat/[chatid]/page.tsx | 9 ++++---- app/main/_components/FooterButtonList.tsx | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/chat/[chatid]/_components/ScreenChatRoom.tsx b/app/chat/[chatid]/_components/ScreenChatRoom.tsx index 5cad9f3..6d68cba 100644 --- a/app/chat/[chatid]/_components/ScreenChatRoom.tsx +++ b/app/chat/[chatid]/_components/ScreenChatRoom.tsx @@ -6,6 +6,8 @@ import { useEffect, useMemo, useState } from "react"; import { ArrowUp, ChevronLeft, MoreVertical, UserRound } from "lucide-react"; import { useChatRoomSocket } from "@/hooks/useChatRoomSocket"; import { useChatMessages } from "@/hooks/useChatMessages"; +import { useMyProfile } from "@/hooks/useProfile"; +import { cn } from "@/lib/utils"; type ScreenChatRoomProps = { chatId: string; @@ -19,8 +21,6 @@ type ChatMessage = { readCount: number; }; -const currentUserId = 1; // TODO: 실제 로그인 사용자 ID로 변경 필요 - const formatMessageTime = (isoString: string) => { const parsed = new Date(isoString); if (Number.isNaN(parsed.getTime())) return ""; @@ -88,6 +88,10 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { const router = useRouter(); const [messageText, setMessageText] = useState(""); + // 0. 내 프로필 정보 가져오기 (현재 사용자 ID 확인용) + const { data: myProfile } = useMyProfile(); + const currentUserId = myProfile?.data.memberId; + // 1. 과거 대화 내역 가져오기 (API) const { data: historyData } = useChatMessages(chatId); @@ -114,7 +118,7 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { time: formatMessageTime(m.createdAt), readCount: m.readCount, })) as ChatMessage[]; - }, [historyData, socketMessages]); + }, [historyData, socketMessages, currentUserId]); const handleSendMessage = () => { if (!messageText.trim()) return; @@ -127,7 +131,6 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { }; const isSendEnabled = messageText.trim().length > 0; - const messages = allMessages; return (
@@ -197,12 +200,12 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { aria-label="메시지 보내기" disabled={!isSendEnabled} onClick={handleSendMessage} - className={ - "flex h-10 w-12 items-center justify-center rounded-[24px] border border-white/30 transition-colors " + - (isSendEnabled + className={cn( + "flex h-10 w-12 items-center justify-center rounded-[24px] border border-white/30 transition-colors", + isSendEnabled ? "bg-button-primary text-button-primary-text-default" - : "bg-[#E5E5E5] text-[#CCCCCC]") - } + : "bg-[#E5E5E5] text-[#CCCCCC]", + )} > diff --git a/app/chat/[chatid]/page.tsx b/app/chat/[chatid]/page.tsx index 4de5dde..42f7ae9 100644 --- a/app/chat/[chatid]/page.tsx +++ b/app/chat/[chatid]/page.tsx @@ -1,11 +1,12 @@ import ScreenChatRoom from "./_components/ScreenChatRoom"; type ChatRoomPageProps = { - params: { + params: Promise<{ chatid: string; - }; + }>; }; -export default function ChatRoomPage({ params }: ChatRoomPageProps) { - return ; +export default async function ChatRoomPage({ params }: ChatRoomPageProps) { + const { chatid } = await params; + return ; } diff --git a/app/main/_components/FooterButtonList.tsx b/app/main/_components/FooterButtonList.tsx index 7f0f057..a933b8e 100644 --- a/app/main/_components/FooterButtonList.tsx +++ b/app/main/_components/FooterButtonList.tsx @@ -24,7 +24,7 @@ const MatchingButton = () => { 현재{" "} - {participantsCount?.data || 0}명 + {participantsCount?.data?.count ?? 0}명 {" "} 참여중이에요! From 239a12c2ded2f220e161997010ccbf350beec8ce Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 15 May 2026 15:18:53 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EA=B3=B5=EC=A7=80=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main/_components/ScreenMainPage.tsx | 58 ++++++++++++------- app/main/page.tsx | 10 ++++ .../_components/MatchingListCard.tsx | 2 +- .../_components/ScreenMatchingList.tsx | 10 +++- .../_components/YesMatchingList.tsx | 6 +- .../_components/ScreenMatchingResult.tsx | 10 ++++ app/matching/_components/AgeRangeDrawer.tsx | 5 +- .../_components/MatchingAgeOption.tsx | 5 +- app/matching/_components/ScreenMatching.tsx | 51 ++++++++++++---- app/mypage/_components/ScreenMyPage.tsx | 15 +---- .../charge-confirm/ConfirmChargeDrawer.tsx | 15 ++++- hooks/useActiveNotices.ts | 30 ++++++++++ hooks/useMatching.ts | 6 +- hooks/useMatchingHistory.ts | 1 + lib/constants/matching.ts | 5 ++ next.config.ts | 4 ++ 16 files changed, 175 insertions(+), 58 deletions(-) create mode 100644 hooks/useActiveNotices.ts create mode 100644 lib/constants/matching.ts diff --git a/app/main/_components/ScreenMainPage.tsx b/app/main/_components/ScreenMainPage.tsx index 51816c0..0782e30 100644 --- a/app/main/_components/ScreenMainPage.tsx +++ b/app/main/_components/ScreenMainPage.tsx @@ -20,34 +20,50 @@ import { MatchingPartner, } from "@/hooks/useMatchingHistory"; import { useRequestStatus } from "@/hooks/useRequestStatus"; +import { useActiveNotices, Notice } from "@/hooks/useActiveNotices"; const ScreenMainPage = () => { - // 실제 서비스 시에는 서버에서 받아온 데이터(notice)가 있는지 여부에 따라 렌더링을 결정할 수 있습니다. - const noticeData = { - id: "match-notice-001", // 공지사항 고유 ID (데이터를 받아올 때 id가 포함되어 있다고 가정) - title: "매칭 안내문", - detail: - "현재 많은 수요로 인해 일부 유형에 이용자가 몰리는 현상이 일어나고 있습니다. 원하는 유형이 나오지 않을 수도 있으니 이 점 양해 부탁드립니다. 코매칭을 이용해 주셔서 감사합니다.", - }; + const [noticePopup, setNoticePopup] = useState<{ + active: Notice | null; + isVisible: boolean; + }>({ active: null, isVisible: false }); - const [isNoticeVisible, setIsNoticeVisible] = useState(false); + const { data: noticesData } = useActiveNotices(); const { data: historyData, isLoading } = useMatchingHistory(); const { isPurchasePending } = useRequestStatus(); useEffect(() => { - // 로컬스토리지에 해당 공지 ID가 저장되어 있는지 확인 - const confirmed = localStorage.getItem(`notice_confirmed_${noticeData.id}`); - if (!confirmed) { - // 컴포넌트 마운트 직후 상태 변경으로 인한 cascading render 린트 에러 방지를 위해 비동기 처리 - const timeoutId = setTimeout(() => setIsNoticeVisible(true), 0); - return () => clearTimeout(timeoutId); + if (noticesData?.data && noticesData.data.length > 0) { + const firstUnconfirmed = noticesData.data.find( + (notice) => + !localStorage.getItem(`notice_confirmed_${notice.noticeId}`), + ); + + if (firstUnconfirmed) { + // eslint-disable-next-line react-hooks/set-state-in-effect + setNoticePopup({ active: firstUnconfirmed, isVisible: true }); + } else { + + setNoticePopup((prev) => + prev.isVisible ? { ...prev, isVisible: false } : prev, + ); + } + } else { + + setNoticePopup((prev) => + prev.isVisible ? { ...prev, isVisible: false } : prev, + ); } - }, [noticeData.id]); + }, [noticesData]); const handleNoticeClose = () => { - // 확인 버튼 클릭 시 로컬스토리지에 해당 공지 ID 저장 - localStorage.setItem(`notice_confirmed_${noticeData.id}`, "true"); - setIsNoticeVisible(false); + if (noticePopup.active) { + localStorage.setItem( + `notice_confirmed_${noticePopup.active.noticeId}`, + "true", + ); + setNoticePopup((prev) => ({ ...prev, isVisible: false })); + } }; // 매칭 히스토리 데이터에서 파트너 정보를 추출하여 프로필 목록 생성 @@ -82,10 +98,10 @@ const ScreenMainPage = () => { {isPurchasePending && } - {isNoticeVisible && noticeData && ( + {noticePopup.isVisible && noticePopup.active && ( )} diff --git a/app/main/page.tsx b/app/main/page.tsx index 1382257..092f5e2 100644 --- a/app/main/page.tsx +++ b/app/main/page.tsx @@ -10,6 +10,7 @@ import ScreenMainPage from "./_components/ScreenMainPage"; import { ItemsResponse } from "@/hooks/useItems"; import { MatchingHistoryResponse } from "@/hooks/useMatchingHistory"; import { RequestStatusResponse } from "@/hooks/useRequestStatus"; +import { NoticeResponse } from "@/hooks/useActiveNotices"; export default async function MainPage() { const queryClient = new QueryClient(); @@ -47,6 +48,15 @@ export default async function MainPage() { }, initialPageParam: 0, }), + queryClient.prefetchQuery({ + queryKey: ["activeNotices"], + queryFn: async () => { + const res = await serverApi.get({ + path: "/api/v1/notices/active", + }); + return res.data; + }, + }), ]); return ( diff --git a/app/matching-list/_components/MatchingListCard.tsx b/app/matching-list/_components/MatchingListCard.tsx index 6dcdcfd..f7c1bf9 100644 --- a/app/matching-list/_components/MatchingListCard.tsx +++ b/app/matching-list/_components/MatchingListCard.tsx @@ -98,7 +98,7 @@ const CardStats = ({ partner }: { partner: MatchingPartner }) => (
나이 - {getAge(partner.birthDate)} + {partner.age || getAge(partner.birthDate)}
diff --git a/app/matching-list/_components/ScreenMatchingList.tsx b/app/matching-list/_components/ScreenMatchingList.tsx index 4d53433..05f3d89 100644 --- a/app/matching-list/_components/ScreenMatchingList.tsx +++ b/app/matching-list/_components/ScreenMatchingList.tsx @@ -1,7 +1,7 @@ "use client"; import { BackButton } from "@/components/ui/BackButton"; -import React, { useMemo } from "react"; +import React, { useMemo, useEffect } from "react"; import { useMatchingHistory } from "@/hooks/useMatchingHistory"; import NoMatchingList from "./NoMatchingList"; import YesMatchingList from "./YesMatchingList"; @@ -15,6 +15,14 @@ const ScreenMatchingList = () => { return data?.pages.flatMap((page) => page?.data?.content || []) ?? []; }, [data]); + // DEBUG: 매칭 내역 데이터 로그 출력 + useEffect(() => { + if (data) { + console.log("📂 [Matching History Raw Data]:", data); + console.log("📜 [Matching History List]:", allHistory); + } + }, [data, allHistory]); + return (
diff --git a/app/matching-list/_components/YesMatchingList.tsx b/app/matching-list/_components/YesMatchingList.tsx index b51c3c6..1213f0a 100644 --- a/app/matching-list/_components/YesMatchingList.tsx +++ b/app/matching-list/_components/YesMatchingList.tsx @@ -108,7 +108,7 @@ const YesMatchingList = ({ return (
{/* 검색 바 */} -
+
{/* 필터 바 */} -
+
{/* 즐겨찾기 필터 */}
diff --git a/components/common/charge-confirm/ConfirmChargeDrawer.tsx b/components/common/charge-confirm/ConfirmChargeDrawer.tsx index 9b32294..ff79aeb 100644 --- a/components/common/charge-confirm/ConfirmChargeDrawer.tsx +++ b/components/common/charge-confirm/ConfirmChargeDrawer.tsx @@ -31,22 +31,33 @@ interface ConfirmChargeDrawerProps { /* ────────────────────────────────────── */ import { usePurchaseProduct } from "@/hooks/usePurchaseProduct"; +import { useRealName } from "@/hooks/useRealName"; import { ChargeDrawerContext } from "@/components/common/ChargeDrawer"; export default function ConfirmChargeDrawer({ trigger, productId, amount, - depositorName = "천승환", + depositorName, }: ConfirmChargeDrawerProps) { const queryClient = useQueryClient(); const drawerContext = React.useContext(ChargeDrawerContext); const { mutate: purchase, isPending } = usePurchaseProduct(); + const { data: realNameData } = useRealName(); const [open, setOpen] = React.useState(false); const [agreed, setAgreed] = React.useState(false); - const [name, setName] = React.useState(depositorName); + const [name, setName] = React.useState( + depositorName || realNameData?.data?.realName || "", + ); const [isEditingName, setIsEditingName] = React.useState(false); + + // 실명이 로드되면 이름 업데이트 (단, 사용자가 수동으로 수정 중이지 않을 때) + React.useEffect(() => { + if (realNameData?.data?.realName && !depositorName && !isEditingName) { + setName(realNameData.data.realName); + } + }, [realNameData, depositorName, isEditingName]); const inputRef = React.useRef(null); /* 입금자명 수정 시 input focus */ diff --git a/hooks/useActiveNotices.ts b/hooks/useActiveNotices.ts new file mode 100644 index 0000000..7551dc4 --- /dev/null +++ b/hooks/useActiveNotices.ts @@ -0,0 +1,30 @@ +import { api } from "@/lib/axios"; +import { useQuery } from "@tanstack/react-query"; + +export interface Notice { + noticeId: number; + title: string; + content: string; +} + +export interface NoticeResponse { + code: string; + status: number; + message: string; + data: Notice[]; +} + +/** 활성 공지사항 조회 API */ +export const fetchActiveNotices = async (): Promise => { + const { data } = await api.get("/api/v1/notices/active"); + return data; +}; + +/** 활성 공지사항 조회 훅 */ +export const useActiveNotices = () => { + return useQuery({ + queryKey: ["activeNotices"], + queryFn: fetchActiveNotices, + staleTime: 1000 * 60 * 5, // 5분 + }); +}; diff --git a/hooks/useMatching.ts b/hooks/useMatching.ts index dc4f35b..09e8ee8 100644 --- a/hooks/useMatching.ts +++ b/hooks/useMatching.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMatchingStore } from "@/stores/matching-store"; import { useRouter } from "next/navigation"; import { postMatchingAction } from "@/lib/actions/matchingAction"; +import { MatchingRequest } from "@/lib/types/matching"; /** * 매칭 실행 Mutation 훅 @@ -13,7 +14,10 @@ export const useMatching = () => { const { setResult, setLastPayload, setIsMatching } = useMatchingStore(); return useMutation({ - mutationFn: postMatchingAction, + mutationFn: (payload: MatchingRequest) => { + console.log("🚀 [Matching Request] Sending payload:", payload); + return postMatchingAction(payload); + }, onMutate: () => { setIsMatching(true); }, diff --git a/hooks/useMatchingHistory.ts b/hooks/useMatchingHistory.ts index 3779297..5d2115c 100644 --- a/hooks/useMatchingHistory.ts +++ b/hooks/useMatchingHistory.ts @@ -18,6 +18,7 @@ export interface MatchingPartner { nickname: string; gender: Gender; birthDate: string | null; + age: number | null; mbti: MBTI; intro: string | null; profileImageUrl: string | null; diff --git a/lib/constants/matching.ts b/lib/constants/matching.ts new file mode 100644 index 0000000..ba06f06 --- /dev/null +++ b/lib/constants/matching.ts @@ -0,0 +1,5 @@ +/** 매칭 나이 설정 관련 상수 */ +export const MATCHING_AGE_LIMITS = { + MIN: 20, + MAX: 27, +} as const; diff --git a/next.config.ts b/next.config.ts index c55c39e..ae1e456 100644 --- a/next.config.ts +++ b/next.config.ts @@ -32,6 +32,10 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "comatching.site", }, + { + protocol: "https", + hostname: "comatching5.s3.ap-northeast-2.amazonaws.com", + }, ], }, }; From 7f14bcc709a53878ea76723df8a38a8b73afc52a Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 15 May 2026 16:27:20 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/chat-list/_components/ScreenChatList.tsx | 9 ++- .../[chatid]/_components/ScreenChatRoom.tsx | 10 +++ app/main/_components/ProfileCard.tsx | 2 +- app/main/_components/ScreenMainPage.tsx | 3 +- .../_components/MatchingListCard.tsx | 11 +-- .../_components/ScreenMatchingList.tsx | 14 +++- .../_components/YesMatchingList.tsx | 10 ++- .../charge-confirm/ConfirmChargeDrawer.tsx | 76 +++++-------------- lib/types/profile.ts | 1 + lib/utils/profile.ts | 48 +++++++++++- 10 files changed, 115 insertions(+), 69 deletions(-) diff --git a/app/chat-list/_components/ScreenChatList.tsx b/app/chat-list/_components/ScreenChatList.tsx index b2ba6dd..5acc94d 100644 --- a/app/chat-list/_components/ScreenChatList.tsx +++ b/app/chat-list/_components/ScreenChatList.tsx @@ -4,7 +4,7 @@ import Image from "next/image"; import Link from "next/link"; import { BackButton } from "@/components/ui/BackButton"; import { useChatRooms } from "@/hooks/useChatRooms"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; type ChatListItem = { id: string; @@ -224,6 +224,13 @@ const formatChatTime = (isoString: string | null) => { export default function ScreenChatList() { const { data } = useChatRooms(); + // DEBUG: 채팅방 목록 데이터 로그 + useEffect(() => { + if (data) { + console.log("📡 [Chat Rooms List Data]:", data); + } + }, [data]); + const chatItems = useMemo(() => { const rooms = data?.data ?? []; if (rooms.length === 0) { diff --git a/app/chat/[chatid]/_components/ScreenChatRoom.tsx b/app/chat/[chatid]/_components/ScreenChatRoom.tsx index 6d68cba..748d865 100644 --- a/app/chat/[chatid]/_components/ScreenChatRoom.tsx +++ b/app/chat/[chatid]/_components/ScreenChatRoom.tsx @@ -98,6 +98,16 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { // 2. 소켓 연결 및 실시간 메시지 수신 const { messages: socketMessages, sendMessage } = useChatRoomSocket(chatId); + // DEBUG: 채팅방 데이터 로그 + useEffect(() => { + if (historyData) { + console.log("📡 [Chat Room History Data]:", historyData); + } + if (socketMessages.length > 0) { + console.log("🔌 [Chat Room Socket Messages]:", socketMessages); + } + }, [historyData, socketMessages]); + // 3. 전체 메시지 목록 변환 및 중복 제거 (Derived State) const messages = useMemo(() => { // API 데이터와 소켓 데이터를 합침 diff --git a/app/main/_components/ProfileCard.tsx b/app/main/_components/ProfileCard.tsx index bdc996d..189cdc3 100644 --- a/app/main/_components/ProfileCard.tsx +++ b/app/main/_components/ProfileCard.tsx @@ -74,7 +74,7 @@ const ProfileStats = ({ profile }: { profile: ProfileData }) => (
나이 - {getAge(profile.birthDate)} + {profile.age || getAge(profile.birthDate)}
diff --git a/app/main/_components/ScreenMainPage.tsx b/app/main/_components/ScreenMainPage.tsx index 0782e30..26cd832 100644 --- a/app/main/_components/ScreenMainPage.tsx +++ b/app/main/_components/ScreenMainPage.tsx @@ -43,13 +43,11 @@ const ScreenMainPage = () => { // eslint-disable-next-line react-hooks/set-state-in-effect setNoticePopup({ active: firstUnconfirmed, isVisible: true }); } else { - setNoticePopup((prev) => prev.isVisible ? { ...prev, isVisible: false } : prev, ); } } else { - setNoticePopup((prev) => prev.isVisible ? { ...prev, isVisible: false } : prev, ); @@ -76,6 +74,7 @@ const ScreenMainPage = () => { nickname: partner.nickname, gender: partner.gender, birthDate: partner.birthDate ?? undefined, + age: partner.age, mbti: partner.mbti, intro: partner.intro ?? undefined, profileImageUrl: diff --git a/app/matching-list/_components/MatchingListCard.tsx b/app/matching-list/_components/MatchingListCard.tsx index f7c1bf9..577d109 100644 --- a/app/matching-list/_components/MatchingListCard.tsx +++ b/app/matching-list/_components/MatchingListCard.tsx @@ -8,7 +8,10 @@ import Image from "next/image"; import { Send, Star, ChevronDown } from "lucide-react"; import React, { useRef, useState } from "react"; import { getAge } from "@/lib/utils/date"; -import { getContactFrequencyLabel } from "@/lib/utils/profile"; +import { + getContactFrequencyLabel, + getProfileImageUrl, +} from "@/lib/utils/profile"; /* ── 태그 컴포넌트 ── */ const Tag = ({ text }: { text: string }) => ( @@ -34,7 +37,7 @@ const CardHeader = ({
{`${partner.nickname diff --git a/app/matching-list/_components/ScreenMatchingList.tsx b/app/matching-list/_components/ScreenMatchingList.tsx index 05f3d89..d22b09d 100644 --- a/app/matching-list/_components/ScreenMatchingList.tsx +++ b/app/matching-list/_components/ScreenMatchingList.tsx @@ -2,13 +2,24 @@ import { BackButton } from "@/components/ui/BackButton"; import React, { useMemo, useEffect } from "react"; -import { useMatchingHistory } from "@/hooks/useMatchingHistory"; +import { + useMatchingHistory, + useUpdateFavorite, +} from "@/hooks/useMatchingHistory"; import NoMatchingList from "./NoMatchingList"; import YesMatchingList from "./YesMatchingList"; const ScreenMatchingList = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useMatchingHistory(); + const { mutate: toggleFavorite } = useUpdateFavorite(); + + const handleFavoriteToggle = ( + historyId: number, + currentFavorite: boolean, + ) => { + toggleFavorite({ historyId, favorite: !currentFavorite }); + }; // 모든 페이지의 content를 하나의 배열로 평탄화 const allHistory = useMemo(() => { @@ -43,6 +54,7 @@ const ScreenMatchingList = () => { fetchNextPage={fetchNextPage} hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} + onFavoriteToggle={handleFavoriteToggle} />
)} diff --git a/app/matching-list/_components/YesMatchingList.tsx b/app/matching-list/_components/YesMatchingList.tsx index 1213f0a..6727edd 100644 --- a/app/matching-list/_components/YesMatchingList.tsx +++ b/app/matching-list/_components/YesMatchingList.tsx @@ -25,6 +25,7 @@ interface YesMatchingListProps { fetchNextPage: () => void; hasNextPage: boolean; isFetchingNextPage: boolean; + onFavoriteToggle?: (historyId: number, currentFavorite: boolean) => void; } const YesMatchingList = ({ @@ -32,6 +33,7 @@ const YesMatchingList = ({ fetchNextPage, hasNextPage, isFetchingNextPage, + onFavoriteToggle, }: YesMatchingListProps) => { const [searchQuery, setSearchQuery] = useState(""); const [showFavoritesOnly, setShowFavoritesOnly] = useState(false); @@ -167,7 +169,13 @@ const YesMatchingList = ({
{filteredHistory.length > 0 ? ( filteredHistory.map((item) => ( - + + onFavoriteToggle?.(item.historyId, item.favorite) + } + /> )) ) : (
diff --git a/components/common/charge-confirm/ConfirmChargeDrawer.tsx b/components/common/charge-confirm/ConfirmChargeDrawer.tsx index ff79aeb..3509fc5 100644 --- a/components/common/charge-confirm/ConfirmChargeDrawer.tsx +++ b/components/common/charge-confirm/ConfirmChargeDrawer.tsx @@ -3,7 +3,7 @@ import React from "react"; import { AxiosError } from "axios"; import { cn } from "@/lib/utils"; -import { X, PencilLine, Check } from "lucide-react"; +import { X, Check } from "lucide-react"; import { Drawer, DrawerClose, @@ -16,6 +16,9 @@ import { import { useQueryClient } from "@tanstack/react-query"; import Button from "@/components/ui/Button"; import { BANK_INFO } from "@/lib/constants/charge"; +import { usePurchaseProduct } from "@/hooks/usePurchaseProduct"; +import { useRealName } from "@/hooks/useRealName"; +import { ChargeDrawerContext } from "@/components/common/ChargeDrawer"; /* ── Props ── */ interface ConfirmChargeDrawerProps { @@ -28,12 +31,6 @@ interface ConfirmChargeDrawerProps { depositorName?: string; } -/* ────────────────────────────────────── */ - -import { usePurchaseProduct } from "@/hooks/usePurchaseProduct"; -import { useRealName } from "@/hooks/useRealName"; -import { ChargeDrawerContext } from "@/components/common/ChargeDrawer"; - export default function ConfirmChargeDrawer({ trigger, productId, @@ -50,20 +47,13 @@ export default function ConfirmChargeDrawer({ const [name, setName] = React.useState( depositorName || realNameData?.data?.realName || "", ); - const [isEditingName, setIsEditingName] = React.useState(false); - // 실명이 로드되면 이름 업데이트 (단, 사용자가 수동으로 수정 중이지 않을 때) + // 실명이 로드되면 이름 업데이트 React.useEffect(() => { - if (realNameData?.data?.realName && !depositorName && !isEditingName) { + if (realNameData?.data?.realName && !depositorName && !name) { setName(realNameData.data.realName); } - }, [realNameData, depositorName, isEditingName]); - const inputRef = React.useRef(null); - - /* 입금자명 수정 시 input focus */ - React.useEffect(() => { - if (isEditingName) inputRef.current?.focus(); - }, [isEditingName]); + }, [realNameData, depositorName, name]); /* 계좌번호 복사 */ const handleCopy = async () => { @@ -85,10 +75,7 @@ export default function ConfirmChargeDrawer({ const tossDeepLink = `supertoss://send?accountNo=${accountNo}&bankCode=${bankCode}&amount=${amount}&message=${encodeURIComponent(message)}`; if (/Android|iPhone|iPad/i.test(navigator.userAgent)) { - // 토스 이동 상태 기록 (기존 코드 참고) localStorage.setItem("tossPaymentInProgress", "1"); - - // 딥링크 이동 const a = document.createElement("a"); a.href = tossDeepLink; a.style.display = "none"; @@ -106,6 +93,8 @@ export default function ConfirmChargeDrawer({ onSuccess: () => { alert("충전 요청이 완료되었습니다!"); setOpen(false); + queryClient.invalidateQueries({ queryKey: ["myProfile"] }); + if (drawerContext) drawerContext.onClose(); }, onError: (error: AxiosError<{ code?: string; message?: string }>) => { const errorData = error.response?.data; @@ -115,7 +104,6 @@ export default function ConfirmChargeDrawer({ } else if (errorData?.code === "PAY-004") { alert("먼저 입금자명을 설정해 주세요."); setOpen(false); - // 실명 설정 탭으로 이동 (TABS[2]가 입금자명 설정) drawerContext?.setActiveTab(2); } else { alert( @@ -140,10 +128,10 @@ export default function ConfirmChargeDrawer({ className="rounded-t-[24px] bg-white outline-none" showHandle={false} > -
-
+
+
{/* ── Header ── */} - +
계좌이체 @@ -172,16 +160,13 @@ export default function ConfirmChargeDrawer({
{/* 계좌 정보 박스 */} -
+
{BANK_INFO.bank} - + {BANK_INFO.account} - - 예금주 : {BANK_INFO.holder} -
@@ -190,33 +175,11 @@ export default function ConfirmChargeDrawer({ 입금자명 - {isEditingName ? ( -
- setName(e.target.value)} - onBlur={() => setIsEditingName(false)} - onKeyDown={(e) => { - if (e.key === "Enter") setIsEditingName(false); - }} - className="typo-16-700 text-color-gray-900 h-[24px] w-[90px] bg-transparent text-center outline-none" - maxLength={6} - /> -
- ) : ( - - )} +
+ + {name || "미지정"} + +
{/* 입금액 */} @@ -262,7 +225,6 @@ export default function ConfirmChargeDrawer({ {isPending ? "요청 중..." : "충전 요청 보내기"} - {/* Toss 링크 */} @@ -214,6 +220,7 @@ const MatchingListCard = ({ }: MatchingListCardProps) => { const [isExpanded, setIsExpanded] = useState(false); const touchStartTime = useRef(0); + const router = useRouter(); const handleCardClick = () => { const touchDuration = Date.now() - touchStartTime.current; @@ -222,6 +229,14 @@ const MatchingListCard = ({ } }; + const handleChatClick = () => { + if (item.chatRoomId) { + router.push(`/chat/${item.chatRoomId}`); + } else { + alert("채팅방 정보를 찾을 수 없습니다."); + } + }; + return (
{/* 카드 본체 */} @@ -245,6 +260,7 @@ const MatchingListCard = ({ partner={item.partner} isFavorite={item.favorite} onFavoriteToggle={() => onFavoriteToggle?.(item.historyId)} + onChatClick={handleChatClick} />
diff --git a/app/matching-list/_components/YesMatchingList.tsx b/app/matching-list/_components/YesMatchingList.tsx index 6727edd..d069989 100644 --- a/app/matching-list/_components/YesMatchingList.tsx +++ b/app/matching-list/_components/YesMatchingList.tsx @@ -65,13 +65,9 @@ const YesMatchingList = ({ // 정렬 result.sort((a, b) => { if (sortOrder === "age") { - const ageA = a.partner.birthDate - ? new Date(a.partner.birthDate).getTime() - : 0; - const ageB = b.partner.birthDate - ? new Date(b.partner.birthDate).getTime() - : 0; - // birthDate가 작을수록(오래될수록) 나이가 많음 → 오름차순 + const ageA = a.partner.age || getAge(a.partner.birthDate, CURRENT_YEAR); + const ageB = b.partner.age || getAge(b.partner.birthDate, CURRENT_YEAR); + // 나이 오름차순 (어린 순) return ageA - ageB; } const dateA = new Date(a.matchedAt).getTime(); diff --git a/hooks/useChatRooms.ts b/hooks/useChatRooms.ts index 91829b9..8bb8dfa 100644 --- a/hooks/useChatRooms.ts +++ b/hooks/useChatRooms.ts @@ -1,14 +1,13 @@ import { api } from "@/lib/axios"; import { useQuery } from "@tanstack/react-query"; +import { MatchingPartner } from "./useMatchingHistory"; export type ChatRoom = { - id: string; - matchingId: number; - initiatorUserId: number; - targetUserId: number; - lastMessage: string | null; - lastMessageTime: string | null; - unreadCount: number; + historyId: number; + chatRoomId: string; + partner: MatchingPartner; + favorite: boolean; + matchedAt: string; }; export type ChatRoomsResponse = { @@ -18,9 +17,16 @@ export type ChatRoomsResponse = { data: ChatRoom[]; }; -export const fetchChatRooms = async (): Promise => { - const { data } = await api.get("/api/chat/rooms"); - return data; +/** + * 채팅방 목록 조회 (전체 응답이 배열인 경우와 객체인 경우 대응) + */ +export const fetchChatRooms = async (): Promise => { + const { data } = await api.get( + "/api/chat/rooms", + ); + // 백엔드 응답이 { data: [...] } 형태인지 아니면 배열 그 자체인지 확인하여 처리 + if (Array.isArray(data)) return data; + return data.data || []; }; export const useChatRooms = () => { diff --git a/hooks/useMatchingHistory.ts b/hooks/useMatchingHistory.ts index 5d2115c..0760782 100644 --- a/hooks/useMatchingHistory.ts +++ b/hooks/useMatchingHistory.ts @@ -36,6 +36,7 @@ export interface MatchingPartner { export interface MatchingHistoryItem { historyId: number; + chatRoomId?: string; partner: MatchingPartner; favorite: boolean; matchedAt: string; From d1d59316b8a136878c90f9d4d52b48f7c1b88962 Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 15 May 2026 17:29:30 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=AC=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/chat-list/_components/ScreenChatList.tsx | 137 +- app/chat-list/page.tsx | 8 +- .../_components/PartnerProfileModal.tsx | 242 +++ .../[chatid]/_components/ScreenChatRoom.tsx | 91 +- app/main/_components/ProfileSlider.tsx | 10 +- components/ui/dialog.tsx | 158 ++ hooks/useChatRooms.ts | 20 +- package.json | 1 + pnpm-lock.yaml | 1366 ++++++++++++++++- 9 files changed, 1832 insertions(+), 201 deletions(-) create mode 100644 app/chat/[chatid]/_components/PartnerProfileModal.tsx create mode 100644 components/ui/dialog.tsx diff --git a/app/chat-list/_components/ScreenChatList.tsx b/app/chat-list/_components/ScreenChatList.tsx index 71f9512..1374b63 100644 --- a/app/chat-list/_components/ScreenChatList.tsx +++ b/app/chat-list/_components/ScreenChatList.tsx @@ -3,7 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { BackButton } from "@/components/ui/BackButton"; -import { useChatRooms } from "@/hooks/useChatRooms"; +import { useChatRooms, type ChatRoom } from "@/hooks/useChatRooms"; import { useEffect, useMemo } from "react"; import { getProfileImageUrl } from "@/lib/utils/profile"; @@ -18,112 +18,6 @@ type ChatListItem = { avatar: string; }; -const inboxItems: ChatListItem[] = [ - { - id: "chat-1", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "22세, 정보통신전자공학부", - preview: - "안녕하세요! 오늘 시간 괜찮으세요? 혹시 축제 끝나고 같이 산책하면서 이야기 나눌 수 있을까요?", - time: "방금", - unread: true, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-2", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "22세, 정보통신전자공학부", - preview: - "혹시 축제 끝나고 커피 한 잔 하실래요? 요즘 좋아하는 음악이나 취미도 궁금해요!", - time: "방금", - unread: false, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-3", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "22세, 정보통신전자공학부", - preview: - "오늘 매칭 너무 반가웠어요 :) 혹시 시간 괜찮으시면 잠깐 만나서 인사하고 싶어요!", - time: "방금", - unread: true, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-4", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "23세, 컴퓨터공학과", - preview: "지금 어디에 계세요?", - time: "1분 전", - unread: false, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-5", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "21세, 심리학과", - preview: "공연 같이 보실래요?", - time: "3분 전", - unread: false, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-6", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "24세, 경영학과", - preview: "인사 늦어서 미안해요!", - time: "5분 전", - unread: true, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-7", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "22세, 전자공학과", - preview: "혹시 좋아하는 음악 장르 있어요?", - time: "8분 전", - unread: false, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-8", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "23세, 디자인학과", - preview: "지금 부스 앞에서 기다리고 있어요.", - time: "12분 전", - unread: false, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-9", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "21세, 사회학과", - preview: "요즘 관심사 뭐예요?", - time: "18분 전", - unread: false, - avatar: "/profile/default-profile.svg", - }, - { - id: "chat-10", - roomId: "6969e61b866d67f1c3b68106", - name: "username", - detail: "24세, 기계공학과", - preview: "저도 그 동아리 관심 있어요!", - time: "25분 전", - unread: true, - avatar: "/profile/default-profile.svg", - }, -]; - const pickedItems: ChatListItem[] = [ { id: "picked-1", @@ -233,21 +127,28 @@ export default function ScreenChatList() { }, [rooms]); const chatItems = useMemo(() => { - if (!rooms || rooms.length === 0) { + // 이제 SSR/CSR 모두 배열로 통일되었으므로 단순하게 처리합니다. + const roomsArray = (rooms || []) as ChatRoom[]; + + if (roomsArray.length === 0) { return []; } - return rooms.map((room) => ({ - id: String(room.historyId), - roomId: room.chatRoomId, - name: room.partner.nickname, - detail: `${room.partner.age}세, ${room.partner.major}`, - preview: "채팅을 시작해보세요.", // 마지막 메시지 필드가 없으므로 기본값 - time: formatChatTime(room.matchedAt), - unread: false, // 현재 응답에 unreadCount 없음 + return roomsArray.map((room) => ({ + id: String(room.matchingId), + roomId: room.id, + name: room.otherUser?.nickname || "익명", + detail: room.otherUser + ? `${room.otherUser.age}세, ${room.otherUser.major}` + : "정보 없음", + preview: room.lastMessage || "채팅을 시작해보세요.", + time: room.lastMessageTime + ? formatChatTime(room.lastMessageTime) + : "방금", + unread: (room.unreadCount || 0) > 0, avatar: getProfileImageUrl( - room.partner.profileImageUrl, - room.partner.gender, + room.otherUser?.profileImageUrl, + "UNKNOWN", // 성별 정보가 없으므로 기본값 처리 ), })); }, [rooms]); diff --git a/app/chat-list/page.tsx b/app/chat-list/page.tsx index cd9e6ad..d922585 100644 --- a/app/chat-list/page.tsx +++ b/app/chat-list/page.tsx @@ -17,11 +17,9 @@ export default async function ChatListPage() { const res = await serverApi.get({ path: "/api/chat/rooms", }); - console.log("[SSR] Chat rooms response", { - count: res.data.data.length, - status: res.data.status, - }); - return res.data; + console.log("[SSR] Chat rooms response count:", res.data.data.length); + // 클라이언트 훅과 동일하게 배열만 반환하도록 수정 + return res.data.data || []; }, }); diff --git a/app/chat/[chatid]/_components/PartnerProfileModal.tsx b/app/chat/[chatid]/_components/PartnerProfileModal.tsx new file mode 100644 index 0000000..d2ae5d3 --- /dev/null +++ b/app/chat/[chatid]/_components/PartnerProfileModal.tsx @@ -0,0 +1,242 @@ +"use client"; + +import React from "react"; +import Image from "next/image"; +import { X, Star, Send } from "lucide-react"; +import { MatchingPartner } from "@/hooks/useMatchingHistory"; +import { + getProfileImageUrl, + getContactFrequencyLabel, +} from "@/lib/utils/profile"; +import { cn } from "@/lib/utils"; +import { + findWithEmoji, + ALL_HOBBIES, + ALL_ADVANTAGES, +} from "@/lib/utils/matching"; +import { + Dialog, + DialogContent, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; + +interface PartnerProfileModalProps { + isOpen: boolean; + onClose: () => void; + partner: MatchingPartner; + isFavorite?: boolean; + onFavoriteToggle?: () => void; +} + +const PartnerProfileModal = ({ + isOpen, + onClose, + partner, + isFavorite, + onFavoriteToggle, +}: PartnerProfileModalProps) => { + return ( + !open && onClose()}> + + + + 상대방 프로필 + + 상대방의 닉네임, 나이, MBTI, 관심사 등의 상세 정보를 확인합니다. + + {/* 카드 + 닫기 버튼 그룹 */} +
+ {/* 모달 컨테이너 (MatchingListCard 스타일 계승) */} +
+ {/* 카드 본체 (내부 내용에 따라 높이 조절, 최대 높이시 스크롤) */} +
+ {/* 헤더 */} +
+
+
+ 프로필 +
+
+
+ + 내가 뽑은 사람 + + + {partner.nickname || "익명"} + +
+
+ + +
+
+
+
+
+
+
+ + {/* 스펙 섹션 (나이, MBTI, 연락빈도) */} +
+
+ 나이 + + {partner.age || "??"} + +
+
+ MBTI + + {partner.mbti} + +
+
+ + 연락빈도 + + + {getContactFrequencyLabel(partner.contactFrequency)} + +
+
+ + {/* 상세 정보 섹션 (CardDetails 스타일) */} +
+
+ 관심사 +
+ {partner.hobbies?.map((h) => ( +
+ + {findWithEmoji(ALL_HOBBIES, h.name)} + +
+ ))} +
+
+ +
+ 장점 +
+ {partner.tags?.map((t) => ( +
+ + {findWithEmoji(ALL_ADVANTAGES, t.tag)} + +
+ ))} +
+
+ +
+ + 좋아하는 노래 + +
+ 🎵 + + {partner.song || "아직 없어요!"} + +
+
+ +
+ + 나를 소개하는 한마디 + +
+ 💬 + + {partner.intro} + +
+
+
+
+ + {/* 그라디언트 푸터 */} +
+ {partner.socialType === "KAKAO" ? ( +
+ kakao + + {partner.socialAccountId} + +
+ ) : ( + + {partner.socialAccountId + ? `@${partner.socialAccountId}` + : "비공개"} + + )} +
+
+ + {/* 닫기 버튼 */} + +
+ + +
+ ); +}; + +export default PartnerProfileModal; diff --git a/app/chat/[chatid]/_components/ScreenChatRoom.tsx b/app/chat/[chatid]/_components/ScreenChatRoom.tsx index 748d865..8c3e24d 100644 --- a/app/chat/[chatid]/_components/ScreenChatRoom.tsx +++ b/app/chat/[chatid]/_components/ScreenChatRoom.tsx @@ -7,7 +7,10 @@ import { ArrowUp, ChevronLeft, MoreVertical, UserRound } from "lucide-react"; import { useChatRoomSocket } from "@/hooks/useChatRoomSocket"; import { useChatMessages } from "@/hooks/useChatMessages"; import { useMyProfile } from "@/hooks/useProfile"; +import { getProfileImageUrl } from "@/lib/utils/profile"; +import { useUpdateFavorite } from "@/hooks/useMatchingHistory"; import { cn } from "@/lib/utils"; +import PartnerProfileModal from "./PartnerProfileModal"; type ScreenChatRoomProps = { chatId: string; @@ -41,19 +44,27 @@ const ChatDateDivider = ({ label }: { label: string }) => { ); }; -const IncomingMessage = ({ message }: { message: ChatMessage }) => { +const IncomingMessage = ({ + message, + partner, +}: { + message: ChatMessage; + partner?: MatchingPartner; +}) => { return (
프로필
- 겨울이오길 + + {partner?.nickname || "..."} +
{message.text} @@ -87,11 +98,59 @@ const OutgoingMessage = ({ message }: { message: ChatMessage }) => { export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { const router = useRouter(); const [messageText, setMessageText] = useState(""); + const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); + const [isFavorite, setIsFavorite] = useState(false); // 0. 내 프로필 정보 가져오기 (현재 사용자 ID 확인용) const { data: myProfile } = useMyProfile(); const currentUserId = myProfile?.data.memberId; + // 즐겨찾기 토글 mutation + const updateFavorite = useUpdateFavorite(); + + // 0-1. 현재 채팅방 정보 가져오기 (임시 더미 데이터 사용) + // const { data: rooms } = useChatRooms(); + // const currentRoom = rooms?.find((r) => r.chatRoomId === chatId); + // const partner = currentRoom?.partner; + + const partner: MatchingPartner = { + memberId: 999, + email: "winter@example.com", + nickname: "겨울이오길", + age: 20, + gender: "FEMALE", + birthDate: "2004-01-01", + major: "정보통신전자공학부", + university: "가톨릭대학교", + mbti: "ENTP", + contactFrequency: "NORMAL", + profileImageUrl: "animal_cat", + profileImageKey: null, + intro: "친하게 지내요! 😆", + song: "한로로 - 사랑하게 될 거야", + hobbies: [ + { category: "취미", name: "독서" }, + { category: "취미", name: "영화감상" }, + { category: "취미", name: "음악감상" }, + ], + tags: [{ tag: "친절한" }, { tag: "열정적인" }], + intros: [], + socialType: "INSTAGRAM", + socialAccountId: "winterizcoming_", + }; + + const handleFavoriteToggle = () => { + if (!partner) return; + + const newFavoriteStatus = !isFavorite; + setIsFavorite(newFavoriteStatus); + + updateFavorite.mutate({ + historyId: partner.memberId, // MatchingPartner에는 historyId가 없으므로 memberId를 임시로 사용 + favorite: newFavoriteStatus, + }); + }; + // 1. 과거 대화 내역 가져오기 (API) const { data: historyData } = useChatMessages(chatId); @@ -158,17 +217,22 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) {
- 겨울이오길 + + {partner?.nickname || "..."} +
- 20세 + {partner?.age || "??"}세 , - 정보통신전자공학부 + + {partner?.major || "..."} +
chatId: {chatId}
+ {partner && ( + setIsProfileModalOpen(false)} + partner={partner} + isFavorite={isFavorite} + onFavoriteToggle={handleFavoriteToggle} + /> + )}
); } diff --git a/app/main/_components/ProfileSlider.tsx b/app/main/_components/ProfileSlider.tsx index 00b7acc..8a52dd6 100644 --- a/app/main/_components/ProfileSlider.tsx +++ b/app/main/_components/ProfileSlider.tsx @@ -14,11 +14,11 @@ const CARD_GAP = 8; // 카드 사이 간격 (px) const ProfileSlider = ({ profiles }: ProfileSliderProps) => { const [activeIndex, setActiveIndex] = useState(0); - const [expandedCardId, setExpandedCardId] = useState(null); + const [isAllExpanded, setIsAllExpanded] = useState(false); const scrollRef = useRef(null); - const toggleExpanded = useCallback((id: number) => { - setExpandedCardId((prev) => (prev === id ? null : id)); + const toggleExpanded = useCallback(() => { + setIsAllExpanded((prev) => !prev); }, []); useEffect(() => { @@ -60,8 +60,8 @@ const ProfileSlider = ({ profiles }: ProfileSliderProps) => { > toggleExpanded(profile.memberId)} + isExpanded={isAllExpanded} + onToggleExpanded={toggleExpanded} />
))} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..3c9ecf0 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,158 @@ +"use client"; + +import * as React from "react"; +import { XIcon } from "lucide-react"; +import { Dialog as DialogPrimitive } from "radix-ui"; + +import { cn } from "@/lib/utils"; +import Button from "@/components/ui/Button"; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean; +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/hooks/useChatRooms.ts b/hooks/useChatRooms.ts index 8bb8dfa..03852f8 100644 --- a/hooks/useChatRooms.ts +++ b/hooks/useChatRooms.ts @@ -3,11 +3,21 @@ import { useQuery } from "@tanstack/react-query"; import { MatchingPartner } from "./useMatchingHistory"; export type ChatRoom = { - historyId: number; - chatRoomId: string; - partner: MatchingPartner; - favorite: boolean; - matchedAt: string; + id: string; // 채팅방 ID + matchingId: number; // 매칭 ID + initiatorUserId: number; + targetUserId: number; + otherUser: { + memberId: number; + nickname: string; + profileImageUrl: string; + university: string; + major: string; + age: number; + }; + lastMessage: string | null; + lastMessageTime: string | null; + unreadCount: number; }; export type ChatRoomsResponse = { diff --git a/package.json b/package.json index b51bbea..5563cb2 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "lucide-react": "^0.562.0", "motion": "^12.38.0", "next": "16.1.3", + "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", "sockjs-client": "^1.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fe8038..c1a578a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: next: specifier: 16.1.3 version: 16.1.3(@babel/core@7.28.6)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + radix-ui: + specifier: ^1.4.3 + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -61,7 +64,7 @@ importers: version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) zustand: specifier: ^5.0.12 - version: 5.0.12(@types/react@19.2.10)(react@19.2.3) + version: 5.0.12(@types/react@19.2.10)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: '@tailwindcss/postcss': specifier: ^4.1.18 @@ -439,6 +442,21 @@ packages: '@firebase/webchannel-wrapper@1.0.5': resolution: {integrity: sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@grpc/grpc-js@1.9.15': resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==} engines: {node: ^8.13.0 || >=10.10.0} @@ -720,9 +738,129 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-accessible-icon@1.1.7': + resolution: {integrity: sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.7': + resolution: {integrity: sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -732,6 +870,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-context@1.1.2': resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: @@ -754,6 +905,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dismissable-layer@1.1.11': resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} peerDependencies: @@ -767,6 +927,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.3': resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: @@ -789,6 +962,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-form@0.1.8': + resolution: {integrity: sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: @@ -798,8 +997,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-portal@1.1.9': - resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -811,8 +1010,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.5': - resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -824,8 +1023,8 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-primitive@2.1.3': - resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + '@radix-ui/react-menubar@1.1.16': + resolution: {integrity: sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -837,76 +1036,414 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-slot@1.2.3': - resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} peerDependencies: '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true - '@radix-ui/react-use-callback-ref@1.1.1': - resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + '@radix-ui/react-one-time-password-field@0.1.8': + resolution: {integrity: sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==} peerDependencies: '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true - '@radix-ui/react-use-controllable-state@1.2.2': - resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + '@radix-ui/react-password-toggle-field@0.1.3': + resolution: {integrity: sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==} peerDependencies: '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true - '@radix-ui/react-use-effect-event@0.0.2': - resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} peerDependencies: '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true - '@radix-ui/react-use-escape-keydown@1.1.1': - resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} peerDependencies: '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true - '@radix-ui/react-use-layout-effect@1.1.1': - resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': optional: true + '@types/react-dom': + optional: true - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@stomp/stompjs@7.3.0': - resolution: {integrity: sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==} + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@radix-ui/react-progress@1.1.7': + resolution: {integrity: sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/node@4.1.18': - resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true - '@tailwindcss/oxide-android-arm64@4.1.18': - resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} - engines: {node: '>= 10'} - cpu: [arm64] + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toast@1.2.15': + resolution: {integrity: sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.11': + resolution: {integrity: sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@stomp/stompjs@7.3.0': + resolution: {integrity: sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] os: [android] '@tailwindcss/oxide-darwin-arm64@4.1.18': @@ -2417,6 +2954,19 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + radix-ui@1.4.3: + resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -2777,6 +3327,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -3362,6 +3917,23 @@ snapshots: '@firebase/webchannel-wrapper@1.0.5': {} + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@floating-ui/utils@0.2.11': {} + '@grpc/grpc-js@1.9.15': dependencies: '@grpc/proto-loader': 0.7.15 @@ -3558,51 +4130,543 @@ snapshots: '@protobufjs/codegen@2.0.4': {} - '@protobufjs/eventemitter@1.1.0': {} + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-context@1.1.2(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@protobufjs/fetch@1.1.0': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.10)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@protobufjs/float@1.0.2': {} + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@protobufjs/inquire@1.1.0': {} + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@protobufjs/path@1.1.2': {} + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@protobufjs/pool@1.1.0': {} + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@protobufjs/utf8@1.1.0': {} + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.10)(react@19.2.3)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-context@1.1.2(@types/react@19.2.10)(react@19.2.3)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: + '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) aria-hidden: 1.2.6 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -3611,78 +4675,152 @@ snapshots: '@types/react': 19.2.10 '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: + '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.10)(react@19.2.3)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.10)(react@19.2.3)': dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 optionalDependencies: '@types/react': 19.2.10 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-id@1.1.1(@types/react@19.2.10)(react@19.2.3)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 '@types/react-dom': 19.2.3(@types/react@19.2.10) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.10)(react@19.2.3)': + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: + '@radix-ui/primitive': 1.1.3 '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) optionalDependencies: '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.10)(react@19.2.3)': dependencies: @@ -3712,12 +4850,50 @@ snapshots: optionalDependencies: '@types/react': 19.2.10 + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.10)(react@19.2.3)': dependencies: react: 19.2.3 optionalDependencies: '@types/react': 19.2.10 + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.10)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.10 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + + '@radix-ui/rect@1.1.1': {} + '@rtsao/scc@1.1.0': {} '@stomp/stompjs@7.3.0': {} @@ -5320,6 +6496,69 @@ snapshots: queue-microtask@1.2.3: {} + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.10)(react@19.2.3) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.10 + '@types/react-dom': 19.2.3(@types/react@19.2.10) + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -5787,6 +7026,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.10 + use-sync-external-store@1.6.0(react@19.2.3): + dependencies: + react: 19.2.3 + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -5891,7 +7134,8 @@ snapshots: zod@4.3.6: {} - zustand@5.0.12(@types/react@19.2.10)(react@19.2.3): + zustand@5.0.12(@types/react@19.2.10)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): optionalDependencies: '@types/react': 19.2.10 react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) From 909808ec8454d34336e8a3c6563965a16925815a Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 15 May 2026 23:06:25 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20build=EC=97=90=EB=9F=AC=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[chatid]/_components/ScreenChatRoom.tsx | 5 +- app/main/_components/ScreenMainPage.tsx | 2 +- app/matching-list/_components/dummyData.ts | 312 ------------------ 3 files changed, 5 insertions(+), 314 deletions(-) delete mode 100644 app/matching-list/_components/dummyData.ts diff --git a/app/chat/[chatid]/_components/ScreenChatRoom.tsx b/app/chat/[chatid]/_components/ScreenChatRoom.tsx index 8c3e24d..b1af23a 100644 --- a/app/chat/[chatid]/_components/ScreenChatRoom.tsx +++ b/app/chat/[chatid]/_components/ScreenChatRoom.tsx @@ -8,7 +8,10 @@ import { useChatRoomSocket } from "@/hooks/useChatRoomSocket"; import { useChatMessages } from "@/hooks/useChatMessages"; import { useMyProfile } from "@/hooks/useProfile"; import { getProfileImageUrl } from "@/lib/utils/profile"; -import { useUpdateFavorite } from "@/hooks/useMatchingHistory"; +import { + useUpdateFavorite, + type MatchingPartner, +} from "@/hooks/useMatchingHistory"; import { cn } from "@/lib/utils"; import PartnerProfileModal from "./PartnerProfileModal"; diff --git a/app/main/_components/ScreenMainPage.tsx b/app/main/_components/ScreenMainPage.tsx index 26cd832..0979334 100644 --- a/app/main/_components/ScreenMainPage.tsx +++ b/app/main/_components/ScreenMainPage.tsx @@ -74,7 +74,7 @@ const ScreenMainPage = () => { nickname: partner.nickname, gender: partner.gender, birthDate: partner.birthDate ?? undefined, - age: partner.age, + age: partner.age ?? undefined, mbti: partner.mbti, intro: partner.intro ?? undefined, profileImageUrl: diff --git a/app/matching-list/_components/dummyData.ts b/app/matching-list/_components/dummyData.ts deleted file mode 100644 index d44eae2..0000000 --- a/app/matching-list/_components/dummyData.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { MatchingHistoryItem } from "@/hooks/useMatchingHistory"; - -export const DUMMY_MATCHING_HISTORY: MatchingHistoryItem[] = [ - { - historyId: 1, - partner: { - memberId: 1, - email: "user1@test.com", - nickname: "봄날의햇살", - gender: "FEMALE", - birthDate: "2002-03-15", - mbti: "ENFP", - intro: "안녕하세요! 맛집 탐방을 좋아하는 대학생입니다 😊", - profileImageUrl: null, - profileImageKey: null, - socialType: "INSTAGRAM", - socialAccountId: "spring_sunshine_", - university: "한국대학교", - major: "경영학과", - contactFrequency: "FREQUENT", - hobbies: [ - { category: "CULTURE", name: "카페투어" }, - { category: "TRAVEL", name: "여행" }, - { category: "SPORTS", name: "필라테스" }, - ], - tags: [{ tag: "친절함" }, { tag: "활발함" }], - song: "IU - Love Wins All", - intros: [ - { question: "제 키는", answer: "163cm" }, - { question: "제 음주 습관은", answer: "가끔" }, - ], - }, - favorite: true, - matchedAt: "2026-04-20T10:30:00.000000", - }, - { - historyId: 2, - partner: { - memberId: 2, - email: "user2@test.com", - nickname: "겨울바다", - gender: "FEMALE", - birthDate: "2001-11-22", - mbti: "INFJ", - intro: "조용하지만 깊은 대화를 좋아해요.", - profileImageUrl: null, - profileImageKey: null, - socialType: "INSTAGRAM", - socialAccountId: "winter_sea22", - university: "서울대학교", - major: "심리학과", - contactFrequency: "NORMAL", - hobbies: [ - { category: "CULTURE", name: "독서" }, - { category: "MUSIC", name: "피아노" }, - ], - tags: [{ tag: "조용함" }, { tag: "진중함" }], - song: "백예린 - 산책", - intros: [ - { question: "제 직업은", answer: "대학원생" }, - { question: "제 키는", answer: "158cm" }, - ], - }, - favorite: false, - matchedAt: "2026-04-19T14:20:00.000000", - }, - { - historyId: 3, - partner: { - memberId: 3, - email: "user3@test.com", - nickname: "달빛소녀", - gender: "FEMALE", - birthDate: "2003-07-01", - mbti: "ENTP", - intro: "토론하는 거 좋아합니다! 같이 이야기해요~", - profileImageUrl: null, - profileImageKey: null, - socialType: "KAKAO", - socialAccountId: "moonlight_girl", - university: "연세대학교", - major: "컴퓨터공학과", - contactFrequency: "FREQUENT", - hobbies: [ - { category: "GAME", name: "보드게임" }, - { category: "DAILY", name: "코딩" }, - { category: "SPORTS", name: "배드민턴" }, - { category: "CULTURE", name: "영화감상" }, - ], - tags: [{ tag: "재치있음" }, { tag: "논리적" }], - song: "NewJeans - Ditto", - intros: [ - { question: "제 직업은", answer: "개발자" }, - { question: "저는 흡연을", answer: "비흡연" }, - ], - }, - favorite: true, - matchedAt: "2026-04-18T09:15:00.000000", - }, - { - historyId: 4, - partner: { - memberId: 4, - email: "user4@test.com", - nickname: "여름향기", - gender: "FEMALE", - birthDate: "2000-08-10", - mbti: "ISFP", - intro: "그림 그리는 걸 좋아해요 🎨", - profileImageUrl: null, - profileImageKey: null, - socialType: "INSTAGRAM", - socialAccountId: "summer_scent__", - university: "고려대학교", - major: "미술학과", - contactFrequency: "RARE", - hobbies: [ - { category: "MUSIC", name: "그림" }, - { category: "TRAVEL", name: "사진촬영" }, - ], - tags: [{ tag: "예술적" }, { tag: "차분함" }], - song: "10CM - 그라데이션", - intros: [ - { question: "제 키는", answer: "165cm" }, - { question: "제가 좋아하는 음식은", answer: "파스타" }, - ], - }, - favorite: false, - matchedAt: "2026-04-17T16:45:00.000000", - }, - { - historyId: 5, - partner: { - memberId: 5, - email: "user5@test.com", - nickname: "별빛나래", - gender: "FEMALE", - birthDate: "2002-01-28", - mbti: "ESTJ", - intro: "운동 좋아하고 활발한 성격이에요!", - profileImageUrl: null, - profileImageKey: null, - socialType: "INSTAGRAM", - socialAccountId: "star_narae", - university: "한양대학교", - major: "체육학과", - contactFrequency: "FREQUENT", - hobbies: [ - { category: "SPORTS", name: "헬스" }, - { category: "SPORTS", name: "러닝" }, - { category: "DAILY", name: "요리" }, - ], - tags: [{ tag: "에너지넘침" }, { tag: "건강함" }], - song: "LE SSERAFIM - UNFORGIVEN", - intros: [ - { question: "제 음주 습관은", answer: "즐기는 편" }, - { question: "제 키는", answer: "170cm" }, - ], - }, - favorite: false, - matchedAt: "2026-04-16T11:00:00.000000", - }, - { - historyId: 6, - partner: { - memberId: 6, - email: "user6@test.com", - nickname: "꽃잎사이로", - gender: "FEMALE", - birthDate: "2001-04-05", - mbti: "INFP", - intro: "감성적인 대화를 좋아해요 💐", - profileImageUrl: null, - profileImageKey: null, - socialType: "INSTAGRAM", - socialAccountId: "flower_between", - university: "이화여자대학교", - major: "국문학과", - contactFrequency: "NORMAL", - hobbies: [ - { category: "CULTURE", name: "시 쓰기" }, - { category: "CULTURE", name: "전시회" }, - ], - tags: [{ tag: "섬세함" }, { tag: "감성적" }], - song: "성시경 - 거리에서", - intros: [ - { question: "제가 좋아하는 음식은", answer: "한식" }, - { question: "저는 흡연을", answer: "비흡연" }, - ], - }, - favorite: true, - matchedAt: "2026-04-15T08:30:00.000000", - }, - { - historyId: 7, - partner: { - memberId: 7, - email: "user7@test.com", - nickname: "하늘구름", - gender: "FEMALE", - birthDate: "2003-09-12", - mbti: "ESFJ", - intro: null, - profileImageUrl: null, - profileImageKey: null, - socialType: null, - socialAccountId: null, - university: "성균관대학교", - major: "의상학과", - contactFrequency: "FREQUENT", - hobbies: [ - { category: "DAILY", name: "쇼핑" }, - { category: "CULTURE", name: "뮤지컬" }, - ], - tags: null, - song: null, - intros: [{ question: "제 키는", answer: "162cm" }], - }, - favorite: false, - matchedAt: "2026-04-14T13:20:00.000000", - }, - { - historyId: 8, - partner: { - memberId: 8, - email: "user8@test.com", - nickname: "파도소리", - gender: "FEMALE", - birthDate: "2000-12-25", - mbti: "INTJ", - intro: "효율적인 만남을 선호합니다.", - profileImageUrl: null, - profileImageKey: null, - socialType: "INSTAGRAM", - socialAccountId: "wave_sound25", - university: "KAIST", - major: "전산학부", - contactFrequency: "RARE", - hobbies: [ - { category: "GAME", name: "체스" }, - { category: "DAILY", name: "독서" }, - { category: "DAILY", name: "자기계발" }, - ], - tags: [{ tag: "공부하는" }, { tag: "계획적" }], - song: "Day6 - 한 페이지가 될 수 있게", - intros: [ - { question: "제 직업은", answer: "연구원" }, - { question: "제 음주 습관은", answer: "전혀 안 함" }, - ], - }, - favorite: false, - matchedAt: "2026-04-13T17:50:00.000000", - }, - { - historyId: 9, - partner: { - memberId: 9, - email: "withdrawn_9@deleted.com", - nickname: "탈퇴한 사용자", - gender: "FEMALE", - birthDate: null, - mbti: "ENFP", - intro: null, - profileImageUrl: null, - profileImageKey: null, - socialType: null, - socialAccountId: null, - university: "한국대학교", - major: "(알 수 없음)", - contactFrequency: "NORMAL", - hobbies: [{ category: "SPORTS", name: "헬스" }], - tags: null, - song: null, - intros: [], - }, - favorite: false, - matchedAt: "2026-04-12T10:10:00.000000", - }, - { - historyId: 10, - partner: { - memberId: 10, - email: "user10@test.com", - nickname: "새벽이슬", - gender: "FEMALE", - birthDate: "2002-06-18", - mbti: "ISTP", - intro: "자전거 타고 한강 달리는 거 좋아해요 🚴", - profileImageUrl: null, - profileImageKey: null, - socialType: "INSTAGRAM", - socialAccountId: "dawn_dew_18", - university: "중앙대학교", - major: "기계공학과", - contactFrequency: "NORMAL", - hobbies: [ - { category: "SPORTS", name: "자전거" }, - { category: "TRAVEL", name: "캠핑" }, - { category: "DAILY", name: "요리" }, - ], - tags: [{ tag: "쿨함" }, { tag: "독립적" }], - song: "ZICO - Any Song", - intros: [ - { question: "제 키는", answer: "167cm" }, - { question: "제가 좋아하는 음식은", answer: "일식" }, - { question: "저는 흡연을", answer: "비흡연" }, - ], - }, - favorite: true, - matchedAt: "2026-04-11T19:00:00.000000", - }, -]; From 46d72bf58aa00dca64ef17493af73dcf6bacfdee Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 15 May 2026 23:19:35 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/chat-list/_components/ScreenChatList.tsx | 6 +----- app/main/_components/ProfileCard.tsx | 7 +------ app/matching-list/_components/YesMatchingList.tsx | 8 +++----- app/mypage/_components/ScreenMyPage.tsx | 2 -- hooks/useChatRooms.ts | 1 + 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/app/chat-list/_components/ScreenChatList.tsx b/app/chat-list/_components/ScreenChatList.tsx index 1374b63..65ca74a 100644 --- a/app/chat-list/_components/ScreenChatList.tsx +++ b/app/chat-list/_components/ScreenChatList.tsx @@ -5,7 +5,6 @@ import Link from "next/link"; import { BackButton } from "@/components/ui/BackButton"; import { useChatRooms, type ChatRoom } from "@/hooks/useChatRooms"; import { useEffect, useMemo } from "react"; -import { getProfileImageUrl } from "@/lib/utils/profile"; type ChatListItem = { id: string; @@ -146,10 +145,7 @@ export default function ScreenChatList() { ? formatChatTime(room.lastMessageTime) : "방금", unread: (room.unreadCount || 0) > 0, - avatar: getProfileImageUrl( - room.otherUser?.profileImageUrl, - "UNKNOWN", // 성별 정보가 없으므로 기본값 처리 - ), + avatar: room.otherUser?.profileImageUrl || "/default-profile.png", })); }, [rooms]); diff --git a/app/main/_components/ProfileCard.tsx b/app/main/_components/ProfileCard.tsx index 189cdc3..90cfc89 100644 --- a/app/main/_components/ProfileCard.tsx +++ b/app/main/_components/ProfileCard.tsx @@ -6,12 +6,7 @@ import { Send } from "lucide-react"; import React, { useRef } from "react"; import { getContactFrequencyLabel } from "@/lib/utils/profile"; - -/* ── 유틸 함수 ── */ -const getAge = (birthDate?: string) => { - if (!birthDate) return "?? "; - return new Date().getFullYear() - new Date(birthDate).getFullYear() + 1; -}; +import { getAge } from "@/lib/utils/date"; /* ── 태그 컴포넌트 ── */ const Tag = ({ text }: { text: string }) => ( diff --git a/app/matching-list/_components/YesMatchingList.tsx b/app/matching-list/_components/YesMatchingList.tsx index d069989..ea8f6a6 100644 --- a/app/matching-list/_components/YesMatchingList.tsx +++ b/app/matching-list/_components/YesMatchingList.tsx @@ -12,7 +12,6 @@ import { Search, ArrowUpNarrowWide, Loader2 } from "lucide-react"; import MatchingListCard from "./MatchingListCard"; import { getAge } from "@/lib/utils/date"; import SortDrawer, { SortOrder } from "./SortDrawer"; -import { CURRENT_YEAR } from "@/lib/constants/date"; const SORT_LABELS: Record = { oldest: "오래된 순", @@ -51,8 +50,7 @@ const YesMatchingList = ({ p.nickname.toLowerCase().includes(query) || p.mbti.toLowerCase().includes(query) || p.major.toLowerCase().includes(query) || - (p.birthDate && - String(getAge(p.birthDate, CURRENT_YEAR)).includes(query)) + (p.birthDate && String(getAge(p.birthDate)).includes(query)) ); }); } @@ -65,8 +63,8 @@ const YesMatchingList = ({ // 정렬 result.sort((a, b) => { if (sortOrder === "age") { - const ageA = a.partner.age || getAge(a.partner.birthDate, CURRENT_YEAR); - const ageB = b.partner.age || getAge(b.partner.birthDate, CURRENT_YEAR); + const ageA = a.partner.age || getAge(a.partner.birthDate); + const ageB = b.partner.age || getAge(b.partner.birthDate); // 나이 오름차순 (어린 순) return ageA - ageB; } diff --git a/app/mypage/_components/ScreenMyPage.tsx b/app/mypage/_components/ScreenMyPage.tsx index 47d54e7..91816ef 100644 --- a/app/mypage/_components/ScreenMyPage.tsx +++ b/app/mypage/_components/ScreenMyPage.tsx @@ -329,8 +329,6 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { const birthYear = initialProfile.birthDate ? initialProfile.birthDate.split("-")[0] : undefined; - const currentYear = new Date().getFullYear(); - const age = birthYear ? currentYear - Number(birthYear) + 1 : undefined; const { mutateAsync: uploadImage } = useImageUpload(); diff --git a/hooks/useChatRooms.ts b/hooks/useChatRooms.ts index 03852f8..4239a19 100644 --- a/hooks/useChatRooms.ts +++ b/hooks/useChatRooms.ts @@ -14,6 +14,7 @@ export type ChatRoom = { university: string; major: string; age: number; + gender?: import("@/lib/types/profile").Gender; }; lastMessage: string | null; lastMessageTime: string | null; From 38dd6a76223edf31d6822e1d70ea90bd55f1998e Mon Sep 17 00:00:00 2001 From: dasosann Date: Fri, 15 May 2026 23:24:21 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat:=20build=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/matching-list/_components/YesMatchingList.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/matching-list/_components/YesMatchingList.tsx b/app/matching-list/_components/YesMatchingList.tsx index ea8f6a6..5547951 100644 --- a/app/matching-list/_components/YesMatchingList.tsx +++ b/app/matching-list/_components/YesMatchingList.tsx @@ -63,9 +63,11 @@ const YesMatchingList = ({ // 정렬 result.sort((a, b) => { if (sortOrder === "age") { - const ageA = a.partner.age || getAge(a.partner.birthDate); - const ageB = b.partner.age || getAge(b.partner.birthDate); - // 나이 오름차순 (어린 순) + const rawAgeA = a.partner.age || getAge(a.partner.birthDate); + const rawAgeB = b.partner.age || getAge(b.partner.birthDate); + const ageA = typeof rawAgeA === "number" ? rawAgeA : 999; + const ageB = typeof rawAgeB === "number" ? rawAgeB : 999; + // 나이 오름차순 (어린 순), 모르는 나이는 맨 뒤로 return ageA - ageB; } const dateA = new Date(a.matchedAt).getTime(); From 268b1467255d84da7d0c261f28c3f176e1b8e9cc Mon Sep 17 00:00:00 2001 From: dasosann Date: Sun, 17 May 2026 23:39:16 +0900 Subject: [PATCH 11/12] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[chatid]/_components/ScreenChatRoom.tsx | 211 ++++++++++++------ app/mypage/_components/ScreenMyPage.tsx | 15 ++ .../_components/ScreenProfileBuilder.tsx | 15 ++ app/profile-builder/_lib/options.ts | 2 +- .../charge-confirm/ConfirmChargeDrawer.tsx | 8 +- hooks/useChatMemberProfile.ts | 37 +++ hooks/useChatRooms.ts | 28 ++- hooks/useMatchingHistory.ts | 38 ++-- public/chat/heart.png | Bin 0 -> 26685 bytes 9 files changed, 258 insertions(+), 96 deletions(-) create mode 100644 hooks/useChatMemberProfile.ts create mode 100644 public/chat/heart.png diff --git a/app/chat/[chatid]/_components/ScreenChatRoom.tsx b/app/chat/[chatid]/_components/ScreenChatRoom.tsx index b1af23a..cb56fc9 100644 --- a/app/chat/[chatid]/_components/ScreenChatRoom.tsx +++ b/app/chat/[chatid]/_components/ScreenChatRoom.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { ArrowUp, ChevronLeft, MoreVertical, UserRound } from "lucide-react"; import { useChatRoomSocket } from "@/hooks/useChatRoomSocket"; import { useChatMessages } from "@/hooks/useChatMessages"; @@ -14,6 +14,8 @@ import { } from "@/hooks/useMatchingHistory"; import { cn } from "@/lib/utils"; import PartnerProfileModal from "./PartnerProfileModal"; +import { useChatRooms } from "@/hooks/useChatRooms"; +import { useChatMemberProfile } from "@/hooks/useChatMemberProfile"; type ScreenChatRoomProps = { chatId: string; @@ -25,6 +27,7 @@ type ChatMessage = { text: string; time: string; readCount: number; + createdAt: string; }; const formatMessageTime = (isoString: string) => { @@ -49,33 +52,30 @@ const ChatDateDivider = ({ label }: { label: string }) => { const IncomingMessage = ({ message, - partner, + nickname, + profileImageUrl, }: { message: ChatMessage; - partner?: MatchingPartner; + nickname?: string; + profileImageUrl?: string; }) => { return (
프로필
- - {partner?.nickname || "..."} - + {nickname || "..."}
{message.text}
- {message.readCount === 1 ? ( - 1 - ) : null} {message.time}
@@ -88,7 +88,6 @@ const OutgoingMessage = ({ message }: { message: ChatMessage }) => { return (
- {message.readCount === 1 ? 1 : null} {message.time}
@@ -102,7 +101,6 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { const router = useRouter(); const [messageText, setMessageText] = useState(""); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); - const [isFavorite, setIsFavorite] = useState(false); // 0. 내 프로필 정보 가져오기 (현재 사용자 ID 확인용) const { data: myProfile } = useMyProfile(); @@ -111,46 +109,58 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { // 즐겨찾기 토글 mutation const updateFavorite = useUpdateFavorite(); - // 0-1. 현재 채팅방 정보 가져오기 (임시 더미 데이터 사용) - // const { data: rooms } = useChatRooms(); - // const currentRoom = rooms?.find((r) => r.chatRoomId === chatId); - // const partner = currentRoom?.partner; - - const partner: MatchingPartner = { - memberId: 999, - email: "winter@example.com", - nickname: "겨울이오길", - age: 20, - gender: "FEMALE", - birthDate: "2004-01-01", - major: "정보통신전자공학부", - university: "가톨릭대학교", - mbti: "ENTP", - contactFrequency: "NORMAL", - profileImageUrl: "animal_cat", - profileImageKey: null, - intro: "친하게 지내요! 😆", - song: "한로로 - 사랑하게 될 거야", - hobbies: [ - { category: "취미", name: "독서" }, - { category: "취미", name: "영화감상" }, - { category: "취미", name: "음악감상" }, - ], - tags: [{ tag: "친절한" }, { tag: "열정적인" }], - intros: [], - socialType: "INSTAGRAM", - socialAccountId: "winterizcoming_", - }; + // 0-1. 현재 채팅방 정보 가져오기 + const { data: chatRooms } = useChatRooms(); + const currentRoom = chatRooms?.find((r) => r.id === chatId); + const partnerMemberId = currentRoom?.otherUser.memberId; - const handleFavoriteToggle = () => { - if (!partner) return; + // 0-2. 상대방 경량 프로필 API 호출 + const { data: profileRes } = useChatMemberProfile(partnerMemberId); + const opponentProfile = profileRes?.data; + + const mergedPartner = useMemo(() => { + return { + memberId: partnerMemberId || 999, + email: "winter@example.com", + nickname: + opponentProfile?.nickname || + currentRoom?.otherUser.nickname || + "겨울이오길", + age: currentRoom?.otherUser.age || 20, + gender: currentRoom?.otherUser.gender || "FEMALE", + birthDate: "2004-01-01", + major: + opponentProfile?.major || + currentRoom?.otherUser.major || + "정보통신전자공학부", + university: currentRoom?.otherUser.university || "가톨릭대학교", + mbti: "ENTP", + contactFrequency: "NORMAL", + profileImageUrl: + opponentProfile?.profileImageUrl || + currentRoom?.otherUser.profileImageUrl || + "animal_cat", + profileImageKey: null, + intro: "친하게 지내요! 😆", + song: "한로로 - 사랑하게 될 거야", + hobbies: [ + { category: "취미", name: "독서" }, + { category: "취미", name: "영화감상" }, + { category: "취미", name: "음악감상" }, + ], + tags: [{ tag: "친절한" }, { tag: "열정적인" }], + intros: [], + socialType: "INSTAGRAM", + socialAccountId: "winterizcoming_", + } as MatchingPartner; + }, [opponentProfile, currentRoom, partnerMemberId]); - const newFavoriteStatus = !isFavorite; - setIsFavorite(newFavoriteStatus); + const handleFavoriteToggle = () => { + if (!opponentProfile?.historyId) return; updateFavorite.mutate({ - historyId: partner.memberId, // MatchingPartner에는 historyId가 없으므로 memberId를 임시로 사용 - favorite: newFavoriteStatus, + historyId: opponentProfile.historyId, + favorite: !opponentProfile.favorite, }); }; @@ -168,7 +178,13 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { if (socketMessages.length > 0) { console.log("🔌 [Chat Room Socket Messages]:", socketMessages); } - }, [historyData, socketMessages]); + if (opponentProfile) { + console.log("👤 [Chat Opponent Real-time Profile API]:", opponentProfile); + } + if (mergedPartner) { + console.log("💎 [Chat Opponent Merged Partner Object]:", mergedPartner); + } + }, [historyData, socketMessages, opponentProfile, mergedPartner]); // 3. 전체 메시지 목록 변환 및 중복 제거 (Derived State) const messages = useMemo(() => { @@ -189,6 +205,7 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { text: m.content, time: formatMessageTime(m.createdAt), readCount: m.readCount, + createdAt: m.createdAt, })) as ChatMessage[]; }, [historyData, socketMessages, currentUserId]); @@ -205,7 +222,7 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) { const isSendEnabled = messageText.trim().length > 0; return ( -
+
@@ -221,13 +238,17 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) {
- {partner?.nickname || "..."} + {opponentProfile?.nickname || + currentRoom?.otherUser.nickname || + "..."}
- {partner?.age || "??"}세 + {currentRoom?.otherUser.age || "??"}세 , - {partner?.major || "..."} + {opponentProfile?.major || + currentRoom?.otherUser.major || + "..."}
@@ -244,23 +265,75 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) {
-
- - {messages.map((message) => - message.sender === "me" ? ( - - ) : ( - - ), +
+ {messages.length === 0 ? ( +
+
+ 하트 +

+ 내가 뽑은 사람과 +
대화를 시작해 보세요 +

+
+ +
+ ) : ( + messages.map((message, index) => { + const prevMessage = index > 0 ? messages[index - 1] : null; + + // YYYY년 MM월 DD일 형식으로 변환 + const getFormattedDate = (isoString: string) => { + const d = new Date(isoString); + if (Number.isNaN(d.getTime())) return ""; + return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${d.getDate()}일`; + }; + + const currentDateStr = getFormattedDate(message.createdAt); + const prevDateStr = prevMessage + ? getFormattedDate(prevMessage.createdAt) + : ""; + const showDivider = currentDateStr !== prevDateStr; + + return ( + + {showDivider && } + {message.sender === "me" ? ( + + ) : ( + + )} + + ); + }) )}
@@ -293,12 +366,12 @@ export default function ScreenChatRoom({ chatId }: ScreenChatRoomProps) {
chatId: {chatId}
- {partner && ( + {opponentProfile && ( setIsProfileModalOpen(false)} - partner={partner} - isFavorite={isFavorite} + partner={mergedPartner} + isFavorite={opponentProfile.favorite} onFavoriteToggle={handleFavoriteToggle} /> )} diff --git a/app/mypage/_components/ScreenMyPage.tsx b/app/mypage/_components/ScreenMyPage.tsx index 91816ef..f204b2a 100644 --- a/app/mypage/_components/ScreenMyPage.tsx +++ b/app/mypage/_components/ScreenMyPage.tsx @@ -340,6 +340,21 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { return; } + // 나이(출생연도) 검증 (2000년생~2007년생) + if (!editableBirthYear) { + alert("출생연도를 입력해 주세요."); + return; + } + const birthYearNum = parseInt(editableBirthYear, 10); + if ( + Number.isNaN(birthYearNum) || + birthYearNum < 2000 || + birthYearNum > 2007 + ) { + alert("2000년생부터 2007년생까지만 가입 가능합니다."); + return; + } + // 닉네임이 변경된 경우에만 중복 검사 if (trimmedNickname !== (initialProfile.nickname || "")) { try { diff --git a/app/profile-builder/_components/ScreenProfileBuilder.tsx b/app/profile-builder/_components/ScreenProfileBuilder.tsx index dd3042a..996f346 100644 --- a/app/profile-builder/_components/ScreenProfileBuilder.tsx +++ b/app/profile-builder/_components/ScreenProfileBuilder.tsx @@ -147,6 +147,21 @@ export const ScreenProfileBuilder = () => { }; const handleComplete = () => { + // 나이(출생연도) 검증 (2000년생~2007년생) + if (!selectedBirthYear) { + alert("출생연도를 선택해 주세요."); + return; + } + const birthYearNum = parseInt(selectedBirthYear, 10); + if ( + Number.isNaN(birthYearNum) || + birthYearNum < 2000 || + birthYearNum > 2007 + ) { + alert("2000년생부터 2007년생까지만 가입 가능합니다."); + return; + } + const normalizedMBTI = selectedMBTI.toUpperCase(); const profileData: Partial = { diff --git a/app/profile-builder/_lib/options.ts b/app/profile-builder/_lib/options.ts index 0776dbc..88f372b 100644 --- a/app/profile-builder/_lib/options.ts +++ b/app/profile-builder/_lib/options.ts @@ -12,7 +12,7 @@ type MajorCategory = { }; export const getYearOptions = (): Option[] => - Array.from({ length: 11 }, (_, index) => ({ + Array.from({ length: 8 }, (_, index) => ({ value: String(2000 + index), label: `${2000 + index}년`, })); diff --git a/components/common/charge-confirm/ConfirmChargeDrawer.tsx b/components/common/charge-confirm/ConfirmChargeDrawer.tsx index 3509fc5..902e670 100644 --- a/components/common/charge-confirm/ConfirmChargeDrawer.tsx +++ b/components/common/charge-confirm/ConfirmChargeDrawer.tsx @@ -91,10 +91,14 @@ export default function ConfirmChargeDrawer({ const handleConfirm = () => { purchase(productId, { onSuccess: () => { - alert("충전 요청이 완료되었습니다!"); + alert( + "충전 요청이 완료되었습니다! 토스가 설치되어 있다면 토스로 바로 이동합니다.", + ); setOpen(false); queryClient.invalidateQueries({ queryKey: ["myProfile"] }); - if (drawerContext) drawerContext.onClose(); + + // 토스 송금 앱으로 즉시 자동 연동 + handleTossTransfer(); }, onError: (error: AxiosError<{ code?: string; message?: string }>) => { const errorData = error.response?.data; diff --git a/hooks/useChatMemberProfile.ts b/hooks/useChatMemberProfile.ts new file mode 100644 index 0000000..b77ffd8 --- /dev/null +++ b/hooks/useChatMemberProfile.ts @@ -0,0 +1,37 @@ +import { api } from "@/lib/axios"; +import { useQuery } from "@tanstack/react-query"; + +export interface ChatMemberProfile { + memberId: number; + nickname: string; + profileImageUrl: string; + major: string; + historyId: number | null; + favorite: boolean; +} + +export interface ChatMemberProfileResponse { + code: string; + status: number; + message: string; + data: ChatMemberProfile; +} + +export const fetchChatMemberProfile = async ( + memberId: number, +): Promise => { + const { data } = await api.get( + `/api/chat/members/${memberId}/profile`, + ); + return data; +}; + +export const useChatMemberProfile = (memberId?: number) => { + return useQuery({ + queryKey: ["chatMemberProfile", memberId], + queryFn: () => fetchChatMemberProfile(memberId!), + enabled: !!memberId, + staleTime: 1000 * 60 * 60, // 📡 상대방 프로필 정보는 1시간 동안 완전 신선(Fresh)하다고 판단하여 API 재호출 완벽 차단 + gcTime: 1000 * 60 * 90, // 🧠 1시간 30분 동안 메모리에 든든하게 캐시 보관 + }); +}; diff --git a/hooks/useChatRooms.ts b/hooks/useChatRooms.ts index 4239a19..7dc76f4 100644 --- a/hooks/useChatRooms.ts +++ b/hooks/useChatRooms.ts @@ -28,23 +28,29 @@ export type ChatRoomsResponse = { data: ChatRoom[]; }; -/** - * 채팅방 목록 조회 (전체 응답이 배열인 경우와 객체인 경우 대응) - */ export const fetchChatRooms = async (): Promise => { - const { data } = await api.get( - "/api/chat/rooms", - ); - // 백엔드 응답이 { data: [...] } 형태인지 아니면 배열 그 자체인지 확인하여 처리 - if (Array.isArray(data)) return data; - return data.data || []; + console.log("📡 [fetchChatRooms] API 호출 시작..."); + try { + const { data } = await api.get( + "/api/chat/rooms", + ); + console.log("📡 [fetchChatRooms] API 호출 성공:", data); + + // 백엔드 응답이 { data: [...] } 형태인지 아니면 배열 그 자체인지 확인하여 처리 + if (Array.isArray(data)) return data; + return data.data || []; + } catch (error) { + console.error("❌ [fetchChatRooms] API 호출 실패:", error); + throw error; + } }; export const useChatRooms = () => { return useQuery({ queryKey: ["chatRooms"], queryFn: fetchChatRooms, - staleTime: 1000 * 30, - gcTime: 1000 * 60 * 5, + staleTime: 0, // 항상 최신 데이터를 유지하도록 설정 + gcTime: 0, // 메모리 캐싱을 아예 하지 않고 컴포넌트가 꺼지면 즉시 캐시 소멸 + // refetchInterval: 1000 * 5, // 5초마다 주기적으로 서버에서 데이터를 갱신하여 상대방 메시지 실시간 반영 }); }; diff --git a/hooks/useMatchingHistory.ts b/hooks/useMatchingHistory.ts index 0760782..119a91c 100644 --- a/hooks/useMatchingHistory.ts +++ b/hooks/useMatchingHistory.ts @@ -64,18 +64,29 @@ export const fetchMatchingHistoryPage = async ( page: number = 0, size: number = 30, ): Promise => { - const { data } = await api.get( - "/api/matching/history", - { - params: { - page, - size, - sort: "matchedAt,desc", - }, - }, + console.log( + `📡 [fetchMatchingHistoryPage] 매칭 내역 API 호출 시작... (pageParam: ${page}, size: ${size})`, ); - console.log("Matching History Data:", data); - return data; + try { + const { data } = await api.get( + "/api/matching/history", + { + params: { + page, + size, + sort: "matchedAt,desc", + }, + }, + ); + console.log("✅ [fetchMatchingHistoryPage] 매칭 내역 API 호출 성공:", data); + return data; + } catch (error) { + console.error( + "❌ [fetchMatchingHistoryPage] 매칭 내역 API 호출 실패:", + error, + ); + throw error; + } }; /** React Query useInfiniteQuery 훅 */ @@ -89,8 +100,8 @@ export const useMatchingHistory = () => { ? lastPage.data.currentPage + 1 : undefined; }, - staleTime: Infinity, - gcTime: 1000 * 60 * 60, + staleTime: 0, + gcTime: 0, }); }; @@ -155,6 +166,7 @@ export const useUpdateFavorite = () => { onSettled: () => { // 최종적으로 무효화 queryClient.invalidateQueries({ queryKey: ["matchingHistory"] }); + queryClient.invalidateQueries({ queryKey: ["chatMemberProfile"] }); }, }); }; diff --git a/public/chat/heart.png b/public/chat/heart.png new file mode 100644 index 0000000000000000000000000000000000000000..4453c5e5e347960e479f20b68bdbe44fb789c998 GIT binary patch literal 26685 zcmbSyQP*gR&3iD zhq_(gfNtlz=*_uB^NQ6xM|)dJ^7IY2(cGEmdW{`@+gu&%1rG^dxvo|iA6lW>nfxw1 zo6|cSQ`Rh-aecm9NYrrV;Keh^v7ghQ4zmscj~s$6OsVPi3tHZS(6 zES7gcP__=N6jdxS{uVsD9>mK}{g+;rcKGn;{zvJjh8V@X7Oa#LPV^a0xwgt)f{u_V zFz(-N9_PMSgxx1&fFkEpUsq zghBk_OP8@AN=Bvl`EVf5KcOJ2T3oPF#IEwKs1g1exKEw5>| zz-ztM7G|uR+3E40%el<5_5CLq)hpUjOQ!Y(WtLYXKzIWzy#SBRZv@A#5H14=lIuE= zUL#&$%NNO?OAz8>v*MOpufGD~?$~&Z0EV>`s6+}^hw()-#~s*@9~N6YhE&Hr(8b;# zyzYSGBO#96Hr-HPzab7eFT}wS3~YiMo4xLj_*uRVJ(~{Xe6y>UY)V>B5xs;NJHI`~ zR-|D7HwzWWSj6BOl}go$#4#irk;0szi{(woXva_q4%e~wl~Z0vr5s9AOA}Tx6Om(p z*U_(z-<*FZ!DWwCGeu9`=@{JB;;KpK=qMC^E#AEw3Ti~!y}_IeGpt2TO0y{sm^50s z$hXaiT3+F7@Iw=-;vL@M6i|79MGwJX{!0kWbdapwR6b>FBs-=qvu8hs*CNjS=fg*S z&hRCD?%4m#z#Zj#AZ@cjHT@_+){yf)7J4V`Xy{JLE(wtGA=cr?I$AzE2)k;m(1^gM zGUzoL0QRZB^=Tk&?vbo>LEmrQ5`}#S+j9paAGyuoJ}iZMrWyF2Vc9V0_9GUa8*n3S zr!q#BfYN<_DjLJ36a-NX6qt~`C``x5L#zRb|kw!svV}qle z4I`Di9wTCy-4bClt5O5F7A^CbCI` zEwYbf^{H=v4-Qwn5DA4&=^DIuZJnO~BQ$96u4O+yJRf|c!|kXI^gpKS@WYPqd_4jO zS+Njl#SeGF9E$H}WM;eHJbxBzb^_Vg+<9vZv>O%`+!CJAi+o<(O}gWsV5Y#sAA;2w zpX8n03bjw&{fi=+>qEzT{*1>b_*}v?_ILLmj?FZ?rX1= zvJl)px0&v%h~NrHmhdv4GBjHY@_5Nqh?TO}7QG4iewt79E$aw$Uu5^n9B9K{!bNU$p`77aGSn^D~ z`VR;z0kP1;1+k)=+DGTj8;T!Z@NGgv8I*$CUVw0CgzEsmRn>`(l59eeRDTevmkS6W z)ok$NFNIJuj}C%=*NJldrvXu!b)7w*zL|YJ{DEf@eg&{zG1~yfC>hq}gEr0^AtoYw z{q&h!&W1h@y16}LquF%h^L8cp3TmxgjvyrVQO=0ogo1~DXU@UNm`yLM8LwnsqJ{5f z=47>E4YHf8nLneTSEg)vx7ZtvwHm)t_vbGVkH46Gz`)_UbgzDT|LW%XD z^IcRvWvr{`*F`Hx_H8q2Fpx)w9i-gcRm-%UkTC3Zp6lR4*NYMVBFAgNrZ0KD$$JEy z{6Ih$gQkeb>gVCPLIZrrtRmKs+skTps+?oD@H;*Bp$qDvi?qZqAUc4URWNcfT6X;3 z{7s?VzQ;f9iiUOabEb~PB~ZY;#3{D+n2+!erUI}kNfPtHjP1m25uVD1lN;`m%vWhc z=a$Ecv|Z}yu+09zvT8PXPV8D3*1R8Z*QavSA9K%>!geP5kq24(v2E+Q^{MH=`9X%a z^yp~tP&~Ju{{b1Jgxc|GmU~4dhXPJ=Ek(5ZD&R z_2hh8DjCxiq8chuunj-vx1TMlo#278-}c-upv$ z)kpqz%N|qzRXKLw$K^j+?BU><2r5DThdSRtx*p&4Nm!TS^4K$Nw;H*A$GET1yoATi z*aw^*Z(sgnj8Vn6MDEkyf^VJ52c7&xIV6vqS3f$SVLD7 z5c!_$P1tf(3z5r}&4pf9!1%8Pme%;+zxPH?-z)0>3qbKcpjz2u~uRy;C|$F zWWzHBU{Sc&=}h1e}B6Fi^nr|D|b1Bm9i*RZFwVxyXIEr^x7S*TNsS~d@zvA@wg`2XU`9O{PaxEoc|e@p2uHeHudmu{X55Wo*QW> zc0T1%YT4sMb<}~W%BbuKd*sUe>AFO}(o0Rwf&R+lA7^i^lJnkpSR5S_lIde|s@Ovt5z-#KwhPF&37&84JLPSqrhX>rc9(NB~fREP}8h4@lM%izUFieQe zuHAm3wcmC4wjl6*Kys0#8??}rh+q093%&#iXgfT|ri^_{U)WZq@^3G6P3{*CX#s(t zm}H60kisYua0f6y?6jlujP}L5Ig=vI>S7tdo6<|nNZzRM8R-Owe3Eh{)|jGElBDbl z2WMFmg)TdVal%#N=M-XwD|f|e1gseJAm#@VTJ86&dhb~wBl0no@x-ycyz}KshJj2D zUB=-|vW3Gum@1A+?53|m?>~6LPigkwFB4?uJzyQSEZ<#L_}@)YHP3I+-^p=Zw{L## zdf~ykgP2`GJFAfV_&rEhfl`o1hsf+$tRyNd2_x%5HKUucJca_eH-ql-!)OhHz*hGs zw}gg|b>8e#qqzp(v%?S{C!Fip7Pk$ev|6z83&TKJ{+&c80J!xd7fu;h;x3-kotwwl ze2uOH{mOSAex~=dy-%(8y_#E(ny^OL`{)I_~{qhO7JV7Kz<{43la0V~*2 z;Mq0KQXu~%vSM!|FXFLFnQ7p{^M(D)rCiepD9QA+V)8yJo{ri|t@AG?Ur}-|DM5T% ztt7bO0={!i)3NG$OCMv{1P7GN<|kk;C*IFf#9J7pJqeg;mSivq;{4DJza9!WkwJ~T zSiNu2xM)7~y!1z$F|$Eq0b>d6Sv4_d&#eBYJ~eY+hvV30x^IAan8`=T0s<#3)gDGqf4NS44(Bj@yKcZ`L8v>mfTf@aB;RL2Aaglg z!CPg%BmY0ZJ$b7sZ4|ff+}zW5$JoE;Ezh;RpOmVHzMduk-!6h#9$M;zaJLP-8Y53W z5v~mdmC;&vLi1=HPJEKqF?$hY;J8Hp@H&Nk_UYVL1ybd)G#=WSEjx*rL%cWcAu5A4 zXLsge8gyzE0bhcb{TqptLcxs_jnA(*bF#&Z3+WbX!V6-ZNGA+Ji8uTkysuxjrAwPs zcClyA-~4~>{&Juf6ukZsDS%0;C@`7yAPn)_Mj1{ynStczxgl9Ov$Basj8e=)jG$yt zK)@vSVNOvW@la;sQNVB(CGDrmdxpVTY-Ydr@=Ko=J(Qr|Fld23zQjvHBqemgJ+`f= zpI{+Fkme+vZ6(9e9joFYJF#l%Gfkquprp?s^0`|toubnS=<+yq4b&ZbgI!J-mqU4= zJU>i)OZ>ZwyssK?7_+xF&(&oGZ4G$jWvR^|>745z5GLV=p0b>az;Dt@&|+c*YrI62 zexiAv&j%j_v*c%+O$H1lL`X`1iNn~TNs!h`JH@%DK_%$7TcSVuL+`#_@$k_ug0Tnp zAU!}BFYIl@))m%8EPYfqTXS9~XXa&BT%i(DxJniX?2%go9HPX=|NTf@_{~o*G-U<7 zit*)FkOc-i0m)1yLz^x$z#W#7O2d$n2t?1T0b=RrjN<3u)fL#V9&Y3*BA#Ke4SAQ(q>x$hx z3X;ky4|&+%%vtfEK4`vA6g5U(41xBpsyrtrr$&4m)EkL>Ab`V@32wftNA1Q4K7KN= zc~ljc@W;qcS_nt+i2Q<@2P~@<0dcJYJmD-VFFVn4Eic=|#teq`oFVcva1XM)C4-L_ zmIDc%-1a34I<*nQ7DI(ns>m|&CiGAFeDE0o-Xf~>=?)K->3vHP%4wv;!|Q{*Cp4eB zupV`Oxf+ICj81#cQ9*h^?(s?XvYwZ`7&_oYLCUJ6c!B&0wMXH~1UjA`q=Vh$r>)lo zX(++C%FrKcWzUUK0hkx&wQ1#p=-g^X?`#@;^U-m?eM+4!y|o|K6AT zkU#ftJr<|?fhs8Skb_7rfw&n2H?;cMp5HgIFye33V}V)Pl#Z%!Cx%=yt1NSQa8&;9@QYBgr5(p6GatULP$&jGL;*A$Fo~`?l1m8}JtMRi_&1|1>DhF{ z{zI-TlF)Q-u2HFjU^Yz^NM%~TG;Mb|c zXST(HS%93#Hz8uc>NAf|23^w4j24ca;jIK052v3>e|fCd745hSwYpv~Yv5!1LWo6b zmax=qSvnc9HeCch8BJzJfRbaz9RaJ5Vt189@U@~|@%o_oyUFD!?`{CFK-fWCf`Z23 zcRGmd|KvEuWFVMCgv};gKm|*>$cjBrF{9!C!Xt(L=2Lr~hvJzB5I%g^;I~YGo)+hL zq{B=t(OUF+G8tumu#^bP4Rp_mn~%k&Ax-_$L}pX%KU{m5BF?59%f3lg9&EGQirc;S zPbJ|ng>4`jD{h+JmajYfv+Ygn^c#?kYtt}X1Qa6%-{XTNv1V*GTf7F%{1?@*L^mJw zXF4;KL2}D?4wrhQ(S#tT-#t-*yM@6`^O{uog9PEqcx0sHIF$&v;S9oZrsAg4ZR0?y z^1FmNBSY@kx8VJCK6!{Yxs>C^ojPup(Lf%|mUn67Pjb}8A}rff+)n(Dv!CwVx}+T#Px%U_MZH==@hkz2#Imo8<2n{9B7df zhS;#L*9R$WyRreDyYT*cv+x(X!Tn7$!P}rwbnHsb@7M!>MRmiOq%snXaRU7GoR~zC zcd|%$;k=G$2l+I!AY#o3lKBp}mCQCp){c$zGxc}LbCwAydC0O`a(gb1X6=e(ja*NL zo1o9NqJJYG}e3- z+{!?g$E?kExBF~x!SAj|VDC99cLYO7sU2w;FfO0x=d`f5^D)A;j$l6Db)l7i_-VE$ zxu_tLfF+hAMJ67=Jqc*_E`kMxW2?J@j-uA}PFM+faqhlb6#=h?c&Et9Vu04;HH z$s;a9b%|L z_tXg!O=E}*j|L|A(8(#b1vKN%9+6!9#BzzArdK*ivDT=_=uT>f1eF{Cp_W`~yUGo> zT>fQ$43!H`sUClyl&}@Xn<_+5F*cCE7+z&t&4V@YsKjZus}icH5u0YtlCEPTHHC{eriq z2kncTZ*SpF%+M060)iTv#JMp}u=-K2Sv0O^Vv^A-&zfF_C{L@y1bC{+Qo6{Q)R~0{ z1Rzd2$vBrL-{CQB&{t(Mgr%&tW~D6KHIdcP6cZEKIBz7flkf5;56TMM^4L)+wvEj*mTYr#JZ z$W(Ce`BNo$y=FKt97NBVfw z`_@brGGZT?dS8eoO4e~TX-_T3j;wJ{ea)f!P$C|LBUur0Ob`Br-{!euI5uxayp$Gw z|M-+C=8M5gr|uZ3pTzs!!wv z8Hj|Ksd$M4+W)rWpGaw>+v>jUeKX^H)|K>fz)toXsH$w0dh`eJ93r`b9CtGLUVUwD zQuGAzd_HJROjl`G@tm8}IxuD(U{Ud;$*47?!?NoPa`Y1uCdcN9%ud7e83;kC>sKz#AC(Z(l>{9WX@<&NN%79{W=Qwl;02>Bk( zLbc#wn!co|*?QxEjg{UJ0HZhv8p4O(k6%zwLye{dj`zF2a%ti3xwnrOqt4m;-M3HM zI{HC!JRJ_SXlz0K?)|<|#uyaMXVMb6yxNr|(Xr>ChOn%}R$UCh;XJEYN5N{iS>AAy zGr`~Ml^CcKvG|JHK2fdlml)?(Jyu})Q@Z-V06Hlo2`kHS)=J*DtRs@XJT8k58JQ#_ zS;C54({;hsm45aAT|FcUr6;SPYvf7eNZ(v+SYH~6f*3cJ;Cm|;;p2Bg^Wds=MIAx3 z2_G)wTJbxrJI>dK%f}@Bzl@$MUO;Rdx}je_q_E{@WQt$u&V~Pu+hz&IIDJPQ7CBie zOp+eECu}aq8Ue2=5MeYi$;f#K>}XgQkM?@~Z-yOPzmOS(F!!F0QAaj6bM&9k5Tupq z(*c(gY~sxL#hO}z_9VH;oD?(m6yjUyDXW~%tS4_$Emc&uJB;}x{ z|IVS@z~)>sMI$3Q+T;m5Oa~M6%qUaC)E^Z3a^6t13KkW)@diwwh49O1Q)Swy|1!>i z7w*f@TJGa{}}Jhf1!F{k24(+0!5Br}Iz+fy&g)yvy+@(qg6q#G&yZbbp_9165O{Ic?px| z#Ew(xv(E5oi!Cm+pPJB{{Q*1W58t3xf{l=UwkPT=Ws}qH>D-R(-0R6yJPGpMK2$k> z>QIWoSeI=%`~DuD*4cl}`}gXw1ee-Y`0rQzO#PY*dL;|pIZQ&u0J$c})L?Y-AL(+Y z0k`;7#u0@g{0?^Cu6im<##m6s$ZzL~;_XEm)(bH4+Y9hlSgf z$v;q+Omn-UgZB6cpbMI%0OH!(*k}!~B%TUOI;t_#D zV@02EyMy56?!$kz+ixN~8f}5_wK$TaF}$o#zT*y4z(M<)k7^Ey6HB-|avu1J@qLVw z7)A?-oLbAr;l31Pp|Dn8@1neJm6<2kui*n*Xx02S0^#sFY+= z5b0mk*}Jje4P*kx-zZzy9cNNf*9xcTtIb%5KkY#`74-(yj!`{k?6?s$crmmFD|!(>ox-IG?cLb=_azB z_mEOgdPoX48KNUwS_m~UdZ^Fi_i*BJcb)#z#ifUr*W~eHj3j-OsLAg^q9ft8$I`V)TSrvjrgQ z`$F$Rr^##9&fHUolqN}W6fwg^NKg@scrpcBx;)9pw~i@*{{@RrgV;Duk1%QEVr_x)5#fowu zf}c1+*R&fFxJ)B_es?k`Ed4CO5nCxFcMv{>JQe{eUM7u|ENe$RpFCcpViHX5YJdMt zQ6}CzD5g+4s!_t1@Z|=Jb(D8O?u2|ArUz-jp7mIzpoAz#+3?cmR97(M97uMnx_pS< zfB({^m{kKdj=EGxkuFaKLgCj4Tk)habknaq_nvUB=qc*iNMUOhHm-J?+U+$I&nB5( z)j7BW$=PSVYO5suGOXNAh6z;pefhJPDd}4IFykW2s%}ei%F;6aLJ}O9E8U-$B^n=b zU+i6s;m*7Q;XT+2>IT<{K}5T6&QQD(*^69?NvL_os5-s{QYwSLe!LE%7%~k>zC4Pv z^6ZT>>8P-+IT#xo2JTDmR%|CK7-oOnj|q*iB%n-=Tt|jw z2bj7SZXh?k(qQ-(iF?X}y`l@x(TP20k*fMFWU{IP!8$TWjigB3`~+R~$KZndJpmm- z%)EQU^$fY(m8>fxo4-cpLo0g|7j@G0AxJ%Uibws6W-mmps5Dsy)w{^pZ7sHOx;p`( zShC%5MwzXP;qhGxgf9Aop4Zqd^w*YB_*VY!q?zG^$Ov*0uYjxD%@_(}&=rekZ+n)- zFliX7EYG8hU)e3R{Fp0!QYw;p&Z@_1&3<<<`_MG;Zhsx3GUr`n=`GxNa_CZ`DV+w$ z6{u&Q3EM;R1lKqKTl;-?o(I(1i&Ed2FCi#Q@dC~GVPF2siaMJL}B zr0}nvR3`ae4ml88gv>T0X&%`wWhSoR#^B&dobz^+D(V3*8U1Wwygz2DPZtfP<7T5x z2GbIAdDE`W7$`r#m8(42|m+dLf-A}bMus+O5NHItY9~HeVITMLzY`i1h z;)D&7g;B{SU!Fuwe2ns7Ey57r=1;|ZVi4%f zI>qTXT&3Vbb%v@K)mE(n3}FK&{X>DAkUuE)M(WZw1S27F5HPVUm;PoVFD4u>U6j>e zaz{)8lXkrlC$2zH$g(XuxK^PJVJZ8AEEt5SXK%G!%n)rDV)XfGfKoNC6Q%B#wp>G? z#Y>v}q9VQ_6>h{D3Mvti(+w#OM)DRUgQL(cAO=e)Q5() z$AmmXCrr$*^i41%eF71snEr=hs%-i0X++g8^I=-1fYg?Y4p+6sn{{wDM`^I|JhjlU zzQNfV$vDNL#o>d2Ns>Y%C=dB0Y#aF|=p?O6?rPyN;-u`-;Zh7Pe2q%6d-d$J9_O)I zt&v!VefcccGvAN>u>Zd_P*b-ty%T0st0&Q!*3u!HeoLu4wTxOm?)kWutK>lID7F;1 z7mY+quOdKp$|`~e3)8%>zj+&_3>TIJ*Y?JAIugg#grCf#BN)cx;Hee1Smb$ zZoiY1PmrXi?)}c!Gy*ek&Rux{Jf$)o4l@kIcr$S3_&pco?qb{X!6!5l#1ktV*by>$ z(b4gzAKE}D_?kQ9i(}5n`F4JQVwI?ZK8Rvq>?=2LyFm;oWlcq6G3zP;KmAK}6a#!1 zl)YBNFR_eOXeSDVP-FxoTC$#q>t=i#bA0aRET!MEF^5go8P09UQNrZW*J8F0E3tipqT^e7t` zn0v4gF3BAYinB(SF8|2;)>Y@wrP_)TYh{NsJCFO;e!mEZ3KQpX4j73RDL%Ta+W7#O z)+a&TZeXe^%Qr=MS>EwQuRzd40C1J(f~Nr+E0~?l_i0bU09lvXK*&vjn7SvY$JmT= zJGYQ7HRrX?m~IIH$6G#~JdRsZ88+s!=x2=d=ytFU_U%dOq(f0~y)DOc66O|YV*QqL zKkgw9wJ?vR?WRzZjM_c*q)uRDmTkAP3vU%~x)mKAwgeWOwjw)p3twA6QXb$KYTc&} z_dwvU92(}gk+(cnR+$_mDDXJqOb1e=S0#}JlpVDV1r38<)rcZcXl&H81%Z-}+NZg7 zLGE!^X(L}kp33r9s~m@a|9m)@LcC!3YIfMP&?1?@Bv< zsD+5QfPP-C)PrTL=p~$LbUXAtb0}ND*eWgTj#3P zA8p!o3G)RsNkyk-3tDgzvOCQ?@e&?C)h9h8dP#P%B8t#$b@@XURixCVT9-?&-<>s` zkzhy{Jdlyqug_qVk-nMF+SWU`ICZ>~&KiT-u&5++fSaozHxG z5U7vbMUq!9wOjnrUh99eFny*VqND;&7K)@P98{33!Cj-K)JK3OoRHS!wCpci4&$Nb zGRZz3S(;BcHY#%J+L4eY%U{FJ+&ej|cWKC;Vj`~cpck0b$%Q5{XDxfGOA14)Wj9tr zDx}QXWRF?qp>28QpAiF>-0%%@iysGyp$jF7k-@UqKt1R%z$dde$~M~c-KRBARza0I zE&Wp{21gnMhQS?RpErEqCOM)leXA1wbo3!OpB%Gt4(f2CYPm#JLr^Z%%EWqgH~&&j zRt#4;4RJoDjoqc2cs%=3!_IB0{W1N#w9wn~`Si*y?!xty;*daJqn5?mC66 zo~Gd7(d3%;L+a-9_HPH(Sz@6O{(S7OM|ZCQB=c>nR2(imdW z&;6>BYq_LFe$d{<~K3BL9k7AL}9u&CdWvI%9OhN>#^vP%}ufsXtT2`X`|3y{?8 z1$|e;zk|UjAj>3Je1gevWtV40ew3OiK=q=`kzT6D--9Cw>UU#}s(G$JFVdQVyFGh4 zC=QcZ=@IdI0kgR!%Tb?X z?2`%$X>rx26Vfx^DbF8K+f-f3Q6C>*>vv?DqzM2GlUZlX^(}t2-1s}f@ZTZAGG}qC zGA4|sJyjZ+SUOtrt%w0RwNkXF!Ktl`{eAa2x7`wLWk>Q|t4COPS=Jxa0EQd3G2`a{ z$h(lG6DilMw9@LVWm8L*Tot{k_Z>Kn(UN?O-+u(o7-!D&fieL_$10VmJffKJwbHK6 zoi4DZGMDJI-_omImuE|AYhPyK*SWXyHYrcu6Jc*2CUz(uLrj|qGAPser@M9BQz+nL zYWriHRB$^B9vh*tA4;+``87z+b>p+diwN~kAbFOg;MV{Ec?Mn1d>|$5%6QUf_{m_J zPsooc-U9F^(`gX1bMASF(Mv6S%(aX+$R$P!PU>>xp;nGD1m>Xck(-vi8YUYue%dnl zHT>wqUAk_(TjQ*P&c>XgdeC=0`DLw23JyxW4g@NsH#IJwMs|KHVRMmLo$ker)kBXu zRhLWg2sXr2avDMFQtA)xDRIb=57Jw(EJ*Q56l_Njg1HeCz6c+0LpG<^|LtBX11DjLN06!aF9T8`i!%aes$%DUAms8aQWaq}BJ(bIthb2fC@ z{IMuq#7azeTejI#R!|76lnr9O^?#IW9X6Ic>J-hgBzw3Am?}`oO~1Lou#g9tGME?F zjb6$~IE#YwvRVO`IGy`jceII7IZ(_MH3d2WiDvuMyjTK}Qf&3j#NeqZuZgwgopd%CkX-8Wli@bu6)w#f4MEfMT>op`ZxKb`nL0Lt6(3kRz{A5>+*c~N3 zM@l^Eae?2GnZ_dDeDv)J%%&4Ib+|06kz3y3QTjAe38qE#Uh8<9)0U}#>Utqd`YC)~ zM0|L7QuL53e*ncPBV}c9O8NLwCa5w2Wu3ccdP^bpXZF;w3^Cf&Y@98_g$mB9)aZ!9 z0WGr=WgX;yOJE5p5M=c*Kxn`#gPUAqy;|1l7})WvYe_o;Qi@fnnyfqiD4Cfh&C*#T z3qXX3(o!L#6fVw@7vlLc&@yE0d>(OBIWG5^eZWAtBSkA*m67V|T?v#g$P;dBp!{RA zA`U5DO6raJLXA0lj>4b%6@jhQS{ZQ>>w}__Q0JQa^?d7JcxBNPU z*;=##QqNv(J4lY}dviRt!jLTtJ!SV{=i;BGIQA|gWUJDi4VQbCmaw6B(BLqhw59S% zBQ*Nt!GuS=dL4)p$HR1SIYWwEuK}ic4L^ET%^)%uRPoPC36-pvqi=lfI8uY3q<>YQ z1h{*vn^p`$sJt6B3#SHsg--T%dAh|KsUj|3x{Ds^sGQ~%WrNY0l^hD+31qQvW$w{5 zi)$_X(ZGG@lJQ!N;Y2;fpR$ptv0g#tqnF|fRSx#1HQ74HoOX^Ek6TDGHNE$DXffsv z@o2ru0rlbSP1UGprtqGvpJ2@3g*RdL&bR$z!PR}XlqSfjaDkrRVy|u9fDtd*+H?o?CrEr^V&^VcRe@ZH~;T}lsW;W zN}O1qPTnhl1S`fAHzt`|TOe7eN)aVr#?aaY*R>?-w~a~3f`Fn0DO6m%@SmI*b1oOg zVLCMdNyb_|J8LMfXT{W{XPOu5*`61&*v2?xq_kTKXRpgO=N^15EJ^eqz-&a~Qp^CZ z(rR)3KFFFU=RmEqChAhLsU)YiU%U;eQFNnwp7p9OcZWWe;-!?v=iWCoIb!k+ocS*~ zPAK7Nkq7v6U=!z|N4>PN^qQdYY0%I*m2Gt@EN9GOC0f$?_&@!2i_?)Y#}MJ1CUq2M zJ)UN9a2Y$13Ufy81usbX%WL&xO?zV8+3K?6dq+XM(taY)l5t_Dwe>5d6s>ut<4L$#Vh zLoa}^ymrasRm{Rv;j0|n zdtqlvC#Djhdn?i~%rrL)ZIBLU5SzUdaLr1>33|uh-o~$x@K;Mn1~BT{LT zi&NQq4jqiJrA+Ad#Yay-4_Crtc66nYh}QUqUEZ*q7F%e15nuk0&U&}cp~2TsT?B^u zV*e{ETg@R{3fj1zdyhTon75!|!V~|j=<7Lx4vRL{>bL6=`ZHE+5a#IbR~>nE7=o(< zVbl+h`;|x=b+S3A)F$o2lPc50Qq1IDWaf0kK!&$s9eI50%*YFe6@?sh(yZ!Q^k>{{ z%yW2r+=gOz>FPqQ5*`Y?YA-?|vXI6K1w!in)b7|1ce*VmZQ?jL!l^N9-Pam2%yt)J zZiQ+gPUJ^|4|dh+S)nB|st)FQ5aLmDi9ReKe=z76j}T?u$d^O`&2n%g z1(B5cd$yQuym$uiP)r7{stv9P+7AUNaABk2K<@lu&&h2 zL2i$eBiSd+lm50b+QqkUSwsFb({Of9ag`C|e3X$#0aeqgH?p4i*#>*k!F#Dr8q~f$ z@X>#q-6U)hKB~AS)3Q;w*1HpHN#Lvlp+{mZw5_=8y>-0#{%_}$)u|oZT*@vATTZ!y zS^MzBP0|= z%6p&4CnKiX^jTi>Zhy|7@a807+Hf>*p$+z zD`?5$$ca6lJC?m8m!Wddy9;Irv`T@_#Vegs z0~pH``NPTol6pt&kp?Mj$b|{H@>|0huwcOrbgO6bSMNflv#2qE#__Wy(vD;(Gs6K7)Va^yV~0Y`K>2+N8isN^pZ!M(Pbazv0>D}ej}8~B zprNR{1+o=HR@^es9TxIyh)BY>wXnHpfXZKCE8#m`yANf)@N9|~peTp!q`;M64t;kz zlq>Dw!R;qyYiN}jT94+agEO1ukA-jSs}~ZbQaB2(|F)$+tY9=jQw(;{y7&TwD`&$L zNwv9M9Kwg_o{?4cx5mvPr!`ZAR_ zA60kQvt&Q(bi>Hns?a{eYD0Y%h9w=3GMZ)_02(%AN%u(7dDIZ#fUciAw-9BB)_1~j zMdH^*o%wpObW7&T96iNuhmSpnFx$tgQ7b=!I!#7peKxsx=zxdeg}LiV*p_1dc~ca{ zD>Dv7)GWKVs%7SC@=}`#MW~5CwdXRQ2fVA23am`E8o`{x#pN4m=i;Qo6L>b19cv~t zzzOMCa~(p872#Vxg)?+V@o+ga7e_%Ltij$Y`xmW+8^1ZlgSj3%kgXp+564PX1Ubi| zVkYG6%1%eFTaP1`fihUa2%=P{jqn?0tg=Eoo4dEY>QJ;%jW&X=IGLj~3fpD0m7ZLO zJ(Ec+Cz07WEFpUFp0N+uPzqh#oZum;PxUkKIH2OAwg8H6*5{s6c}##9H#vscF~bGAkSDHDFC>r3@}8 zEz-rQKF*1vfd@l%JXJejZB2-;I{;CqHmJ=?B6I zcIIBW?G{6aZ%jI1os4}sa>x#jehk^DnAft5V(uV#I>-QH-+gsbOX)s^bE`pIY~@Ri)nz>b7KOW@M^4_6K0WOj+VD2x2a#{UVpoVKp<0{@-Pz?sgW zfRA24vGcGEbw2CzO@DOYJm<~v=Zc10*}kuKUG|&>Nx4|+GkR{O4_1XK6DnItFjI^1a*xd||AEAShyq~HU=xxb^t zAeVdJGVzvNO0@KA!GRoIIoDH$EX>x)DG_(?_$At8sA%y-Ed#&<+w+OrS8)QoOO*H# zD+}(2jn*b3zKiQ%`k{~7=;^dHwH#~?Cl##kR7lY*_fM-|w?(2%zsOuty+)Y||8PP|-F?&S_>iQZI_#apQ9{|la zi9@2^RUaO^W?yp%C`)T<@>^4dr*N76G>G&cgEV+X-3L>TGixGNFJUZ$w^=SUKMht{4ttt^|_3^R?-oa*qPhq0Ts1YteL zzS;}3<7K4$GNYbu?JJN=)$pdh3Elnnf)TxX5HPA*DX3u zS-$-*vvDvo8!rBZhZ;6Vsr9>Hw^fhcL5K6`WXTAiZ>td{dq8snfRtMhM|HJ2KOyj_ zZNNF117KFw8LM^QTVzwM24joLNfdU!r6Q9@ja;1*y1eD;xK_I$o~J}|X~5Ve+L#5` zW209%Q4>NLIvWV6ORHJO^%Gh#I5cUbUKw#A1jrGi;ws(<-|K(6(gPMX=U@_ET9p}{ zh$EQ+lD2lL-WK(d{!;fO*}Igh3xwyemef?3Ukuw_ifeVp_Vd?aCnMQ$Wb&S(+>68s zdZGUBvP@ZU+fA&qD`Rq=|uGlLUO%&|BdnHj|V9>qf9@L`>fyQ7P7x1|DWeC2|_Wg7&fQ^lMc{QSEt4YSm4`RKP!I7~or-djKOl%0uic}x^W zF_BCCv%0^I#2w1WyKR#kcE~oj%u;gWFoHQaWe)e^0OgD24gP5VMX=e2 zaGBT73Vz_`6s8)l7vY3(z=4jAtNe@g;@}eu=wkk(B{&Uc6FE-3U?hk$B#iC&3jW7B zeISiCVL4B1Rs@2Y6iZ{)hq16HA2vokh7`TKJv^6&SR4rw{{f755}kW_KG0H8%&8Wp z@Hx2yWC|iE;Px}a6L{d*d3S#ZL4oV=LVAjkl62Z*9nB0!@DL_uHVoLLM+-;GN9W5S zh1i}UbUV&*AIoz)vQU$dObf+s|F5FKi|qM5P_6HT$FLav>G@|C<;~sKNU?>17qscf z{TIs4YtMiWm-T48%Bf(of-EJt6AnIC+0tcAv>Y-09zsa$RS16R2K78#e6EZ_863&q zm4!S-P(WgSPNLZb9hM$!BoC(pmd3v>UXi0nj5&1l)n3s$Bn7J-jwfL&k}T%twGrRL z_8@YfK${%zK#Y!+%$9n89P43c^R2%HOHW=***JaT1VH>2F9RVn+t(5ti9~O!kSA$t zJpCGc3GvLw|3064w%_0pMLb^3e;D3#Z#At&k;hs0Ivt#KGEEkhhB^~VD%a`~=Fk@m zBCkq(R=92H8968IGsQWLwOO=yNuWSD&MGgqyr<1-i2=#xWDTYo-JhWmN1)d&<0C@yQU zV?Jpu`wphcMW}=oEbSnQZhI}%qq9pOpUr-;gwmt^%tIXeBT zhZ9$YA5bj(Xs|ZJ#TkD&=jqma5LQ}Z-%8;6AColXrG8nW+Am4y{KdH9v6~+ftA`Qw zUe>kHYt1lTm@3Z36I$A51Mk}jbivz=2v$6U3Kp;(HA--#nQ2iWO-(@qLzoD~{Hj!k zR)JiyRpv+}kv|F-zBj#~_Q&rl~Y9ZN^O#l66a;mey4UiwTZwT20KvXA=R5KdZ~CtwNn*uaMQ< zv<3?HS|25!^Zy0ACq&p2(ucP|fEUX2^?0dtq9XPb>{hrhLu;mtbH43ZaF(Fty=*c?@S^QVnlrq9N|GreFa4Iu}WQ(fyvm{mx5Z6CGrDc6t^ zU$ZUyII)@OoS{Q*{R$D^kL|

PKz36XHu&zVE5t!T!H(cyrBUX5XhwWGCX8vmo8J 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 literal 0 HcmV?d00001 From 07d2211519aa74bde0216ff1190498eff3a3168e Mon Sep 17 00:00:00 2001 From: dasosann Date: Sun, 17 May 2026 23:40:17 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20build=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/dialog.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 3c9ecf0..863a025 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -111,7 +111,9 @@ function DialogFooter({ {children} {showCloseButton && ( - + )}