From 281200c152c3a5a345827f6078156dad88460067 Mon Sep 17 00:00:00 2001 From: Yonghun Yi Date: Tue, 8 Sep 2026 10:28:01 +0900 Subject: [PATCH 01/14] =?UTF-8?q?fix:=20fetchData=20throw=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20=EC=9D=B4=ED=9B=84=20=EC=A3=BD=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20=EC=97=90=EB=9F=AC=20=EA=B2=BD=EB=A1=9C=20UX=20?= =?UTF-8?q?=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버 컴포넌트는 lib/server-fetch-guard로 401/404를 분류해 AuthError/NotFound를 렌더하고, 클라이언트 호출부는 try/catch(FetchError) /finally 패턴으로 마이그레이션해 로딩 stuck과 상태별 에러 메시지 미표시, Alert.onClickAsync 에러 삼킴 문제를 해결한다. --- app/(home)/page.tsx | 5 +- app/admin/admin-client.tsx | 24 +-- app/admin/page.tsx | 24 ++- app/mypage/bookmark/page.tsx | 9 +- app/mypage/myreport/myreport-client.tsx | 64 ++++---- app/mypage/myreport/page.tsx | 21 +-- app/mypage/page.tsx | 15 +- app/mypage/report-admin/page.tsx | 12 +- .../report-admin/report-admin-client.tsx | 64 ++++---- app/mypage/report/page.tsx | 13 +- app/mypage/report/report-client.tsx | 21 ++- app/mypage/user/page.tsx | 9 +- .../[id]/facilities/facilities-client.tsx | 37 +++-- app/pullup/[id]/page.tsx | 36 +++-- app/pullup/[id]/pullup-client.tsx | 35 ++--- app/pullup/[id]/report/page.tsx | 7 +- app/pullup/[id]/report/report-client.tsx | 118 +++++++------- app/register/register-client.tsx | 137 ++++++++-------- app/signup/signup-client.tsx | 21 ++- app/social/page.tsx | 6 +- app/user-info/[user]/page-client.tsx | 20 ++- components/pages/config/user-setting.tsx | 31 ++-- .../pages/home/around-marker-carousel.tsx | 23 +-- components/pages/moments/around.tsx | 29 ++-- .../pages/mypage/bookmark/bookmark-list.tsx | 15 +- .../mypage/locate/registered-locate-list.tsx | 20 ++- .../pages/mypage/user/username-card.tsx | 15 +- components/pages/pullup/bookmark-button.tsx | 83 +++++----- components/pages/pullup/comments.tsx | 147 +++++++++--------- components/pages/pullup/delete-button.tsx | 13 +- components/pages/pullup/description.tsx | 20 ++- components/pages/pullup/image-list.tsx | 26 ++-- .../pages/pullup/moment/add-moment-page.tsx | 21 +-- .../pages/pullup/moment/moment-item.tsx | 10 +- components/pages/pullup/weather-badge.tsx | 13 +- components/pages/register/select-location.tsx | 42 ++--- .../reset-password/reset-password-form.tsx | 27 ++-- .../reset-password/send-password-form.tsx | 16 +- components/pages/signup/verify-email.tsx | 76 ++++----- .../pages/social/marker-ranking-list.tsx | 13 +- lib/server-fetch-guard.ts | 43 +++++ 41 files changed, 758 insertions(+), 623 deletions(-) create mode 100644 lib/server-fetch-guard.ts diff --git a/app/(home)/page.tsx b/app/(home)/page.tsx index 920bca8..bb4b962 100644 --- a/app/(home)/page.tsx +++ b/app/(home)/page.tsx @@ -16,7 +16,10 @@ import { headers } from "next/headers"; import { type Device } from "../mypage/page"; const Home = async () => { - const [images, moment] = await Promise.all([newPictures(), getAllMoment()]); + const [images, moment] = await Promise.all([ + newPictures().catch(() => [] as NewPictures[]), + getAllMoment().catch(() => []), + ]); const headersList = headers(); const userAgent = headersList.get("user-agent"); diff --git a/app/admin/admin-client.tsx b/app/admin/admin-client.tsx index 54b2dd2..594b4e4 100644 --- a/app/admin/admin-client.tsx +++ b/app/admin/admin-client.tsx @@ -124,17 +124,7 @@ const AdminClient = ({ data }: { data: AllReportRes }) => { setProcessingIds((prev) => new Set(prev).add(reportId)); try { - const response = await approveReport(reportId); - - if (!response.ok) { - closeAlert(); - openAlert({ - title: "승인할 수 없습니다.", - description: "잠시 후 다시 시도해주세요.", - onClick: () => {}, - }); - return; - } + await approveReport(reportId); closeAlert(); openAlert({ @@ -175,17 +165,7 @@ const AdminClient = ({ data }: { data: AllReportRes }) => { setProcessingIds((prev) => new Set(prev).add(reportId)); try { - const response = await denyReport(reportId); - - if (!response.ok) { - closeAlert(); - openAlert({ - title: "거절할 수 없습니다.", - description: "잠시 후 다시 시도해주세요.", - onClick: () => {}, - }); - return; - } + await denyReport(reportId); closeAlert(); openAlert({ diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 1d7a3f2..b650e57 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,18 +1,18 @@ import getAllReports from "@/lib/api/report/get-all-reports"; -import AdminClient from "./admin-client"; -import { cookies } from "next/headers"; import myInfo from "@/lib/api/user/myInfo"; +import guardServerFetch from "@lib/server-fetch-guard"; +import { cookies } from "next/headers"; +import AdminClient from "./admin-client"; const AdminPage = async () => { const cookieStore = cookies(); const decodeCookie = decodeURIComponent(cookieStore.toString()); - const data = await getAllReports(decodeCookie); - const user = await myInfo(decodeCookie); - - const noUser = !user || user.error; + const { status, data: user } = await guardServerFetch(() => + myInfo(decodeCookie) + ); - if (noUser) { + if (status !== "ok" || !user || !user.chulbong) { return (

접근 권한이 없습니다. @@ -20,6 +20,16 @@ const AdminPage = async () => { ); } + const { data } = await guardServerFetch(() => getAllReports(decodeCookie)); + + if (!data) { + return ( +

+ 데이터를 불러올 수 없습니다. +

+ ); + } + return ; }; diff --git a/app/mypage/bookmark/page.tsx b/app/mypage/bookmark/page.tsx index 2d4cf41..c79564e 100644 --- a/app/mypage/bookmark/page.tsx +++ b/app/mypage/bookmark/page.tsx @@ -5,6 +5,7 @@ import WarningText from "@common/warning-text"; import AuthError from "@layout/auth-error"; import NotFound from "@layout/not-found"; import getDeviceType from "@lib/get-device-type"; +import guardServerFetch from "@lib/server-fetch-guard"; import BookmarkList from "@pages/mypage/bookmark/bookmark-list"; import { cookies, headers } from "next/headers"; import { type Device } from "../page"; @@ -19,9 +20,11 @@ const RankingPage = async () => { const deviceType: Device = getDeviceType(userAgent as string); - const markers = await favorites(decodeCookie); + const { status, data: markers } = await guardServerFetch(() => + favorites(decodeCookie) + ); - if (markers.error === "No authorization token provided") { + if (status === "unauthorized") { return ( { ); } - if (!markers.data || markers.data.length <= 0) { + if (!markers?.data || markers.data.length <= 0) { return ( { - const response = await denyReport(curData.reportID); - if (!response.ok) { + try { + await denyReport(curData.reportID); + + openAlert({ + title: "거절 완료", + description: "거절이 완료되었습니다.", + onClick: () => {}, + }); + + setCurData((prev) => { + if (!prev) return null; + return { ...prev, status: "DENIED" }; + }); + + router.refresh(); + } catch { closeAlert(); openAlert({ title: "거절할 수 없습니다.", @@ -72,21 +86,7 @@ const MyreportClient = ({ }); router.refresh(); - return; } - - openAlert({ - title: "거절 완료", - description: "거절이 완료되었습니다.", - onClick: () => {}, - }); - - setCurData((prev) => { - if (!prev) return null; - return { ...prev, status: "DENIED" }; - }); - - router.refresh(); }, cancel: true, }); @@ -106,8 +106,22 @@ const MyreportClient = ({ title: "정말 승인하시겠습니까?", description: "해당 위치의 정보가 바뀝니다.", onClickAsync: async () => { - const response = await approveReport(curData.reportID); - if (!response.ok) { + try { + await approveReport(curData.reportID); + + openAlert({ + title: "승인 완료", + description: "승인이 완료되었습니다.", + onClick: () => {}, + }); + + setCurData((prev) => { + if (!prev) return null; + return { ...prev, status: "APPROVED" }; + }); + + router.refresh(); + } catch { closeAlert(); openAlert({ title: "승인할 수 없습니다.", @@ -116,21 +130,7 @@ const MyreportClient = ({ }); router.refresh(); - return; } - - openAlert({ - title: "승인 완료", - description: "승인이 완료되었습니다.", - onClick: () => {}, - }); - - setCurData((prev) => { - if (!prev) return null; - return { ...prev, status: "APPROVED" }; - }); - - router.refresh(); }, cancel: true, }); diff --git a/app/mypage/myreport/page.tsx b/app/mypage/myreport/page.tsx index 03ec81a..eafee66 100644 --- a/app/mypage/myreport/page.tsx +++ b/app/mypage/myreport/page.tsx @@ -2,6 +2,7 @@ import reportForMymarker from "@api/report/report-for-mymarker"; import AuthError from "@layout/auth-error"; import NotFound from "@layout/not-found"; import getDeviceType from "@lib/get-device-type"; +import guardServerFetch from "@lib/server-fetch-guard"; import { cookies, headers } from "next/headers"; import { type Device } from "../page"; import MyreportClient from "./myreport-client"; @@ -16,29 +17,31 @@ const MyreportPage = async () => { const deviceType: Device = getDeviceType(userAgent as string); - const reports = await reportForMymarker(decodeCookie); + const { status, data: reports } = await guardServerFetch(() => + reportForMymarker(decodeCookie) + ); - if (!reports || reports.message === "No reports found") { + if (status === "unauthorized") { return ( - ); } - if (reports.error === "No authorization token provided") { + if (!reports || reports.message === "No reports found") { return ( - ); diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index be6e7ac..3911af0 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -6,6 +6,7 @@ import SideMain from "@common/side-main"; import Text from "@common/text"; import ArrowRightIcon from "@icons/arrow-right-icon"; import getDeviceType from "@lib/get-device-type"; +import guardServerFetch from "@lib/server-fetch-guard"; import LinkList from "@pages/mypage/link-list"; import UserInfo from "@pages/mypage/user-info"; import { cookies, headers } from "next/headers"; @@ -34,9 +35,11 @@ const Mypage = async () => { const deviceType: Device = getDeviceType(userAgent as string); - const user = await myInfo(decodeCookie); + const { status, data: user } = await guardServerFetch(() => + myInfo(decodeCookie) + ); - const noUser = !user || user.error; + const noUser = status !== "ok" || !user; return ( { /> ) : ( - + user && )} @@ -81,7 +84,7 @@ const Mypage = async () => { @@ -92,7 +95,7 @@ const Mypage = async () => { {/* 기여 등급 */} - {!noUser && ( + {user && (
@@ -128,7 +131,7 @@ const Mypage = async () => { )} {/* 링크 버튼 */} - {!noUser && } + {user && }
diff --git a/app/mypage/report-admin/page.tsx b/app/mypage/report-admin/page.tsx index 44a968f..782dd6b 100644 --- a/app/mypage/report-admin/page.tsx +++ b/app/mypage/report-admin/page.tsx @@ -1,6 +1,7 @@ import getAllReports from "@/lib/api/report/get-all-reports"; import myInfo from "@api/user/myInfo"; import getDeviceType from "@lib/get-device-type"; +import guardServerFetch from "@lib/server-fetch-guard"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { type Device } from "../page"; @@ -17,12 +18,17 @@ const ReportAdminPage = async () => { const deviceType: Device = getDeviceType(userAgent as string); // 관리자 권한 체크: chulbong이 아니면 마이페이지로 리다이렉트 - const user = await myInfo(decodeCookie); - if (!user || user.error || !user.chulbong) { + const { status, data: user } = await guardServerFetch(() => + myInfo(decodeCookie) + ); + if (status !== "ok" || !user || !user.chulbong) { redirect("/mypage"); } - const data = await getAllReports(decodeCookie); + const { data } = await guardServerFetch(() => getAllReports(decodeCookie)); + if (!data) { + redirect("/mypage"); + } return ( <> diff --git a/app/mypage/report-admin/report-admin-client.tsx b/app/mypage/report-admin/report-admin-client.tsx index d738702..dc9a29d 100644 --- a/app/mypage/report-admin/report-admin-client.tsx +++ b/app/mypage/report-admin/report-admin-client.tsx @@ -60,8 +60,22 @@ const MyreportClient = ({ title: "정말 거절하시겠습니까?", description: "다시 승인할 수 없습니다.", onClickAsync: async () => { - const response = await denyReport(curData.reportId); - if (!response.ok) { + try { + await denyReport(curData.reportId); + + openAlert({ + title: "거절 완료", + description: "거절이 완료되었습니다.", + onClick: () => {}, + }); + + setCurData((prev) => { + if (!prev) return null; + return { ...prev, status: "DENIED" }; + }); + + router.refresh(); + } catch { closeAlert(); openAlert({ title: "거절할 수 없습니다.", @@ -70,21 +84,7 @@ const MyreportClient = ({ }); router.refresh(); - return; } - - openAlert({ - title: "거절 완료", - description: "거절이 완료되었습니다.", - onClick: () => {}, - }); - - setCurData((prev) => { - if (!prev) return null; - return { ...prev, status: "DENIED" }; - }); - - router.refresh(); }, cancel: true, }); @@ -104,8 +104,22 @@ const MyreportClient = ({ title: "정말 승인하시겠습니까?", description: "해당 위치의 정보가 바뀝니다.", onClickAsync: async () => { - const response = await approveReport(curData.reportId); - if (!response.ok) { + try { + await approveReport(curData.reportId); + + openAlert({ + title: "승인 완료", + description: "승인이 완료되었습니다.", + onClick: () => {}, + }); + + setCurData((prev) => { + if (!prev) return null; + return { ...prev, status: "APPROVED" }; + }); + + router.refresh(); + } catch { closeAlert(); openAlert({ title: "승인할 수 없습니다.", @@ -114,21 +128,7 @@ const MyreportClient = ({ }); router.refresh(); - return; } - - openAlert({ - title: "승인 완료", - description: "승인이 완료되었습니다.", - onClick: () => {}, - }); - - setCurData((prev) => { - if (!prev) return null; - return { ...prev, status: "APPROVED" }; - }); - - router.refresh(); }, cancel: true, }); diff --git a/app/mypage/report/page.tsx b/app/mypage/report/page.tsx index 89ce3f8..6c9a076 100644 --- a/app/mypage/report/page.tsx +++ b/app/mypage/report/page.tsx @@ -1,7 +1,8 @@ -import mySuggested, { type ReportsRes } from "@api/report/my-suggested"; +import mySuggested from "@api/report/my-suggested"; import AuthError from "@layout/auth-error"; import NotFound from "@layout/not-found"; import getDeviceType from "@lib/get-device-type"; +import guardServerFetch from "@lib/server-fetch-guard"; import { cookies, headers } from "next/headers"; import { type Device } from "../page"; import ReportClient from "./report-client"; @@ -16,9 +17,11 @@ const ReportPage = async () => { const deviceType: Device = getDeviceType(userAgent as string); - const reports = await mySuggested(decodeCookie); + const { status, data: reports } = await guardServerFetch(() => + mySuggested(decodeCookie) + ); - if (reports.error === "No authorization token provided") { + if (status === "unauthorized") { return ( { /> ); } - if (!reports.data || reports.error === "No reports found") { + if (!reports?.data || reports.data.length <= 0) { return ( { return ( <> diff --git a/app/mypage/report/report-client.tsx b/app/mypage/report/report-client.tsx index 647f9cd..d65f15f 100644 --- a/app/mypage/report/report-client.tsx +++ b/app/mypage/report/report-client.tsx @@ -7,6 +7,7 @@ import SideMain from "@common/side-main"; import Text from "@common/text"; import NotFound from "@layout/not-found"; import ReportListItem from "@pages/mypage/report/report-list-item"; +import useAlertStore from "@store/useAlertStore"; import { useState } from "react"; import { type Device } from "../page"; @@ -21,17 +22,27 @@ const ReportClient = ({ referrer, deviceType = "desktop", }: ReportClientProps) => { + const { openAlert } = useAlertStore(); + const [reports, setReports] = useState(data); const handleDelete = async (markerId: number, reportId: number) => { - await deleteReport(markerId, reportId); + try { + await deleteReport(markerId, reportId); - setReports((prev) => { - return prev.filter((item) => item.reportId !== reportId); - }); + setReports((prev) => { + return prev.filter((item) => item.reportId !== reportId); + }); + } catch { + openAlert({ + title: "삭제할 수 없습니다.", + description: "잠시 후 다시 시도해주세요.", + onClick: () => {}, + }); + } }; - if (reports.length <= 0 || !reports) { + if (reports.length <= 0) { return ( { const deviceType: Device = getDeviceType(userAgent as string); - const user = await myInfo(decodeCookie); - - const noUser = !user || user.error; + const { status, data: user } = await guardServerFetch(() => + myInfo(decodeCookie) + ); - if (noUser) { + if (status !== "ok" || !user) { return ( { return; } - const response = await setNewFacilities({ - markerId: markerId, - facilities: [ - { - facilityId: 1, - quantity: facilities.철봉, - }, - { - facilityId: 2, - quantity: facilities.평행봉, - }, - ], - }); + try { + await setNewFacilities({ + markerId: markerId, + facilities: [ + { + facilityId: 1, + quantity: facilities.철봉, + }, + { + facilityId: 2, + quantity: facilities.평행봉, + }, + ], + }); - if (!response.ok) { + router.push(`/pullup/${markerId}`); + router.refresh(); + } catch { setErrorMessage("잠시 후 다시 시도해주세요."); + } finally { setLoading(false); - return; } - - router.push(`/pullup/${markerId}`); - router.refresh(); - setLoading(false); }; return ( diff --git a/app/pullup/[id]/page.tsx b/app/pullup/[id]/page.tsx index b3a89a4..874ea3a 100644 --- a/app/pullup/[id]/page.tsx +++ b/app/pullup/[id]/page.tsx @@ -3,6 +3,7 @@ import getComments from "@api/comment/get-comments"; import getFacilities from "@api/marker/get-facilities"; import markerDetail from "@api/marker/marker-detail"; import getDeviceType from "@lib/get-device-type"; +import guardServerFetch from "@lib/server-fetch-guard"; import NotFoud from "@pages/pullup/not-foud"; import { cookies, headers } from "next/headers"; import { cache } from "react"; @@ -22,11 +23,18 @@ export const generateMetadata = async ({ params }: { params: Params }) => { const cookieStore = cookies(); const decodeCookie = decodeURIComponent(cookieStore.toString()); - const { address, description, favCount } = await getCachedMarkerDetail( - ~~id, - decodeCookie + const { status, data: marker } = await guardServerFetch(() => + getCachedMarkerDetail(~~id, decodeCookie) ); + if (status !== "ok" || !marker) { + return { + title: "대한민국 철봉 지도", + }; + } + + const { address, description, favCount } = marker; + const shortDesc = description.length > 80 ? description.slice(0, 80) + "…" : description; @@ -65,16 +73,24 @@ const PullupPage = async ({ params }: { params: Params }) => { const cookieStore = cookies(); const decodeCookie = decodeURIComponent(cookieStore.toString()); - const [marker, facilities, initialComments] = await Promise.all([ - getCachedMarkerDetail(~~id, decodeCookie), - getFacilities(~~id), - getComments({ id: ~~id, pageParam: 1 }), - ]); + const { status, data: marker } = await guardServerFetch(() => + getCachedMarkerDetail(~~id, decodeCookie) + ); - if (marker.error === "Marker not found") { - return ; + if (status === "notfound" || status === "unauthorized" || !marker) { + return ; } + const [facilities, initialComments] = await Promise.all([ + getFacilities(~~id).catch(() => []), + getComments({ id: ~~id, pageParam: 1 }).catch(() => ({ + currentPage: 1, + comments: [], + totalComments: 0, + totalPages: 0, + })), + ]); + return ( { setDeleteLoading(true); - const response = await deleteComment(commentId); - - if (!response.ok) { + try { + await deleteComment(commentId); + + const newComment = providerInfo.filter( + (comment) => comment.commentId !== commentId + ); + setProviderInfo([...newComment]); + } catch { toast({ description: "잠시 후 다시 시도해주세요" }); + } finally { setDeleteLoading(false); - return; } - - const newComment = providerInfo.filter( - (comment) => comment.commentId !== commentId - ); - setProviderInfo([...newComment]); - setDeleteLoading(false); }; const handlePhotoDeleted = (photoId: number) => { @@ -138,15 +137,15 @@ const PullupClient = ({
- {!철봉 || + {(!철봉 || !평행봉 || - (철봉.quantity <= 0 && 평행봉.quantity <= 0 && ( - - ))} + (철봉.quantity <= 0 && 평행봉.quantity <= 0)) && ( + + )} {철봉 && 철봉.quantity > 0 && ( { const deviceType: Device = getDeviceType(userAgent as string); - const marker = await markerDetail({ id: ~~id, cookie: decodeCookie }); + const { status, data: marker } = await guardServerFetch(() => + markerDetail({ id: ~~id, cookie: decodeCookie }) + ); - if (marker.error === "Marker not found") { + if (status !== "ok" || !marker) { return ( >(null); + // #change-map 을 매 newLatLng 마다 새로 생성하지 않고 1회 생성 후 재사용한다. (P2-9) + const changeMapRef = useRef(null); + const changeMarkerRef = useRef(null); + useEffect(() => { if (!markers) return; @@ -87,36 +92,42 @@ const ReportClient = ({ useEffect(() => { if (!newLatLng || !map) return; - const mapContainer = document.getElementById("change-map"); - const mapOption = { - center: new window.kakao.maps.LatLng(newLatLng.lat, newLatLng.lng), - level: map.getLevel(), - }; - - const newMap = new window.kakao.maps.Map(mapContainer, mapOption); - newMap.setDraggable(false); - newMap.setZoomable(false); - - const imageSize = new window.kakao.maps.Size(32, 45); - const imageOption = { offset: new window.kakao.maps.Point(16, 47) }; + const position = new window.kakao.maps.LatLng(newLatLng.lat, newLatLng.lng); - const imageUrl = "/active-selected.png"; + // 최초 1회만 지도/마커 생성, 이후에는 center/position 만 갱신한다. (P2-9) + if (!changeMapRef.current) { + const mapContainer = document.getElementById("change-map"); + if (!mapContainer) return; - const pin = new window.kakao.maps.MarkerImage( - imageUrl, - imageSize, - imageOption - ); + const newMap = new window.kakao.maps.Map(mapContainer, { + center: position, + level: map.getLevel(), + }); + newMap.setDraggable(false); + newMap.setZoomable(false); + + const imageSize = new window.kakao.maps.Size(32, 45); + const imageOption = { offset: new window.kakao.maps.Point(16, 47) }; + const pin = new window.kakao.maps.MarkerImage( + "/active-selected.png", + imageSize, + imageOption + ); - const position = new window.kakao.maps.LatLng(newLatLng.lat, newLatLng.lng); + const pinMarker = new window.kakao.maps.Marker({ + map: newMap, + position: position, + image: pin, + clickable: false, + zIndex: 5, + }); - new window.kakao.maps.Marker({ - map: newMap, - position: position, - image: pin, - clickable: false, - zIndex: 5, - }); + changeMapRef.current = newMap; + changeMarkerRef.current = pinMarker; + } else { + changeMapRef.current.setCenter(position); + changeMarkerRef.current?.setPosition(position); + } }, [map, newLatLng]); useEffect(() => { @@ -225,37 +236,40 @@ const ReportClient = ({ } : reportValue; - const response = await reportMarker(data); - - if (!response.ok) { - if (response.status === 400) { - toast({ - description: "유효하지 않은 입력 정보입니다.", - }); - } else if (response.status === 403) { - toast({ - description: "한국 내부에서만 위치를 지정할 수 있습니다.", - }); - } else if (response.status === 406) { - toast({ - description: "새로운 위치가 기존 위치와 너무 멀리 떨어져 있습니다.", - }); - } else if (response.status === 409) { - toast({ - description: "이미지를 등록해주세요.", - }); + try { + await reportMarker(data); + setCompleted(true); + } catch (e) { + if (e instanceof FetchError) { + if (e.status === 400) { + toast({ + description: "유효하지 않은 입력 정보입니다.", + }); + } else if (e.status === 403) { + toast({ + description: "한국 내부에서만 위치를 지정할 수 있습니다.", + }); + } else if (e.status === 406) { + toast({ + description: "새로운 위치가 기존 위치와 너무 멀리 떨어져 있습니다.", + }); + } else if (e.status === 409) { + toast({ + description: "이미지를 등록해주세요.", + }); + } else { + toast({ + description: "잠시 후 다시 시도해주세요", + }); + } } else { toast({ description: "잠시 후 다시 시도해주세요", }); } - + } finally { setLoading(false); - return; } - - setLoading(false); - setCompleted(true); }; if (completed) { diff --git a/app/register/register-client.tsx b/app/register/register-client.tsx index a63e541..ef51fad 100644 --- a/app/register/register-client.tsx +++ b/app/register/register-client.tsx @@ -6,6 +6,7 @@ import setNewMarker, { SetMarkerRes } from "@api/marker/set-new-marker"; import Skeleton from "@common/skeleton"; import SideMain from "@common/side-main"; import useIsMounted from "@hooks/useIsMounted"; +import { FetchError } from "@lib/fetchData"; import AuthError from "@layout/auth-error"; import SelectLocation from "@pages/register/select-location"; import SetDescription from "@pages/register/set-description"; @@ -138,79 +139,85 @@ const RegisterClient = ({ return; } - const response = await setNewMarker({ - description: registerValue.description || "", - latitude: registerValue.latitude, - longitude: registerValue.longitude, - photos: registerValue.photos || [], - }); + try { + const response = await setNewMarker({ + description: registerValue.description || "", + latitude: registerValue.latitude, + longitude: registerValue.longitude, + photos: registerValue.photos || [], + }); - if (!response.ok) { - if (response.status === 409) { - setErrorMessage(registerError[409]); - } else if (response.status === 401) { - setErrorMessage(registerError[401]); - } else if (response.status === 403) { - setErrorMessage(registerError[403]); - } else if (response.status === 400) { - setErrorMessage(registerError[400]); - } else if (response.status === 422) { - setErrorMessage(registerError[422]); - } else { - setErrorMessage("잠시 후 다시 시도해주세요"); + const newMarker = (await response.json()) as SetMarkerRes; + if (uploadStatus === "image") { + await wait(1.2); + setUploadStatus("location"); + } + await wait(0.6); + if ( + registerValue.facilities[0].quantity > 0 || + registerValue.facilities[1].quantity > 0 + ) { + setUploadStatus("facilities"); } - setUploadStatus("error"); - return; - } - const newMarker = (await response.json()) as SetMarkerRes; - if (uploadStatus === "image") { - await wait(1.2); - setUploadStatus("location"); - } - await wait(0.6); - if ( - registerValue.facilities[0].quantity > 0 || - registerValue.facilities[1].quantity > 0 - ) { - setUploadStatus("facilities"); - } - const responseFac = await setNewFacilities({ - markerId: newMarker.markerId, - facilities: [ - { - facilityId: 1, - quantity: registerValue.facilities[0].quantity, - }, - { - facilityId: 2, - quantity: registerValue.facilities[1].quantity, - }, - ], - }); + try { + await setNewFacilities({ + markerId: newMarker.markerId, + facilities: [ + { + facilityId: 1, + quantity: registerValue.facilities[0].quantity, + }, + { + facilityId: 2, + quantity: registerValue.facilities[1].quantity, + }, + ], + }); + } catch { + setErrorMessage("잠시 후 다시 시도해주세요."); + setUploadStatus("error"); + submitRequestedRef.current = false; + return; + } - if (!responseFac.ok) { - setErrorMessage("잠시 후 다시 시도해주세요."); - setUploadStatus("error"); - return; - } + setNewMarkerId(newMarker.markerId); - setNewMarkerId(newMarker.markerId); + marker?.setMap(null); - marker?.setMap(null); + if (registerValue.photos && registerValue.photos.length > 0) { + setMarkerToStore([{ ...newMarker, hasPhoto: true }]); + } else { + setMarkerToStore([newMarker]); + } - if (registerValue.photos && registerValue.photos.length > 0) { - setMarkerToStore([{ ...newMarker, hasPhoto: true }]); - } else { - setMarkerToStore([newMarker]); + map.setCenter( + new window.kakao.maps.LatLng(newMarker.latitude, newMarker.longitude) + ); + + await wait(0.7); + setUploadStatus("complete"); + } catch (e) { + if (e instanceof FetchError) { + if (e.status === 409) { + setErrorMessage(registerError[409]); + } else if (e.status === 401) { + setErrorMessage(registerError[401]); + } else if (e.status === 403) { + setErrorMessage(registerError[403]); + } else if (e.status === 400) { + setErrorMessage(registerError[400]); + } else if (e.status === 422) { + setErrorMessage(registerError[422]); + } else { + setErrorMessage("잠시 후 다시 시도해주세요"); + } + } else { + setErrorMessage("잠시 후 다시 시도해주세요"); + } + setUploadStatus("error"); + submitRequestedRef.current = false; } - - map.setCenter( - new window.kakao.maps.LatLng(newMarker.latitude, newMarker.longitude) - ); - - await wait(0.7); - setUploadStatus("complete"); }; fetch(); diff --git a/app/signup/signup-client.tsx b/app/signup/signup-client.tsx index 7e533ed..3b4639b 100644 --- a/app/signup/signup-client.tsx +++ b/app/signup/signup-client.tsx @@ -61,22 +61,21 @@ const SignupClient = ({ useEffect(() => { const fetchSignup = async () => { - const response = await signup({ - email: signupValue.email, - username: signupValue.username, - password: signupValue.password, - }); + try { + await signup({ + email: signupValue.email, + username: signupValue.username, + password: signupValue.password, + }); - if (!response.ok) { + setTimeout(() => { + setSignupStatus("complete"); + }, 1100); + } catch { setTimeout(() => { setSignupStatus("error"); }, 1100); - return; } - - setTimeout(() => { - setSignupStatus("complete"); - }, 1100); }; if (signupValue.step === 3) { fetchSignup(); diff --git a/app/social/page.tsx b/app/social/page.tsx index 893a119..eea99ee 100644 --- a/app/social/page.tsx +++ b/app/social/page.tsx @@ -18,8 +18,10 @@ export const generateMetadata = () => { }; const Social = async () => { - const rankingData = await markerRanking(); - const moment = await getAllMoment(); + const [rankingData, moment] = await Promise.all([ + markerRanking().catch(() => []), + getAllMoment().catch(() => []), + ]); const headersList = headers(); const userAgent = headersList.get("user-agent"); diff --git a/app/user-info/[user]/page-client.tsx b/app/user-info/[user]/page-client.tsx index 9d8f2c2..40f0cc7 100644 --- a/app/user-info/[user]/page-client.tsx +++ b/app/user-info/[user]/page-client.tsx @@ -32,15 +32,19 @@ const PageClient = ({ data, userName }: Props) => { if (isLoading || currentPage >= data.totalPages) return; setIsLoading(true); - const newData = await userMarkers({ - userName: userName, - page: currentPage + 1, - }); + try { + const newData = await userMarkers({ + userName: userName, + page: currentPage + 1, + }); - setMarkers((prevMarkers) => [...prevMarkers, ...newData.markers]); - setCurrentPage(newData.currentPage); - - setIsLoading(false); + setMarkers((prevMarkers) => [...prevMarkers, ...newData.markers]); + setCurrentPage(newData.currentPage); + } catch { + // 추가 로딩 실패 시 조용히 중단 + } finally { + setIsLoading(false); + } }, [userName, currentPage, isLoading, data.totalPages]); useEffect(() => { diff --git a/components/pages/config/user-setting.tsx b/components/pages/config/user-setting.tsx index f736730..6f1d291 100644 --- a/components/pages/config/user-setting.tsx +++ b/components/pages/config/user-setting.tsx @@ -3,6 +3,7 @@ import signout from "@api/auth/signout"; import deleteUser from "@api/user/deleteUser"; import { clearSessionCache } from "@lib/session-cache"; +import { FetchError } from "@lib/fetchData"; import List, { ListItem } from "@pages/config/config-list"; import useAlertStore from "@store/useAlertStore"; import useUserStore from "@store/useUserStore"; @@ -44,10 +45,21 @@ const UserSetting = () => { description: "추가하신 마커는 유지되고, 작성한 댓글 밑 사진은 모두 삭제됩니다!", onClickAsync: async () => { - const response = await deleteUser(); + try { + await deleteUser(); - if (!response.ok) { - if (response.status === 401) { + openAlert({ + title: "회원 탈퇴가 완료되었습니다.", + description: + "그동안 이용해주셔서 감사합니다. 언제든 다시 찾아주세요!", + onClick: () => { + router.replace("/"); + router.refresh(); + setUser(null); + }, + }); + } catch (e) { + if (e instanceof FetchError && e.status === 401) { openAlert({ title: "접근 권한이 없습니다.", description: "로그인 후 다시 시도해 주세요.", @@ -62,20 +74,7 @@ const UserSetting = () => { onClick: () => {}, }); } - - return; } - - openAlert({ - title: "회원 탈퇴가 완료되었습니다.", - description: - "그동안 이용해주셔서 감사합니다. 언제든 다시 찾아주세요!", - onClick: () => { - router.replace("/"); - router.refresh(); - setUser(null); - }, - }); }, cancel: true, }); diff --git a/components/pages/home/around-marker-carousel.tsx b/components/pages/home/around-marker-carousel.tsx index 02dff78..5b5236c 100644 --- a/components/pages/home/around-marker-carousel.tsx +++ b/components/pages/home/around-marker-carousel.tsx @@ -34,15 +34,20 @@ const AroundMarkerCarousel = () => { setGeolocationError(false); const fetchMarker = async () => { - const data = await closeMarker({ - lat: myLocation.lat, - lng: myLocation.lng, - distance: 2000, - pageParam: 1, - }); - - setData(data.markers); - setLoading(false); + try { + const data = await closeMarker({ + lat: myLocation.lat, + lng: myLocation.lng, + distance: 2000, + pageParam: 1, + }); + + setData(data.markers); + } catch { + setData([]); + } finally { + setLoading(false); + } }; fetchMarker(); diff --git a/components/pages/moments/around.tsx b/components/pages/moments/around.tsx index d3e9d90..5a5e1d8 100644 --- a/components/pages/moments/around.tsx +++ b/components/pages/moments/around.tsx @@ -41,24 +41,23 @@ const Around = () => { const handleSearch = async () => { setIsLoading(true); - const data = await closeMarker({ - lat: myLocation.lat, - lng: myLocation.lng, - distance: 1500, - pageParam: 1, - }); - - if (data.error || data.message) { + try { + const data = await closeMarker({ + lat: myLocation.lat, + lng: myLocation.lng, + distance: 1500, + pageParam: 1, + }); + + if (cancelled) return; + const deduped = dedupeMarkersByAddress(data.markers); + setMarkers(deduped.slice(0, 5)); + } catch { if (cancelled) return; setMarkers([]); - setIsLoading(false); - return; + } finally { + if (!cancelled) setIsLoading(false); } - - if (cancelled) return; - const deduped = dedupeMarkersByAddress(data.markers); - setMarkers(deduped.slice(0, 5)); - setIsLoading(false); }; handleSearch(); diff --git a/components/pages/mypage/bookmark/bookmark-list.tsx b/components/pages/mypage/bookmark/bookmark-list.tsx index b376615..0bbd4e1 100644 --- a/components/pages/mypage/bookmark/bookmark-list.tsx +++ b/components/pages/mypage/bookmark/bookmark-list.tsx @@ -71,16 +71,17 @@ const ListItem = ({ const handleDelete = async () => { setLoading(true); - const res = await deleteFavorite(id); - - if (!res.ok) { + try { + await deleteFavorite(id); + deleteMarker(id); + } catch { toast({ description: "잠시 후 다시 시도해주세요.", }); - } else deleteMarker(id); - - hide(); - setLoading(false); + } finally { + hide(); + setLoading(false); + } }; return ( diff --git a/components/pages/mypage/locate/registered-locate-list.tsx b/components/pages/mypage/locate/registered-locate-list.tsx index f43fa33..a7f58db 100644 --- a/components/pages/mypage/locate/registered-locate-list.tsx +++ b/components/pages/mypage/locate/registered-locate-list.tsx @@ -34,14 +34,18 @@ const RegisteredLocateList = ({ data }: RegisteredListProps) => { if (isLoading || currentPage >= data.totalPages) return; setIsLoading(true); - const newData = await myRegisteredLocation({ - pageParam: currentPage + 1, - }); - - setMarkers((prevMarkers) => [...prevMarkers, ...newData.markers]); - setCurrentPage(newData.currentPage); - - setIsLoading(false); + try { + const newData = await myRegisteredLocation({ + pageParam: currentPage + 1, + }); + + setMarkers((prevMarkers) => [...prevMarkers, ...newData.markers]); + setCurrentPage(newData.currentPage); + } catch { + // 추가 로딩 실패 시 조용히 중단 + } finally { + setIsLoading(false); + } }, [currentPage, isLoading, data.totalPages]); useEffect(() => { diff --git a/components/pages/mypage/user/username-card.tsx b/components/pages/mypage/user/username-card.tsx index 5060154..31535ae 100644 --- a/components/pages/mypage/user/username-card.tsx +++ b/components/pages/mypage/user/username-card.tsx @@ -42,18 +42,17 @@ const UsernameCard = ({ user }: UsernameCardProps) => { setLoading(false); return; } - const response = await updateUserName(username.value); + try { + await updateUserName(username.value); - if (!response.ok) { + setUsernameValue(username.value); + setEdit(false); + router.refresh(); + } catch { setEditError("잠시 후 다시 시도해주세요"); + } finally { setLoading(false); - return; } - - setUsernameValue(username.value); - setLoading(false); - setEdit(false); - router.refresh(); }; return ( diff --git a/components/pages/pullup/bookmark-button.tsx b/components/pages/pullup/bookmark-button.tsx index 42c058e..f909578 100644 --- a/components/pages/pullup/bookmark-button.tsx +++ b/components/pages/pullup/bookmark-button.tsx @@ -3,6 +3,7 @@ import IconButton from "@common/icon-button"; import { useToast } from "@hooks/useToast"; import deleteFavorite from "@lib/api/favorite/delete-favorite"; +import { FetchError } from "@lib/fetchData"; import setFavorite from "@lib/api/favorite/set-favorite"; import useAlertStore from "@store/useAlertStore"; import { useRouter } from "next/navigation"; @@ -30,54 +31,58 @@ const BookmarkButton = ({ const [isActive, setIsActive] = useState(favorited); const handleBookmark = async () => { - let response; let description; - if (isActive) { - response = await deleteFavorite(markerId); - description = "삭제가 완료되었습니다."; - } else { - response = await setFavorite(markerId); - description = "저장이 완료되었습니다."; - } + try { + if (isActive) { + await deleteFavorite(markerId); + description = "삭제가 완료되었습니다."; + } else { + await setFavorite(markerId); + description = "저장이 완료되었습니다."; + } - if (!response.ok) { - if (response.status === 401) { - openAlert({ - title: "로그인이 필요합니다.", - description: "로그인 페이지로 이동하시겠습니까?", - onClick: () => { - router.push(`/signin?returnUrl=/pullup/${markerId}`); - }, - cancel: true, - }); - } else if (response.status === 403) { - openAlert({ - title: "개수 초과", - description: "즐겨찾기는 최대 10개까지 가능합니다.", - onClick: () => {}, - }); + toast({ + description: description, + }); + + closeAlert(); + + if (isActive) { + decreaseFavCount(); + } else { + increaseFavCount(); + } + setIsActive((prev) => !prev); + } catch (e) { + if (e instanceof FetchError) { + if (e.status === 401) { + openAlert({ + title: "로그인이 필요합니다.", + description: "로그인 페이지로 이동하시겠습니까?", + onClick: () => { + router.push(`/signin?returnUrl=/pullup/${markerId}`); + }, + cancel: true, + }); + } else if (e.status === 403) { + openAlert({ + title: "개수 초과", + description: "즐겨찾기는 최대 10개까지 가능합니다.", + onClick: () => {}, + }); + } else { + toast({ + description: "잠시 후 다시 시도해주세요.", + }); + closeAlert(); + } } else { toast({ description: "잠시 후 다시 시도해주세요.", }); closeAlert(); - return; } - return; - } - - toast({ - description: description, - }); - - closeAlert(); - - if (isActive) { - decreaseFavCount(); - } else { - increaseFavCount(); } - setIsActive((prev) => !prev); }; const handleClick = () => { diff --git a/components/pages/pullup/comments.tsx b/components/pages/pullup/comments.tsx index 5b1c92a..f4547e6 100644 --- a/components/pages/pullup/comments.tsx +++ b/components/pages/pullup/comments.tsx @@ -11,6 +11,7 @@ import Textarea from "@common/textarea"; import useInput from "@hooks/useInput"; import { useToast } from "@hooks/useToast"; import cn from "@lib/cn"; +import { FetchError } from "@lib/fetchData"; import { formatDate } from "@lib/format-date"; import useAlertStore from "@store/useAlertStore"; import { useBottomSheetStore } from "@store/useBottomSheetStore"; @@ -52,20 +53,20 @@ const Comments = ({ markerId, initialComments }: CommentsProps) => { if (commentsLoading || currentPage >= totalPages) return; setCommentsLoading(true); - const newData = await getComments({ - id: markerId, - pageParam: currentPage + 1, - }); - - if (newData.error) { - return; + try { + const newData = await getComments({ + id: markerId, + pageParam: currentPage + 1, + }); + + setComments((prev) => [...prev, ...newData.comments]); + setCurrentPage(newData.currentPage); + } catch { + toast({ description: "잠시 후 다시 시도해주세요" }); + } finally { + setCommentsLoading(false); } - - setComments((prev) => [...prev, ...newData.comments]); - setCurrentPage(newData.currentPage); - - setCommentsLoading(false); - }, [currentPage, commentsLoading, totalPages, markerId]); + }, [currentPage, commentsLoading, totalPages, markerId, toast]); useEffect(() => { // Use server-provided initial data instead of fetching on mount @@ -116,74 +117,78 @@ const Comments = ({ markerId, initialComments }: CommentsProps) => { return; } setCreateLoading(true); - const response = await createComment({ - markerId: markerId, - commentText: commentValue.value, - }); - - const data = await response.json(); - - if (!response.ok) { - if (data.error === "Comment contains inappropriate content.") { - toast({ description: "댓글에 비속어를 포함할 수 없습니다." }); - setCreateLoading(false); - return; - } else if (response.status === 400) { - toast({ description: "이미 3개의 댓들을 작성하였습니다." }); - setCreateLoading(false); - return; - } else if (response.status === 401) { - openAlert({ - title: "로그인이 필요합니다.", - description: "로그인 페이지로 이동하시겠습니까?", - onClick: () => { - router.push(`/signin?returnUrl=/pullup/${markerId}`); - }, - cancel: true, - }); - setCreateLoading(false); - return; - } - toast({ description: "잠시 후 다시 시도해주세요" }); - setCreateLoading(false); - return; - } + try { + const response = await createComment({ + markerId: markerId, + commentText: commentValue.value, + }); + + const data = await response.json(); + + setComments((prev) => { + if (data.username !== "k-pullup") { + const nonKIndex = prev.findIndex( + (comment) => comment.username !== "k-pullup" + ); + if (nonKIndex === -1) { + return [data, ...prev]; + } + return [...prev.slice(0, nonKIndex), data, ...prev.slice(nonKIndex)]; + } + setProviderInfo([...providerInfo, data]); + return [...prev]; + }); + hide(); + commentValue.resetValue(); + } catch (e) { + if (e instanceof FetchError) { + let errorBody: { error?: string } | null = null; + try { + errorBody = e.responseBody ? JSON.parse(e.responseBody) : null; + } catch { + errorBody = null; + } - setComments((prev) => { - if (data.username !== "k-pullup") { - const nonKIndex = prev.findIndex( - (comment) => comment.username !== "k-pullup" - ); - if (nonKIndex === -1) { - return [data, ...prev]; + if (errorBody?.error === "Comment contains inappropriate content.") { + toast({ description: "댓글에 비속어를 포함할 수 없습니다." }); + } else if (e.status === 400) { + toast({ description: "이미 3개의 댓들을 작성하였습니다." }); + } else if (e.status === 401) { + openAlert({ + title: "로그인이 필요합니다.", + description: "로그인 페이지로 이동하시겠습니까?", + onClick: () => { + router.push(`/signin?returnUrl=/pullup/${markerId}`); + }, + cancel: true, + }); + } else { + toast({ description: "잠시 후 다시 시도해주세요" }); } - return [...prev.slice(0, nonKIndex), data, ...prev.slice(nonKIndex)]; + } else { + toast({ description: "잠시 후 다시 시도해주세요" }); } - setProviderInfo([...providerInfo, data]); - return [...prev]; - }); - setCreateLoading(false); - hide(); - commentValue.resetValue(); + } finally { + setCreateLoading(false); + } }; const handleDelete = async (commentId: number) => { setDeleteLoading(true); - const response = await deleteComment(commentId); - - if (!response.ok) { + try { + await deleteComment(commentId); + + const newComment = await getComments({ + id: markerId, + pageParam: 1, + }); + setComments(newComment.comments); + setCurrentPage(1); + } catch { toast({ description: "잠시 후 다시 시도해주세요" }); + } finally { setDeleteLoading(false); - return; } - - const newComment = await getComments({ - id: markerId, - pageParam: 1, - }); - setComments(newComment.comments); - setCurrentPage(1); - setDeleteLoading(false); }; return ( diff --git a/components/pages/pullup/delete-button.tsx b/components/pages/pullup/delete-button.tsx index 2883b2f..6b7c31b 100644 --- a/components/pages/pullup/delete-button.tsx +++ b/components/pages/pullup/delete-button.tsx @@ -18,16 +18,15 @@ const DeleteButton = ({ markerId }: DeleteButtonProps) => { const { toast } = useToast(); const handleDelete = async () => { - const response = await deleteMarker(markerId); + try { + await deleteMarker(markerId); - if (!response.ok) { + deleteOne(markerId); + closeAlert(); + router.replace("/"); + } catch { toast({ description: "잠시 후 다시 시도해주세요" }); - return; } - - deleteOne(markerId); - closeAlert(); - router.replace("/"); }; const handleClick = () => { diff --git a/components/pages/pullup/description.tsx b/components/pages/pullup/description.tsx index d2202dd..828c26c 100644 --- a/components/pages/pullup/description.tsx +++ b/components/pages/pullup/description.tsx @@ -8,6 +8,7 @@ import Textarea from "@common/textarea"; import useInput from "@hooks/useInput"; import EditIcon from "@icons/edit-icon"; import LoadingIcon from "@icons/loading-icon"; +import { FetchError } from "@lib/fetchData"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -38,20 +39,23 @@ const Description = ({ description, markerId, isAdmin }: DescriptionProps) => { if (description === descriptionInput.value) return; setLoading(true); - const data = await updateDescription(descriptionInput.value, markerId); - if (data.error || data.message) { - if (data.error === "Description contains profanity") { + try { + await updateDescription(descriptionInput.value, markerId); + setDescriptionValue(descriptionInput.value); + setEdit(false); + router.refresh(); + } catch (e) { + if ( + e instanceof FetchError && + e.responseBody?.includes("Description contains profanity") + ) { setEditError("설명에 비속어를 포함할 수 없습니다."); } else { setEditError("잠시 후 다시 시도해주세요."); } + } finally { setLoading(false); - return; } - setDescriptionValue(descriptionInput.value); - setLoading(false); - setEdit(false); - router.refresh(); }; if (edit) { diff --git a/components/pages/pullup/image-list.tsx b/components/pages/pullup/image-list.tsx index 18fb112..43eb44a 100644 --- a/components/pages/pullup/image-list.tsx +++ b/components/pages/pullup/image-list.tsx @@ -5,6 +5,7 @@ import type { Photo } from "@/types/marker.types"; import Text from "@common/text"; import { useToast } from "@hooks/useToast"; import deleteMarkerPhoto from "@lib/api/marker/delete-marker-photo"; +import { FetchError } from "@lib/fetchData"; import useAlertStore from "@store/useAlertStore"; import useImageModalStore from "@store/useImageModalStore"; import useUserStore from "@store/useUserStore"; @@ -110,19 +111,6 @@ const ImageList = ({ try { const response = await deleteMarkerPhoto(markerId, photoId); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - - if (response.status === 403) { - toast({ description: "사진을 삭제할 권한이 없습니다" }); - } else if (response.status === 400) { - toast({ description: "잘못된 요청입니다" }); - } else { - toast({ description: "잠시 후 다시 시도해주세요" }); - } - return; - } - const data = await response.json(); // Handle both success and idempotent cases @@ -139,7 +127,17 @@ const ImageList = ({ closeAlert(); } catch (error) { - toast({ description: "사진 삭제 중 오류가 발생했습니다" }); + if (error instanceof FetchError) { + if (error.status === 403) { + toast({ description: "사진을 삭제할 권한이 없습니다" }); + } else if (error.status === 400) { + toast({ description: "잘못된 요청입니다" }); + } else { + toast({ description: "잠시 후 다시 시도해주세요" }); + } + } else { + toast({ description: "사진 삭제 중 오류가 발생했습니다" }); + } } finally { setDeletingPhotoId(null); } diff --git a/components/pages/pullup/moment/add-moment-page.tsx b/components/pages/pullup/moment/add-moment-page.tsx index c71e1f9..d696645 100644 --- a/components/pages/pullup/moment/add-moment-page.tsx +++ b/components/pages/pullup/moment/add-moment-page.tsx @@ -5,6 +5,7 @@ import postMoment from "@api/moment/post-moment"; import SideMain from "@common/side-main"; import useInput from "@hooks/useInput"; import LoadingIcon from "@icons/loading-icon"; +import { FetchError } from "@lib/fetchData"; import { ChevronLeft, SendHorizontal } from "lucide-react"; import Image from "next/image"; import { useRouter } from "next/navigation"; @@ -42,10 +43,15 @@ const AddMomentPage = ({ photo: imageFile, }; - const res = await postMoment(data); + try { + const res = await postMoment(data); - if (!res.ok) { - if (res.status === 401) { + const moment: Moment = await res.json(); + if (!moment) return; + addMoment(moment); + clear(); + } catch (e) { + if (e instanceof FetchError && e.status === 401) { openAlert({ title: "접근 권한이 없습니다.", description: "로그인 후 다시 시도해 주세요.", @@ -62,14 +68,9 @@ const AddMomentPage = ({ onClick: () => {}, }); } - } else { - const data: Moment = await res.json(); - if (!data) return; - addMoment(data); - clear(); + } finally { + setLoading(false); } - - setLoading(false); }; return ( diff --git a/components/pages/pullup/moment/moment-item.tsx b/components/pages/pullup/moment/moment-item.tsx index dbba206..182620e 100644 --- a/components/pages/pullup/moment/moment-item.tsx +++ b/components/pages/pullup/moment/moment-item.tsx @@ -24,17 +24,17 @@ const MomentItem = ({ moment, filterMoment }: MomentItem) => { description: "정말 삭제하시겠습니까?", cancel: true, onClickAsync: async () => { - const res = await deleteMoment(moment.markerID, moment.storyID); - if (!res.ok) { + try { + await deleteMoment(moment.markerID, moment.storyID); + filterMoment(moment.storyID); + closeAlert(); + } catch { openAlert({ title: "실패", description: "잠시 후 다시 시도해주세요.", cancel: true, onClick: () => {}, }); - } else { - filterMoment(moment.storyID); - closeAlert(); } }, }); diff --git a/components/pages/pullup/weather-badge.tsx b/components/pages/pullup/weather-badge.tsx index 2517394..7c9b7ec 100644 --- a/components/pages/pullup/weather-badge.tsx +++ b/components/pages/pullup/weather-badge.tsx @@ -19,10 +19,15 @@ const WeatherBadge = ({ lat, lng }: WeatherBadgeProps) => { useEffect(() => { const fetch = async () => { setLoading(true); - const weather = await getWeather(lat, lng); - - setWeather(weather); - setLoading(false); + try { + const weather = await getWeather(lat, lng); + + setWeather(weather); + } catch { + setWeather(null); + } finally { + setLoading(false); + } }; fetch(); diff --git a/components/pages/register/select-location.tsx b/components/pages/register/select-location.tsx index e463969..f6c24b8 100644 --- a/components/pages/register/select-location.tsx +++ b/components/pages/register/select-location.tsx @@ -10,6 +10,7 @@ import Image from "next/image"; import { useEffect, useState } from "react"; import { Proposal } from "../mypage/link-list"; import locateVerify from "@/lib/api/marker/locate-verify"; +import { FetchError } from "@lib/fetchData"; interface SelectLocationProps { next: ({ @@ -72,30 +73,33 @@ const SelectLocation = ({ const handleNext = async () => { if (!position.lat || !position.lng) return; setLoading(true); - const res = await locateVerify(position.lat, position.lng); - - if (res.error) { - if (res.error === "there is a marker already nearby") { - setErrorMessage("주변에 이미 철봉이 있습니다."); - } else if (res.error.includes("marker is in restricted area")) { - setErrorMessage("위치 등록이 제한된 구역입니다."); - } else if (res.error === "operation is only allowed within South Korea") { - setErrorMessage("위치는 대한민국에만 등록 가능합니다."); - } else if (res.error === "invalid latitude (Must be between 32 and 39)") { - setErrorMessage("위치 등록이 제한된 구역입니다."); + try { + await locateVerify(position.lat, position.lng); + + next({ + latitude: position.lat as number, + longitude: position.lng as number, + }); + } catch (e) { + if (e instanceof FetchError) { + const body = e.responseBody ?? ""; + if (body.includes("there is a marker already nearby")) { + setErrorMessage("주변에 이미 철봉이 있습니다."); + } else if (body.includes("marker is in restricted area")) { + setErrorMessage("위치 등록이 제한된 구역입니다."); + } else if (body.includes("operation is only allowed within South Korea")) { + setErrorMessage("위치는 대한민국에만 등록 가능합니다."); + } else if (body.includes("invalid latitude (Must be between 32 and 39)")) { + setErrorMessage("위치 등록이 제한된 구역입니다."); + } else { + setErrorMessage("잠시 후 다시 시도해주세요."); + } } else { setErrorMessage("잠시 후 다시 시도해주세요."); } - + } finally { setLoading(false); - return; } - - setLoading(false); - next({ - latitude: position.lat as number, - longitude: position.lng as number, - }); }; return ( diff --git a/components/pages/reset-password/reset-password-form.tsx b/components/pages/reset-password/reset-password-form.tsx index 6d23df2..a395fa2 100644 --- a/components/pages/reset-password/reset-password-form.tsx +++ b/components/pages/reset-password/reset-password-form.tsx @@ -35,25 +35,28 @@ const ResetPasswordForm = ({ token }: { token: string }) => { title: "정말 초기화하시겠습니까?", onClick: async () => { setLoading(true); - const response = await resetPassword({ - password: inputValue.value, - token, - }); + try { + await resetPassword({ + password: inputValue.value, + token, + }); + + try { + await signout(); + } catch { + // 로그아웃 실패는 흡수하고 로그인 페이지로 계속 진행한다. + } - if (!response.ok) { + router.replace("/signin"); + router.refresh(); + } catch { openAlert({ title: "잠시 후 다시 시도해주세요", onClick: () => {}, }); + } finally { setLoading(false); - return; } - - await signout(); - - router.replace("/signin"); - router.refresh(); - setLoading(false); }, cancel: true, }); diff --git a/components/pages/reset-password/send-password-form.tsx b/components/pages/reset-password/send-password-form.tsx index aa28121..a9e2905 100644 --- a/components/pages/reset-password/send-password-form.tsx +++ b/components/pages/reset-password/send-password-form.tsx @@ -25,15 +25,9 @@ const SendPasswordForm = () => { }, [inputValue.value]); const onSubmit = async () => { - const response = await sendPasswordResetEmail(inputValue.value); + try { + await sendPasswordResetEmail(inputValue.value); - if (!response.ok) { - openAlert({ - title: "정확한 정보를 입력해주세요", - description: "이메일 정보를 다시 확인해주세요", - onClick: () => {}, - }); - } else { openAlert({ title: "메일 전송 완료", description: "이메일을 확인한 후 비밀번호 초기화를 완료해주세요", @@ -42,6 +36,12 @@ const SendPasswordForm = () => { setViewError(false); }, }); + } catch { + openAlert({ + title: "정확한 정보를 입력해주세요", + description: "이메일 정보를 다시 확인해주세요", + onClick: () => {}, + }); } }; diff --git a/components/pages/signup/verify-email.tsx b/components/pages/signup/verify-email.tsx index f180c15..26584bd 100644 --- a/components/pages/signup/verify-email.tsx +++ b/components/pages/signup/verify-email.tsx @@ -9,6 +9,7 @@ import Timer from "@common/timer"; import useInput from "@hooks/useInput"; import LoadingIcon from "@icons/loading-icon"; import { validateCode, validateEmail, validateMassage } from "@lib/validate"; +import { FetchError } from "@lib/fetchData"; import { useEffect, useState } from "react"; interface VerifyEmailProps { @@ -60,70 +61,59 @@ const VerifyEmail = ({ next }: VerifyEmailProps) => { setEmailLoading(true); - const response = await sendSignupCode(email.value); + try { + await sendSignupCode(email.value); - if (!response.ok) { - if (response.status === 409) { + setViewCode(true); + + setCompleted((prev) => ({ + ...prev, + email: true, + })); + + setTimerReset(true); + } catch (e) { + if (e instanceof FetchError && e.status === 409) { setErrorMessage((prev) => ({ ...prev, email: "이미 가입되어 있는 이메일입니다.", })); - - setEmailLoading(false); - return; + } else { + setErrorMessage((prev) => ({ + ...prev, + email: "잠시 후 다시 시도해주세요", + })); } - setErrorMessage((prev) => ({ - ...prev, - email: "잠시 후 다시 시도해주세요", - })); - + } finally { setEmailLoading(false); - return; } - - setViewCode(true); - - setCompleted((prev) => ({ - ...prev, - email: true, - })); - - setEmailLoading(false); - - setTimerReset(true); }; const verify = async () => { setCodeLoading(true); - const response = await verifyCode({ email: email.value, code: code.value }); + try { + await verifyCode({ email: email.value, code: code.value }); - if (!response.ok) { - if (response.status === 400) { + setCompleted((prev) => ({ + ...prev, + code: true, + })); + } catch (e) { + if (e instanceof FetchError && e.status === 400) { setErrorMessage((prev) => ({ ...prev, code: "유효하지 않은 인증 코드입니다.", })); - - setCodeLoading(false); - return; + } else { + setErrorMessage((prev) => ({ + ...prev, + code: "잠시 후 다시 시도해주세요.", + })); } - - setErrorMessage((prev) => ({ - ...prev, - code: "잠시 후 다시 시도해주세요.", - })); - + } finally { setCodeLoading(false); - return; } - - setCompleted((prev) => ({ - ...prev, - code: true, - })); - - setCodeLoading(false); }; return ( diff --git a/components/pages/social/marker-ranking-list.tsx b/components/pages/social/marker-ranking-list.tsx index 6c8b6db..2bc712f 100644 --- a/components/pages/social/marker-ranking-list.tsx +++ b/components/pages/social/marker-ranking-list.tsx @@ -22,10 +22,15 @@ const MarkerRankingList = ({ allRanking }: { allRanking: RankingInfo[] }) => { if (!myLocation || rankingType === "all") return; const fetchData = async () => { setLoading(true); - const data = await areaRanking(myLocation.lat, myLocation.lng); - - setAroundMarker(data); - setLoading(false); + try { + const data = await areaRanking(myLocation.lat, myLocation.lng); + + setAroundMarker(data); + } catch { + setAroundMarker(null); + } finally { + setLoading(false); + } }; fetchData(); }, [myLocation, rankingType]); diff --git a/lib/server-fetch-guard.ts b/lib/server-fetch-guard.ts new file mode 100644 index 0000000..347f15a --- /dev/null +++ b/lib/server-fetch-guard.ts @@ -0,0 +1,43 @@ +import { FetchError } from "@lib/fetchData"; + +export type ServerFetchStatus = "ok" | "unauthorized" | "notfound"; + +export interface ServerFetchResult { + status: ServerFetchStatus; + data: T | null; +} + +/** + * 서버 컴포넌트에서 API를 호출할 때 공통으로 사용하는 가드. + * + * fetchData 가 non-2xx 에서 FetchError 를 throw 하도록 바뀐 뒤(2026-07-09), + * 서버 페이지들이 401/404 를 직접 판별하지 못하고 전역 error.tsx 로 떨어지는 + * 문제가 있었다. 이 헬퍼는 401 -> "unauthorized", 404 -> "notfound" 로 분류하고 + * 그 외 에러는 그대로 re-throw 하여 상위 error boundary 가 처리하게 한다. + * + * 사용 예: + * const { status, data } = await guardServerFetch(() => myInfo(cookie)); + * if (status === "unauthorized") return ; + * if (status === "notfound") return ; + * // status === "ok" 이면 data 사용 + */ +const guardServerFetch = async ( + fetcher: () => Promise +): Promise> => { + try { + const data = await fetcher(); + return { status: "ok", data }; + } catch (e) { + if (e instanceof FetchError) { + if (e.status === 401 || e.status === 403) { + return { status: "unauthorized", data: null }; + } + if (e.status === 404) { + return { status: "notfound", data: null }; + } + } + throw e; + } +}; + +export default guardServerFetch; From b644f12e6d7c83a92f595a2ffcef3ad58b566bf9 Mon Sep 17 00:00:00 2001 From: Yonghun Yi Date: Tue, 8 Sep 2026 10:28:18 +0900 Subject: [PATCH 02/14] =?UTF-8?q?fix:=20=EC=A7=80=EB=8F=84/GPS=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EC=82=AC=EC=9D=B4=ED=81=B4=20=EB=88=84?= =?UTF-8?q?=EC=88=98=EC=99=80=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=AC=EC=83=9D=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 마커/오버레이 배열 clear 누락으로 인한 무한 누적, 클러스터 오버레이 React root unmount 누락, kakao-map 마커 재요청(pathname deps), useGps stale 반환·watcher 누적, 나침반 센서 스로틀·권한 시점, roadview·report· around-search 지도 인스턴스 재생성을 정리한다. useGps는 get-my-location으로 대체. --- components/layout/kakao-map.tsx | 36 +++++-- components/layout/roadview.tsx | 33 +++++- components/pages/pullup/share-button.tsx | 35 +++---- components/pages/search/around-search.tsx | 63 ++++++------ hooks/useCompass.ts | 117 ++++++++++++---------- hooks/useGps.ts | 46 --------- hooks/useGpsTracking.ts | 27 ++--- hooks/useMarkerControl.tsx | 3 + lib/get-my-location.ts | 40 ++++++++ store/useMapStore.ts | 6 +- types/kakao-map.types.ts | 6 ++ 11 files changed, 232 insertions(+), 180 deletions(-) delete mode 100644 hooks/useGps.ts create mode 100644 lib/get-my-location.ts diff --git a/components/layout/kakao-map.tsx b/components/layout/kakao-map.tsx index 62788b1..0c87295 100644 --- a/components/layout/kakao-map.tsx +++ b/components/layout/kakao-map.tsx @@ -86,6 +86,11 @@ const KakaoMap = () => { const longPressTimer = useRef(null); const touchStartPos = useRef<{ x: number; y: number } | null>(null); + // pathname 을 effect deps 에 직접 넣으면 클라이언트 네비게이션마다 마커를 + // 전량 재요청하게 되므로, 가드용으로만 ref 로 읽는다. (P2-2) + const pathnameRef = useRef(pathname); + pathnameRef.current = pathname; + useEffect(() => { if (!isMounted || pathname === "/admin") return; @@ -117,21 +122,26 @@ const KakaoMap = () => { }, [isMounted, pathname]); useEffect(() => { - if (!isMounted || pathname === "/admin" || !shouldLoadMapSdk) return; + if (!isMounted || pathnameRef.current === "/admin" || !shouldLoadMapSdk) + return; let disposed = false; const fetch = async () => { - const data = await getAllMarker(); + try { + const data = await getAllMarker(); - if (disposed) return; + if (disposed) return; - const imageMarker = data.filter((marker) => { - return !!marker.hasPhoto; - }); + const imageMarker = data.filter((marker) => { + return !!marker.hasPhoto; + }); - setCount(imageMarker.length); - replaceMarker(data); + setCount(imageMarker.length); + replaceMarker(data); + } catch { + // 마커 로딩 실패 시 조용히 실패 (unhandled rejection 방지) + } }; const cancelIdleTask = scheduleIdleTask(() => { @@ -142,12 +152,18 @@ const KakaoMap = () => { disposed = true; cancelIdleTask(); }; - }, [isMounted, pathname, replaceMarker, setCount, shouldLoadMapSdk]); + }, [isMounted, replaceMarker, setCount, shouldLoadMapSdk]); useEffect(() => { if (!window.ReactNativeWebView || !map) return; const handleMessage = (e: any) => { - const data = JSON.parse(e.data); + if (typeof e.data !== "string") return; + let data: any; + try { + data = JSON.parse(e.data); + } catch { + return; + } if (data.latitude && data.longitude) { setMyLocation({ lat: data.latitude, lng: data.longitude }); diff --git a/components/layout/roadview.tsx b/components/layout/roadview.tsx index 1800b40..c25c94a 100644 --- a/components/layout/roadview.tsx +++ b/components/layout/roadview.tsx @@ -41,6 +41,19 @@ const Roadview = () => { if (!map || !open || !lat || !lng) return; if (!mapContainer.current || !roadviewContainer.current) return; + let disposed = false; + // 이 effect 에서 등록한 kakao 리스너들을 모아 cleanup 에서 해제한다. (P2-8) + const listeners: { target: any; type: string; handler: (...a: any[]) => void }[] = + []; + const addListener = ( + target: any, + type: string, + handler: (...a: any[]) => void + ) => { + window.kakao.maps.event.addListener(target, type, handler); + listeners.push({ target, type, handler }); + }; + const mapCenter = new window.kakao.maps.LatLng(lat, lng); const mapOption = { center: mapCenter, @@ -72,6 +85,8 @@ const Roadview = () => { const position = new window.kakao.maps.LatLng(lat, lng); roadviewClient.getNearestPanoId(position, 50, (panoId: number) => { + // 콜백이 모달이 닫힌 뒤 실행될 수 있으므로 disposed 가드 (P2-8) + if (disposed) return; if (panoId === null) { toast({ description: "로드뷰를 지원하지 않는 주소입니다." }); closeModal(); @@ -82,7 +97,7 @@ const Roadview = () => { let mapWalker: any = null; - window.kakao.maps.event.addListener(roadview, "init", () => { + addListener(roadview, "init", () => { // 로드뷰에 마커 표시 const rMarker = new window.kakao.maps.Marker({ position: mapCenter, @@ -110,17 +125,25 @@ const Roadview = () => { mapWalker.setMap(); mapWalker.init(); - window.kakao.maps.event.addListener(roadview, "viewpoint_changed", () => { + addListener(roadview, "viewpoint_changed", () => { const viewpoint = roadview.getViewpoint(); mapWalker.setAngle(viewpoint.pan); }); - window.kakao.maps.event.addListener(roadview, "position_changed", () => { + addListener(roadview, "position_changed", () => { const position = roadview.getPosition(); mapWalker.setPosition(position); miniMap.setCenter(position); }); }); + + return () => { + disposed = true; + listeners.forEach(({ target, type, handler }) => { + window.kakao.maps.event.removeListener(target, type, handler); + }); + marker.setMap(null); + }; }, [map, open, lng, lat, toast, closeModal]); useEffect(() => { @@ -128,7 +151,9 @@ const Roadview = () => { if (mapHover) mapData.addOverlayMapTypeId(window.kakao.maps.MapTypeId.ROADVIEW); - else mapData.addOverlayMapTypeId(window.kakao.maps.MapTypeId.ROADMAP); + // leave 시에는 ROADVIEW 오버레이를 제거해야 한다. ROADMAP 은 베이스 타입이라 + // 추가해도 ROADVIEW 오버레이가 벗겨지지 않는 버그가 있었다. (P2-7) + else mapData.removeOverlayMapTypeId(window.kakao.maps.MapTypeId.ROADVIEW); }, [mapHover, mapData]); // Fetch and display roadview date for 5 seconds diff --git a/components/pages/pullup/share-button.tsx b/components/pages/pullup/share-button.tsx index 75fb82f..2ab4402 100644 --- a/components/pages/pullup/share-button.tsx +++ b/components/pages/pullup/share-button.tsx @@ -4,9 +4,9 @@ import BottomSheet, { BottomSheetItem } from "@/components/common/bottom-sheet"; import { useBottomSheetStore } from "@/store/useBottomSheetStore"; import convertWgs from "@api/marker/convert-wgs"; import IconButton from "@common/icon-button"; -import useGps from "@hooks/useGps"; import { useToast } from "@hooks/useToast"; import downloadPdf from "@lib/api/marker/download-pdf"; +import getMyLocation from "@lib/get-my-location"; import { FileDown, Link2, MapPinned, Route, Share2 } from "lucide-react"; import { useState } from "react"; @@ -20,7 +20,6 @@ interface ShareButtonProps { const ShareButton = ({ markerId, lat, lng, address }: ShareButtonProps) => { const { show } = useBottomSheetStore(); const { toast } = useToast(); - const { handleGps } = useGps(); const [downLoading, setDownLoading] = useState(false); @@ -53,31 +52,29 @@ const ShareButton = ({ markerId, lat, lng, address }: ShareButtonProps) => { const downloadMap = async () => { setDownLoading(true); - const response = await downloadPdf({ lat, lng }); + try { + const response = await downloadPdf({ lat, lng }); - if (!response.ok) { - setDownLoading(false); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + + a.href = url; + a.download = `${markerId}.pdf`; + + a.click(); + URL.revokeObjectURL(url); + } catch { toast({ description: "잠시 후 다시 시도해주세요.", }); - return; + } finally { + setDownLoading(false); } - - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - - a.href = url; - a.download = `${markerId}.pdf`; - - setDownLoading(false); - - a.click(); - URL.revokeObjectURL(url); }; const openLocation = async () => { - const myLocate = handleGps(); + const myLocate = await getMyLocation(); if (myLocate) { const sp = await convertWgs(myLocate.lat, myLocate.lng); const dst = await convertWgs(lat, lng); diff --git a/components/pages/search/around-search.tsx b/components/pages/search/around-search.tsx index 753409c..ee5174a 100644 --- a/components/pages/search/around-search.tsx +++ b/components/pages/search/around-search.tsx @@ -7,6 +7,7 @@ import Skeleton from "@common/skeleton"; import Text from "@common/text"; import LocationIcon from "@icons/location-icon"; import PinIcon from "@icons/pin-icon"; +import useMapStore from "@store/useMapStore"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -45,26 +46,28 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { const miniMapInstanceRef = useRef(null); const circleOverlayRef = useRef(null); + // 전역 map 이 준비되면 kakao SDK 도 로드됐다는 신호로 사용한다. (P2-10) + const globalMap = useMapStore((state) => state.map); + const loadMoreMarkers = useCallback(async () => { if (isLoading || currentPage >= totalPages || currentPage === 0) return; setIsLoading(true); - const newData = await closeMarker({ - lat: Number(lat), - lng: Number(lng), - distance: distance, - pageParam: currentPage + 1, - }); - - if (newData.error || newData.message) { + try { + const newData = await closeMarker({ + lat: Number(lat), + lng: Number(lng), + distance: distance, + pageParam: currentPage + 1, + }); + + setMarkers((prevMarkers) => [...prevMarkers, ...newData.markers]); + setCurrentPage(newData.currentPage); + } catch { + // 추가 로딩 실패 시 조용히 중단 + } finally { setIsLoading(false); - return; } - - setMarkers((prevMarkers) => [...prevMarkers, ...newData.markers]); - setCurrentPage(newData.currentPage); - - setIsLoading(false); }, [currentPage, isLoading, totalPages, lat, lng, distance]); useEffect(() => { @@ -112,14 +115,6 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { const results = await Promise.all(pagePromises); - // Check if first request failed - if (results[0].error || results[0].message) { - setIsLoading(false); - setMarkers([]); - setHasSearched(true); - return; - } - // Combine all markers from loaded pages const allMarkers = results.flatMap(result => result.markers || [] @@ -128,22 +123,29 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { setMarkers(allMarkers); setCurrentPage(initialPagesToLoad); setTotalPages(results[0].totalPages || 0); - setIsLoading(false); setHasSearched(true); } catch (error) { - setIsLoading(false); setMarkers([]); setHasSearched(true); + } finally { + setIsLoading(false); } }, [lat, lng, distance]); - // Initialize mini Kakao Map + // Initialize mini Kakao Map (SDK 준비 후 1회 생성, 이후 center 만 갱신) useEffect(() => { - if (!miniMapRef.current || !window.kakao?.maps) return; + if (!miniMapRef.current) return; + if (!globalMap || !window.kakao?.maps) return; - const container = miniMapRef.current; const center = new window.kakao.maps.LatLng(Number(lat), Number(lng)); + // 이미 생성돼 있으면 재생성하지 않고 center 만 이동한다. (P2-10) + if (miniMapInstanceRef.current) { + miniMapInstanceRef.current.setCenter(center); + return; + } + + const container = miniMapRef.current; const options = { center: center, level: 7, // Zoom level @@ -158,9 +160,8 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { miniMapInstanceRef.current = miniMap; // Add center marker - const markerPosition = new window.kakao.maps.LatLng(Number(lat), Number(lng)); new window.kakao.maps.Marker({ - position: markerPosition, + position: center, map: miniMap, }); @@ -169,7 +170,7 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { circleOverlayRef.current.setMap(null); } }; - }, [lat, lng]); + }, [lat, lng, globalMap]); // Update circle overlay when distance changes useEffect(() => { @@ -495,7 +496,7 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { {marker.address}
- {marker.distance && ( + {marker.distance > 0 && (
diff --git a/hooks/useCompass.ts b/hooks/useCompass.ts index 537de20..c4ac038 100644 --- a/hooks/useCompass.ts +++ b/hooks/useCompass.ts @@ -1,84 +1,91 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; // Extend DeviceOrientationEvent to include iOS-specific webkitCompassHeading interface DeviceOrientationEventWithWebkit extends DeviceOrientationEvent { webkitCompassHeading?: number; } +interface UseCompassReturn { + heading: number | null; + /** + * iOS 13+ 에서 나침반 권한을 요청한다. + * 반드시 사용자 제스처(버튼 클릭 등) 안에서 호출해야 iOS 가 허용한다. (P2-5) + */ + requestPermission: () => Promise; +} + /** * Custom hook to get device compass heading (Device Orientation API) * Works even when device is stationary (unlike GPS heading) * Requires HTTPS and user permission on iOS 13+ * - * @returns Current compass heading in degrees (0-360, where 0 = North) + * @param enabled - true 일 때만 deviceorientation 을 구독한다. + * (추적 중이 아닐 때 센서 속도 리렌더 방지, P2-4) */ -const useCompass = (): number | null => { +const useCompass = (enabled: boolean = true): UseCompassReturn => { const [heading, setHeading] = useState(null); - useEffect(() => { - // Check if DeviceOrientationEvent is supported - if (!window.DeviceOrientationEvent) { - console.warn("Device Orientation API not supported"); - return; + // iOS 권한이 허용된 뒤에만 구독을 시작하기 위한 플래그 + const [permissionGranted, setPermissionGranted] = useState(false); + + const handleOrientation = useCallback((event: DeviceOrientationEvent) => { + const alpha = event.alpha; + if (alpha === null) return; + + let compassHeading = alpha; + const webkitEvent = event as DeviceOrientationEventWithWebkit; + if (webkitEvent.webkitCompassHeading !== undefined) { + compassHeading = webkitEvent.webkitCompassHeading; + } else { + compassHeading = (360 - alpha) % 360; } - const handleOrientation = (event: DeviceOrientationEvent) => { - // alpha: 0-360 degrees (compass heading) - // 0 = North, 90 = East, 180 = South, 270 = West - const alpha = event.alpha; - - if (alpha !== null) { - // On iOS, alpha is relative to device orientation - // For proper compass, we need to adjust based on screen orientation - let compassHeading = alpha; - - // Adjust for screen orientation (iOS Safari) - const webkitEvent = event as DeviceOrientationEventWithWebkit; - if (webkitEvent.webkitCompassHeading !== undefined) { - // iOS provides webkitCompassHeading (true compass heading) - compassHeading = webkitEvent.webkitCompassHeading; - } else { - // Android: alpha is already compass heading - // Ensure 0-360 range - compassHeading = (360 - alpha) % 360; - } - - setHeading(compassHeading); - } + // 센서는 초당 수십 회 float 값을 쏟아내므로 정수로 반올림하고, + // 값이 바뀌지 않으면 setState 를 건너뛰어 불필요한 리렌더를 막는다. (P2-4) + const rounded = Math.round(compassHeading); + setHeading((prev) => (prev === rounded ? prev : rounded)); + }, []); + + const requestPermission = useCallback(async () => { + if (typeof window === "undefined" || !window.DeviceOrientationEvent) return; + + const DOE = DeviceOrientationEvent as unknown as { + requestPermission?: () => Promise<"granted" | "denied">; }; - // Request permission on iOS 13+ - const requestPermission = async () => { - if ( - typeof DeviceOrientationEvent !== "undefined" && - typeof (DeviceOrientationEvent as any).requestPermission === "function" - ) { - try { - const permission = await ( - DeviceOrientationEvent as any - ).requestPermission(); - if (permission === "granted") { - window.addEventListener("deviceorientation", handleOrientation); - } else { - console.warn("Device orientation permission denied"); - } - } catch (error) { - console.error("Error requesting device orientation permission:", error); - } - } else { - // Non-iOS or older iOS - no permission needed - window.addEventListener("deviceorientation", handleOrientation); + if (typeof DOE.requestPermission === "function") { + try { + const permission = await DOE.requestPermission(); + setPermissionGranted(permission === "granted"); + } catch { + // 사용자가 거부했거나 제스처 밖에서 호출된 경우 — 조용히 무시 + setPermissionGranted(false); } + } else { + // 비 iOS 또는 구형 iOS: 권한 불필요 + setPermissionGranted(true); + } + }, []); + + useEffect(() => { + if (typeof window === "undefined" || !window.DeviceOrientationEvent) return; + if (!enabled) return; + + const DOE = DeviceOrientationEvent as unknown as { + requestPermission?: () => Promise<"granted" | "denied">; }; + const needsPermission = typeof DOE.requestPermission === "function"; - requestPermission(); + // iOS 는 권한이 허용된 뒤에만 구독한다. + if (needsPermission && !permissionGranted) return; + window.addEventListener("deviceorientation", handleOrientation); return () => { window.removeEventListener("deviceorientation", handleOrientation); }; - }, []); + }, [enabled, permissionGranted, handleOrientation]); - return heading; + return { heading, requestPermission }; }; export default useCompass; diff --git a/hooks/useGps.ts b/hooks/useGps.ts deleted file mode 100644 index 1f1bd14..0000000 --- a/hooks/useGps.ts +++ /dev/null @@ -1,46 +0,0 @@ -import useAlertStore from "@store/useAlertStore"; -import useGeolocationStore, { type Location } from "@store/useGeolocationStore"; -import { useState } from "react"; - -const useGps = () => { - const { myLocation } = useGeolocationStore(); - const { openAlert } = useAlertStore(); - - const [location, setLocation] = useState(null); - - const handleGps = () => { - if (!myLocation) { - const setPosition = (position: GeolocationPosition) => { - setLocation({ - lat: position.coords.latitude, - lng: position.coords.longitude, - }); - }; - - if (navigator.geolocation) { - navigator.geolocation.watchPosition( - (position) => { - setPosition(position); - }, - (err) => { - console.error(err); - openAlert({ - title: "위치 서비스 사용", - description: - "위치 서비스를 사용할 수 없습니다. 브라우저 설정에서 위치서비스를 켜주세요.", - onClick: () => {}, - }); - } - ); - } - - return location; - } - - return myLocation; - }; - - return { handleGps }; -}; - -export default useGps; diff --git a/hooks/useGpsTracking.ts b/hooks/useGpsTracking.ts index 08fe8b2..02879d6 100644 --- a/hooks/useGpsTracking.ts +++ b/hooks/useGpsTracking.ts @@ -49,8 +49,10 @@ const useGpsTracking = ({ const [gpsState, setGpsState] = useState("idle"); const hasReceivedFirstLocation = useRef(false); - // Get compass heading (works even when stationary) - const compassHeading = useCompass(); + // 추적 중일 때만 나침반을 구독해 센서 속도 리렌더를 막는다. (P2-4) + const isTracking = useMapStore((state) => state.isTrackingLocation); + const { heading: compassHeading, requestPermission: requestCompassPermission } = + useCompass(isTracking); // Create or update user location marker const updateUserLocationMarker = useCallback( @@ -98,6 +100,11 @@ const useGpsTracking = ({ setGpsState("locating"); hasReceivedFirstLocation.current = false; + // iOS 나침반 권한은 사용자 제스처(이 클릭) 안에서 요청해야 허용된다. (P2-5) + if (isMobile) { + requestCompassPermission(); + } + // Handle React Native WebView if (window.ReactNativeWebView) { window.ReactNativeWebView.postMessage("gps-permission"); @@ -282,6 +289,7 @@ const useGpsTracking = ({ setGpsWatchId, setIsTrackingLocation, compassHeading, + requestCompassPermission, ]); // Update marker rotation when compass heading changes (stationary device) @@ -295,18 +303,11 @@ const useGpsTracking = ({ } }, [compassHeading, myLocation, map, updateUserLocationMarker]); - // Cleanup: Stop tracking location when component unmounts - // Only clean up if map is being destroyed (not just navigating between pages) + // Cleanup: 컴포넌트 언마운트 시 활성 watch/마커를 정리한다. + // 기존 구현은 `map` 이 null 일 때만 정리했는데 map 은 스토어에 계속 유지되어 + // 사실상 dead 였다. gpsWatchId 존재 여부를 기준으로 정리한다. (P2-6) useEffect(() => { return () => { - // Don't clean up if map still exists (just navigation) - // Map will persist across page navigations - if (map) { - // Map still exists, keep tracking and marker - return; - } - - // Map is being destroyed, clean up everything const currentWatchId = useMapStore.getState().gpsWatchId; const currentMarker = useMapStore.getState().userLocationMarker; @@ -322,7 +323,7 @@ const useGpsTracking = ({ setIsTrackingLocation(false); }; - }, [map, setGpsWatchId, setUserLocationMarker, setIsTrackingLocation]); + }, [setGpsWatchId, setUserLocationMarker, setIsTrackingLocation]); return { gpsState, diff --git a/hooks/useMarkerControl.tsx b/hooks/useMarkerControl.tsx index 2aa8c43..09699c0 100644 --- a/hooks/useMarkerControl.tsx +++ b/hooks/useMarkerControl.tsx @@ -108,6 +108,9 @@ const useMarkerControl = () => { overlay.setMap(map); + // deleteOverlays 시 unmount 할 수 있도록 root 를 오버레이에 보관 (P2-1 누수 방지) + overlay.__reactRoot = root; + appendOverlay(overlay); }, [appendOverlay] diff --git a/lib/get-my-location.ts b/lib/get-my-location.ts new file mode 100644 index 0000000..14605d1 --- /dev/null +++ b/lib/get-my-location.ts @@ -0,0 +1,40 @@ +import useGeolocationStore, { type Location } from "@store/useGeolocationStore"; + +/** + * 현재 위치를 Promise 로 반환한다. + * + * 기존 `useGps` 훅은 watchPosition 을 시작한 뒤 stale 한 useState 값을 동기 + * 반환해 "첫 클릭 무동작 + 클릭마다 watcher 누적(해제 없음)" 문제가 있었다. (P2-3) + * 이 헬퍼는 스토어에 캐시된 위치가 있으면 그대로 쓰고, 없으면 getCurrentPosition + * 으로 1회 조회한다. watcher 를 남기지 않는다. + */ +const getMyLocation = (): Promise => { + const cached = useGeolocationStore.getState().myLocation; + if (cached) { + return Promise.resolve(cached); + } + + if (typeof navigator === "undefined" || !navigator.geolocation) { + return Promise.resolve(null); + } + + return new Promise((resolve) => { + navigator.geolocation.getCurrentPosition( + (position) => { + const location: Location = { + lat: position.coords.latitude, + lng: position.coords.longitude, + }; + // 다음 호출을 위해 캐시에 저장 + useGeolocationStore.getState().setMyLocation(location); + resolve(location); + }, + () => { + resolve(null); + }, + { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 } + ); + }); +}; + +export default getMyLocation; diff --git a/store/useMapStore.ts b/store/useMapStore.ts index 642314b..5a72ca2 100644 --- a/store/useMapStore.ts +++ b/store/useMapStore.ts @@ -54,15 +54,17 @@ const useMapStore = create()((set) => ({ marker.setMap(null); }); - return { ...prev }; + return { markers: [] }; }), deleteOverlays: () => set((prev) => { prev.overlays.forEach((overlay) => { overlay.setMap(null); + // 클러스터 오버레이가 보유한 React root 를 unmount 하여 누수 방지 (P2-1) + overlay.__reactRoot?.unmount(); }); - return { ...prev }; + return { overlays: [] }; }), })); diff --git a/types/kakao-map.types.ts b/types/kakao-map.types.ts index 25dce05..f6362a8 100644 --- a/types/kakao-map.types.ts +++ b/types/kakao-map.types.ts @@ -21,6 +21,7 @@ export interface KakaoMap { getLevel: () => number; relayout: VoidFunction; addOverlayMapTypeId: (data: any) => void; + removeOverlayMapTypeId: (data: any) => void; getProjection: () => any; setDraggable: (draggable: boolean) => void; } @@ -38,6 +39,11 @@ export interface KakaoMarker { export interface KakaoOverlay { setMap: (map: KakaoMap | null) => void; + /** + * 클러스터 오버레이가 createRoot 로 렌더한 React root. + * deleteOverlays 시 unmount 하여 React root 누수를 방지한다. (내부 전용) + */ + __reactRoot?: { unmount: () => void }; } export interface Qa { From 1b2167f883b876c2bf503e48f4e5868ab41297fa Mon Sep 17 00:00:00 2001 From: Yonghun Yi Date: Tue, 8 Sep 2026 10:28:34 +0900 Subject: [PATCH 03/14] =?UTF-8?q?fix:=20=EB=82=A0=EC=A7=9C=C2=B7=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=EC=8B=9D=C2=B7=EC=B1=84=ED=8C=85=20=EB=93=B1=20?= =?UTF-8?q?=EA=B0=9C=EB=B3=84=20=EB=A1=9C=EC=A7=81=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit format-date를 Asia/Seoul로 고정하고 minutes-ago 음수 clamp, 기구 배지 조건식, 채팅 cid 파싱·ping·keep-alive, URL 인코딩, message JSON 파싱 가드, blob/리스너/loaded 상태 누수, alert 버튼 게이트 등을 바로잡는다. --- app/pullup/[id]/chat/pullup-chat-client.tsx | 29 ++++++---- app/pullup/[id]/moment/moment-client.tsx | 10 +++- app/sitemap.ts | 1 - app/social/chat/[code]/chat-detail-client.tsx | 28 ++++++---- components/common/alert.tsx | 2 +- components/common/carousel.tsx | 1 + components/common/image-modal.tsx | 55 +++++++++++-------- components/pages/home/moment-list.tsx | 29 ++++++---- components/pages/mypage/user-info.tsx | 6 +- components/pages/pullup/image-carousel.tsx | 47 +++++++++------- components/pages/register/set-description.tsx | 4 +- .../pages/search/marker-search-result.tsx | 10 +--- components/provider/geo-provider.tsx | 12 +++- lib/api/marker/user-marker.ts | 2 +- lib/api/search/search.ts | 4 +- lib/format-date.ts | 17 ++++-- lib/minutes-ago.ts | 4 +- lib/session-cache.ts | 6 +- 18 files changed, 166 insertions(+), 101 deletions(-) diff --git a/app/pullup/[id]/chat/pullup-chat-client.tsx b/app/pullup/[id]/chat/pullup-chat-client.tsx index 568a90f..24aa7ec 100644 --- a/app/pullup/[id]/chat/pullup-chat-client.tsx +++ b/app/pullup/[id]/chat/pullup-chat-client.tsx @@ -65,8 +65,23 @@ const PullupChatClient = ({ }, [inputRef]); useEffect(() => { - const cid = localStorage.getItem("cid"); - setCid(cid); + const raw = localStorage.getItem("cid"); + let parsedCid: string | null = null; + + if (raw) { + try { + parsedCid = JSON.parse(raw)?.cid ?? null; + } catch { + parsedCid = null; + } + } + + if (!parsedCid) { + parsedCid = v4(); + localStorage.setItem("cid", JSON.stringify({ cid: parsedCid })); + } + + setCid(parsedCid); }, []); useEffect(() => { @@ -122,21 +137,15 @@ const PullupChatClient = ({ setIsChatError(true); }; - return () => { - ws.current?.close(); - }; - }, [cid, markerId]); - - useEffect(() => { - if (!ws) return; const pingInterval = setInterval(() => { ws.current?.send(JSON.stringify({ type: "ping" })); }, 30000); return () => { clearInterval(pingInterval); + ws.current?.close(); }; - }, []); + }, [cid, markerId]); useEffect(() => { const scrollBox = chatBox.current; diff --git a/app/pullup/[id]/moment/moment-client.tsx b/app/pullup/[id]/moment/moment-client.tsx index 3b039af..5b466d9 100644 --- a/app/pullup/[id]/moment/moment-client.tsx +++ b/app/pullup/[id]/moment/moment-client.tsx @@ -74,7 +74,10 @@ const MomentClient = ({ setSelectedFile(optimizedFile); const url = URL.createObjectURL(optimizedFile); - setPreviewURL(url); + setPreviewURL((prev) => { + if (prev) URL.revokeObjectURL(prev); + return url; + }); setErrorMessage(""); } catch (error) { if (error instanceof ImageValidationError) { @@ -105,7 +108,10 @@ const MomentClient = ({ const clearSelect = () => { setSelectedFile(null); - setPreviewURL(null); + setPreviewURL((prev) => { + if (prev) URL.revokeObjectURL(prev); + return null; + }); }; if (selectedFile && previewURL) { diff --git a/app/sitemap.ts b/app/sitemap.ts index 633a4e0..e49b3eb 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -17,7 +17,6 @@ const sitemap = async (): Promise => { const routesMap = [ "", - "/home", "/mypage", "/search", "/signin", diff --git a/app/social/chat/[code]/chat-detail-client.tsx b/app/social/chat/[code]/chat-detail-client.tsx index b45b14c..218aec5 100644 --- a/app/social/chat/[code]/chat-detail-client.tsx +++ b/app/social/chat/[code]/chat-detail-client.tsx @@ -9,6 +9,7 @@ import useInput from "@hooks/useInput"; import LoadingIcon from "@icons/loading-icon"; import { SendHorizontal } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { v4 } from "uuid"; export interface ChatMessage { uid: string; @@ -57,8 +58,21 @@ const ChatDetailClient = ({ useEffect(() => { const cidJson = localStorage.getItem("cid"); - if (!cidJson) return; - const newCid = JSON.parse(cidJson).cid; + let newCid: string | null = null; + + if (cidJson) { + try { + newCid = JSON.parse(cidJson)?.cid ?? null; + } catch { + newCid = null; + } + } + + if (!newCid) { + newCid = v4(); + localStorage.setItem("cid", JSON.stringify({ cid: newCid })); + } + setCid(newCid); }, []); @@ -102,21 +116,15 @@ const ChatDetailClient = ({ setIsConnectionError(true); }; - return () => { - ws.current?.close(); - }; - }, [cid, code]); - - useEffect(() => { - if (!ws.current) return; const pingInterval = setInterval(() => { ws.current?.send(JSON.stringify({ type: "ping" })); }, 30000); return () => { clearInterval(pingInterval); + ws.current?.close(); }; - }, []); + }, [cid, code]); useEffect(() => { const scrollBox = chatBox.current; diff --git a/components/common/alert.tsx b/components/common/alert.tsx index 5c4b505..228c4ad 100644 --- a/components/common/alert.tsx +++ b/components/common/alert.tsx @@ -141,7 +141,7 @@ const Alert = ({ {description && {description}} - {(onClick || cancel) && ( + {(onClick || onClickAsync || cancel) && (
{cancel && ( diff --git a/components/common/carousel.tsx b/components/common/carousel.tsx index ea0cabf..a039099 100644 --- a/components/common/carousel.tsx +++ b/components/common/carousel.tsx @@ -116,6 +116,7 @@ const Carousel = React.forwardRef< return () => { api?.off("select", onSelect); + api?.off("reInit", onSelect); }; }, [api, onSelect]); diff --git a/components/common/image-modal.tsx b/components/common/image-modal.tsx index 55da5b9..064478e 100644 --- a/components/common/image-modal.tsx +++ b/components/common/image-modal.tsx @@ -36,7 +36,7 @@ const ImageModal = ({ const { closeModal } = useImageModalStore(); const [imageSize, setImageSize] = useState(DEFAULT_IMAGE_SIZE); - const [isLoaded, setIsLoaded] = useState(false); + const [loadedUrls, setLoadedUrls] = useState>(new Set()); const zoomIn = () => { if ( @@ -61,7 +61,7 @@ const ImageModal = ({ }; const handleClose = () => { - setIsLoaded(false); + setLoadedUrls(new Set()); setImageSize(DEFAULT_IMAGE_SIZE); closeModal(); }; @@ -86,29 +86,38 @@ const ImageModal = ({ className="web:w-[80%] mo:w-dvw absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" > - {imageUrl.map((image) => ( - - {!isLoaded && ( - - )} - { + const isLoaded = loadedUrls.has(image); + return ( + + {!isLoaded && ( + )} - onLoadingComplete={() => setIsLoaded(true)} - unoptimized - /> - - ))} + 상세 + setLoadedUrls((prev) => { + const next = new Set(prev); + next.add(image); + return next; + }) + } + unoptimized + /> + + ); + })} {imageUrl.length > 1 && ( <> diff --git a/components/pages/home/moment-list.tsx b/components/pages/home/moment-list.tsx index 6fb21aa..832359e 100644 --- a/components/pages/home/moment-list.tsx +++ b/components/pages/home/moment-list.tsx @@ -52,7 +52,7 @@ const MomentList = ({ data }: { data: Moment[] }) => { }); }, [data, isMounted]); - let animationFrameId: number; + const animationFrameId = useRef(null); const handleMouseDown = (e: MouseEvent) => { if (!sliderRef.current) return; @@ -90,9 +90,9 @@ const MomentList = ({ data }: { data: Moment[] }) => { minTranslateX ); - if (animationFrameId) cancelAnimationFrame(animationFrameId); + if (animationFrameId.current) cancelAnimationFrame(animationFrameId.current); - animationFrameId = requestAnimationFrame(() => { + animationFrameId.current = requestAnimationFrame(() => { setStyle({ transform: `translateX(${translateX.current}px)` }); }); }; @@ -130,6 +130,19 @@ const MomentList = ({ data }: { data: Moment[] }) => { if (viewMoment && curMoment) { const { hours, minutes } = minutesAgo(curMoment.createdAt); + let curBlurDataURL = "/placeholder_image.png"; + if (isMounted) { + try { + curBlurDataURL = pixelsToDataUrl( + decodeBlurhash(curMoment.blurhash, 100, 200), + 100, + 200 + ); + } catch { + curBlurDataURL = "/placeholder_image.png"; + } + } + return (
{data.length > 1 && ( @@ -194,15 +207,7 @@ const MomentList = ({ data }: { data: Moment[] }) => { alt={curMoment.caption} className="object-contain z-10" placeholder="blur" - blurDataURL={ - isMounted - ? pixelsToDataUrl( - decodeBlurhash(curMoment.blurhash, 100, 200), - 100, - 200 - ) - : "/placeholder_image.png" - } + blurDataURL={curBlurDataURL} />
diff --git a/components/pages/mypage/user-info.tsx b/components/pages/mypage/user-info.tsx index 6058448..dd8b16d 100644 --- a/components/pages/mypage/user-info.tsx +++ b/components/pages/mypage/user-info.tsx @@ -11,9 +11,9 @@ const UserInfo = ({ user }: { user: MyInfo }) => {
안녕하세요. - {(user.reportCount || user.markerCount) && ( + {((user.reportCount ?? 0) > 0 || (user.markerCount ?? 0) > 0) && (
- {user.reportCount && ( + {(user.reportCount ?? 0) > 0 && ( { )} - {user.markerCount && ( + {(user.markerCount ?? 0) > 0 && ( { - const [loading, setLoading] = useState(false); + const [loadedUrls, setLoadedUrls] = useState>(new Set()); const [emblaRef] = useEmblaCarousel({ loop: true }, [ Autoplay({ delay: 4000 }), @@ -24,25 +24,34 @@ const ImageCarousel = ({ photos }: ImageCarouselProps) => { return (
- {photos.map((photo, index) => ( -
-
- {!loading && } - 상세 setLoading(true)} - priority={index === 0} - /> + {photos.map((photo, index) => { + const isLoaded = loadedUrls.has(photo.photoUrl); + return ( +
+
+ {!isLoaded && } + 상세 + setLoadedUrls((prev) => { + const next = new Set(prev); + next.add(photo.photoUrl); + return next; + }) + } + priority={index === 0} + /> +
-
- ))} + ); + })}
); diff --git a/components/pages/register/set-description.tsx b/components/pages/register/set-description.tsx index 891e1d0..8c6443d 100644 --- a/components/pages/register/set-description.tsx +++ b/components/pages/register/set-description.tsx @@ -63,11 +63,11 @@ const SetDescription = ({
); diff --git a/components/pages/search/marker-search-result.tsx b/components/pages/search/marker-search-result.tsx index eaab3fc..030d73b 100644 --- a/components/pages/search/marker-search-result.tsx +++ b/components/pages/search/marker-search-result.tsx @@ -24,7 +24,7 @@ const MarkerSearchResult = ({ address, markerId }: MarkerSearchResultProps) => { setLoading(true); const data = await markerDetail({ id: markerId }); - if (marker?.error) { + if (data.error) { setError(true); setLoading(false); return; @@ -36,7 +36,7 @@ const MarkerSearchResult = ({ address, markerId }: MarkerSearchResultProps) => { }; fetch(); - }, [marker?.error, markerId]); + }, [markerId]); if (loading) { return ( @@ -62,11 +62,7 @@ const MarkerSearchResult = ({ address, markerId }: MarkerSearchResultProps) => { >
{`${marker.markerId} { if (window.ReactNativeWebView) { window.ReactNativeWebView.postMessage("gps-permission"); const handleMessage = (e: any) => { - const data = JSON.parse(e.data); + if (typeof e.data !== "string") return; - if (data.latitude && data.longitude) { - setMyLocation({ lat: data.latitude, lng: data.longitude }); + try { + const data = JSON.parse(e.data); + + if (data.latitude && data.longitude) { + setMyLocation({ lat: data.latitude, lng: data.longitude }); + } + } catch { + return; } }; diff --git a/lib/api/marker/user-marker.ts b/lib/api/marker/user-marker.ts index b869cd6..221cb05 100644 --- a/lib/api/marker/user-marker.ts +++ b/lib/api/marker/user-marker.ts @@ -31,7 +31,7 @@ const userMarkers = async ({ const url = isServer ? process.env.NEXT_PUBLIC_BASE_URL : "/api/v1"; const response = await fetchData( - `${url}/markers/user/${userName}?page=${page}&pageSize=${pageSize}`, + `${url}/markers/user/${encodeURIComponent(userName)}?page=${page}&pageSize=${pageSize}`, { headers: { Cookie: cookie || "", diff --git a/lib/api/search/search.ts b/lib/api/search/search.ts index 7e43d73..120f1d6 100644 --- a/lib/api/search/search.ts +++ b/lib/api/search/search.ts @@ -13,7 +13,9 @@ export interface SearchRes { } const search = async (query: string): Promise => { - const response = await fetchData(`/api/v1/search/marker?term=${query}`); + const response = await fetchData( + `/api/v1/search/marker?term=${encodeURIComponent(query)}` + ); const data = await response.json(); diff --git a/lib/format-date.ts b/lib/format-date.ts index cd36128..11b8fed 100644 --- a/lib/format-date.ts +++ b/lib/format-date.ts @@ -1,9 +1,18 @@ export const formatDate = (dateString: string | Date): string => { const date = new Date(dateString); - const year = date.getUTCFullYear(); - const month = String(date.getUTCMonth() + 1).padStart(2, "0"); - const day = String(date.getUTCDate()).padStart(2, "0"); + // API 는 UTC ISO 문자열을 반환하므로, UTC getter 로 포매팅하면 KST 새벽 + // (00:00~08:59) 게시물이 하루 전으로 표시된다. SSR(UTC 서버)/CSR 양쪽에서 + // 동일한 결과를 내기 위해 Asia/Seoul 타임존으로 고정한다. (P3-1) + const parts = new Intl.DateTimeFormat("ko-KR", { + timeZone: "Asia/Seoul", + year: "numeric", + month: "2-digit", + day: "2-digit", + }).formatToParts(date); - return `${year}.${month}.${day}`; + const get = (type: string) => + parts.find((p) => p.type === type)?.value ?? ""; + + return `${get("year")}.${get("month")}.${get("day")}`; }; diff --git a/lib/minutes-ago.ts b/lib/minutes-ago.ts index 5647ea4..ca1f0ff 100644 --- a/lib/minutes-ago.ts +++ b/lib/minutes-ago.ts @@ -2,7 +2,9 @@ const minutesAgo = (dateString: string | Date) => { const pastDate: Date = new Date(dateString); const now: Date = new Date(); - const diffMs: number = now.getTime() - pastDate.getTime(); + // 클라이언트 시계가 서버보다 느리면 diff 가 음수가 되어 "-1분 전" 이 표시된다. + // 0 미만으로 내려가지 않도록 clamp 한다. (P3-2) + const diffMs: number = Math.max(0, now.getTime() - pastDate.getTime()); const totalMinutes: number = Math.floor(diffMs / (1000 * 60)); diff --git a/lib/session-cache.ts b/lib/session-cache.ts index 7d4f09f..dd1f300 100644 --- a/lib/session-cache.ts +++ b/lib/session-cache.ts @@ -40,7 +40,11 @@ export const getSessionCache = (): User | null => { export const setSessionCache = (user: User): void => { if (typeof window === "undefined") return; - sessionStorage.setItem(SESSION_CACHE_KEY, JSON.stringify(user)); + try { + sessionStorage.setItem(SESSION_CACHE_KEY, JSON.stringify(user)); + } catch { + // 저장 실패(용량 초과/직렬화 오류 등)는 조용히 무시한다. + } }; /** From c637ee1f664485c77fcd707de41daaed7413e2bf Mon Sep 17 00:00:00 2001 From: Yonghun Yi Date: Tue, 8 Sep 2026 10:28:53 +0900 Subject: [PATCH 04/14] =?UTF-8?q?chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=C2=B7=EC=9D=98=EC=A1=B4=EC=84=B1=C2=B7?= =?UTF-8?q?=EB=8D=B0=EB=93=9C=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 미사용 파일 13개와 의존성 9개(react-query, remark-breaks, react-slot, jsdom, jest-dom, plugin-react, webpack-cli 등)를 제거하고, dead !response.ok 분기·중복 분기·no-op·identity 래퍼를 정리한다. move-map-input의 KakaoPlace 타입은 types로 이관, storybook staticDirs 경로 오타도 수정. --- .pnp.cjs | 1092 +---------------- .storybook/main.ts | 2 +- app/mypage/device-type.tsx | 12 - app/search/search-client.tsx | 2 +- components/common/event-popup.tsx | 123 -- components/common/scroll-to-top.tsx | 33 - components/icons/bookmark-icon.tsx | 25 - components/icons/checked-icon.tsx | 24 - components/icons/config-icon.tsx | 37 - components/icons/location-pin-icon.tsx | 30 - components/layout/move-map-input.tsx | 168 --- components/layout/overlay.tsx | 4 +- .../pages/challenge/celebration-motion.tsx | 9 +- components/pages/home/slide-icons.tsx | 254 ---- components/pages/search/search-list.tsx | 2 +- components/ui/badge.tsx | 36 - hooks/useDeviceType.ts | 68 - hooks/useEventPopup.ts | 38 - lib/api/report/my-suggested.ts | 16 +- lib/api/user/favorites.ts | 16 +- lib/map-walker.ts | 3 - package.json | 11 +- store/useAlertStore.ts | 6 +- types/cluster.types.ts | 43 - types/kakao-location.type.ts | 32 - types/kakao-place.types.ts | 14 + yarn.lock | 934 ++------------ 27 files changed, 137 insertions(+), 2897 deletions(-) delete mode 100644 app/mypage/device-type.tsx delete mode 100644 components/common/event-popup.tsx delete mode 100644 components/common/scroll-to-top.tsx delete mode 100644 components/icons/bookmark-icon.tsx delete mode 100644 components/icons/checked-icon.tsx delete mode 100644 components/icons/config-icon.tsx delete mode 100644 components/icons/location-pin-icon.tsx delete mode 100644 components/layout/move-map-input.tsx delete mode 100644 components/pages/home/slide-icons.tsx delete mode 100644 components/ui/badge.tsx delete mode 100644 hooks/useDeviceType.ts delete mode 100644 hooks/useEventPopup.ts delete mode 100644 types/cluster.types.ts delete mode 100644 types/kakao-location.type.ts create mode 100644 types/kakao-place.types.ts diff --git a/.pnp.cjs b/.pnp.cjs index 88fb760..a1b0e39 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -31,7 +31,6 @@ const RAW_RUNTIME_STATE = ["@playwright/test", "npm:1.52.0"],\ ["@radix-ui/react-dialog", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.1.14"],\ ["@radix-ui/react-select", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:2.2.5"],\ - ["@radix-ui/react-slot", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3"],\ ["@radix-ui/react-toast", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.1"],\ ["@storybook/addon-essentials", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ ["@storybook/addon-interactions", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ @@ -45,17 +44,12 @@ const RAW_RUNTIME_STATE = ["@storybook/react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ ["@storybook/test", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ ["@tailwindcss/postcss", "npm:4.1.18"],\ - ["@tanstack/react-query", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.72.0"],\ ["@testing-library/dom", "npm:10.4.1"],\ - ["@testing-library/jest-dom", "npm:6.9.1"],\ ["@testing-library/react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:16.3.2"],\ - ["@types/jsdom", "npm:20.0.1"],\ ["@types/node", "npm:20.14.10"],\ ["@types/react", "npm:18.3.3"],\ ["@types/react-dom", "npm:18.3.0"],\ - ["@types/sharp", "npm:0.32.0"],\ ["@types/uuid", "npm:10.0.0"],\ - ["@vitejs/plugin-react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:6.0.3"],\ ["class-variance-authority", "npm:0.7.1"],\ ["clsx", "npm:2.1.1"],\ ["embla-carousel-autoplay", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.1.7"],\ @@ -68,7 +62,6 @@ const RAW_RUNTIME_STATE = ["happy-dom", "npm:20.10.6"],\ ["husky", "npm:9.1.7"],\ ["img-toolkit", "npm:1.0.2"],\ - ["jsdom", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:20.0.3"],\ ["lucide-react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:0.525.0"],\ ["next", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:14.2.5"],\ ["next-themes", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:0.3.0"],\ @@ -79,7 +72,6 @@ const RAW_RUNTIME_STATE = ["react-markdown", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:9.0.1"],\ ["react-player", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:2.16.0"],\ ["rehype-raw", "npm:7.0.0"],\ - ["remark-breaks", "npm:4.0.0"],\ ["remark-gfm", "npm:4.0.0"],\ ["sharp", "npm:0.34.5"],\ ["storybook", "npm:8.2.2"],\ @@ -91,7 +83,6 @@ const RAW_RUNTIME_STATE = ["vite", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.1.4"],\ ["vitest", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:4.1.10"],\ ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"],\ - ["webpack-cli", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4"],\ ["zustand", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:4.5.4"]\ ],\ "linkType": "SOFT"\ @@ -104,13 +95,6 @@ const RAW_RUNTIME_STATE = ["@adobe/css-tools", "npm:4.4.0"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:4.5.0", {\ - "packageLocation": "../../.yarn/berry/cache/@adobe-css-tools-npm-4.5.0-a5b5be48dc-10c0.zip/node_modules/@adobe/css-tools/",\ - "packageDependencies": [\ - ["@adobe/css-tools", "npm:4.5.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["@alloc/quick-lru", [\ @@ -2838,15 +2822,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@discoveryjs/json-ext", [\ - ["npm:0.5.7", {\ - "packageLocation": "../../.yarn/berry/cache/@discoveryjs-json-ext-npm-0.5.7-fe04af1f31-10c0.zip/node_modules/@discoveryjs/json-ext/",\ - "packageDependencies": [\ - ["@discoveryjs/json-ext", "npm:0.5.7"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@emnapi/core", [\ ["npm:1.11.1", {\ "packageLocation": "../../.yarn/berry/cache/@emnapi-core-npm-1.11.1-99a234b095-10c0.zip/node_modules/@emnapi/core/",\ @@ -4146,7 +4121,7 @@ const RAW_RUNTIME_STATE = ["@radix-ui/react-compose-refs", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.1.2"],\ ["@radix-ui/react-context", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.1.2"],\ ["@radix-ui/react-primitive", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:2.1.3"],\ - ["@radix-ui/react-slot", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3"],\ + ["@radix-ui/react-slot", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.3"],\ ["@types/react", "npm:18.3.3"],\ ["@types/react-dom", "npm:18.3.0"],\ ["react", "npm:18.3.1"],\ @@ -4267,7 +4242,7 @@ const RAW_RUNTIME_STATE = ["@radix-ui/react-portal", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.1.9"],\ ["@radix-ui/react-presence", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.1.4"],\ ["@radix-ui/react-primitive", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:2.1.3"],\ - ["@radix-ui/react-slot", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3"],\ + ["@radix-ui/react-slot", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.3"],\ ["@radix-ui/react-use-controllable-state", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.2"],\ ["@types/react", "npm:18.3.3"],\ ["@types/react-dom", "npm:18.3.0"],\ @@ -4622,7 +4597,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "./.yarn/__virtual__/@radix-ui-react-primitive-virtual-5ff227f159/3/.yarn/berry/cache/@radix-ui-react-primitive-npm-2.1.3-6080896851-10c0.zip/node_modules/@radix-ui/react-primitive/",\ "packageDependencies": [\ ["@radix-ui/react-primitive", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:2.1.3"],\ - ["@radix-ui/react-slot", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3"],\ + ["@radix-ui/react-slot", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.3"],\ ["@types/react", "npm:18.3.3"],\ ["@types/react-dom", "npm:18.3.0"],\ ["react", "npm:18.3.1"],\ @@ -4662,7 +4637,7 @@ const RAW_RUNTIME_STATE = ["@radix-ui/react-popper", "virtual:bab5eeeec775338556cf2eac17de00fb465320d828825d41d9f4ef1e426dfe9f90e63cb8d96f0c2e2dee477dcc274c914313544a22b3ea384d875e6916b2a68d#npm:1.2.7"],\ ["@radix-ui/react-portal", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.1.9"],\ ["@radix-ui/react-primitive", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:2.1.3"],\ - ["@radix-ui/react-slot", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3"],\ + ["@radix-ui/react-slot", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.3"],\ ["@radix-ui/react-use-callback-ref", "virtual:b990d5189c20bf4c185e39f7d748afcf2955dccfee392cfca16bf020f583e90f46615a61aa160361dfef08c2d013fa3118571b76cbdaea36e2e22273b29728c8#npm:1.1.1"],\ ["@radix-ui/react-use-controllable-state", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.2"],\ ["@radix-ui/react-use-layout-effect", "virtual:b93451ce4238e43e3daa3569b29578af01b5e6e01c342f9111351e37cab636092ce4b1ace7c8731e9e42c4334f103770dda05d5ad1f1dfbe79819ad7e35e32dc#npm:1.1.1"],\ @@ -4713,10 +4688,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3", {\ - "packageLocation": "./.yarn/__virtual__/@radix-ui-react-slot-virtual-2eeaf8731f/3/.yarn/berry/cache/@radix-ui-react-slot-npm-1.2.3-6e45e6d89b-10c0.zip/node_modules/@radix-ui/react-slot/",\ + ["virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.3", {\ + "packageLocation": "./.yarn/__virtual__/@radix-ui-react-slot-virtual-5a58e24b47/3/.yarn/berry/cache/@radix-ui-react-slot-npm-1.2.3-6e45e6d89b-10c0.zip/node_modules/@radix-ui/react-slot/",\ "packageDependencies": [\ - ["@radix-ui/react-slot", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3"],\ + ["@radix-ui/react-slot", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.2.3"],\ ["@radix-ui/react-compose-refs", "virtual:bcf82fd5d00f1ad901e9f60fb7f2e125b5aa8497ad88b44c89521ae6f55ba1d2e2678304cd6b12543ad902cc18b4e80222f238c3e2f7d3a972cb779ed1cba597#npm:1.1.2"],\ ["@types/react", "npm:18.3.3"],\ ["react", "npm:18.3.1"]\ @@ -5777,7 +5752,7 @@ const RAW_RUNTIME_STATE = ["url", "npm:0.11.3"],\ ["util", "npm:0.12.5"],\ ["util-deprecate", "npm:1.0.2"],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"],\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"],\ ["webpack-dev-middleware", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:6.1.3"],\ ["webpack-hot-middleware", "npm:2.26.1"],\ ["webpack-virtual-modules", "npm:0.6.2"]\ @@ -6014,7 +5989,7 @@ const RAW_RUNTIME_STATE = ["@types/typescript", null],\ ["@types/webpack", null],\ ["babel-loader", "virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:9.1.3"],\ - ["css-loader", "virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:6.11.0"],\ + ["css-loader", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:6.11.0"],\ ["find-up", "npm:5.0.0"],\ ["fs-extra", "npm:11.2.0"],\ ["image-size", "npm:1.1.1"],\ @@ -6032,7 +6007,7 @@ const RAW_RUNTIME_STATE = ["semver", "npm:7.6.2"],\ ["sharp", "npm:0.33.4"],\ ["storybook", "npm:8.2.2"],\ - ["style-loader", "virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:3.3.4"],\ + ["style-loader", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:3.3.4"],\ ["styled-jsx", "virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:5.1.1"],\ ["ts-dedent", "npm:2.2.0"],\ ["tsconfig-paths", "npm:4.2.0"],\ @@ -6111,7 +6086,7 @@ const RAW_RUNTIME_STATE = ["storybook", "npm:8.2.2"],\ ["tsconfig-paths", "npm:4.2.0"],\ ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ "@types/react-dom",\ @@ -6199,7 +6174,7 @@ const RAW_RUNTIME_STATE = ["react-docgen-typescript", "virtual:2bef91dc8ff0adc5c4a97586a47fcf3bcab7170e450966d60d9adb2f2c8b791d49d7d1b7a2fc0eca7da02844195c6ff0dd68ae4e4dcd3dc44ab83fd532bd7cd9#npm:2.2.2"],\ ["tslib", "npm:2.6.3"],\ ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ "@types/typescript",\ @@ -6476,38 +6451,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@tanstack/query-core", [\ - ["npm:5.72.0", {\ - "packageLocation": "../../.yarn/berry/cache/@tanstack-query-core-npm-5.72.0-8dcd84559f-10c0.zip/node_modules/@tanstack/query-core/",\ - "packageDependencies": [\ - ["@tanstack/query-core", "npm:5.72.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@tanstack/react-query", [\ - ["npm:5.72.0", {\ - "packageLocation": "../../.yarn/berry/cache/@tanstack-react-query-npm-5.72.0-801dd3d704-10c0.zip/node_modules/@tanstack/react-query/",\ - "packageDependencies": [\ - ["@tanstack/react-query", "npm:5.72.0"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.72.0", {\ - "packageLocation": "./.yarn/__virtual__/@tanstack-react-query-virtual-a231d1ec9b/3/.yarn/berry/cache/@tanstack-react-query-npm-5.72.0-801dd3d704-10c0.zip/node_modules/@tanstack/react-query/",\ - "packageDependencies": [\ - ["@tanstack/react-query", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.72.0"],\ - ["@tanstack/query-core", "npm:5.72.0"],\ - ["@types/react", "npm:18.3.3"],\ - ["react", "npm:18.3.1"]\ - ],\ - "packagePeers": [\ - "@types/react",\ - "react"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@testing-library/dom", [\ ["npm:10.1.0", {\ "packageLocation": "../../.yarn/berry/cache/@testing-library-dom-npm-10.1.0-720175996f-10c0.zip/node_modules/@testing-library/dom/",\ @@ -6548,19 +6491,6 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["npm:6.9.1", {\ - "packageLocation": "../../.yarn/berry/cache/@testing-library-jest-dom-npm-6.9.1-125d97d1a5-10c0.zip/node_modules/@testing-library/jest-dom/",\ - "packageDependencies": [\ - ["@testing-library/jest-dom", "npm:6.9.1"],\ - ["@adobe/css-tools", "npm:4.5.0"],\ - ["aria-query", "npm:5.3.0"],\ - ["css.escape", "npm:1.5.1"],\ - ["dom-accessibility-api", "npm:0.6.3"],\ - ["picocolors", "npm:1.1.1"],\ - ["redent", "npm:3.0.0"]\ - ],\ - "linkType": "HARD"\ - }],\ ["virtual:43692ed3825df519427520ffd89da7a9549fdc7484584a0bc2e0aa7004449262e26bec60812d3fe3d37c71e5e7c7e9e5398c8dcf50384f00efc0266c85561353#npm:6.4.5", {\ "packageLocation": "./.yarn/__virtual__/@testing-library-jest-dom-virtual-5741e28304/3/.yarn/berry/cache/@testing-library-jest-dom-npm-6.4.5-e02d3c89b2-10c0.zip/node_modules/@testing-library/jest-dom/",\ "packageDependencies": [\ @@ -6646,15 +6576,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@tootallnate/once", [\ - ["npm:2.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/@tootallnate-once-npm-2.0.1-cd8ffd9654-10c0.zip/node_modules/@tootallnate/once/",\ - "packageDependencies": [\ - ["@tootallnate/once", "npm:2.0.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@tybys/wasm-util", [\ ["npm:0.10.1", {\ "packageLocation": "../../.yarn/berry/cache/@tybys-wasm-util-npm-0.10.1-607c8a7e5c-10c0.zip/node_modules/@tybys/wasm-util/",\ @@ -6917,18 +6838,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@types/jsdom", [\ - ["npm:20.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/@types-jsdom-npm-20.0.1-5bb899e006-10c0.zip/node_modules/@types/jsdom/",\ - "packageDependencies": [\ - ["@types/jsdom", "npm:20.0.1"],\ - ["@types/node", "npm:20.14.10"],\ - ["@types/tough-cookie", "npm:4.0.5"],\ - ["parse5", "npm:7.2.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@types/json-schema", [\ ["npm:7.0.15", {\ "packageLocation": "../../.yarn/berry/cache/@types-json-schema-npm-7.0.15-fd16381786-10c0.zip/node_modules/@types/json-schema/",\ @@ -7117,25 +7026,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@types/sharp", [\ - ["npm:0.32.0", {\ - "packageLocation": "../../.yarn/berry/cache/@types-sharp-npm-0.32.0-b89fbf03a9-10c0.zip/node_modules/@types/sharp/",\ - "packageDependencies": [\ - ["@types/sharp", "npm:0.32.0"],\ - ["sharp", "npm:0.34.5"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@types/tough-cookie", [\ - ["npm:4.0.5", {\ - "packageLocation": "../../.yarn/berry/cache/@types-tough-cookie-npm-4.0.5-8c5e2162e1-10c0.zip/node_modules/@types/tough-cookie/",\ - "packageDependencies": [\ - ["@types/tough-cookie", "npm:4.0.5"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@types/unist", [\ ["npm:2.0.11", {\ "packageLocation": "../../.yarn/berry/cache/@types-unist-npm-2.0.11-44eea90bde-10c0.zip/node_modules/@types/unist/",\ @@ -7370,37 +7260,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@vitejs/plugin-react", [\ - ["npm:6.0.3", {\ - "packageLocation": "../../.yarn/berry/cache/@vitejs-plugin-react-npm-6.0.3-f3d1d2b373-10c0.zip/node_modules/@vitejs/plugin-react/",\ - "packageDependencies": [\ - ["@vitejs/plugin-react", "npm:6.0.3"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:6.0.3", {\ - "packageLocation": "./.yarn/__virtual__/@vitejs-plugin-react-virtual-8429b83f47/3/.yarn/berry/cache/@vitejs-plugin-react-npm-6.0.3-f3d1d2b373-10c0.zip/node_modules/@vitejs/plugin-react/",\ - "packageDependencies": [\ - ["@vitejs/plugin-react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:6.0.3"],\ - ["@rolldown/plugin-babel", null],\ - ["@rolldown/pluginutils", "npm:1.0.1"],\ - ["@types/babel-plugin-react-compiler", null],\ - ["@types/rolldown__plugin-babel", null],\ - ["@types/vite", null],\ - ["babel-plugin-react-compiler", null],\ - ["vite", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.1.4"]\ - ],\ - "packagePeers": [\ - "@rolldown/plugin-babel",\ - "@types/babel-plugin-react-compiler",\ - "@types/rolldown__plugin-babel",\ - "@types/vite",\ - "babel-plugin-react-compiler",\ - "vite"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@vitest/expect", [\ ["npm:1.6.0", {\ "packageLocation": "../../.yarn/berry/cache/@vitest-expect-npm-1.6.0-0e382f8212-10c0.zip/node_modules/@vitest/expect/",\ @@ -7700,88 +7559,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["@webpack-cli/configtest", [\ - ["npm:2.1.1", {\ - "packageLocation": "../../.yarn/berry/cache/@webpack-cli-configtest-npm-2.1.1-2aa637b6bc-10c0.zip/node_modules/@webpack-cli/configtest/",\ - "packageDependencies": [\ - ["@webpack-cli/configtest", "npm:2.1.1"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.1.1", {\ - "packageLocation": "./.yarn/__virtual__/@webpack-cli-configtest-virtual-427df0f1ec/3/.yarn/berry/cache/@webpack-cli-configtest-npm-2.1.1-2aa637b6bc-10c0.zip/node_modules/@webpack-cli/configtest/",\ - "packageDependencies": [\ - ["@webpack-cli/configtest", "virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.1.1"],\ - ["@types/webpack", null],\ - ["@types/webpack-cli", null],\ - ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"],\ - ["webpack-cli", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4"]\ - ],\ - "packagePeers": [\ - "@types/webpack-cli",\ - "@types/webpack",\ - "webpack-cli",\ - "webpack"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@webpack-cli/info", [\ - ["npm:2.0.2", {\ - "packageLocation": "../../.yarn/berry/cache/@webpack-cli-info-npm-2.0.2-494be2e91a-10c0.zip/node_modules/@webpack-cli/info/",\ - "packageDependencies": [\ - ["@webpack-cli/info", "npm:2.0.2"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.0.2", {\ - "packageLocation": "./.yarn/__virtual__/@webpack-cli-info-virtual-5ede6d6af2/3/.yarn/berry/cache/@webpack-cli-info-npm-2.0.2-494be2e91a-10c0.zip/node_modules/@webpack-cli/info/",\ - "packageDependencies": [\ - ["@webpack-cli/info", "virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.0.2"],\ - ["@types/webpack", null],\ - ["@types/webpack-cli", null],\ - ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"],\ - ["webpack-cli", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4"]\ - ],\ - "packagePeers": [\ - "@types/webpack-cli",\ - "@types/webpack",\ - "webpack-cli",\ - "webpack"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@webpack-cli/serve", [\ - ["npm:2.0.5", {\ - "packageLocation": "../../.yarn/berry/cache/@webpack-cli-serve-npm-2.0.5-5a220c2601-10c0.zip/node_modules/@webpack-cli/serve/",\ - "packageDependencies": [\ - ["@webpack-cli/serve", "npm:2.0.5"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.0.5", {\ - "packageLocation": "./.yarn/__virtual__/@webpack-cli-serve-virtual-dc6b6c6dda/3/.yarn/berry/cache/@webpack-cli-serve-npm-2.0.5-5a220c2601-10c0.zip/node_modules/@webpack-cli/serve/",\ - "packageDependencies": [\ - ["@webpack-cli/serve", "virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.0.5"],\ - ["@types/webpack", null],\ - ["@types/webpack-cli", null],\ - ["@types/webpack-dev-server", null],\ - ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"],\ - ["webpack-cli", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4"],\ - ["webpack-dev-server", null]\ - ],\ - "packagePeers": [\ - "@types/webpack-cli",\ - "@types/webpack-dev-server",\ - "@types/webpack",\ - "webpack-cli",\ - "webpack-dev-server",\ - "webpack"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["@xtuc/ieee754", [\ ["npm:1.2.0", {\ "packageLocation": "../../.yarn/berry/cache/@xtuc-ieee754-npm-1.2.0-ec0ce4e025-10c0.zip/node_modules/@xtuc/ieee754/",\ @@ -7822,15 +7599,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["abab", [\ - ["npm:2.0.6", {\ - "packageLocation": "../../.yarn/berry/cache/abab-npm-2.0.6-2662fba7f0-10c0.zip/node_modules/abab/",\ - "packageDependencies": [\ - ["abab", "npm:2.0.6"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["abbrev", [\ ["npm:2.0.0", {\ "packageLocation": "../../.yarn/berry/cache/abbrev-npm-2.0.0-0eb38a17e5-10c0.zip/node_modules/abbrev/",\ @@ -7875,24 +7643,6 @@ const RAW_RUNTIME_STATE = ["acorn", "npm:8.12.1"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:8.17.0", {\ - "packageLocation": "../../.yarn/berry/cache/acorn-npm-8.17.0-c53f71d3b1-10c0.zip/node_modules/acorn/",\ - "packageDependencies": [\ - ["acorn", "npm:8.17.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["acorn-globals", [\ - ["npm:7.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/acorn-globals-npm-7.0.1-97c48c0140-10c0.zip/node_modules/acorn-globals/",\ - "packageDependencies": [\ - ["acorn-globals", "npm:7.0.1"],\ - ["acorn", "npm:8.17.0"],\ - ["acorn-walk", "npm:8.3.5"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["acorn-import-attributes", [\ @@ -7903,10 +7653,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:7729f732e879de8132d1503b2cfcb8fd5ec16ae6a920df8bab9acf4f2b8458c6befc1b7e4b9ffbff894007b6a440c002ef0abeac6df0f8e81dc75f4fcf9a859a#npm:1.9.5", {\ - "packageLocation": "./.yarn/__virtual__/acorn-import-attributes-virtual-4657d34be7/3/.yarn/berry/cache/acorn-import-attributes-npm-1.9.5-d1e666eb35-10c0.zip/node_modules/acorn-import-attributes/",\ + ["virtual:faa581e438d2bfc3b35103b96c4f73b9221cae95d4dbb77583d2f3adf3b53812d4470423e2f9051948f9d107db5a9af7637a8a9937a701c8a21dfb42028912a2#npm:1.9.5", {\ + "packageLocation": "./.yarn/__virtual__/acorn-import-attributes-virtual-77f8716996/3/.yarn/berry/cache/acorn-import-attributes-npm-1.9.5-d1e666eb35-10c0.zip/node_modules/acorn-import-attributes/",\ "packageDependencies": [\ - ["acorn-import-attributes", "virtual:7729f732e879de8132d1503b2cfcb8fd5ec16ae6a920df8bab9acf4f2b8458c6befc1b7e4b9ffbff894007b6a440c002ef0abeac6df0f8e81dc75f4fcf9a859a#npm:1.9.5"],\ + ["acorn-import-attributes", "virtual:faa581e438d2bfc3b35103b96c4f73b9221cae95d4dbb77583d2f3adf3b53812d4470423e2f9051948f9d107db5a9af7637a8a9937a701c8a21dfb42028912a2#npm:1.9.5"],\ ["@types/acorn", null],\ ["acorn", "npm:8.12.1"]\ ],\ @@ -7959,14 +7709,6 @@ const RAW_RUNTIME_STATE = ["acorn-walk", "npm:7.2.0"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:8.3.5", {\ - "packageLocation": "../../.yarn/berry/cache/acorn-walk-npm-8.3.5-871d141ed6-10c0.zip/node_modules/acorn-walk/",\ - "packageDependencies": [\ - ["acorn-walk", "npm:8.3.5"],\ - ["acorn", "npm:8.17.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["adjust-sourcemap-loader", [\ @@ -7981,14 +7723,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["agent-base", [\ - ["npm:6.0.2", {\ - "packageLocation": "../../.yarn/berry/cache/agent-base-npm-6.0.2-428f325a93-10c0.zip/node_modules/agent-base/",\ - "packageDependencies": [\ - ["agent-base", "npm:6.0.2"],\ - ["debug", "virtual:c9e1a4b59e37cb479517edede3bf2093b28c0ca1a9d0e517f3c345075bd1e468980b94b4957e389116607ee7155441dfd7d09e19a4229d5d09bcf06244401590#npm:4.3.5"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.1.1", {\ "packageLocation": "../../.yarn/berry/cache/agent-base-npm-7.1.1-c9e1a4b59e-10c0.zip/node_modules/agent-base/",\ "packageDependencies": [\ @@ -8417,33 +8151,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["async-function", [\ - ["npm:1.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/async-function-npm-1.0.0-a81667ebcd-10c0.zip/node_modules/async-function/",\ - "packageDependencies": [\ - ["async-function", "npm:1.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["async-generator-function", [\ - ["npm:1.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/async-generator-function-npm-1.0.0-14cf981d13-10c0.zip/node_modules/async-generator-function/",\ - "packageDependencies": [\ - ["async-generator-function", "npm:1.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["asynckit", [\ - ["npm:0.4.0", {\ - "packageLocation": "../../.yarn/berry/cache/asynckit-npm-0.4.0-c718858525-10c0.zip/node_modules/asynckit/",\ - "packageDependencies": [\ - ["asynckit", "npm:0.4.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["available-typed-arrays", [\ ["npm:1.0.7", {\ "packageLocation": "../../.yarn/berry/cache/available-typed-arrays-npm-1.0.7-e5e5d79687-10c0.zip/node_modules/available-typed-arrays/",\ @@ -8949,17 +8656,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["call-bind-apply-helpers", [\ - ["npm:1.0.2", {\ - "packageLocation": "../../.yarn/berry/cache/call-bind-apply-helpers-npm-1.0.2-3eedbea3bb-10c0.zip/node_modules/call-bind-apply-helpers/",\ - "packageDependencies": [\ - ["call-bind-apply-helpers", "npm:1.0.2"],\ - ["es-errors", "npm:1.3.0"],\ - ["function-bind", "npm:1.1.2"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["callsites", [\ ["npm:3.1.0", {\ "packageLocation": "../../.yarn/berry/cache/callsites-npm-3.1.0-268f989910-10c0.zip/node_modules/callsites/",\ @@ -9349,16 +9045,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["combined-stream", [\ - ["npm:1.0.8", {\ - "packageLocation": "../../.yarn/berry/cache/combined-stream-npm-1.0.8-dc14d4a63a-10c0.zip/node_modules/combined-stream/",\ - "packageDependencies": [\ - ["combined-stream", "npm:1.0.8"],\ - ["delayed-stream", "npm:1.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["comma-separated-tokens", [\ ["npm:2.0.3", {\ "packageLocation": "../../.yarn/berry/cache/comma-separated-tokens-npm-2.0.3-a4a34086b3-10c0.zip/node_modules/comma-separated-tokens/",\ @@ -9369,13 +9055,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["commander", [\ - ["npm:10.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/commander-npm-10.0.1-f17613b72b-10c0.zip/node_modules/commander/",\ - "packageDependencies": [\ - ["commander", "npm:10.0.1"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:2.20.3", {\ "packageLocation": "../../.yarn/berry/cache/commander-npm-2.20.3-d8dcbaa39b-10c0.zip/node_modules/commander/",\ "packageDependencies": [\ @@ -9670,31 +9349,6 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:6.11.0", {\ - "packageLocation": "./.yarn/__virtual__/css-loader-virtual-6803f3e3d7/3/.yarn/berry/cache/css-loader-npm-6.11.0-d945f9f4c0-10c0.zip/node_modules/css-loader/",\ - "packageDependencies": [\ - ["css-loader", "virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:6.11.0"],\ - ["@rspack/core", null],\ - ["@types/rspack__core", null],\ - ["@types/webpack", null],\ - ["icss-utils", "virtual:174ec3d5b04cecc00a3be65a326832ab6373d44665bc574038ae41907ee215e1442095432631f394c7c880d5e83564b395ecb96a27997a3b2f09641747b1b99b#npm:5.1.0"],\ - ["postcss", "npm:8.4.39"],\ - ["postcss-modules-extract-imports", "virtual:174ec3d5b04cecc00a3be65a326832ab6373d44665bc574038ae41907ee215e1442095432631f394c7c880d5e83564b395ecb96a27997a3b2f09641747b1b99b#npm:3.1.0"],\ - ["postcss-modules-local-by-default", "virtual:174ec3d5b04cecc00a3be65a326832ab6373d44665bc574038ae41907ee215e1442095432631f394c7c880d5e83564b395ecb96a27997a3b2f09641747b1b99b#npm:4.0.5"],\ - ["postcss-modules-scope", "virtual:174ec3d5b04cecc00a3be65a326832ab6373d44665bc574038ae41907ee215e1442095432631f394c7c880d5e83564b395ecb96a27997a3b2f09641747b1b99b#npm:3.2.0"],\ - ["postcss-modules-values", "virtual:174ec3d5b04cecc00a3be65a326832ab6373d44665bc574038ae41907ee215e1442095432631f394c7c880d5e83564b395ecb96a27997a3b2f09641747b1b99b#npm:4.0.0"],\ - ["postcss-value-parser", "npm:4.2.0"],\ - ["semver", "npm:7.6.2"],\ - ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ - ],\ - "packagePeers": [\ - "@rspack/core",\ - "@types/rspack__core",\ - "@types/webpack",\ - "webpack"\ - ],\ - "linkType": "HARD"\ - }],\ ["virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:6.11.0", {\ "packageLocation": "./.yarn/__virtual__/css-loader-virtual-174ec3d5b0/3/.yarn/berry/cache/css-loader-npm-6.11.0-d945f9f4c0-10c0.zip/node_modules/css-loader/",\ "packageDependencies": [\ @@ -9710,7 +9364,7 @@ const RAW_RUNTIME_STATE = ["postcss-modules-values", "virtual:174ec3d5b04cecc00a3be65a326832ab6373d44665bc574038ae41907ee215e1442095432631f394c7c880d5e83564b395ecb96a27997a3b2f09641747b1b99b#npm:4.0.0"],\ ["postcss-value-parser", "npm:4.2.0"],\ ["semver", "npm:7.6.2"],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ "@rspack/core",\ @@ -9762,32 +9416,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["cssom", [\ - ["npm:0.3.8", {\ - "packageLocation": "../../.yarn/berry/cache/cssom-npm-0.3.8-a9291d36ff-10c0.zip/node_modules/cssom/",\ - "packageDependencies": [\ - ["cssom", "npm:0.3.8"]\ - ],\ - "linkType": "HARD"\ - }],\ - ["npm:0.5.0", {\ - "packageLocation": "../../.yarn/berry/cache/cssom-npm-0.5.0-44ab2704f2-10c0.zip/node_modules/cssom/",\ - "packageDependencies": [\ - ["cssom", "npm:0.5.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["cssstyle", [\ - ["npm:2.3.0", {\ - "packageLocation": "../../.yarn/berry/cache/cssstyle-npm-2.3.0-b5d112c450-10c0.zip/node_modules/cssstyle/",\ - "packageDependencies": [\ - ["cssstyle", "npm:2.3.0"],\ - ["cssom", "npm:0.3.8"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["csstype", [\ ["npm:3.1.3", {\ "packageLocation": "../../.yarn/berry/cache/csstype-npm-3.1.3-e9a1c85013-10c0.zip/node_modules/csstype/",\ @@ -9806,18 +9434,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["data-urls", [\ - ["npm:3.0.2", {\ - "packageLocation": "../../.yarn/berry/cache/data-urls-npm-3.0.2-c8b2050319-10c0.zip/node_modules/data-urls/",\ - "packageDependencies": [\ - ["data-urls", "npm:3.0.2"],\ - ["abab", "npm:2.0.6"],\ - ["whatwg-mimetype", "npm:3.0.0"],\ - ["whatwg-url", "npm:11.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["data-view-buffer", [\ ["npm:1.0.1", {\ "packageLocation": "../../.yarn/berry/cache/data-view-buffer-npm-1.0.1-d911beebce-10c0.zip/node_modules/data-view-buffer/",\ @@ -9940,15 +9556,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["decimal.js", [\ - ["npm:10.6.0", {\ - "packageLocation": "../../.yarn/berry/cache/decimal.js-npm-10.6.0-a72c1b8a2f-10c0.zip/node_modules/decimal.js/",\ - "packageDependencies": [\ - ["decimal.js", "npm:10.6.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["decode-named-character-reference", [\ ["npm:1.0.2", {\ "packageLocation": "../../.yarn/berry/cache/decode-named-character-reference-npm-1.0.2-db17a755fd-10c0.zip/node_modules/decode-named-character-reference/",\ @@ -10066,18 +9673,9 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["delayed-stream", [\ - ["npm:1.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/delayed-stream-npm-1.0.0-c5a4c4cc02-10c0.zip/node_modules/delayed-stream/",\ - "packageDependencies": [\ - ["delayed-stream", "npm:1.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["depd", [\ - ["npm:2.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/depd-npm-2.0.0-b6c51a4b43-10c0.zip/node_modules/depd/",\ + ["depd", [\ + ["npm:2.0.0", {\ + "packageLocation": "../../.yarn/berry/cache/depd-npm-2.0.0-b6c51a4b43-10c0.zip/node_modules/depd/",\ "packageDependencies": [\ ["depd", "npm:2.0.0"]\ ],\ @@ -10262,16 +9860,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["domexception", [\ - ["npm:4.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/domexception-npm-4.0.0-5093673f9b-10c0.zip/node_modules/domexception/",\ - "packageDependencies": [\ - ["domexception", "npm:4.0.0"],\ - ["webidl-conversions", "npm:7.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["domhandler", [\ ["npm:4.3.1", {\ "packageLocation": "../../.yarn/berry/cache/domhandler-npm-4.3.1-493539c1ca-10c0.zip/node_modules/domhandler/",\ @@ -10305,18 +9893,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["dunder-proto", [\ - ["npm:1.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/dunder-proto-npm-1.0.1-90eb6829db-10c0.zip/node_modules/dunder-proto/",\ - "packageDependencies": [\ - ["dunder-proto", "npm:1.0.1"],\ - ["call-bind-apply-helpers", "npm:1.0.2"],\ - ["es-errors", "npm:1.3.0"],\ - ["gopd", "npm:1.2.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["eastasianwidth", [\ ["npm:0.2.0", {\ "packageLocation": "../../.yarn/berry/cache/eastasianwidth-npm-0.2.0-c37eb16bd1-10c0.zip/node_modules/eastasianwidth/",\ @@ -10528,13 +10104,6 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ - ["npm:6.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/entities-npm-6.0.1-84692dab43-10c0.zip/node_modules/entities/",\ - "packageDependencies": [\ - ["entities", "npm:6.0.1"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.0.1", {\ "packageLocation": "../../.yarn/berry/cache/entities-npm-7.0.1-61f8ba3430-10c0.zip/node_modules/entities/",\ "packageDependencies": [\ @@ -10653,13 +10222,6 @@ const RAW_RUNTIME_STATE = ["get-intrinsic", "npm:1.2.4"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:1.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/es-define-property-npm-1.0.1-3fc6324f1c-10c0.zip/node_modules/es-define-property/",\ - "packageDependencies": [\ - ["es-define-property", "npm:1.0.1"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["es-errors", [\ @@ -10736,14 +10298,6 @@ const RAW_RUNTIME_STATE = ["es-errors", "npm:1.3.0"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:1.1.2", {\ - "packageLocation": "../../.yarn/berry/cache/es-object-atoms-npm-1.1.2-97972d8992-10c0.zip/node_modules/es-object-atoms/",\ - "packageDependencies": [\ - ["es-object-atoms", "npm:1.1.2"],\ - ["es-errors", "npm:1.3.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["es-set-tostringtag", [\ @@ -10756,17 +10310,6 @@ const RAW_RUNTIME_STATE = ["hasown", "npm:2.0.2"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:2.1.0", {\ - "packageLocation": "../../.yarn/berry/cache/es-set-tostringtag-npm-2.1.0-4e55705d3f-10c0.zip/node_modules/es-set-tostringtag/",\ - "packageDependencies": [\ - ["es-set-tostringtag", "npm:2.1.0"],\ - ["es-errors", "npm:1.3.0"],\ - ["get-intrinsic", "npm:1.3.1"],\ - ["has-tostringtag", "npm:1.0.2"],\ - ["hasown", "npm:2.0.2"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["es-shim-unscopables", [\ @@ -11589,15 +11132,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["fastest-levenshtein", [\ - ["npm:1.0.16", {\ - "packageLocation": "../../.yarn/berry/cache/fastest-levenshtein-npm-1.0.16-192d328856-10c0.zip/node_modules/fastest-levenshtein/",\ - "packageDependencies": [\ - ["fastest-levenshtein", "npm:1.0.16"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["fastq", [\ ["npm:1.17.1", {\ "packageLocation": "../../.yarn/berry/cache/fastq-npm-1.17.1-56d4554993-10c0.zip/node_modules/fastq/",\ @@ -11762,15 +11296,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["flat", [\ - ["npm:5.0.2", {\ - "packageLocation": "../../.yarn/berry/cache/flat-npm-5.0.2-12748102a5-10c0.zip/node_modules/flat/",\ - "packageDependencies": [\ - ["flat", "npm:5.0.2"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["flat-cache", [\ ["npm:3.2.0", {\ "packageLocation": "../../.yarn/berry/cache/flat-cache-npm-3.2.0-9a887f084e-10c0.zip/node_modules/flat-cache/",\ @@ -11849,7 +11374,7 @@ const RAW_RUNTIME_STATE = ["semver", "npm:7.6.2"],\ ["tapable", "npm:2.2.1"],\ ["typescript", "patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=379a07"],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ "@types/typescript",\ @@ -11860,20 +11385,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["form-data", [\ - ["npm:4.0.6", {\ - "packageLocation": "../../.yarn/berry/cache/form-data-npm-4.0.6-fb3ea14cf3-10c0.zip/node_modules/form-data/",\ - "packageDependencies": [\ - ["form-data", "npm:4.0.6"],\ - ["asynckit", "npm:0.4.0"],\ - ["combined-stream", "npm:1.0.8"],\ - ["es-set-tostringtag", "npm:2.1.0"],\ - ["hasown", "npm:2.0.4"],\ - ["mime-types", "npm:2.1.35"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["forwarded", [\ ["npm:0.2.0", {\ "packageLocation": "../../.yarn/berry/cache/forwarded-npm-0.2.0-6473dabe35-10c0.zip/node_modules/forwarded/",\ @@ -12030,15 +11541,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["generator-function", [\ - ["npm:2.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/generator-function-npm-2.0.1-aed34a724a-10c0.zip/node_modules/generator-function/",\ - "packageDependencies": [\ - ["generator-function", "npm:2.0.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["gensync", [\ ["npm:1.0.0-beta.2", {\ "packageLocation": "../../.yarn/berry/cache/gensync-npm-1.0.0-beta.2-224666d72f-10c0.zip/node_modules/gensync/",\ @@ -12069,26 +11571,6 @@ const RAW_RUNTIME_STATE = ["hasown", "npm:2.0.2"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:1.3.1", {\ - "packageLocation": "../../.yarn/berry/cache/get-intrinsic-npm-1.3.1-2f734f40ec-10c0.zip/node_modules/get-intrinsic/",\ - "packageDependencies": [\ - ["get-intrinsic", "npm:1.3.1"],\ - ["async-function", "npm:1.0.0"],\ - ["async-generator-function", "npm:1.0.0"],\ - ["call-bind-apply-helpers", "npm:1.0.2"],\ - ["es-define-property", "npm:1.0.1"],\ - ["es-errors", "npm:1.3.0"],\ - ["es-object-atoms", "npm:1.1.2"],\ - ["function-bind", "npm:1.1.2"],\ - ["generator-function", "npm:2.0.1"],\ - ["get-proto", "npm:1.0.1"],\ - ["gopd", "npm:1.2.0"],\ - ["has-symbols", "npm:1.1.0"],\ - ["hasown", "npm:2.0.2"],\ - ["math-intrinsics", "npm:1.1.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["get-nonce", [\ @@ -12100,17 +11582,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["get-proto", [\ - ["npm:1.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/get-proto-npm-1.0.1-4d30bac614-10c0.zip/node_modules/get-proto/",\ - "packageDependencies": [\ - ["get-proto", "npm:1.0.1"],\ - ["dunder-proto", "npm:1.0.1"],\ - ["es-object-atoms", "npm:1.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["get-stream", [\ ["npm:6.0.1", {\ "packageLocation": "../../.yarn/berry/cache/get-stream-npm-6.0.1-83e51a4642-10c0.zip/node_modules/get-stream/",\ @@ -12306,13 +11777,6 @@ const RAW_RUNTIME_STATE = ["get-intrinsic", "npm:1.2.4"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:1.2.0", {\ - "packageLocation": "../../.yarn/berry/cache/gopd-npm-1.2.0-df89ffa78e-10c0.zip/node_modules/gopd/",\ - "packageDependencies": [\ - ["gopd", "npm:1.2.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["graceful-fs", [\ @@ -12400,13 +11864,6 @@ const RAW_RUNTIME_STATE = ["has-symbols", "npm:1.0.3"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:1.1.0", {\ - "packageLocation": "../../.yarn/berry/cache/has-symbols-npm-1.1.0-9aa7dc2ac1-10c0.zip/node_modules/has-symbols/",\ - "packageDependencies": [\ - ["has-symbols", "npm:1.1.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["has-tostringtag", [\ @@ -12459,14 +11916,6 @@ const RAW_RUNTIME_STATE = ["function-bind", "npm:1.1.2"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:2.0.4", {\ - "packageLocation": "../../.yarn/berry/cache/hasown-npm-2.0.4-75e16c9c2a-10c0.zip/node_modules/hasown/",\ - "packageDependencies": [\ - ["hasown", "npm:2.0.4"],\ - ["function-bind", "npm:1.1.2"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["hast-util-from-parse5", [\ @@ -12633,16 +12082,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["html-encoding-sniffer", [\ - ["npm:3.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/html-encoding-sniffer-npm-3.0.0-daac3dfe41-10c0.zip/node_modules/html-encoding-sniffer/",\ - "packageDependencies": [\ - ["html-encoding-sniffer", "npm:3.0.0"],\ - ["whatwg-encoding", "npm:2.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["html-entities", [\ ["npm:2.5.2", {\ "packageLocation": "../../.yarn/berry/cache/html-entities-npm-2.5.2-0b6113e376-10c0.zip/node_modules/html-entities/",\ @@ -12715,7 +12154,7 @@ const RAW_RUNTIME_STATE = ["lodash", "npm:4.17.21"],\ ["pretty-error", "npm:4.0.0"],\ ["tapable", "npm:2.2.1"],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ "@rspack/core",\ @@ -12763,16 +12202,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["http-proxy-agent", [\ - ["npm:5.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/http-proxy-agent-npm-5.0.0-7f1f121b83-10c0.zip/node_modules/http-proxy-agent/",\ - "packageDependencies": [\ - ["http-proxy-agent", "npm:5.0.0"],\ - ["@tootallnate/once", "npm:2.0.1"],\ - ["agent-base", "npm:6.0.2"],\ - ["debug", "virtual:c9e1a4b59e37cb479517edede3bf2093b28c0ca1a9d0e517f3c345075bd1e468980b94b4957e389116607ee7155441dfd7d09e19a4229d5d09bcf06244401590#npm:4.3.5"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.0.2", {\ "packageLocation": "../../.yarn/berry/cache/http-proxy-agent-npm-7.0.2-643ed7cc33-10c0.zip/node_modules/http-proxy-agent/",\ "packageDependencies": [\ @@ -12793,15 +12222,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["https-proxy-agent", [\ - ["npm:5.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/https-proxy-agent-npm-5.0.1-42d65f358e-10c0.zip/node_modules/https-proxy-agent/",\ - "packageDependencies": [\ - ["https-proxy-agent", "npm:5.0.1"],\ - ["agent-base", "npm:6.0.2"],\ - ["debug", "virtual:c9e1a4b59e37cb479517edede3bf2093b28c0ca1a9d0e517f3c345075bd1e468980b94b4957e389116607ee7155441dfd7d09e19a4229d5d09bcf06244401590#npm:4.3.5"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:7.0.5", {\ "packageLocation": "../../.yarn/berry/cache/https-proxy-agent-npm-7.0.5-94c14d4619-10c0.zip/node_modules/https-proxy-agent/",\ "packageDependencies": [\ @@ -12925,17 +12345,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["import-local", [\ - ["npm:3.1.0", {\ - "packageLocation": "../../.yarn/berry/cache/import-local-npm-3.1.0-8960af5e51-10c0.zip/node_modules/import-local/",\ - "packageDependencies": [\ - ["import-local", "npm:3.1.0"],\ - ["pkg-dir", "npm:4.2.0"],\ - ["resolve-cwd", "npm:3.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["imurmurhash", [\ ["npm:0.1.4", {\ "packageLocation": "../../.yarn/berry/cache/imurmurhash-npm-0.1.4-610c5068a0-10c0.zip/node_modules/imurmurhash/",\ @@ -12995,15 +12404,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["interpret", [\ - ["npm:3.1.1", {\ - "packageLocation": "../../.yarn/berry/cache/interpret-npm-3.1.1-715bac2bd7-10c0.zip/node_modules/interpret/",\ - "packageDependencies": [\ - ["interpret", "npm:3.1.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["ip-address", [\ ["npm:9.0.5", {\ "packageLocation": "../../.yarn/berry/cache/ip-address-npm-9.0.5-9fa024d42a-10c0.zip/node_modules/ip-address/",\ @@ -13338,15 +12738,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["is-potential-custom-element-name", [\ - ["npm:1.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/is-potential-custom-element-name-npm-1.0.1-f352f606f8-10c0.zip/node_modules/is-potential-custom-element-name/",\ - "packageDependencies": [\ - ["is-potential-custom-element-name", "npm:1.0.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["is-regex", [\ ["npm:1.1.4", {\ "packageLocation": "../../.yarn/berry/cache/is-regex-npm-1.1.4-cca193ef11-10c0.zip/node_modules/is-regex/",\ @@ -13668,54 +13059,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["jsdom", [\ - ["npm:20.0.3", {\ - "packageLocation": "../../.yarn/berry/cache/jsdom-npm-20.0.3-906a2f7005-10c0.zip/node_modules/jsdom/",\ - "packageDependencies": [\ - ["jsdom", "npm:20.0.3"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:20.0.3", {\ - "packageLocation": "./.yarn/__virtual__/jsdom-virtual-d1c0dc0929/3/.yarn/berry/cache/jsdom-npm-20.0.3-906a2f7005-10c0.zip/node_modules/jsdom/",\ - "packageDependencies": [\ - ["jsdom", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:20.0.3"],\ - ["@types/canvas", null],\ - ["abab", "npm:2.0.6"],\ - ["acorn", "npm:8.17.0"],\ - ["acorn-globals", "npm:7.0.1"],\ - ["canvas", null],\ - ["cssom", "npm:0.5.0"],\ - ["cssstyle", "npm:2.3.0"],\ - ["data-urls", "npm:3.0.2"],\ - ["decimal.js", "npm:10.6.0"],\ - ["domexception", "npm:4.0.0"],\ - ["escodegen", "npm:2.1.0"],\ - ["form-data", "npm:4.0.6"],\ - ["html-encoding-sniffer", "npm:3.0.0"],\ - ["http-proxy-agent", "npm:5.0.0"],\ - ["https-proxy-agent", "npm:5.0.1"],\ - ["is-potential-custom-element-name", "npm:1.0.1"],\ - ["nwsapi", "npm:2.2.24"],\ - ["parse5", "npm:7.3.0"],\ - ["saxes", "npm:6.0.0"],\ - ["symbol-tree", "npm:3.2.4"],\ - ["tough-cookie", "npm:4.1.4"],\ - ["w3c-xmlserializer", "npm:4.0.0"],\ - ["webidl-conversions", "npm:7.0.0"],\ - ["whatwg-encoding", "npm:2.0.0"],\ - ["whatwg-mimetype", "npm:3.0.0"],\ - ["whatwg-url", "npm:11.0.0"],\ - ["ws", "virtual:dc896b83d081c6997c9337b24e705c9b36c8ed8f76d14638f4fdffa784b868c2707f73be6e59159c9411ebae5f860fe2c9205af27d00e7d67f9fbfba8170c1ec#npm:8.21.0"],\ - ["xml-name-validator", "npm:4.0.0"]\ - ],\ - "packagePeers": [\ - "@types/canvas",\ - "canvas"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["jsesc", [\ ["npm:0.5.0", {\ "packageLocation": "../../.yarn/berry/cache/jsesc-npm-0.5.0-6827074492-10c0.zip/node_modules/jsesc/",\ @@ -14412,15 +13755,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["math-intrinsics", [\ - ["npm:1.1.0", {\ - "packageLocation": "../../.yarn/berry/cache/math-intrinsics-npm-1.1.0-9204d80e7d-10c0.zip/node_modules/math-intrinsics/",\ - "packageDependencies": [\ - ["math-intrinsics", "npm:1.1.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["md5.js", [\ ["npm:1.3.5", {\ "packageLocation": "../../.yarn/berry/cache/md5.js-npm-1.3.5-130901125a-10c0.zip/node_modules/md5.js/",\ @@ -14601,17 +13935,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["mdast-util-newline-to-break", [\ - ["npm:2.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/mdast-util-newline-to-break-npm-2.0.0-1499468942-10c0.zip/node_modules/mdast-util-newline-to-break/",\ - "packageDependencies": [\ - ["mdast-util-newline-to-break", "npm:2.0.0"],\ - ["@types/mdast", "npm:4.0.4"],\ - ["mdast-util-find-and-replace", "npm:3.0.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["mdast-util-phrasing", [\ ["npm:4.1.0", {\ "packageLocation": "../../.yarn/berry/cache/mdast-util-phrasing-npm-4.1.0-30939ebbcd-10c0.zip/node_modules/mdast-util-phrasing/",\ @@ -15656,15 +14979,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["nwsapi", [\ - ["npm:2.2.24", {\ - "packageLocation": "../../.yarn/berry/cache/nwsapi-npm-2.2.24-3d2b1ed6d3-10c0.zip/node_modules/nwsapi/",\ - "packageDependencies": [\ - ["nwsapi", "npm:2.2.24"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["nypm", [\ ["npm:0.3.9", {\ "packageLocation": "../../.yarn/berry/cache/nypm-npm-0.3.9-1cd7b5618c-10c0.zip/node_modules/nypm/",\ @@ -16057,14 +15371,6 @@ const RAW_RUNTIME_STATE = ["entities", "npm:4.5.0"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:7.3.0", {\ - "packageLocation": "../../.yarn/berry/cache/parse5-npm-7.3.0-b0410074a3-10c0.zip/node_modules/parse5/",\ - "packageDependencies": [\ - ["parse5", "npm:7.3.0"],\ - ["entities", "npm:6.0.1"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["parseurl", [\ @@ -16685,16 +15991,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["psl", [\ - ["npm:1.15.0", {\ - "packageLocation": "../../.yarn/berry/cache/psl-npm-1.15.0-410584ca6b-10c0.zip/node_modules/psl/",\ - "packageDependencies": [\ - ["psl", "npm:1.15.0"],\ - ["punycode", "npm:2.3.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["public-encrypt", [\ ["npm:4.0.3", {\ "packageLocation": "../../.yarn/berry/cache/public-encrypt-npm-4.0.3-b25e19fada-10c0.zip/node_modules/public-encrypt/",\ @@ -16719,7 +16015,6 @@ const RAW_RUNTIME_STATE = ["@playwright/test", "npm:1.52.0"],\ ["@radix-ui/react-dialog", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.1.14"],\ ["@radix-ui/react-select", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:2.2.5"],\ - ["@radix-ui/react-slot", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.3"],\ ["@radix-ui/react-toast", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:1.2.1"],\ ["@storybook/addon-essentials", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ ["@storybook/addon-interactions", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ @@ -16733,17 +16028,12 @@ const RAW_RUNTIME_STATE = ["@storybook/react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ ["@storybook/test", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.2.2"],\ ["@tailwindcss/postcss", "npm:4.1.18"],\ - ["@tanstack/react-query", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.72.0"],\ ["@testing-library/dom", "npm:10.4.1"],\ - ["@testing-library/jest-dom", "npm:6.9.1"],\ ["@testing-library/react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:16.3.2"],\ - ["@types/jsdom", "npm:20.0.1"],\ ["@types/node", "npm:20.14.10"],\ ["@types/react", "npm:18.3.3"],\ ["@types/react-dom", "npm:18.3.0"],\ - ["@types/sharp", "npm:0.32.0"],\ ["@types/uuid", "npm:10.0.0"],\ - ["@vitejs/plugin-react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:6.0.3"],\ ["class-variance-authority", "npm:0.7.1"],\ ["clsx", "npm:2.1.1"],\ ["embla-carousel-autoplay", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.1.7"],\ @@ -16756,7 +16046,6 @@ const RAW_RUNTIME_STATE = ["happy-dom", "npm:20.10.6"],\ ["husky", "npm:9.1.7"],\ ["img-toolkit", "npm:1.0.2"],\ - ["jsdom", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:20.0.3"],\ ["lucide-react", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:0.525.0"],\ ["next", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:14.2.5"],\ ["next-themes", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:0.3.0"],\ @@ -16767,7 +16056,6 @@ const RAW_RUNTIME_STATE = ["react-markdown", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:9.0.1"],\ ["react-player", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:2.16.0"],\ ["rehype-raw", "npm:7.0.0"],\ - ["remark-breaks", "npm:4.0.0"],\ ["remark-gfm", "npm:4.0.0"],\ ["sharp", "npm:0.34.5"],\ ["storybook", "npm:8.2.2"],\ @@ -16779,7 +16067,6 @@ const RAW_RUNTIME_STATE = ["vite", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:8.1.4"],\ ["vitest", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:4.1.10"],\ ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"],\ - ["webpack-cli", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4"],\ ["zustand", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:4.5.4"]\ ],\ "linkType": "SOFT"\ @@ -16837,15 +16124,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["querystringify", [\ - ["npm:2.2.0", {\ - "packageLocation": "../../.yarn/berry/cache/querystringify-npm-2.2.0-4e77c9f606-10c0.zip/node_modules/querystringify/",\ - "packageDependencies": [\ - ["querystringify", "npm:2.2.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["queue", [\ ["npm:6.0.2", {\ "packageLocation": "../../.yarn/berry/cache/queue-npm-6.0.2-ebbcf599cf-10c0.zip/node_modules/queue/",\ @@ -17344,16 +16622,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["rechoir", [\ - ["npm:0.8.0", {\ - "packageLocation": "../../.yarn/berry/cache/rechoir-npm-0.8.0-fb660b3bc8-10c0.zip/node_modules/rechoir/",\ - "packageDependencies": [\ - ["rechoir", "npm:0.8.0"],\ - ["resolve", "patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["redent", [\ ["npm:3.0.0", {\ "packageLocation": "../../.yarn/berry/cache/redent-npm-3.0.0-31892f4906-10c0.zip/node_modules/redent/",\ @@ -17516,18 +16784,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["remark-breaks", [\ - ["npm:4.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/remark-breaks-npm-4.0.0-962f376971-10c0.zip/node_modules/remark-breaks/",\ - "packageDependencies": [\ - ["remark-breaks", "npm:4.0.0"],\ - ["@types/mdast", "npm:4.0.4"],\ - ["mdast-util-newline-to-break", "npm:2.0.0"],\ - ["unified", "npm:11.0.5"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["remark-gfm", [\ ["npm:4.0.0", {\ "packageLocation": "../../.yarn/berry/cache/remark-gfm-npm-4.0.0-8bb699e315-10c0.zip/node_modules/remark-gfm/",\ @@ -17614,15 +16870,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["requires-port", [\ - ["npm:1.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/requires-port-npm-1.0.0-fd036b488a-10c0.zip/node_modules/requires-port/",\ - "packageDependencies": [\ - ["requires-port", "npm:1.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["resolve", [\ ["patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d", {\ "packageLocation": "../../.yarn/berry/cache/resolve-patch-4254c24959-10c0.zip/node_modules/resolve/",\ @@ -17645,16 +16892,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["resolve-cwd", [\ - ["npm:3.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/resolve-cwd-npm-3.0.0-e6f4e296bf-10c0.zip/node_modules/resolve-cwd/",\ - "packageDependencies": [\ - ["resolve-cwd", "npm:3.0.0"],\ - ["resolve-from", "npm:5.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["resolve-from", [\ ["npm:4.0.0", {\ "packageLocation": "../../.yarn/berry/cache/resolve-from-npm-4.0.0-f758ec21bf-10c0.zip/node_modules/resolve-from/",\ @@ -17662,13 +16899,6 @@ const RAW_RUNTIME_STATE = ["resolve-from", "npm:4.0.0"]\ ],\ "linkType": "HARD"\ - }],\ - ["npm:5.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/resolve-from-npm-5.0.0-15c9db4d33-10c0.zip/node_modules/resolve-from/",\ - "packageDependencies": [\ - ["resolve-from", "npm:5.0.0"]\ - ],\ - "linkType": "HARD"\ }]\ ]],\ ["resolve-pkg-maps", [\ @@ -17878,16 +17108,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["saxes", [\ - ["npm:6.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/saxes-npm-6.0.0-31558949f5-10c0.zip/node_modules/saxes/",\ - "packageDependencies": [\ - ["saxes", "npm:6.0.0"],\ - ["xmlchars", "npm:2.2.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["scheduler", [\ ["npm:0.23.2", {\ "packageLocation": "../../.yarn/berry/cache/scheduler-npm-0.23.2-6d1dd9c2b7-10c0.zip/node_modules/scheduler/",\ @@ -18648,25 +17868,12 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:3.3.4", {\ - "packageLocation": "./.yarn/__virtual__/style-loader-virtual-e405c736a1/3/.yarn/berry/cache/style-loader-npm-3.3.4-e2ff5c12be-10c0.zip/node_modules/style-loader/",\ - "packageDependencies": [\ - ["style-loader", "virtual:0f6bcb675e7003f6d579fa44c5ace1089d4f9e2b8723cb327b992a6e61b5db65752a5b22c9ef8a8c8ed85ebd9be540302c259701e6b7466c79aa068e793d2593#npm:3.3.4"],\ - ["@types/webpack", null],\ - ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ - ],\ - "packagePeers": [\ - "@types/webpack",\ - "webpack"\ - ],\ - "linkType": "HARD"\ - }],\ ["virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:3.3.4", {\ "packageLocation": "./.yarn/__virtual__/style-loader-virtual-edc739c120/3/.yarn/berry/cache/style-loader-npm-3.3.4-e2ff5c12be-10c0.zip/node_modules/style-loader/",\ "packageDependencies": [\ ["style-loader", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:3.3.4"],\ ["@types/webpack", null],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ "@types/webpack",\ @@ -18773,15 +17980,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["symbol-tree", [\ - ["npm:3.2.4", {\ - "packageLocation": "../../.yarn/berry/cache/symbol-tree-npm-3.2.4-fe70cdb75b-10c0.zip/node_modules/symbol-tree/",\ - "packageDependencies": [\ - ["symbol-tree", "npm:3.2.4"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["tailwind-merge", [\ ["npm:3.3.1", {\ "packageLocation": "../../.yarn/berry/cache/tailwind-merge-npm-3.3.1-f9ae71f62f-10c0.zip/node_modules/tailwind-merge/",\ @@ -18932,36 +18130,6 @@ const RAW_RUNTIME_STATE = ["serialize-javascript", "npm:6.0.2"],\ ["terser", "npm:5.31.2"],\ ["uglify-js", null],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ - ],\ - "packagePeers": [\ - "@swc/core",\ - "@types/esbuild",\ - "@types/swc__core",\ - "@types/uglify-js",\ - "@types/webpack",\ - "esbuild",\ - "uglify-js",\ - "webpack"\ - ],\ - "linkType": "HARD"\ - }],\ - ["virtual:faa581e438d2bfc3b35103b96c4f73b9221cae95d4dbb77583d2f3adf3b53812d4470423e2f9051948f9d107db5a9af7637a8a9937a701c8a21dfb42028912a2#npm:5.3.10", {\ - "packageLocation": "./.yarn/__virtual__/terser-webpack-plugin-virtual-f010a33872/3/.yarn/berry/cache/terser-webpack-plugin-npm-5.3.10-3bde1920fb-10c0.zip/node_modules/terser-webpack-plugin/",\ - "packageDependencies": [\ - ["terser-webpack-plugin", "virtual:faa581e438d2bfc3b35103b96c4f73b9221cae95d4dbb77583d2f3adf3b53812d4470423e2f9051948f9d107db5a9af7637a8a9937a701c8a21dfb42028912a2#npm:5.3.10"],\ - ["@jridgewell/trace-mapping", "npm:0.3.25"],\ - ["@swc/core", null],\ - ["@types/esbuild", null],\ - ["@types/swc__core", null],\ - ["@types/uglify-js", null],\ - ["@types/webpack", null],\ - ["esbuild", null],\ - ["jest-worker", "npm:27.5.1"],\ - ["schema-utils", "npm:3.3.0"],\ - ["serialize-javascript", "npm:6.0.2"],\ - ["terser", "npm:5.31.2"],\ - ["uglify-js", null],\ ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ @@ -19080,29 +18248,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["tough-cookie", [\ - ["npm:4.1.4", {\ - "packageLocation": "../../.yarn/berry/cache/tough-cookie-npm-4.1.4-8293cc8bd5-10c0.zip/node_modules/tough-cookie/",\ - "packageDependencies": [\ - ["tough-cookie", "npm:4.1.4"],\ - ["psl", "npm:1.15.0"],\ - ["punycode", "npm:2.3.1"],\ - ["universalify", "npm:0.2.0"],\ - ["url-parse", "npm:1.5.10"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["tr46", [\ - ["npm:3.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/tr46-npm-3.0.0-e1ae1ea7c9-10c0.zip/node_modules/tr46/",\ - "packageDependencies": [\ - ["tr46", "npm:3.0.0"],\ - ["punycode", "npm:2.3.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["trim-lines", [\ ["npm:3.0.1", {\ "packageLocation": "../../.yarn/berry/cache/trim-lines-npm-3.0.1-24471f7e84-10c0.zip/node_modules/trim-lines/",\ @@ -19576,13 +18721,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["universalify", [\ - ["npm:0.2.0", {\ - "packageLocation": "../../.yarn/berry/cache/universalify-npm-0.2.0-9984e61c10-10c0.zip/node_modules/universalify/",\ - "packageDependencies": [\ - ["universalify", "npm:0.2.0"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:2.0.1", {\ "packageLocation": "../../.yarn/berry/cache/universalify-npm-2.0.1-040ba5a21e-10c0.zip/node_modules/universalify/",\ "packageDependencies": [\ @@ -19658,17 +18796,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["url-parse", [\ - ["npm:1.5.10", {\ - "packageLocation": "../../.yarn/berry/cache/url-parse-npm-1.5.10-64fa2bcd6d-10c0.zip/node_modules/url-parse/",\ - "packageDependencies": [\ - ["url-parse", "npm:1.5.10"],\ - ["querystringify", "npm:2.2.0"],\ - ["requires-port", "npm:1.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["use-callback-ref", [\ ["npm:1.3.3", {\ "packageLocation": "../../.yarn/berry/cache/use-callback-ref-npm-1.3.3-e40f41fcdb-10c0.zip/node_modules/use-callback-ref/",\ @@ -19923,7 +19050,7 @@ const RAW_RUNTIME_STATE = ["@opentelemetry/api", null],\ ["@types/edge-runtime__vm", null],\ ["@types/happy-dom", null],\ - ["@types/jsdom", "npm:20.0.1"],\ + ["@types/jsdom", null],\ ["@types/node", "npm:20.14.10"],\ ["@types/opentelemetry__api", null],\ ["@types/vite", null],\ @@ -19949,7 +19076,7 @@ const RAW_RUNTIME_STATE = ["es-module-lexer", "npm:2.3.0"],\ ["expect-type", "npm:1.4.0"],\ ["happy-dom", "npm:20.10.6"],\ - ["jsdom", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:20.0.3"],\ + ["jsdom", null],\ ["magic-string", "npm:0.30.21"],\ ["obug", "npm:2.1.3"],\ ["pathe", "npm:2.0.3"],\ @@ -19999,16 +19126,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["w3c-xmlserializer", [\ - ["npm:4.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/w3c-xmlserializer-npm-4.0.0-f09d0ec3fc-10c0.zip/node_modules/w3c-xmlserializer/",\ - "packageDependencies": [\ - ["w3c-xmlserializer", "npm:4.0.0"],\ - ["xml-name-validator", "npm:4.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["walk-up-path", [\ ["npm:3.0.1", {\ "packageLocation": "../../.yarn/berry/cache/walk-up-path-npm-3.0.1-67ab100d5d-10c0.zip/node_modules/walk-up-path/",\ @@ -20048,15 +19165,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["webidl-conversions", [\ - ["npm:7.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/webidl-conversions-npm-7.0.0-e8c8e30c68-10c0.zip/node_modules/webidl-conversions/",\ - "packageDependencies": [\ - ["webidl-conversions", "npm:7.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["webpack", [\ ["npm:5.93.0", {\ "packageLocation": "../../.yarn/berry/cache/webpack-npm-5.93.0-10ee698c0b-10c0.zip/node_modules/webpack/",\ @@ -20065,43 +19173,6 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0", {\ - "packageLocation": "./.yarn/__virtual__/webpack-virtual-7729f732e8/3/.yarn/berry/cache/webpack-npm-5.93.0-10ee698c0b-10c0.zip/node_modules/webpack/",\ - "packageDependencies": [\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"],\ - ["@types/eslint-scope", "npm:3.7.7"],\ - ["@types/estree", "npm:1.0.5"],\ - ["@types/webpack-cli", null],\ - ["@webassemblyjs/ast", "npm:1.12.1"],\ - ["@webassemblyjs/wasm-edit", "npm:1.12.1"],\ - ["@webassemblyjs/wasm-parser", "npm:1.12.1"],\ - ["acorn", "npm:8.12.1"],\ - ["acorn-import-attributes", "virtual:7729f732e879de8132d1503b2cfcb8fd5ec16ae6a920df8bab9acf4f2b8458c6befc1b7e4b9ffbff894007b6a440c002ef0abeac6df0f8e81dc75f4fcf9a859a#npm:1.9.5"],\ - ["browserslist", "npm:4.23.2"],\ - ["chrome-trace-event", "npm:1.0.4"],\ - ["enhanced-resolve", "npm:5.17.0"],\ - ["es-module-lexer", "npm:1.5.4"],\ - ["eslint-scope", "npm:5.1.1"],\ - ["events", "npm:3.3.0"],\ - ["glob-to-regexp", "npm:0.4.1"],\ - ["graceful-fs", "npm:4.2.11"],\ - ["json-parse-even-better-errors", "npm:2.3.1"],\ - ["loader-runner", "npm:4.3.0"],\ - ["mime-types", "npm:2.1.35"],\ - ["neo-async", "npm:2.6.2"],\ - ["schema-utils", "npm:3.3.0"],\ - ["tapable", "npm:2.2.1"],\ - ["terser-webpack-plugin", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.3.10"],\ - ["watchpack", "npm:2.4.1"],\ - ["webpack-cli", null],\ - ["webpack-sources", "npm:3.2.3"]\ - ],\ - "packagePeers": [\ - "@types/webpack-cli",\ - "webpack-cli"\ - ],\ - "linkType": "HARD"\ - }],\ ["virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0", {\ "packageLocation": "./.yarn/__virtual__/webpack-virtual-faa581e438/3/.yarn/berry/cache/webpack-npm-5.93.0-10ee698c0b-10c0.zip/node_modules/webpack/",\ "packageDependencies": [\ @@ -20113,7 +19184,7 @@ const RAW_RUNTIME_STATE = ["@webassemblyjs/wasm-edit", "npm:1.12.1"],\ ["@webassemblyjs/wasm-parser", "npm:1.12.1"],\ ["acorn", "npm:8.12.1"],\ - ["acorn-import-attributes", "virtual:7729f732e879de8132d1503b2cfcb8fd5ec16ae6a920df8bab9acf4f2b8458c6befc1b7e4b9ffbff894007b6a440c002ef0abeac6df0f8e81dc75f4fcf9a859a#npm:1.9.5"],\ + ["acorn-import-attributes", "virtual:faa581e438d2bfc3b35103b96c4f73b9221cae95d4dbb77583d2f3adf3b53812d4470423e2f9051948f9d107db5a9af7637a8a9937a701c8a21dfb42028912a2#npm:1.9.5"],\ ["browserslist", "npm:4.23.2"],\ ["chrome-trace-event", "npm:1.0.4"],\ ["enhanced-resolve", "npm:5.17.0"],\ @@ -20128,9 +19199,9 @@ const RAW_RUNTIME_STATE = ["neo-async", "npm:2.6.2"],\ ["schema-utils", "npm:3.3.0"],\ ["tapable", "npm:2.2.1"],\ - ["terser-webpack-plugin", "virtual:faa581e438d2bfc3b35103b96c4f73b9221cae95d4dbb77583d2f3adf3b53812d4470423e2f9051948f9d107db5a9af7637a8a9937a701c8a21dfb42028912a2#npm:5.3.10"],\ + ["terser-webpack-plugin", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.3.10"],\ ["watchpack", "npm:2.4.1"],\ - ["webpack-cli", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4"],\ + ["webpack-cli", null],\ ["webpack-sources", "npm:3.2.3"]\ ],\ "packagePeers": [\ @@ -20140,53 +19211,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["webpack-cli", [\ - ["npm:5.1.4", {\ - "packageLocation": "../../.yarn/berry/cache/webpack-cli-npm-5.1.4-7be5b53b38-10c0.zip/node_modules/webpack-cli/",\ - "packageDependencies": [\ - ["webpack-cli", "npm:5.1.4"]\ - ],\ - "linkType": "SOFT"\ - }],\ - ["virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4", {\ - "packageLocation": "./.yarn/__virtual__/webpack-cli-virtual-4ec3b9c5ab/3/.yarn/berry/cache/webpack-cli-npm-5.1.4-7be5b53b38-10c0.zip/node_modules/webpack-cli/",\ - "packageDependencies": [\ - ["webpack-cli", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.1.4"],\ - ["@discoveryjs/json-ext", "npm:0.5.7"],\ - ["@types/webpack", null],\ - ["@types/webpack-bundle-analyzer", null],\ - ["@types/webpack-cli__generators", null],\ - ["@types/webpack-dev-server", null],\ - ["@webpack-cli/configtest", "virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.1.1"],\ - ["@webpack-cli/generators", null],\ - ["@webpack-cli/info", "virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.0.2"],\ - ["@webpack-cli/serve", "virtual:4ec3b9c5abb1b27cbe283af5fe819c90121e921c8d66d98354404be295beaf69d1c27b6d28cc0bb61049c1f1666891965e38978db128d5ef2251e09844cf3595#npm:2.0.5"],\ - ["colorette", "npm:2.0.20"],\ - ["commander", "npm:10.0.1"],\ - ["cross-spawn", "npm:7.0.3"],\ - ["envinfo", "npm:7.13.0"],\ - ["fastest-levenshtein", "npm:1.0.16"],\ - ["import-local", "npm:3.1.0"],\ - ["interpret", "npm:3.1.1"],\ - ["rechoir", "npm:0.8.0"],\ - ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"],\ - ["webpack-bundle-analyzer", null],\ - ["webpack-dev-server", null],\ - ["webpack-merge", "npm:5.10.0"]\ - ],\ - "packagePeers": [\ - "@types/webpack-bundle-analyzer",\ - "@types/webpack-cli__generators",\ - "@types/webpack-dev-server",\ - "@types/webpack",\ - "@webpack-cli/generators",\ - "webpack-bundle-analyzer",\ - "webpack-dev-server",\ - "webpack"\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["webpack-dev-middleware", [\ ["npm:6.1.3", {\ "packageLocation": "../../.yarn/berry/cache/webpack-dev-middleware-npm-6.1.3-a10a45228c-10c0.zip/node_modules/webpack-dev-middleware/",\ @@ -20205,7 +19229,7 @@ const RAW_RUNTIME_STATE = ["mime-types", "npm:2.1.35"],\ ["range-parser", "npm:1.2.1"],\ ["schema-utils", "npm:4.2.0"],\ - ["webpack", "virtual:bc64701e347c32435930ca4a5c7640f3c03a70241247c73c0905a0f30af2cf7dbf5f0edcbffbd8a11609d83685942a2a02200355576dfa60d13a27be0b4105a9#npm:5.93.0"]\ + ["webpack", "virtual:d468d1ddb6d96635a3b90a71e031e0a63e7668730c7a27de2a8c0f4e780f41d6e0fd349d8b0d4cb15075a319c0681a137c48966c050ceeec6fd8b6ee33cdf6a8#npm:5.93.0"]\ ],\ "packagePeers": [\ "@types/webpack",\ @@ -20226,18 +19250,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["webpack-merge", [\ - ["npm:5.10.0", {\ - "packageLocation": "../../.yarn/berry/cache/webpack-merge-npm-5.10.0-c2d9fd1f83-10c0.zip/node_modules/webpack-merge/",\ - "packageDependencies": [\ - ["webpack-merge", "npm:5.10.0"],\ - ["clone-deep", "npm:4.0.1"],\ - ["flat", "npm:5.0.2"],\ - ["wildcard", "npm:2.0.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["webpack-sources", [\ ["npm:3.2.3", {\ "packageLocation": "../../.yarn/berry/cache/webpack-sources-npm-3.2.3-6bfb5d9563-10c0.zip/node_modules/webpack-sources/",\ @@ -20256,16 +19268,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["whatwg-encoding", [\ - ["npm:2.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/whatwg-encoding-npm-2.0.0-d7451f51b4-10c0.zip/node_modules/whatwg-encoding/",\ - "packageDependencies": [\ - ["whatwg-encoding", "npm:2.0.0"],\ - ["iconv-lite", "npm:0.6.3"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["whatwg-mimetype", [\ ["npm:3.0.0", {\ "packageLocation": "../../.yarn/berry/cache/whatwg-mimetype-npm-3.0.0-5b617710c1-10c0.zip/node_modules/whatwg-mimetype/",\ @@ -20275,17 +19277,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["whatwg-url", [\ - ["npm:11.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/whatwg-url-npm-11.0.0-073529d93a-10c0.zip/node_modules/whatwg-url/",\ - "packageDependencies": [\ - ["whatwg-url", "npm:11.0.0"],\ - ["tr46", "npm:3.0.0"],\ - ["webidl-conversions", "npm:7.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["which", [\ ["npm:2.0.2", {\ "packageLocation": "../../.yarn/berry/cache/which-npm-2.0.2-320ddf72f7-10c0.zip/node_modules/which/",\ @@ -20377,15 +19368,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["wildcard", [\ - ["npm:2.0.1", {\ - "packageLocation": "../../.yarn/berry/cache/wildcard-npm-2.0.1-7c6a3a3365-10c0.zip/node_modules/wildcard/",\ - "packageDependencies": [\ - ["wildcard", "npm:2.0.1"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["word-wrap", [\ ["npm:1.2.5", {\ "packageLocation": "../../.yarn/berry/cache/word-wrap-npm-1.2.5-42d00c4b09-10c0.zip/node_modules/word-wrap/",\ @@ -20488,24 +19470,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["xml-name-validator", [\ - ["npm:4.0.0", {\ - "packageLocation": "../../.yarn/berry/cache/xml-name-validator-npm-4.0.0-0857c21729-10c0.zip/node_modules/xml-name-validator/",\ - "packageDependencies": [\ - ["xml-name-validator", "npm:4.0.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["xmlchars", [\ - ["npm:2.2.0", {\ - "packageLocation": "../../.yarn/berry/cache/xmlchars-npm-2.2.0-8b78f0f5e4-10c0.zip/node_modules/xmlchars/",\ - "packageDependencies": [\ - ["xmlchars", "npm:2.2.0"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["xtend", [\ ["npm:4.0.2", {\ "packageLocation": "../../.yarn/berry/cache/xtend-npm-4.0.2-7f2375736e-10c0.zip/node_modules/xtend/",\ diff --git a/.storybook/main.ts b/.storybook/main.ts index 0dd379b..c014ad3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -23,6 +23,6 @@ const config: StorybookConfig = { name: getAbsolutePath("@storybook/nextjs"), options: {}, }, - staticDirs: ["..\\public"], + staticDirs: ["../public"], }; export default config; diff --git a/app/mypage/device-type.tsx b/app/mypage/device-type.tsx deleted file mode 100644 index f2771ea..0000000 --- a/app/mypage/device-type.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import Text from "@common/text"; -import useDeviceType from "@hooks/useDeviceType"; - -const DeviceType = () => { - const deviceType = useDeviceType(); - - return {deviceType}; -}; - -export default DeviceType; diff --git a/app/search/search-client.tsx b/app/search/search-client.tsx index 8b247c2..25a1ae6 100644 --- a/app/search/search-client.tsx +++ b/app/search/search-client.tsx @@ -9,7 +9,7 @@ import useInput from "@hooks/useInput"; import useMapControl from "@hooks/useMapControl"; import ArrowRightIcon from "@icons/arrow-right-icon"; import LocationIcon from "@icons/location-icon"; -import type { KakaoPlace } from "@layout/move-map-input"; +import type { KakaoPlace } from "@/types/kakao-place.types"; import SearchHeader from "@pages/search/search-header"; import SearchList, { extractMarkedText, diff --git a/components/common/event-popup.tsx b/components/common/event-popup.tsx deleted file mode 100644 index 0b230bb..0000000 --- a/components/common/event-popup.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client"; - -import cn from "@lib/cn"; -import Image from "next/image"; -import Link from "next/link"; -import { useEffect, useState } from "react"; -import { createPortal } from "react-dom"; -import Dimmed from "./dimmed"; -import Skeleton from "./skeleton"; - -interface EventPopupProps { - showDoNotShowToday?: boolean; - onClose: (doNotShowToday?: boolean) => void; -} - -const EventPopup = ({ - onClose, - showDoNotShowToday = true, -}: EventPopupProps) => { - const [imageLoaded, setImageLoaded] = useState(false); - const [doNotShowToday, setDoNotShowToday] = useState(false); - const [portalEl, setPortalEl] = useState(null); - - useEffect(() => { - setPortalEl(document.getElementById("portal")); - }, []); - - const handleClose = () => { - onClose(doNotShowToday); - }; - - if (!portalEl) return null; - - return createPortal( - -
-
-

- 이벤트 안내 -

- -
- -
- - {!imageLoaded && } - 이벤트 이미지 setImageLoaded(true)} - /> - - {/*

- 특별 이벤트를 놓치지 마세요! 지금 바로 확인하세요. -

*/} -
- -
- {showDoNotShowToday && ( -
- setDoNotShowToday(e.target.checked)} - className="h-4 w-4 text-blue-600 rounded-sm border-gray-300" - /> - -
- )} - -
- - - 자세히 보기 - -
-
-
-
, - portalEl - ); -}; - -export default EventPopup; diff --git a/components/common/scroll-to-top.tsx b/components/common/scroll-to-top.tsx deleted file mode 100644 index cee6c1e..0000000 --- a/components/common/scroll-to-top.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import ArrowUpIcon from "@icons/arrow-up-icon"; -import useScrollRefStore from "@store/useScrollRefStore"; - -const ScrollToTop = () => { - const { containerRef } = useScrollRefStore(); - - const scrollToTop = () => { - if (containerRef && containerRef.current) { - containerRef.current.scrollTo({ - top: 0, - behavior: "smooth", - }); - } - }; - - return ( -
- -
- ); -}; - -export default ScrollToTop; diff --git a/components/icons/bookmark-icon.tsx b/components/icons/bookmark-icon.tsx deleted file mode 100644 index 16b1946..0000000 --- a/components/icons/bookmark-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { type IconProps, iconColorMap } from "./home-icon"; -import cn from "@lib/cn"; - -const BookmarkIcon = ({ - size = 25, - color = "primary", - className, - link, -}: IconProps) => { - const style = link ? "stroke-[#888] stroke-[0.6] fill-[#f3ce9d] dark:fill-[#e9d2b5] dark:stroke-[#555]" : "dark:fill-grey-light"; - - return ( - - - - ); -}; - -export default BookmarkIcon; diff --git a/components/icons/checked-icon.tsx b/components/icons/checked-icon.tsx deleted file mode 100644 index c66a1cf..0000000 --- a/components/icons/checked-icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { type IconProps, iconColorMap } from "./home-icon"; -import cn from "@lib/cn"; - -const CheckedIcon = ({ - size = 25, - color = "primary", - className, -}: IconProps) => { - return ( - - - - ); -}; - -export default CheckedIcon; diff --git a/components/icons/config-icon.tsx b/components/icons/config-icon.tsx deleted file mode 100644 index 4f0e38d..0000000 --- a/components/icons/config-icon.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { type IconProps, iconColorMap } from "./home-icon"; -import cn from "@lib/cn"; - -const ConfigIcon = ({ - size = 25, - color = "primary", - className, - link, -}: IconProps) => { - const style2 = link - ? "stroke-grey-dark fill-[#7E8EF1] dark:stroke-[#555] dark:fill-[#7381df]" - : "dark:fill-grey-light"; - - const style1 = link - ? "stroke-grey-dark fill-grey-dark dark:stroke-[#555] dark:fill-grey" - : "dark:fill-grey-light"; - - return ( - - - - - ); -}; - -export default ConfigIcon; diff --git a/components/icons/location-pin-icon.tsx b/components/icons/location-pin-icon.tsx deleted file mode 100644 index ae3ff62..0000000 --- a/components/icons/location-pin-icon.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { type IconProps, iconColorMap } from "./home-icon"; -import cn from "@lib/cn"; - -const LocationPinIcon = ({ - size = 25, - color = "primary", - className, - link, -}: IconProps) => { - const style = link - ? "stroke-[#888] stroke-[0.6] fill-[#B57EDC] dark:stroke-[#555] dark:fill-[#cf8ce7]" - : "dark:fill-grey-light"; - return ( - - - - ); -}; - -export default LocationPinIcon; diff --git a/components/layout/move-map-input.tsx b/components/layout/move-map-input.tsx deleted file mode 100644 index cc08651..0000000 --- a/components/layout/move-map-input.tsx +++ /dev/null @@ -1,168 +0,0 @@ -"use client"; - -import { type Device } from "@/app/mypage/page"; -import Input from "@common/input"; -import ListItem, { ListContents } from "@common/list-item"; -import Text from "@common/text"; -import useInput from "@hooks/useInput"; -import useMapControl from "@hooks/useMapControl"; -import PinIcon from "@icons/pin-icon"; -import cn from "@lib/cn"; -import { MoveIcon } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import Dimmed from "@common/dimmed"; -import CloseIcon from "@icons/close-icon"; -import Tooltip from "../common/tooltip"; - -export type KakaoPlace = { - address_name: string; - category_group_code: string; - category_group_name: string; - category_name: string; - distance: string; - id: string; - phone: string; - place_name: string; - place_url: string; - road_address_name: string; - x: string; - y: string; -}; - -export type KakaoPagination = { - totalCount: number; - hasNextPage: boolean; - hasPrevPage: boolean; - first: number; - current: number; - last: number; - perPage: number; - nextPage: VoidFunction; - prevPage: VoidFunction; -}; - -const MoveMapInput = ({ deviceType }: { deviceType: Device }) => { - const isMobileApp = - deviceType === "ios-mobile-app" || deviceType === "android-mobile-app"; - - const { move } = useMapControl(); - - const searchValue = useInput(""); - - const resultRef = useRef(null); - - const [result, setResult] = useState([]); - - const [active, setActive] = useState(false); - - const [searchStatus, setSearchStatus] = useState(""); - - useEffect(() => { - if (searchValue.value === "") { - setResult([]); - return; - } - - let ps = new window.kakao.maps.services.Places(); - - const placesSearchCB = ( - data: KakaoPlace[], - status: string, - _: KakaoPagination - ) => { - if (status === window.kakao.maps.services.Status.OK) { - setSearchStatus(""); - setResult([...data]); - } else if (status === window.kakao.maps.services.Status.ZERO_RESULT) { - setSearchStatus("검색 결과가 존재하지 않습니다."); - return; - } else if (status === window.kakao.maps.services.Status.ERROR) { - setSearchStatus("검색 결과 중 오류가 발생했습니다."); - return; - } - }; - - const handler = setTimeout(async () => { - ps.keywordSearch(searchValue.value, placesSearchCB); - }, 300); - - return () => clearTimeout(handler); - }, [searchValue.value]); - - const style = isMobileApp ? "mo:top-[100px]" : "mo:top-16"; - const inputStyle = isMobileApp ? "mo:top-[100px]" : ""; - - const mobile = true; - - if (!active) { - return ( - setActive(true)} - > - - - ); - } - - return ( - setActive(false)}> -
- : true} - onIconClick={mobile ? () => setActive(false) : undefined} - /> - {(result.length > 0 || searchStatus !== "") && ( -
- {searchStatus !== "" ? ( - - {searchStatus} - - ) : ( - <> - {result.map((searchItem) => { - return ( - } - onClick={() => { - move({ - lat: Number(searchItem.y), - lng: Number(searchItem.x), - }); - }} - > - - - ); - })} - - )} -
- )} -
-
- ); -}; - -export default MoveMapInput; diff --git a/components/layout/overlay.tsx b/components/layout/overlay.tsx index 1b09e7e..411b43a 100644 --- a/components/layout/overlay.tsx +++ b/components/layout/overlay.tsx @@ -21,7 +21,7 @@ const Overlay = ({ title, position }: OverlayProps) => { return (
@@ -45,8 +45,6 @@ const getTailwindColorClass = (count: number): string => { return "bg-blue"; } else if (count < 1000) { return "bg-yellow"; - } else if (count < 5000) { - return "bg-red"; } else { return "bg-red"; } diff --git a/components/pages/challenge/celebration-motion.tsx b/components/pages/challenge/celebration-motion.tsx index 6c0836c..54cadd9 100644 --- a/components/pages/challenge/celebration-motion.tsx +++ b/components/pages/challenge/celebration-motion.tsx @@ -41,13 +41,8 @@ const CelebrationMotion = () => { } }, [goalAchieved, alreadyShown, markCelebrationShown]); - // 목표 미달성이면 안 보임 - if (!goalAchieved && !alreadyShown) return null; - // 달성했거나 이미 이번 주에 mark된 상태면 계속 보임 - if (!goalAchieved && alreadyShown) { - // 이번 주에 mark는 됐지만 현재 시점에 달성 안 된 경우 (목표를 올린 경우) - return null; - } + // 목표를 달성하지 않았으면 (이번 주 mark 여부와 무관하게) 표시하지 않는다. + if (!goalAchieved) return null; const reducedMotion = typeof window !== "undefined" && diff --git a/components/pages/home/slide-icons.tsx b/components/pages/home/slide-icons.tsx deleted file mode 100644 index 1b3cc99..0000000 --- a/components/pages/home/slide-icons.tsx +++ /dev/null @@ -1,254 +0,0 @@ -export const PullupIcon = ({ size = 64 }: { size?: number }) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export const PullupProsIcon = ({ size = 64 }: { size?: number }) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export const PullupRankingIcon = ({ size = 64 }: { size?: number }) => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/components/pages/search/search-list.tsx b/components/pages/search/search-list.tsx index e683930..0619ed2 100644 --- a/components/pages/search/search-list.tsx +++ b/components/pages/search/search-list.tsx @@ -5,7 +5,7 @@ import Section from "@common/section"; import Text from "@common/text"; import useMapControl from "@hooks/useMapControl"; import PinIcon from "@icons/pin-icon"; -import { type KakaoPlace } from "@layout/move-map-input"; +import { type KakaoPlace } from "@/types/kakao-place.types"; import useSearchStore from "@store/useSearchStore"; import useSheetHeightStore from "@store/useSheetHeightStore"; import { useRouter } from "next/navigation"; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx deleted file mode 100644 index d970280..0000000 --- a/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import cn from "@/lib/cn" - - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary-subtle", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/hooks/useDeviceType.ts b/hooks/useDeviceType.ts deleted file mode 100644 index 74c70f5..0000000 --- a/hooks/useDeviceType.ts +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -type DeviceType = - | "android-mobile-app" - | "ios-mobile-app" - | "android-mobile-web" - | "ios-mobile-web" - | "desktop"; - -const useDeviceType = (): DeviceType => { - const [deviceType, setDeviceType] = useState("desktop"); - - useEffect(() => { - const userAgent = navigator.userAgent.toLowerCase(); - - if (userAgent.includes("android-mobile-app")) { - setDeviceType("android-mobile-app"); - } else if (userAgent.includes("ios-mobile-app")) { - setDeviceType("ios-mobile-app"); - } else if (/android/i.test(userAgent)) { - setDeviceType("android-mobile-web"); - } else if (/iphone|ipad|ipod/i.test(userAgent)) { - setDeviceType("ios-mobile-web"); - } else { - setDeviceType("desktop"); - } - }, []); - - return deviceType; -}; - -export default useDeviceType; -// "use client"; - -// import { useEffect, useState } from "react"; - -// type DeviceType = -// | "android-mobile-app" -// | "ios-mobile-app" -// | "android-mobile-web" -// | "ios-mobile-web" -// | "desktop"; - -// const useDeviceType = (): DeviceType | null => { -// const [deviceType, setDeviceType] = useState("desktop"); - -// useEffect(() => { -// const userAgent = navigator.userAgent.toLowerCase(); - -// if (userAgent.includes("android-mobile-app")) { -// setDeviceType("android-mobile-app"); -// } else if (userAgent.includes("ios-mobile-app")) { -// setDeviceType("desktop"); -// } else if (/android/i.test(userAgent)) { -// setDeviceType("android-mobile-web"); -// } else if (/iphone|ipad|ipod/i.test(userAgent)) { -// setDeviceType("ios-mobile-web"); -// } else { -// setDeviceType("ios-mobile-app"); -// } -// }, []); - -// return deviceType; -// }; - -// export default useDeviceType; diff --git a/hooks/useEventPopup.ts b/hooks/useEventPopup.ts deleted file mode 100644 index ee291d7..0000000 --- a/hooks/useEventPopup.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; - -const useEventPopup = (popupId = "event-popup") => { - const [showPopup, setShowPopup] = useState(false); - - const checkIfShouldShowPopup = useCallback(() => { - if (typeof window !== "undefined") { - const storageKey = `${popupId}-expiry`; - const expiryDate = localStorage.getItem(storageKey); - const today = new Date().toDateString(); - - if (!expiryDate || expiryDate !== today) { - setShowPopup(true); - } - } - }, [popupId]); - - useEffect(() => { - checkIfShouldShowPopup(); - }, [checkIfShouldShowPopup]); - - const closePopup = (doNotShowToday = false) => { - if (doNotShowToday) { - const storageKey = `${popupId}-expiry`; - const today = new Date(); - const expiryDate = today.toDateString(); - localStorage.setItem(storageKey, expiryDate); - } - setShowPopup(false); - }; - - return { - showPopup, - closePopup, - }; -}; - -export default useEventPopup; diff --git a/lib/api/report/my-suggested.ts b/lib/api/report/my-suggested.ts index dea5f4d..88f0227 100644 --- a/lib/api/report/my-suggested.ts +++ b/lib/api/report/my-suggested.ts @@ -36,21 +36,7 @@ const mySuggested = async (cookie?: string) => { credentials: "include", }); - if (!response.ok) { - const msg = await response.json(); - if (response.status === 401) { - const data: Response = { - error: msg.error, - }; - return data; - } else { - const data: Response = { - error: msg.message, - }; - return data; - } - } - + // fetchData 가 non-2xx 에서 throw 하므로 여기 도달하면 항상 성공. const data: Response = { data: await response.json(), }; diff --git a/lib/api/user/favorites.ts b/lib/api/user/favorites.ts index f906b2b..d32d3a6 100644 --- a/lib/api/user/favorites.ts +++ b/lib/api/user/favorites.ts @@ -26,21 +26,7 @@ const favorites = async (cookie?: string) => { } ); - if (!response.ok) { - const msg = await response.json(); - if (response.status === 401) { - const data: Response = { - error: msg.error, - }; - return data; - } else { - const data: Response = { - error: msg.message, - }; - return data; - } - } - + // fetchData 가 non-2xx 에서 throw 하므로 여기 도달하면 항상 성공. const data: Response = { data: await response.json(), }; diff --git a/lib/map-walker.ts b/lib/map-walker.ts index eab6a9d..3055b5b 100644 --- a/lib/map-walker.ts +++ b/lib/map-walker.ts @@ -51,9 +51,6 @@ class MapWalker { this.roadviewClient = roadviewClient; this.roadview = roadview; - this.newPos; - this.prevPos; - this.onMouseMove = this.onMouseMove.bind(this); this.onMouseUp = this.onMouseUp.bind(this); this.onMouseDown = this.onMouseDown.bind(this); diff --git a/package.json b/package.json index d3e2d4b..49d7fdf 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,7 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-select": "^2.2.5", - "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-toast": "^1.2.1", - "@tanstack/react-query": "^5.72.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "embla-carousel-autoplay": "^8.1.7", @@ -38,7 +36,6 @@ "react-markdown": "^9.0.1", "react-player": "^2.16.0", "rehype-raw": "^7.0.0", - "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "sharp": "^0.34.5", "tailwind-merge": "^3.3.1", @@ -62,30 +59,24 @@ "@storybook/test": "^8.2.2", "@tailwindcss/postcss": "^4.1.18", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", - "@types/jsdom": "^20", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "@types/sharp": "^0.32.0", "@types/uuid": "^10", - "@vitejs/plugin-react": "^6.0.3", "eslint": "^8", "eslint-config-next": "14.2.5", "eslint-plugin-storybook": "^0.8.0", "fast-check": "^4.8.0", "happy-dom": "^20.10.6", "husky": "^9.1.7", - "jsdom": "^20", "postcss": "^8", "storybook": "^8.2.2", "tailwindcss": "^4.1.18", "typescript": "^5", "vite": "^8.1.4", "vitest": "^4.1.10", - "webpack": "^5.93.0", - "webpack-cli": "^5.1.4" + "webpack": "^5.93.0" }, "packageManager": "yarn@4.3.1" } diff --git a/store/useAlertStore.ts b/store/useAlertStore.ts index ccaa40d..d2cd0a1 100644 --- a/store/useAlertStore.ts +++ b/store/useAlertStore.ts @@ -31,11 +31,7 @@ const useAlertStore = create()((set) => ({ onClick(); } : undefined, - onClickAsync: onClickAsync - ? async () => { - await onClickAsync(); - } - : undefined, + onClickAsync: onClickAsync ? onClickAsync : undefined, open: true, }, }), diff --git a/types/cluster.types.ts b/types/cluster.types.ts deleted file mode 100644 index 2ce438f..0000000 --- a/types/cluster.types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { MarkerRes } from "@api/marker/get-all-marker"; -import type { CustomOverlay } from "./custom-overlay.types"; -import type { KakaoMarker } from "./kakao-map.types"; - -export interface MarkerClusterer { - /** - * 클러스터에 마커 하나를 추가한다. - * - * @param marker 추가할 마커 - * @param nodraw 클러스터 redraw 여부. true인 경우 클러스터를 다시 그리지 않는다. - */ - addMarker(marker: KakaoMarker | CustomOverlay, nodraw?: boolean): void; - - /** - * 클러스터에 추가된 마커 중 하나를 삭제한다. - * - * @param marker 삭제할 마커 - * @param nodraw 클러스터 redraw 여부. true인 경우 클러스터를 다시 그리지 않는다. - */ - removeMarker(marker: KakaoMarker | CustomOverlay, nodraw?: boolean): void; - removeMarkers( - marker: KakaoMarker[] | CustomOverlay | MarkerRes[], - nodraw?: boolean - ): void; - - /** - * 여러개의 마커를 추가한다. - * - * @param markers 추가할 마커 객체 배열 - * @param nodraw 클러스터 redraw 여부. true인 경우 클러스터를 다시 그리지 않는다. - */ - addMarkers(markers: (KakaoMarker | CustomOverlay)[], nodraw?: boolean): void; - - /** - * 추가된 모든 마커를 삭제한다. - */ - clear(): void; - - /** - * 클러스터를 다시 그린다. 주로 옵션을 변경한 이후 클러스터를 다시 그릴 때 사용한다. - */ - redraw(): void; -} diff --git a/types/kakao-location.type.ts b/types/kakao-location.type.ts deleted file mode 100644 index 53f37e5..0000000 --- a/types/kakao-location.type.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface LocationResponse { - documents: Document[]; - meta: Meta; -} - -export interface Document { - address_name: string; - category_group_code: string; - category_group_name: string; - category_name: string; - distance: string; - id: string; - phone: string; - place_name: string; - place_url: string; - road_address_name: string; - x: string; - y: string; -} - -export interface Meta { - is_end: boolean; - pageable_count: number; - total_count: number; - same_name: SameName; -} - -export interface SameName { - keyword: string; - region: string[]; - selected_region: string; -} diff --git a/types/kakao-place.types.ts b/types/kakao-place.types.ts new file mode 100644 index 0000000..94c570a --- /dev/null +++ b/types/kakao-place.types.ts @@ -0,0 +1,14 @@ +export type KakaoPlace = { + address_name: string; + category_group_code: string; + category_group_name: string; + category_name: string; + distance: string; + id: string; + phone: string; + place_name: string; + place_url: string; + road_address_name: string; + x: string; + y: string; +}; diff --git a/yarn.lock b/yarn.lock index cc49976..63481c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,13 +12,6 @@ __metadata: languageName: node linkType: hard -"@adobe/css-tools@npm:^4.4.0": - version: 4.5.0 - resolution: "@adobe/css-tools@npm:4.5.0" - checksum: 10c0/fc969e1117098eb4cccdb73beb2508daa0e52760af1183d6288bafea59204943490ab3ede28593032ffb8929c0cee270b2a53254fe61139ab00604ea8fc33cea - languageName: node - linkType: hard - "@alloc/quick-lru@npm:^5.2.0": version: 5.2.0 resolution: "@alloc/quick-lru@npm:5.2.0" @@ -1574,13 +1567,6 @@ __metadata: languageName: node linkType: hard -"@discoveryjs/json-ext@npm:^0.5.0": - version: 0.5.7 - resolution: "@discoveryjs/json-ext@npm:0.5.7" - checksum: 10c0/e10f1b02b78e4812646ddf289b7d9f2cb567d336c363b266bd50cd223cf3de7c2c74018d91cd2613041568397ef3a4a2b500aba588c6e5bd78c38374ba68f38c - languageName: node - linkType: hard - "@emnapi/core@npm:1.11.1": version: 1.11.1 resolution: "@emnapi/core@npm:1.11.1" @@ -3114,7 +3100,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-slot@npm:1.2.3, @radix-ui/react-slot@npm:^1.2.3": +"@radix-ui/react-slot@npm:1.2.3": version: 1.2.3 resolution: "@radix-ui/react-slot@npm:1.2.3" dependencies: @@ -3593,7 +3579,7 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:^1.0.0, @rolldown/pluginutils@npm:^1.0.1": +"@rolldown/pluginutils@npm:^1.0.0": version: 1.0.1 resolution: "@rolldown/pluginutils@npm:1.0.1" checksum: 10c0/99d9b06d90196823e4d8c841f258db7a16e5dbba5824a2962b05d907b79f1ba929d56f22dd744fd530936e568c865ee56a719dc31e57e13bc0a8eb4764a8d8dd @@ -4374,24 +4360,6 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:5.72.0": - version: 5.72.0 - resolution: "@tanstack/query-core@npm:5.72.0" - checksum: 10c0/a22a4536d90069718a8e1d46832ca27d870a5080e98f37021163d4fea4fe2ea499fd7cd3452de340b8805c2378f56f5967bf924eb66105d626f731804500f206 - languageName: node - linkType: hard - -"@tanstack/react-query@npm:^5.72.0": - version: 5.72.0 - resolution: "@tanstack/react-query@npm:5.72.0" - dependencies: - "@tanstack/query-core": "npm:5.72.0" - peerDependencies: - react: ^18 || ^19 - checksum: 10c0/e6b1963bb3a8cd5f68fde8b9a22222450137bf1384fd242c9757ff37feb70abd2627c24763a70d79aa6492f52b4763cff48787bce796662d8359db95e8e33ab9 - languageName: node - linkType: hard - "@testing-library/dom@npm:10.1.0": version: 10.1.0 resolution: "@testing-library/dom@npm:10.1.0" @@ -4457,20 +4425,6 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:^6.9.1": - version: 6.9.1 - resolution: "@testing-library/jest-dom@npm:6.9.1" - dependencies: - "@adobe/css-tools": "npm:^4.4.0" - aria-query: "npm:^5.0.0" - css.escape: "npm:^1.5.1" - dom-accessibility-api: "npm:^0.6.3" - picocolors: "npm:^1.1.1" - redent: "npm:^3.0.0" - checksum: 10c0/4291ebd2f0f38d14cefac142c56c337941775a5807e2a3d6f1a14c2fbd6be76a18e498ed189e95bedc97d9e8cf1738049bc76c85b5bc5e23fae7c9e10f7b3a12 - languageName: node - linkType: hard - "@testing-library/react@npm:^16.3.2": version: 16.3.2 resolution: "@testing-library/react@npm:16.3.2" @@ -4500,13 +4454,6 @@ __metadata: languageName: node linkType: hard -"@tootallnate/once@npm:2": - version: 2.0.1 - resolution: "@tootallnate/once@npm:2.0.1" - checksum: 10c0/23b01a341485be711c602077936d70f8e695405bb88ab4433dc6d1e6cb4556401518789574d399eded790b70b27738136c9a8f02df7ae4219f4ba28bb22d586b - languageName: node - linkType: hard - "@tybys/wasm-util@npm:^0.10.1": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" @@ -4738,17 +4685,6 @@ __metadata: languageName: node linkType: hard -"@types/jsdom@npm:^20": - version: 20.0.1 - resolution: "@types/jsdom@npm:20.0.1" - dependencies: - "@types/node": "npm:*" - "@types/tough-cookie": "npm:*" - parse5: "npm:^7.0.0" - checksum: 10c0/3d4b2a3eab145674ee6da482607c5e48977869109f0f62560bf91ae1a792c9e847ac7c6aaf243ed2e97333cb3c51aef314ffa54a19ef174b8f9592dfcb836b25 - languageName: node - linkType: hard - "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -4909,22 +4845,6 @@ __metadata: languageName: node linkType: hard -"@types/sharp@npm:^0.32.0": - version: 0.32.0 - resolution: "@types/sharp@npm:0.32.0" - dependencies: - sharp: "npm:*" - checksum: 10c0/a101c5bf17220b51e339164fabf779993e1757fcae1610f2647d36a24d7f15bf857fd747ec559b8fef667c9ea38caab09dc3ba4d49095fef2a06e5ae1819e639 - languageName: node - linkType: hard - -"@types/tough-cookie@npm:*": - version: 4.0.5 - resolution: "@types/tough-cookie@npm:4.0.5" - checksum: 10c0/68c6921721a3dcb40451543db2174a145ef915bc8bcbe7ad4e59194a0238e776e782b896c7a59f4b93ac6acefca9161fccb31d1ce3b3445cb6faa467297fb473 - languageName: node - linkType: hard - "@types/unist@npm:*, @types/unist@npm:^3.0.0": version: 3.0.2 resolution: "@types/unist@npm:3.0.2" @@ -5103,24 +5023,6 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-react@npm:^6.0.3": - version: 6.0.3 - resolution: "@vitejs/plugin-react@npm:6.0.3" - dependencies: - "@rolldown/pluginutils": "npm:^1.0.1" - peerDependencies: - "@rolldown/plugin-babel": ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - "@rolldown/plugin-babel": - optional: true - babel-plugin-react-compiler: - optional: true - checksum: 10c0/592a93178c0eec420759d529a2c964a7103860808f549f72993b96a38c287c929a0d371cacbd932c37ee2118b830348db752cc01a31d688c71d19d74567251fa - languageName: node - linkType: hard - "@vitest/expect@npm:1.6.0": version: 1.6.0 resolution: "@vitest/expect@npm:1.6.0" @@ -5386,39 +5288,6 @@ __metadata: languageName: node linkType: hard -"@webpack-cli/configtest@npm:^2.1.1": - version: 2.1.1 - resolution: "@webpack-cli/configtest@npm:2.1.1" - peerDependencies: - webpack: 5.x.x - webpack-cli: 5.x.x - checksum: 10c0/a8da1f15702cb289807da99235ed95326ed7dabeb1a36ca59bd3a5dbe6adcc946a9a2767936050fc4d5ed14efab0e5b5a641dfe8e3d862c36caa5791ac12759d - languageName: node - linkType: hard - -"@webpack-cli/info@npm:^2.0.2": - version: 2.0.2 - resolution: "@webpack-cli/info@npm:2.0.2" - peerDependencies: - webpack: 5.x.x - webpack-cli: 5.x.x - checksum: 10c0/ca88a35604dc9aedac7c26e8f6793c5039dc1eea2b12a85fbfd669a5f21ecf9cf169d7fd157ea366a62666e3fa05b776306a96742ac61a9868f44fdce6b40f7d - languageName: node - linkType: hard - -"@webpack-cli/serve@npm:^2.0.5": - version: 2.0.5 - resolution: "@webpack-cli/serve@npm:2.0.5" - peerDependencies: - webpack: 5.x.x - webpack-cli: 5.x.x - peerDependenciesMeta: - webpack-dev-server: - optional: true - checksum: 10c0/36079d34971ff99a58b66b13f4184dcdd8617853c48cccdbc3f9ab7ea9e5d4fcf504e873c298ea7aa15e0b51ad2c4aee4d7a70bd7d9364e60f57b0eb93ca15fc - languageName: node - linkType: hard - "@xtuc/ieee754@npm:^1.2.0": version: 1.2.0 resolution: "@xtuc/ieee754@npm:1.2.0" @@ -5453,13 +5322,6 @@ __metadata: languageName: node linkType: hard -"abab@npm:^2.0.6": - version: 2.0.6 - resolution: "abab@npm:2.0.6" - checksum: 10c0/0b245c3c3ea2598fe0025abf7cc7bb507b06949d51e8edae5d12c1b847a0a0c09639abcb94788332b4e2044ac4491c1e8f571b51c7826fd4b0bda1685ad4a278 - languageName: node - linkType: hard - "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -5486,16 +5348,6 @@ __metadata: languageName: node linkType: hard -"acorn-globals@npm:^7.0.0": - version: 7.0.1 - resolution: "acorn-globals@npm:7.0.1" - dependencies: - acorn: "npm:^8.1.0" - acorn-walk: "npm:^8.0.2" - checksum: 10c0/7437f58e92d99292dbebd0e79531af27d706c9f272f31c675d793da6c82d897e75302a8744af13c7f7978a8399840f14a353b60cf21014647f71012982456d2b - languageName: node - linkType: hard - "acorn-import-attributes@npm:^1.9.5": version: 1.9.5 resolution: "acorn-import-attributes@npm:1.9.5" @@ -5521,15 +5373,6 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.2": - version: 8.3.5 - resolution: "acorn-walk@npm:8.3.5" - dependencies: - acorn: "npm:^8.11.0" - checksum: 10c0/e31bf5b5423ed1349437029d66d708b9fbd1b77a644b031501e2c753b028d13b56348210ed901d5b1d0d86eb3381c0a0fc0d0998511a9d546d1194936266a332 - languageName: node - linkType: hard - "acorn@npm:^7.4.1": version: 7.4.1 resolution: "acorn@npm:7.4.1" @@ -5539,15 +5382,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.8.1": - version: 8.17.0 - resolution: "acorn@npm:8.17.0" - bin: - acorn: bin/acorn - checksum: 10c0/5dcefea5f8f023b6cc24cbe71fb5a8112b601d36c4fa07d14e4e6ffc2ee47383332c46b36c766d9437725aa6660156eae50efa0c838719823b50d7c327c4ed42 - languageName: node - linkType: hard - "acorn@npm:^8.11.3, acorn@npm:^8.7.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.12.1 resolution: "acorn@npm:8.12.1" @@ -5567,15 +5401,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:6": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: "npm:4" - checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261 - languageName: node - linkType: hard - "agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": version: 7.1.1 resolution: "agent-base@npm:7.1.1" @@ -5946,27 +5771,6 @@ __metadata: languageName: node linkType: hard -"async-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-function@npm:1.0.0" - checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 - languageName: node - linkType: hard - -"async-generator-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-generator-function@npm:1.0.0" - checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 - languageName: node - linkType: hard - -"asynckit@npm:^0.4.0": - version: 0.4.0 - resolution: "asynckit@npm:0.4.0" - checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d - languageName: node - linkType: hard - "available-typed-arrays@npm:^1.0.7": version: 1.0.7 resolution: "available-typed-arrays@npm:1.0.7" @@ -6353,16 +6157,6 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": - version: 1.0.2 - resolution: "call-bind-apply-helpers@npm:1.0.2" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 - languageName: node - linkType: hard - "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -6707,22 +6501,13 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.10, colorette@npm:^2.0.14": +"colorette@npm:^2.0.10": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 languageName: node linkType: hard -"combined-stream@npm:^1.0.8": - version: 1.0.8 - resolution: "combined-stream@npm:1.0.8" - dependencies: - delayed-stream: "npm:~1.0.0" - checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 - languageName: node - linkType: hard - "comma-separated-tokens@npm:^2.0.0": version: 2.0.3 resolution: "comma-separated-tokens@npm:2.0.3" @@ -6730,13 +6515,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^10.0.1": - version: 10.0.1 - resolution: "commander@npm:10.0.1" - checksum: 10c0/53f33d8927758a911094adadda4b2cbac111a5b377d8706700587650fd8f45b0bbe336de4b5c3fe47fd61f420a3d9bd452b6e0e6e5600a7e74d7bf0174f6efe3 - languageName: node - linkType: hard - "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -7040,29 +6818,6 @@ __metadata: languageName: node linkType: hard -"cssom@npm:^0.5.0": - version: 0.5.0 - resolution: "cssom@npm:0.5.0" - checksum: 10c0/8c4121c243baf0678c65dcac29b201ff0067dfecf978de9d5c83b2ff127a8fdefd2bfd54577f5ad8c80ed7d2c8b489ae01c82023545d010c4ecb87683fb403dd - languageName: node - linkType: hard - -"cssom@npm:~0.3.6": - version: 0.3.8 - resolution: "cssom@npm:0.3.8" - checksum: 10c0/d74017b209440822f9e24d8782d6d2e808a8fdd58fa626a783337222fe1c87a518ba944d4c88499031b4786e68772c99dfae616638d71906fe9f203aeaf14411 - languageName: node - linkType: hard - -"cssstyle@npm:^2.3.0": - version: 2.3.0 - resolution: "cssstyle@npm:2.3.0" - dependencies: - cssom: "npm:~0.3.6" - checksum: 10c0/863400da2a458f73272b9a55ba7ff05de40d850f22eb4f37311abebd7eff801cf1cd2fb04c4c92b8c3daed83fe766e52e4112afb7bc88d86c63a9c2256a7d178 - languageName: node - linkType: hard - "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" @@ -7077,17 +6832,6 @@ __metadata: languageName: node linkType: hard -"data-urls@npm:^3.0.2": - version: 3.0.2 - resolution: "data-urls@npm:3.0.2" - dependencies: - abab: "npm:^2.0.6" - whatwg-mimetype: "npm:^3.0.0" - whatwg-url: "npm:^11.0.0" - checksum: 10c0/051c3aaaf3e961904f136aab095fcf6dff4db23a7fc759dd8ba7b3e6ba03fc07ef608086caad8ab910d864bd3b5e57d0d2f544725653d77c96a2c971567045f4 - languageName: node - linkType: hard - "data-view-buffer@npm:^1.0.1": version: 1.0.1 resolution: "data-view-buffer@npm:1.0.1" @@ -7163,13 +6907,6 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.2": - version: 10.6.0 - resolution: "decimal.js@npm:10.6.0" - checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa - languageName: node - linkType: hard - "decode-named-character-reference@npm:^1.0.0": version: 1.0.2 resolution: "decode-named-character-reference@npm:1.0.2" @@ -7273,13 +7010,6 @@ __metadata: languageName: node linkType: hard -"delayed-stream@npm:~1.0.0": - version: 1.0.0 - resolution: "delayed-stream@npm:1.0.0" - checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 - languageName: node - linkType: hard - "depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -7441,15 +7171,6 @@ __metadata: languageName: node linkType: hard -"domexception@npm:^4.0.0": - version: 4.0.0 - resolution: "domexception@npm:4.0.0" - dependencies: - webidl-conversions: "npm:^7.0.0" - checksum: 10c0/774277cd9d4df033f852196e3c0077a34dbd15a96baa4d166e0e47138a80f4c0bdf0d94e4703e6ff5883cec56bb821a6fff84402d8a498e31de7c87eb932a294 - languageName: node - linkType: hard - "domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": version: 4.3.1 resolution: "domhandler@npm:4.3.1" @@ -7480,17 +7201,6 @@ __metadata: languageName: node linkType: hard -"dunder-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "dunder-proto@npm:1.0.1" - dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.2.0" - checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 - languageName: node - linkType: hard - "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -7646,13 +7356,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:^6.0.0": - version: 6.0.1 - resolution: "entities@npm:6.0.1" - checksum: 10c0/ed836ddac5acb34341094eb495185d527bd70e8632b6c0d59548cbfa23defdbae70b96f9a405c82904efa421230b5b3fd2283752447d737beffd3f3e6ee74414 - languageName: node - linkType: hard - "entities@npm:^7.0.1": version: 7.0.1 resolution: "entities@npm:7.0.1" @@ -7764,13 +7467,6 @@ __metadata: languageName: node linkType: hard -"es-define-property@npm:^1.0.1": - version: 1.0.1 - resolution: "es-define-property@npm:1.0.1" - checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c - languageName: node - linkType: hard - "es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" @@ -7840,15 +7536,6 @@ __metadata: languageName: node linkType: hard -"es-object-atoms@npm:^1.1.1": - version: 1.1.2 - resolution: "es-object-atoms@npm:1.1.2" - dependencies: - es-errors: "npm:^1.3.0" - checksum: 10c0/1772861f094f739d6f41b579cfb9a18579daffeb434552a370a5fbef50a32d22227e27b63fdbb757b7ddd429d1b42fe52ccae7966d9302a2ec221b6f1b41bbc4 - languageName: node - linkType: hard - "es-set-tostringtag@npm:^2.0.3": version: 2.0.3 resolution: "es-set-tostringtag@npm:2.0.3" @@ -7860,18 +7547,6 @@ __metadata: languageName: node linkType: hard -"es-set-tostringtag@npm:^2.1.0": - version: 2.1.0 - resolution: "es-set-tostringtag@npm:2.1.0" - dependencies: - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af - languageName: node - linkType: hard - "es-shim-unscopables@npm:^1.0.0, es-shim-unscopables@npm:^1.0.2": version: 1.0.2 resolution: "es-shim-unscopables@npm:1.0.2" @@ -8018,7 +7693,7 @@ __metadata: languageName: node linkType: hard -"escodegen@npm:^2.0.0, escodegen@npm:^2.1.0": +"escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" dependencies: @@ -8539,13 +8214,6 @@ __metadata: languageName: node linkType: hard -"fastest-levenshtein@npm:^1.0.12": - version: 1.0.16 - resolution: "fastest-levenshtein@npm:1.0.16" - checksum: 10c0/7e3d8ae812a7f4fdf8cad18e9cde436a39addf266a5986f653ea0d81e0de0900f50c0f27c6d5aff3f686bcb48acbd45be115ae2216f36a6a13a7dbbf5cad878b - languageName: node - linkType: hard - "fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" @@ -8705,15 +8373,6 @@ __metadata: languageName: node linkType: hard -"flat@npm:^5.0.2": - version: 5.0.2 - resolution: "flat@npm:5.0.2" - bin: - flat: cli.js - checksum: 10c0/f178b13482f0cd80c7fede05f4d10585b1f2fdebf26e12edc138e32d3150c6ea6482b7f12813a1091143bad52bb6d3596bca51a162257a21163c0ff438baa5fe - languageName: node - linkType: hard - "flatted@npm:^3.2.9": version: 3.3.1 resolution: "flatted@npm:3.3.1" @@ -8770,19 +8429,6 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0": - version: 4.0.6 - resolution: "form-data@npm:4.0.6" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.4" - mime-types: "npm:^2.1.35" - checksum: 10c0/43947a77bf0ff45c6ceed789778982d47a3f3e720a74b71721174ebf3310a5f1a8be1d6b38a3ee3688e8a18a2c4273073ec0844cd37efda3eaf46d41c9c318ff - languageName: node - linkType: hard - "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -8935,13 +8581,6 @@ __metadata: languageName: node linkType: hard -"generator-function@npm:^2.0.0": - version: 2.0.1 - resolution: "generator-function@npm:2.0.1" - checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 - languageName: node - linkType: hard - "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -8969,27 +8608,6 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.6": - version: 1.3.1 - resolution: "get-intrinsic@npm:1.3.1" - dependencies: - async-function: "npm:^1.0.0" - async-generator-function: "npm:^1.0.0" - call-bind-apply-helpers: "npm:^1.0.2" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.1.1" - function-bind: "npm:^1.1.2" - generator-function: "npm:^2.0.0" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - math-intrinsics: "npm:^1.1.0" - checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d - languageName: node - linkType: hard - "get-nonce@npm:^1.0.0": version: 1.0.1 resolution: "get-nonce@npm:1.0.1" @@ -8997,16 +8615,6 @@ __metadata: languageName: node linkType: hard -"get-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "get-proto@npm:1.0.1" - dependencies: - dunder-proto: "npm:^1.0.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c - languageName: node - linkType: hard - "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -9199,13 +8807,6 @@ __metadata: languageName: node linkType: hard -"gopd@npm:^1.2.0": - version: 1.2.0 - resolution: "gopd@npm:1.2.0" - checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead - languageName: node - linkType: hard - "graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -9279,13 +8880,6 @@ __metadata: languageName: node linkType: hard -"has-symbols@npm:^1.1.0": - version: 1.1.0 - resolution: "has-symbols@npm:1.1.0" - checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e - languageName: node - linkType: hard - "has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": version: 1.0.2 resolution: "has-tostringtag@npm:1.0.2" @@ -9335,15 +8929,6 @@ __metadata: languageName: node linkType: hard -"hasown@npm:^2.0.4": - version: 2.0.4 - resolution: "hasown@npm:2.0.4" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506 - languageName: node - linkType: hard - "hast-util-from-parse5@npm:^8.0.0": version: 8.0.2 resolution: "hast-util-from-parse5@npm:8.0.2" @@ -9497,15 +9082,6 @@ __metadata: languageName: node linkType: hard -"html-encoding-sniffer@npm:^3.0.0": - version: 3.0.0 - resolution: "html-encoding-sniffer@npm:3.0.0" - dependencies: - whatwg-encoding: "npm:^2.0.0" - checksum: 10c0/b17b3b0fb5d061d8eb15121c3b0b536376c3e295ecaf09ba48dd69c6b6c957839db124fe1e2b3f11329753a4ee01aa7dedf63b7677999e86da17fbbdd82c5386 - languageName: node - linkType: hard - "html-entities@npm:^2.1.0": version: 2.5.2 resolution: "html-entities@npm:2.5.2" @@ -9604,17 +9180,6 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" - dependencies: - "@tootallnate/once": "npm:2" - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/32a05e413430b2c1e542e5c74b38a9f14865301dd69dff2e53ddb684989440e3d2ce0c4b64d25eb63cf6283e6265ff979a61cf93e3ca3d23047ddfdc8df34a32 - languageName: node - linkType: hard - "http-proxy-agent@npm:^7.0.0": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" @@ -9632,16 +9197,6 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.1": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1 - languageName: node - linkType: hard - "https-proxy-agent@npm:^7.0.1": version: 7.0.5 resolution: "https-proxy-agent@npm:7.0.5" @@ -9684,7 +9239,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2": +"iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -9744,18 +9299,6 @@ __metadata: languageName: node linkType: hard -"import-local@npm:^3.0.2": - version: 3.1.0 - resolution: "import-local@npm:3.1.0" - dependencies: - pkg-dir: "npm:^4.2.0" - resolve-cwd: "npm:^3.0.0" - bin: - import-local-fixture: fixtures/cli.js - checksum: 10c0/c67ecea72f775fe8684ca3d057e54bdb2ae28c14bf261d2607c269c18ea0da7b730924c06262eca9aed4b8ab31e31d65bc60b50e7296c85908a56e2f7d41ecd2 - languageName: node - linkType: hard - "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -9805,13 +9348,6 @@ __metadata: languageName: node linkType: hard -"interpret@npm:^3.1.1": - version: 3.1.1 - resolution: "interpret@npm:3.1.1" - checksum: 10c0/6f3c4d0aa6ec1b43a8862375588a249e3c917739895cbe67fe12f0a76260ea632af51e8e2431b50fbcd0145356dc28ca147be08dbe6a523739fd55c0f91dc2a5 - languageName: node - linkType: hard - "ip-address@npm:^9.0.5": version: 9.0.5 resolution: "ip-address@npm:9.0.5" @@ -10097,13 +9633,6 @@ __metadata: languageName: node linkType: hard -"is-potential-custom-element-name@npm:^1.0.1": - version: 1.0.1 - resolution: "is-potential-custom-element-name@npm:1.0.1" - checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 - languageName: node - linkType: hard - "is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" @@ -10367,45 +9896,6 @@ __metadata: languageName: node linkType: hard -"jsdom@npm:^20": - version: 20.0.3 - resolution: "jsdom@npm:20.0.3" - dependencies: - abab: "npm:^2.0.6" - acorn: "npm:^8.8.1" - acorn-globals: "npm:^7.0.0" - cssom: "npm:^0.5.0" - cssstyle: "npm:^2.3.0" - data-urls: "npm:^3.0.2" - decimal.js: "npm:^10.4.2" - domexception: "npm:^4.0.0" - escodegen: "npm:^2.0.0" - form-data: "npm:^4.0.0" - html-encoding-sniffer: "npm:^3.0.0" - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.1" - is-potential-custom-element-name: "npm:^1.0.1" - nwsapi: "npm:^2.2.2" - parse5: "npm:^7.1.1" - saxes: "npm:^6.0.0" - symbol-tree: "npm:^3.2.4" - tough-cookie: "npm:^4.1.2" - w3c-xmlserializer: "npm:^4.0.0" - webidl-conversions: "npm:^7.0.0" - whatwg-encoding: "npm:^2.0.0" - whatwg-mimetype: "npm:^3.0.0" - whatwg-url: "npm:^11.0.0" - ws: "npm:^8.11.0" - xml-name-validator: "npm:^4.0.0" - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - checksum: 10c0/b109073bb826a966db7828f46cb1d7371abecd30f182b143c52be5fe1ed84513bbbe995eb3d157241681fcd18331381e61e3dc004d4949f3a63bca02f6214902 - languageName: node - linkType: hard - "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -11064,13 +10554,6 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.1.0": - version: 1.1.0 - resolution: "math-intrinsics@npm:1.1.0" - checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f - languageName: node - linkType: hard - "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5" @@ -11239,16 +10722,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-newline-to-break@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-newline-to-break@npm:2.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-find-and-replace: "npm:^3.0.0" - checksum: 10c0/756a5660b0a821e0d6d6a0b2d9b13ac32e41cc028c485a91bccf6300977e2557236c6cc93dbd55c68b785f1ed6eae69209a4ffe182533cd1cdfda369021bebd2 - languageName: node - linkType: hard - "mdast-util-phrasing@npm:^4.0.0": version: 4.1.0 resolution: "mdast-util-phrasing@npm:4.1.0" @@ -11720,7 +11193,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -12189,13 +11662,6 @@ __metadata: languageName: node linkType: hard -"nwsapi@npm:^2.2.2": - version: 2.2.24 - resolution: "nwsapi@npm:2.2.24" - checksum: 10c0/9bc04ee9c7698f1b5506778d36f7382962f71667205d441d6a50f6180ee92328e770b76be78b907817ee103241b29984d3a17ae387e4723aebe0aeaed7a7c3a1 - languageName: node - linkType: hard - "nypm@npm:^0.3.8": version: 0.3.9 resolution: "nypm@npm:0.3.9" @@ -12557,15 +12023,6 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^7.1.1": - version: 7.3.0 - resolution: "parse5@npm:7.3.0" - dependencies: - entities: "npm:^6.0.0" - checksum: 10c0/7fd2e4e247e85241d6f2a464d0085eed599a26d7b0a5233790c49f53473232eb85350e8133344d9b3fd58b89339e7ad7270fe1f89d28abe50674ec97b87f80b5 - languageName: node - linkType: hard - "parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -12755,7 +12212,7 @@ __metadata: languageName: node linkType: hard -"pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": +"pkg-dir@npm:^4.1.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" dependencies: @@ -13075,15 +12532,6 @@ __metadata: languageName: node linkType: hard -"psl@npm:^1.1.33": - version: 1.15.0 - resolution: "psl@npm:1.15.0" - dependencies: - punycode: "npm:^2.3.1" - checksum: 10c0/d8d45a99e4ca62ca12ac3c373e63d80d2368d38892daa40cfddaa1eb908be98cd549ac059783ef3a56cfd96d57ae8e2fd9ae53d1378d90d42bc661ff924e102a - languageName: node - linkType: hard - "public-encrypt@npm:^4.0.0": version: 4.0.3 resolution: "public-encrypt@npm:4.0.3" @@ -13106,7 +12554,6 @@ __metadata: "@playwright/test": "npm:^1.52.0" "@radix-ui/react-dialog": "npm:^1.1.14" "@radix-ui/react-select": "npm:^2.2.5" - "@radix-ui/react-slot": "npm:^1.2.3" "@radix-ui/react-toast": "npm:^1.2.1" "@storybook/addon-essentials": "npm:^8.2.2" "@storybook/addon-interactions": "npm:^8.2.2" @@ -13120,17 +12567,12 @@ __metadata: "@storybook/react": "npm:^8.2.2" "@storybook/test": "npm:^8.2.2" "@tailwindcss/postcss": "npm:^4.1.18" - "@tanstack/react-query": "npm:^5.72.0" "@testing-library/dom": "npm:^10.4.1" - "@testing-library/jest-dom": "npm:^6.9.1" "@testing-library/react": "npm:^16.3.2" - "@types/jsdom": "npm:^20" "@types/node": "npm:^20" "@types/react": "npm:^18" "@types/react-dom": "npm:^18" - "@types/sharp": "npm:^0.32.0" "@types/uuid": "npm:^10" - "@vitejs/plugin-react": "npm:^6.0.3" class-variance-authority: "npm:^0.7.1" clsx: "npm:^2.1.1" embla-carousel-autoplay: "npm:^8.1.7" @@ -13143,7 +12585,6 @@ __metadata: happy-dom: "npm:^20.10.6" husky: "npm:^9.1.7" img-toolkit: "npm:^1.0.2" - jsdom: "npm:^20" lucide-react: "npm:^0.525.0" next: "npm:14.2.5" next-themes: "npm:^0.3.0" @@ -13154,7 +12595,6 @@ __metadata: react-markdown: "npm:^9.0.1" react-player: "npm:^2.16.0" rehype-raw: "npm:^7.0.0" - remark-breaks: "npm:^4.0.0" remark-gfm: "npm:^4.0.0" sharp: "npm:^0.34.5" storybook: "npm:^8.2.2" @@ -13166,7 +12606,6 @@ __metadata: vite: "npm:^8.1.4" vitest: "npm:^4.1.10" webpack: "npm:^5.93.0" - webpack-cli: "npm:^5.1.4" zustand: "npm:^4.5.4" languageName: unknown linkType: soft @@ -13178,7 +12617,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1": +"punycode@npm:^2.1.0, punycode@npm:^2.1.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 @@ -13217,13 +12656,6 @@ __metadata: languageName: node linkType: hard -"querystringify@npm:^2.1.1": - version: 2.2.0 - resolution: "querystringify@npm:2.2.0" - checksum: 10c0/3258bc3dbdf322ff2663619afe5947c7926a6ef5fb78ad7d384602974c467fadfc8272af44f5eb8cddd0d011aae8fabf3a929a8eee4b86edcc0a21e6bd10f9aa - languageName: node - linkType: hard - "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -13560,15 +12992,6 @@ __metadata: languageName: node linkType: hard -"rechoir@npm:^0.8.0": - version: 0.8.0 - resolution: "rechoir@npm:0.8.0" - dependencies: - resolve: "npm:^1.20.0" - checksum: 10c0/1a30074124a22abbd5d44d802dac26407fa72a0a95f162aa5504ba8246bc5452f8b1a027b154d9bdbabcd8764920ff9333d934c46a8f17479c8912e92332f3ff - languageName: node - linkType: hard - "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -13715,17 +13138,6 @@ __metadata: languageName: node linkType: hard -"remark-breaks@npm:^4.0.0": - version: 4.0.0 - resolution: "remark-breaks@npm:4.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-newline-to-break: "npm:^2.0.0" - unified: "npm:^11.0.0" - checksum: 10c0/d7b319a7993b54c5d574e9255080c5de68cfa24f993873b0ee296af13f478521c41d4b7ae0fc14b4607ea70c8f6967e998ab7a467de13139141e66a1a34cb6be - languageName: node - linkType: hard - "remark-gfm@npm:^4.0.0": version: 4.0.0 resolution: "remark-gfm@npm:4.0.0" @@ -13803,22 +13215,6 @@ __metadata: languageName: node linkType: hard -"requires-port@npm:^1.0.0": - version: 1.0.0 - resolution: "requires-port@npm:1.0.0" - checksum: 10c0/b2bfdd09db16c082c4326e573a82c0771daaf7b53b9ce8ad60ea46aa6e30aaf475fe9b164800b89f93b748d2c234d8abff945d2551ba47bf5698e04cd7713267 - languageName: node - linkType: hard - -"resolve-cwd@npm:^3.0.0": - version: 3.0.0 - resolution: "resolve-cwd@npm:3.0.0" - dependencies: - resolve-from: "npm:^5.0.0" - checksum: 10c0/e608a3ebd15356264653c32d7ecbc8fd702f94c6703ea4ac2fb81d9c359180cba0ae2e6b71faa446631ed6145454d5a56b227efc33a2d40638ac13f8beb20ee4 - languageName: node - linkType: hard - "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -13826,13 +13222,6 @@ __metadata: languageName: node linkType: hard -"resolve-from@npm:^5.0.0": - version: 5.0.0 - resolution: "resolve-from@npm:5.0.0" - checksum: 10c0/b21cb7f1fb746de8107b9febab60095187781137fd803e6a59a76d421444b1531b641bba5857f5dc011974d8a5c635d61cec49e6bd3b7fc20e01f0fafc4efbf2 - languageName: node - linkType: hard - "resolve-pkg-maps@npm:^1.0.0": version: 1.0.0 resolution: "resolve-pkg-maps@npm:1.0.0" @@ -13853,7 +13242,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.14.2, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8": +"resolve@npm:^1.14.2, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -13879,7 +13268,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": +"resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -14155,15 +13544,6 @@ __metadata: languageName: node linkType: hard -"saxes@npm:^6.0.0": - version: 6.0.0 - resolution: "saxes@npm:6.0.0" - dependencies: - xmlchars: "npm:^2.2.0" - checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 - languageName: node - linkType: hard - "scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" @@ -14335,37 +13715,32 @@ __metadata: languageName: node linkType: hard -"sharp@npm:*, sharp@npm:^0.34.5": - version: 0.34.5 - resolution: "sharp@npm:0.34.5" +"sharp@npm:^0.33.3": + version: 0.33.4 + resolution: "sharp@npm:0.33.4" dependencies: - "@img/colour": "npm:^1.0.0" - "@img/sharp-darwin-arm64": "npm:0.34.5" - "@img/sharp-darwin-x64": "npm:0.34.5" - "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" - "@img/sharp-libvips-darwin-x64": "npm:1.2.4" - "@img/sharp-libvips-linux-arm": "npm:1.2.4" - "@img/sharp-libvips-linux-arm64": "npm:1.2.4" - "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" - "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" - "@img/sharp-libvips-linux-s390x": "npm:1.2.4" - "@img/sharp-libvips-linux-x64": "npm:1.2.4" - "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" - "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" - "@img/sharp-linux-arm": "npm:0.34.5" - "@img/sharp-linux-arm64": "npm:0.34.5" - "@img/sharp-linux-ppc64": "npm:0.34.5" - "@img/sharp-linux-riscv64": "npm:0.34.5" - "@img/sharp-linux-s390x": "npm:0.34.5" - "@img/sharp-linux-x64": "npm:0.34.5" - "@img/sharp-linuxmusl-arm64": "npm:0.34.5" - "@img/sharp-linuxmusl-x64": "npm:0.34.5" - "@img/sharp-wasm32": "npm:0.34.5" - "@img/sharp-win32-arm64": "npm:0.34.5" - "@img/sharp-win32-ia32": "npm:0.34.5" - "@img/sharp-win32-x64": "npm:0.34.5" - detect-libc: "npm:^2.1.2" - semver: "npm:^7.7.3" + "@img/sharp-darwin-arm64": "npm:0.33.4" + "@img/sharp-darwin-x64": "npm:0.33.4" + "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" + "@img/sharp-libvips-darwin-x64": "npm:1.0.2" + "@img/sharp-libvips-linux-arm": "npm:1.0.2" + "@img/sharp-libvips-linux-arm64": "npm:1.0.2" + "@img/sharp-libvips-linux-s390x": "npm:1.0.2" + "@img/sharp-libvips-linux-x64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" + "@img/sharp-linux-arm": "npm:0.33.4" + "@img/sharp-linux-arm64": "npm:0.33.4" + "@img/sharp-linux-s390x": "npm:0.33.4" + "@img/sharp-linux-x64": "npm:0.33.4" + "@img/sharp-linuxmusl-arm64": "npm:0.33.4" + "@img/sharp-linuxmusl-x64": "npm:0.33.4" + "@img/sharp-wasm32": "npm:0.33.4" + "@img/sharp-win32-ia32": "npm:0.33.4" + "@img/sharp-win32-x64": "npm:0.33.4" + color: "npm:^4.2.3" + detect-libc: "npm:^2.0.3" + semver: "npm:^7.6.0" dependenciesMeta: "@img/sharp-darwin-arm64": optional: true @@ -14379,10 +13754,6 @@ __metadata: optional: true "@img/sharp-libvips-linux-arm64": optional: true - "@img/sharp-libvips-linux-ppc64": - optional: true - "@img/sharp-libvips-linux-riscv64": - optional: true "@img/sharp-libvips-linux-s390x": optional: true "@img/sharp-libvips-linux-x64": @@ -14395,10 +13766,6 @@ __metadata: optional: true "@img/sharp-linux-arm64": optional: true - "@img/sharp-linux-ppc64": - optional: true - "@img/sharp-linux-riscv64": - optional: true "@img/sharp-linux-s390x": optional: true "@img/sharp-linux-x64": @@ -14409,42 +13776,45 @@ __metadata: optional: true "@img/sharp-wasm32": optional: true - "@img/sharp-win32-arm64": - optional: true "@img/sharp-win32-ia32": optional: true "@img/sharp-win32-x64": optional: true - checksum: 10c0/fd79e29df0597a7d5704b8461c51f944ead91a5243691697be6e8243b966402beda53ddc6f0a53b96ea3cb8221f0b244aa588114d3ebf8734fb4aefd41ab802f + checksum: 10c0/428c5c6a84ff8968effe50c2de931002f5f30b9f263e1c026d0384e581673c13088a49322f7748114d3d9be4ae9476a74bf003a3af34743e97ef2f880d1cfe45 languageName: node linkType: hard -"sharp@npm:^0.33.3": - version: 0.33.4 - resolution: "sharp@npm:0.33.4" +"sharp@npm:^0.34.5": + version: 0.34.5 + resolution: "sharp@npm:0.34.5" dependencies: - "@img/sharp-darwin-arm64": "npm:0.33.4" - "@img/sharp-darwin-x64": "npm:0.33.4" - "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" - "@img/sharp-libvips-darwin-x64": "npm:1.0.2" - "@img/sharp-libvips-linux-arm": "npm:1.0.2" - "@img/sharp-libvips-linux-arm64": "npm:1.0.2" - "@img/sharp-libvips-linux-s390x": "npm:1.0.2" - "@img/sharp-libvips-linux-x64": "npm:1.0.2" - "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" - "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" - "@img/sharp-linux-arm": "npm:0.33.4" - "@img/sharp-linux-arm64": "npm:0.33.4" - "@img/sharp-linux-s390x": "npm:0.33.4" - "@img/sharp-linux-x64": "npm:0.33.4" - "@img/sharp-linuxmusl-arm64": "npm:0.33.4" - "@img/sharp-linuxmusl-x64": "npm:0.33.4" - "@img/sharp-wasm32": "npm:0.33.4" - "@img/sharp-win32-ia32": "npm:0.33.4" - "@img/sharp-win32-x64": "npm:0.33.4" - color: "npm:^4.2.3" - detect-libc: "npm:^2.0.3" - semver: "npm:^7.6.0" + "@img/colour": "npm:^1.0.0" + "@img/sharp-darwin-arm64": "npm:0.34.5" + "@img/sharp-darwin-x64": "npm:0.34.5" + "@img/sharp-libvips-darwin-arm64": "npm:1.2.4" + "@img/sharp-libvips-darwin-x64": "npm:1.2.4" + "@img/sharp-libvips-linux-arm": "npm:1.2.4" + "@img/sharp-libvips-linux-arm64": "npm:1.2.4" + "@img/sharp-libvips-linux-ppc64": "npm:1.2.4" + "@img/sharp-libvips-linux-riscv64": "npm:1.2.4" + "@img/sharp-libvips-linux-s390x": "npm:1.2.4" + "@img/sharp-libvips-linux-x64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.4" + "@img/sharp-linux-arm": "npm:0.34.5" + "@img/sharp-linux-arm64": "npm:0.34.5" + "@img/sharp-linux-ppc64": "npm:0.34.5" + "@img/sharp-linux-riscv64": "npm:0.34.5" + "@img/sharp-linux-s390x": "npm:0.34.5" + "@img/sharp-linux-x64": "npm:0.34.5" + "@img/sharp-linuxmusl-arm64": "npm:0.34.5" + "@img/sharp-linuxmusl-x64": "npm:0.34.5" + "@img/sharp-wasm32": "npm:0.34.5" + "@img/sharp-win32-arm64": "npm:0.34.5" + "@img/sharp-win32-ia32": "npm:0.34.5" + "@img/sharp-win32-x64": "npm:0.34.5" + detect-libc: "npm:^2.1.2" + semver: "npm:^7.7.3" dependenciesMeta: "@img/sharp-darwin-arm64": optional: true @@ -14458,6 +13828,10 @@ __metadata: optional: true "@img/sharp-libvips-linux-arm64": optional: true + "@img/sharp-libvips-linux-ppc64": + optional: true + "@img/sharp-libvips-linux-riscv64": + optional: true "@img/sharp-libvips-linux-s390x": optional: true "@img/sharp-libvips-linux-x64": @@ -14470,6 +13844,10 @@ __metadata: optional: true "@img/sharp-linux-arm64": optional: true + "@img/sharp-linux-ppc64": + optional: true + "@img/sharp-linux-riscv64": + optional: true "@img/sharp-linux-s390x": optional: true "@img/sharp-linux-x64": @@ -14480,11 +13858,13 @@ __metadata: optional: true "@img/sharp-wasm32": optional: true + "@img/sharp-win32-arm64": + optional: true "@img/sharp-win32-ia32": optional: true "@img/sharp-win32-x64": optional: true - checksum: 10c0/428c5c6a84ff8968effe50c2de931002f5f30b9f263e1c026d0384e581673c13088a49322f7748114d3d9be4ae9476a74bf003a3af34743e97ef2f880d1cfe45 + checksum: 10c0/fd79e29df0597a7d5704b8461c51f944ead91a5243691697be6e8243b966402beda53ddc6f0a53b96ea3cb8221f0b244aa588114d3ebf8734fb4aefd41ab802f languageName: node linkType: hard @@ -15018,13 +14398,6 @@ __metadata: languageName: node linkType: hard -"symbol-tree@npm:^3.2.4": - version: 3.2.4 - resolution: "symbol-tree@npm:3.2.4" - checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 - languageName: node - linkType: hard - "tailwind-merge@npm:^3.3.1": version: 3.3.1 resolution: "tailwind-merge@npm:3.3.1" @@ -15233,27 +14606,6 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^4.1.2": - version: 4.1.4 - resolution: "tough-cookie@npm:4.1.4" - dependencies: - psl: "npm:^1.1.33" - punycode: "npm:^2.1.1" - universalify: "npm:^0.2.0" - url-parse: "npm:^1.5.3" - checksum: 10c0/aca7ff96054f367d53d1e813e62ceb7dd2eda25d7752058a74d64b7266fd07be75908f3753a32ccf866a2f997604b414cfb1916d6e7f69bc64d9d9939b0d6c45 - languageName: node - linkType: hard - -"tr46@npm:^3.0.0": - version: 3.0.0 - resolution: "tr46@npm:3.0.0" - dependencies: - punycode: "npm:^2.1.1" - checksum: 10c0/cdc47cad3a9d0b6cb293e39ccb1066695ae6fdd39b9e4f351b010835a1f8b4f3a6dc3a55e896b421371187f22b48d7dac1b693de4f6551bdef7b6ab6735dfe3b - languageName: node - linkType: hard - "trim-lines@npm:^3.0.0": version: 3.0.1 resolution: "trim-lines@npm:3.0.1" @@ -15654,13 +15006,6 @@ __metadata: languageName: node linkType: hard -"universalify@npm:^0.2.0": - version: 0.2.0 - resolution: "universalify@npm:0.2.0" - checksum: 10c0/cedbe4d4ca3967edf24c0800cfc161c5a15e240dac28e3ce575c689abc11f2c81ccc6532c8752af3b40f9120fb5e454abecd359e164f4f6aa44c29cd37e194fe - languageName: node - linkType: hard - "universalify@npm:^2.0.0": version: 2.0.1 resolution: "universalify@npm:2.0.1" @@ -15710,16 +15055,6 @@ __metadata: languageName: node linkType: hard -"url-parse@npm:^1.5.3": - version: 1.5.10 - resolution: "url-parse@npm:1.5.10" - dependencies: - querystringify: "npm:^2.1.1" - requires-port: "npm:^1.0.0" - checksum: 10c0/bd5aa9389f896974beb851c112f63b466505a04b4807cea2e5a3b7092f6fbb75316f0491ea84e44f66fed55f1b440df5195d7e3a8203f64fcefa19d182f5be87 - languageName: node - linkType: hard - "url@npm:^0.11.0": version: 0.11.3 resolution: "url@npm:0.11.3" @@ -16048,15 +15383,6 @@ __metadata: languageName: node linkType: hard -"w3c-xmlserializer@npm:^4.0.0": - version: 4.0.0 - resolution: "w3c-xmlserializer@npm:4.0.0" - dependencies: - xml-name-validator: "npm:^4.0.0" - checksum: 10c0/02cc66d6efc590bd630086cd88252444120f5feec5c4043932b0d0f74f8b060512f79dc77eb093a7ad04b4f02f39da79ce4af47ceb600f2bf9eacdc83204b1a8 - languageName: node - linkType: hard - "walk-up-path@npm:^3.0.1": version: 3.0.1 resolution: "walk-up-path@npm:3.0.1" @@ -16090,45 +15416,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^7.0.0": - version: 7.0.0 - resolution: "webidl-conversions@npm:7.0.0" - checksum: 10c0/228d8cb6d270c23b0720cb2d95c579202db3aaf8f633b4e9dd94ec2000a04e7e6e43b76a94509cdb30479bd00ae253ab2371a2da9f81446cc313f89a4213a2c4 - languageName: node - linkType: hard - -"webpack-cli@npm:^5.1.4": - version: 5.1.4 - resolution: "webpack-cli@npm:5.1.4" - dependencies: - "@discoveryjs/json-ext": "npm:^0.5.0" - "@webpack-cli/configtest": "npm:^2.1.1" - "@webpack-cli/info": "npm:^2.0.2" - "@webpack-cli/serve": "npm:^2.0.5" - colorette: "npm:^2.0.14" - commander: "npm:^10.0.1" - cross-spawn: "npm:^7.0.3" - envinfo: "npm:^7.7.3" - fastest-levenshtein: "npm:^1.0.12" - import-local: "npm:^3.0.2" - interpret: "npm:^3.1.1" - rechoir: "npm:^0.8.0" - webpack-merge: "npm:^5.7.3" - peerDependencies: - webpack: 5.x.x - peerDependenciesMeta: - "@webpack-cli/generators": - optional: true - webpack-bundle-analyzer: - optional: true - webpack-dev-server: - optional: true - bin: - webpack-cli: bin/cli.js - checksum: 10c0/4266909ae5e2e662c8790ac286e965b2c7fd5a4a2f07f48e28576234c9a5f631847ccddc18e1b3281c7b4be04a7ff4717d2636033a322dde13ac995fd0d9de10 - languageName: node - linkType: hard - "webpack-dev-middleware@npm:^6.1.2": version: 6.1.3 resolution: "webpack-dev-middleware@npm:6.1.3" @@ -16158,17 +15445,6 @@ __metadata: languageName: node linkType: hard -"webpack-merge@npm:^5.7.3": - version: 5.10.0 - resolution: "webpack-merge@npm:5.10.0" - dependencies: - clone-deep: "npm:^4.0.1" - flat: "npm:^5.0.2" - wildcard: "npm:^2.0.0" - checksum: 10c0/b607c84cabaf74689f965420051a55a08722d897bdd6c29cb0b2263b451c090f962d41ecf8c9bf56b0ab3de56e65476ace0a8ecda4f4a4663684243d90e0512b - languageName: node - linkType: hard - "webpack-sources@npm:^3.2.3": version: 3.2.3 resolution: "webpack-sources@npm:3.2.3" @@ -16220,15 +15496,6 @@ __metadata: languageName: node linkType: hard -"whatwg-encoding@npm:^2.0.0": - version: 2.0.0 - resolution: "whatwg-encoding@npm:2.0.0" - dependencies: - iconv-lite: "npm:0.6.3" - checksum: 10c0/91b90a49f312dc751496fd23a7e68981e62f33afe938b97281ad766235c4872fc4e66319f925c5e9001502b3040dd25a33b02a9c693b73a4cbbfdc4ad10c3e3e - languageName: node - linkType: hard - "whatwg-mimetype@npm:^3.0.0": version: 3.0.0 resolution: "whatwg-mimetype@npm:3.0.0" @@ -16236,16 +15503,6 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^11.0.0": - version: 11.0.0 - resolution: "whatwg-url@npm:11.0.0" - dependencies: - tr46: "npm:^3.0.0" - webidl-conversions: "npm:^7.0.0" - checksum: 10c0/f7ec264976d7c725e0696fcaf9ebe056e14422eacbf92fdbb4462034609cba7d0c85ffa1aab05e9309d42969bcf04632ba5ed3f3882c516d7b093053315bf4c1 - languageName: node - linkType: hard - "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2" @@ -16338,13 +15595,6 @@ __metadata: languageName: node linkType: hard -"wildcard@npm:^2.0.0": - version: 2.0.1 - resolution: "wildcard@npm:2.0.1" - checksum: 10c0/08f70cd97dd9a20aea280847a1fe8148e17cae7d231640e41eb26d2388697cbe65b67fd9e68715251c39b080c5ae4f76d71a9a69fa101d897273efdfb1b58bf7 - languageName: node - linkType: hard - "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -16392,9 +15642,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.11.0, ws@npm:^8.21.0": - version: 8.21.0 - resolution: "ws@npm:8.21.0" +"ws@npm:^8.2.3": + version: 8.18.0 + resolution: "ws@npm:8.18.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -16403,13 +15653,13 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 + checksum: 10c0/25eb33aff17edcb90721ed6b0eb250976328533ad3cd1a28a274bd263682e7296a6591ff1436d6cbc50fa67463158b062f9d1122013b361cec99a05f84680e06 languageName: node linkType: hard -"ws@npm:^8.2.3": - version: 8.18.0 - resolution: "ws@npm:8.18.0" +"ws@npm:^8.21.0": + version: 8.21.0 + resolution: "ws@npm:8.21.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -16418,21 +15668,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10c0/25eb33aff17edcb90721ed6b0eb250976328533ad3cd1a28a274bd263682e7296a6591ff1436d6cbc50fa67463158b062f9d1122013b361cec99a05f84680e06 - languageName: node - linkType: hard - -"xml-name-validator@npm:^4.0.0": - version: 4.0.0 - resolution: "xml-name-validator@npm:4.0.0" - checksum: 10c0/c1bfa219d64e56fee265b2bd31b2fcecefc063ee802da1e73bad1f21d7afd89b943c9e2c97af2942f60b1ad46f915a4c81e00039c7d398b53cf410e29d3c30bd - languageName: node - linkType: hard - -"xmlchars@npm:^2.2.0": - version: 2.2.0 - resolution: "xmlchars@npm:2.2.0" - checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 languageName: node linkType: hard From 558c37eec3ec7ee03402c99ef0365b27ebe051b8 Mon Sep 17 00:00:00 2001 From: Yonghun Yi Date: Tue, 8 Sep 2026 10:29:05 +0900 Subject: [PATCH 05/14] =?UTF-8?q?docs:=20=EC=BD=94=EB=93=9C=EB=B2=A0?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EA=B0=90=EC=82=AC=20=EB=A6=AC=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=EC=99=80=20=EC=9A=B0=EC=84=A0=EC=88=9C=EC=9C=84?= =?UTF-8?q?=EB=B3=84=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20=EC=8A=A4?= =?UTF-8?q?=ED=8E=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUDIT-2026-09-08.md | 320 ++++++++++++++++++++++++++++++++++++ REFACTOR-SPEC-2026-09-08.md | 249 ++++++++++++++++++++++++++++ 2 files changed, 569 insertions(+) create mode 100644 AUDIT-2026-09-08.md create mode 100644 REFACTOR-SPEC-2026-09-08.md diff --git a/AUDIT-2026-09-08.md b/AUDIT-2026-09-08.md new file mode 100644 index 0000000..d4b4cf0 --- /dev/null +++ b/AUDIT-2026-09-08.md @@ -0,0 +1,320 @@ +# k-pullup 코드베이스 감사 리포트 (2026-09-08) + +- 대상: `fix/signin-redirect` 브랜치 HEAD `0ede7ff`, 347개 TS/TSX 파일 (~30k LOC) +- 방식: 4개 영역(app / components / hooks·lib·store / lib-api·설정·테스트) 병렬 감사 후, 상위 발견은 소스에서 직접 재검증 +- 베이스라인: `yarn typecheck` ✓ · `yarn lint` ✓ · `yarn test` 106/106 ✓ · `yarn build` ✓ + +> **한 줄 요약** — 컴파일·린트·테스트는 전부 통과하지만, `fetchData`가 throw 방식으로 바뀐 뒤(7/9) 호출부가 한 곳도 따라가지 않아 **에러 경로 UX가 거의 전부 죽어 있음.** 그 외 지도/GPS 라이프사이클 누수 몇 건과 개별 로직 버그, 그리고 사이드 이펙트 없이 정리 가능한 데드 코드가 상당량 있음. + +--- + +## 목차 + +1. [결정 필요: fetchData 에러 처리 전략 (A vs B)](#1-결정-필요-fetchdata-에러-처리-전략-a-vs-b) +2. [버그 — 현재 잘못 동작 (수정 시 동작 변경이 정상)](#2-버그--현재-잘못-동작) + - 2.1 fetchData 호출부 (systemic) + - 2.2 지도 / GPS 라이프사이클 + - 2.3 개별 로직 버그 + - 2.4 설정 / 툴링 +3. [사이드 이펙트 없는 개선 (동작 변화 0)](#3-사이드-이펙트-없는-개선) +4. [권장 실행 순서](#4-권장-실행-순서) +5. [부록 A — API 함수별 dead 분기 / 호출부 try-catch 현황](#부록-a--api-함수별-현황) +6. [부록 B — 검증했으나 문제 없음](#부록-b--검증했으나-문제-없음) + +--- + +## 1. 결정 필요: fetchData 에러 처리 전략 (A vs B) + +### 배경 + +- commit `aa0e071` (2026-07-09, Kiro 스펙 `.kiro/specs/fetchdata-error-handling`)에서 `lib/fetchData.ts`를 **모든 non-2xx에서 `FetchError`를 throw** 하도록 재작성. +- 해당 스펙 `tasks.md`에는 **호출부 마이그레이션 태스크가 없음.** 커밋도 `fetchData.ts`, `useMapStore.ts`, `useMarkerControl.tsx`, `next.config.mjs`, `kakao-map.types.ts` 5개만 수정. +- 결과(grep 검증): + +| 항목 | 수 | +|---|---| +| 호출부의 dead `if (!response.ok)` / `response.status ===` 체크 | **47곳 / 22파일** | +| API 함수 내부 dead `!response.ok` 분기 | 8파일 | +| 서버 페이지의 dead `.error === "..."` 체크 | 9곳 | +| `FetchError`를 실제로 처리하는 소비자 | **3곳** (`user-provider.tsx`, `signin-form.tsx`, `app/moments/page.tsx`) | + +### 옵션 + +| | A. fetchData 되돌리기 | B. throw 유지, 호출부 마이그레이션 (추천) | +|---|---|---| +| 내용 | HTTP 에러 시 `Response` 반환, 네트워크 에러만 throw | 각 호출부에 `catch (e) { if (e instanceof FetchError) … } finally { setLoading(false) }` | +| 변경 범위 | `fetchData.ts` 1파일 **+ `user-provider.tsx`** (401 throw에 의존, 프로퍼티 테스트 5개) + fetchData 테스트 11파일 재작성 | ~35 호출부 + 서버 페이지 공통 헬퍼 1개 | +| 장점 | 47개 dead 분기가 즉시 부활 | 스펙 의도와 일치. 이미 3곳이 이 패턴. `alert()` 제거 등 SSR 안전성 유지 | +| 단점 | 스펙/테스트/최근 로그인 수정 커밋과 충돌 | 작업량 큼 | + +> **결정 전 주의:** 2.1절과 3절의 "dead `!response.ok` 분기 삭제"는 **B로 확정된 뒤에만** 진행. A를 택하면 그 분기들이 다시 살아남. + +--- + +## 2. 버그 — 현재 잘못 동작 + +### 2.1 fetchData 호출부 (systemic) + +증상 패턴 3종: +- **(가) 상태별 메시지 미표시** — `!response.ok` 분기가 dead → 409/401/403/400 안내 문구가 절대 안 뜸 +- **(나) 로딩 stuck** — try/catch 없이 `setLoading(true)` 후 throw → 스피너 무한, unhandled rejection +- **(다) 서버 페이지 에러 페이지 전환** — `.error` 체크 dead → `AuthError`/`NotFound` 대신 `app/error.tsx` + +#### 서버 컴포넌트 (다) + +- [ ] `app/pullup/[id]/page.tsx:74`, `app/pullup/[id]/report/page.tsx:21` — 404 시 `marker.error === "Marker not found"` 도달 불가 → `NotFoud` 대신 에러 페이지. **공개 라우트, 실트래픽 영향.** `generateMetadata`에서도 throw. +- [ ] `app/mypage/page.tsx:39`, `app/mypage/user/page.tsx:22`, `app/admin/page.tsx:13`, `app/mypage/report-admin/page.tsx:21` — `user.error` 체크 dead. 만료/무효 쿠키(미들웨어는 쿠키 *존재*만 검사) → 로그인 안내 대신 에러 페이지. +- [ ] `app/mypage/bookmark/page.tsx:24`, `app/mypage/report/page.tsx:21`, `app/mypage/locate/page.tsx:27`, `app/mypage/myreport/page.tsx:21,34` — `.error === "No authorization token provided"` dead. 같은 증상. +- [ ] `app/(home)/page.tsx:19` — `Promise.all([newPictures(), getAllMoment()])` catch 없음 → 상위 API 하나만 실패해도 홈 전체 error.tsx. `isNewPicturesError` 가드 dead. +- [ ] `app/social/page.tsx:21-22` — 동일. 빌드 로그에서 실제 재현됨 (`/markers/ranking` 실패 → 페이지 throw). 같은 패턴인 `app/moments/page.tsx`는 try/catch 있음 → 불일치. +- [ ] `app/mypage/locate/page.tsx:39` — `!markers` dead, `markers.markers.length`는 non-401 에러 shape에서 throw. + +권장 수정(B 기준): 서버 페이지용 헬퍼 하나 — `try { data = await api(cookie) } catch (e) { if (e instanceof FetchError && e.status === 401) return ; if (e.status === 404) return ; throw e }`. + +#### 클라이언트 — 로딩 stuck (나) + 메시지 dead (가) + +| 파일:라인 | 죽은 UX | stuck 상태 | +|---|---|---| +| `app/register/register-client.tsx:148-164,192` | `registerError[409/401/403/400]` 전부 | `uploadStatus` 영원히 pending, `submitRequestedRef` true 고정 → 재시도 불가 | +| `app/signup/signup-client.tsx:70` | 가입 실패 상태 | `signupStatus` pending 고정 | +| `components/pages/signup/verify-email.tsx:65-82,101-119` | 409 "이미 가입", 400 "유효하지 않은 코드" | `emailLoading`, `codeLoading` | +| `components/pages/pullup/comments.tsx:124-150` | 비속어 / 3개 제한 토스트 | `createLoading` | +| `components/pages/pullup/comments.tsx:174` | 삭제 실패 토스트 | `deleteLoading` | +| `components/pages/pullup/comments.tsx:60-62` | — | `newData.error`에서 early return → `commentsLoading` true, 무한스크롤 정지 | +| `app/pullup/[id]/pullup-client.tsx:58-73` | 삭제 실패 토스트 | `deleteLoading` → 이후 삭제 전부 무시 | +| `app/pullup/[id]/report/report-client.tsx:230-255` | 400/403/406/409 토스트 | `loading` → 버튼 disabled 고정 | +| `app/pullup/[id]/facilities/facilities-client.tsx:68` | 실패 토스트 | `loading` | +| `components/pages/pullup/bookmark-button.tsx:43-67` | **401 → 로그인 이동 알럿** | — | +| `components/pages/pullup/moment/add-moment-page.tsx:47-64` | 401 | `loading` | +| `components/pages/pullup/share-button.tsx:58` | PDF 실패 토스트 | `downLoading` | +| `components/pages/pullup/delete-button.tsx:23`, `moment/moment-item.tsx:28` | 실패 토스트 | — | +| `components/pages/mypage/bookmark/bookmark-list.tsx:76` | 실패 토스트 | `loading`, 바텀시트 안 닫힘 | +| `components/pages/mypage/user/username-card.tsx:47` | 실패 안내 | `loading` | +| `components/pages/config/user-setting.tsx:46-64` | 401 | 알럿 열린 채 | +| `components/pages/reset-password/reset-password-form.tsx:43`, `send-password-form.tsx:30` | 실패 안내 | unhandled rejection | +| `components/pages/register/select-location.tsx:75-83` | 근접/제한구역/국외 안내 (`locate-verify.ts` else 분기 dead) | `loading` | +| `components/pages/pullup/image-list.tsx:113-124` | 403/400 세부 메시지만 dead (try/catch는 있음) | — | +| `app/user-info/[user]/page-client.tsx:35-43` | — | `isLoading` → 무한스크롤 정지 | +| `app/mypage/myreport/myreport-client.tsx:66,110`, `report-admin-client.tsx:64,108`, `mypage/report/report-client.tsx:27` | 승인/거절/삭제 실패 안내 | `Alert.onClickAsync`가 `console.error`로 삼킴 → 다이얼로그만 열린 채 무반응. `app/admin/admin-client.tsx:126-164`엔 올바른 try/catch 있음 → 그대로 복사 | + +#### 클라이언트 — 읽기 호출, catch 없음 (나) + +- [ ] `components/layout/kakao-map.tsx:124-139` `getAllMarker` — `get-all-marker.ts:12`의 `return []` dead → unhandled rejection, 마커 미로딩 +- [ ] `components/pages/home/around-marker-carousel.tsx:36-48`, `moments/around.tsx:42-65`, `mypage/locate/registered-locate-list.tsx:33-45`, `search/around-search.tsx:48-68` (`loadMoreMarkers`), `social/marker-ranking-list.tsx:23-30`, `pullup/weather-badge.tsx:20-28`, `pullup/description.tsx:41` + +#### API 함수 자체 + +- [ ] `lib/api/auth/signout.ts:9` — `return response.json()`; 로그아웃 응답 body 비면 reject. `reset-password-form.tsx:52`가 try 없이 await → `router.replace` 스킵. 아무 호출부도 body를 안 읽으니 `return response`. +- [ ] `lib/api/marker/marker-detail.ts`, `user/myInfo.ts`, `user/favorites.ts`, `report/my-suggested.ts`, `marker/locate-verify.ts` — 위 서버 페이지들이 의존하는 `{ error }` shape 반환 경로가 전부 dead. + +### 2.2 지도 / GPS 라이프사이클 + +- [ ] **`store/useMapStore.ts:51-66`** `deleteAllMarker` / `deleteOverlays` — `setMap(null)`만 호출하고 `{ ...prev }` 반환. `markers`/`overlays` 배열이 **절대 비워지지 않음.** `replaceMarkers`/`replaceOverlays`는 앱 코드 호출 0건(테스트만). 지도 `idle`마다 `reloadMarkers` → append → 무한 누적(메모리 누수 + idle마다 순회 비용 증가) + 새 객체 반환으로 selector 없는 `useMapStore()` 구독자 11곳 전부 리렌더. 수정: `{ markers: [] }` / `{ overlays: [] }` 반환. +- [ ] **`hooks/useMarkerControl.tsx:98-111`** — 클러스터 버블마다 `createRoot` + `root.render()`, `deleteOverlays` 시 `unmount()` 없음 → React root 누수. 누수된 `Overlay`(`components/layout/overlay.tsx:11`)가 전부 `useMapStore()` 통구독 → 스토어 변경마다 전부 리렌더. 수정: root를 overlay 옆에 보관하고 삭제 시 `unmount()`, 또는 정적 HTML 문자열로 렌더. +- [ ] **`components/layout/kakao-map.tsx:119-145`** — `pathname`이 deps에 있음(`/admin` 가드용) → 클라이언트 네비게이션마다 4.2초 후 전체 마커 재요청. 수정: pathname을 ref로 읽거나 `/admin` 가드만 분리. +- [ ] **`hooks/useGps.ts:37`** — `handleGps()`가 `useState` 값을 동기 반환. `myLocation` null이면 `watchPosition` 시작 후 `null` 반환 → `share-button.tsx:80` "내 위치 길찾기" **첫 클릭 무동작**, 클릭마다 watcher 추가(해제 없음), 이후엔 이전 렌더 값 반환. 수정: `useGeolocationStore.getState().myLocation ?? await getCurrentPosition()`; 훅 삭제. +- [ ] **`hooks/useCompass.ts:46`** + `useGpsTracking.ts:53,288-296` — `deviceorientation`마다(안드로이드 ~60Hz, float, 스로틀 없음) `setHeading` → 루트 레이아웃의 `kakao-map.tsx`와 `location-badge.tsx`가 페이지 보이는 동안 센서 속도로 리렌더. 트래킹 중엔 `:288` effect가 그 속도로 CustomOverlay DOM 재생성. 수정: `setHeading(p => { const h = Math.round(a); return p === h ? p : h })`, `isTrackingLocation`일 때만 구독. +- [ ] **`hooks/useCompass.ts:51-74`** — `DeviceOrientationEvent.requestPermission()`을 마운트 시 제스처 없이 호출 → iOS 13+ 항상 거부(`NotAllowedError`), 로드마다 `console.error`, 컴퍼스 영구 무효. 비동기 경로가 cleanup 이후 리스너를 붙일 수도 있음. 수정: `handleGps`(제스처) 안에서 요청, cleanup에 `active` 플래그. +- [ ] **`hooks/useGpsTracking.ts:300-325`** — cleanup effect가 `map`이 null일 때만 동작하는데 `setMap`은 실제 map으로만 호출됨 → 실질 dead. `location-badge` 언마운트 시 watch/마커 정리 안 됨. 삭제 또는 `gpsWatchId` 기준으로 재작성. +- [ ] **`components/layout/roadview.tsx:129-131`** — 마우스 leave 시 `addOverlayMapTypeId(ROADMAP)` 호출(ROADMAP은 베이스 타입) → ROADVIEW 오버레이가 첫 hover 후 영영 안 벗겨짐. 수정: `removeOverlayMapTypeId(MapTypeId.ROADVIEW)`. +- [ ] **`components/layout/roadview.tsx:40-124`** — open마다 Map/Roadview/RoadviewClient/MapWalker + kakao 리스너 3개 생성, cleanup 없음. `getNearestPanoId` 콜백이 닫힌 뒤 `closeModal()`/`toast` 호출 가능. 수정: `removeListener` cleanup + `disposed` 플래그. +- [ ] **`app/pullup/[id]/report/report-client.tsx:87-120`** — `newLatLng` 바뀔 때마다 `#change-map`에 새 `kakao.maps.Map` 생성, 이전 것 미해제. 수정: 한 번 생성(ref) 후 `setCenter` + `marker.setPosition`. +- [ ] **`components/pages/search/around-search.tsx:141-172`** — 마운트 시 `window.kakao?.maps` 있을 때만 미니맵 init. SDK는 지연 로드(idle/인터랙션)라 `/search/around` 직접 진입 시 빈 지도, 재시도 없음. 좌표 변경마다 새 Map 생성. 수정: `useMapStore(s => s.map)`을 "SDK ready" 신호로 사용, 1회 init. + +### 2.3 개별 로직 버그 + +**날짜/시간** +- [ ] `lib/format-date.ts:4-6` — `getUTC*` 사용. API는 UTC ISO(`"2024-07-19T13:04:15Z"`) 반환 → **KST 00:00~08:59 게시물이 하루 전 날짜로 표시.** 사용처: `pullup-client.tsx:178,304`, `comments.tsx:283`, `moment-item.tsx:50`. ⚠️ SSR 서버(Vercel)는 UTC라 로컬 getter로 바꾸면 hydration mismatch → `Intl.DateTimeFormat("ko-KR", { timeZone: "Asia/Seoul", … })`로 타임존 고정. +- [ ] `lib/minutes-ago.ts:7-10` — 클라이언트 시계가 서버보다 몇 초 느리면 음수 diff → `Math.floor(-0.08) = -1` → `moment-list.tsx:162` "-1분 전". 수정: `Math.max(0, now - past)`. + +**조건식 / 상태** +- [ ] `app/pullup/[id]/pullup-client.tsx:141-149` — `{!철봉 || !평행봉 || (… && )}`: 기구 하나라도 없으면 식이 `true` → React가 아무것도 안 그림. "기구 개수 정보 없음" 배지가 정확히 그 케이스에서 안 뜸(`/markers/{id}/facilities`가 `[]` 반환 확인). 수정: `{(!철봉 || !평행봉 || (철봉.quantity <= 0 && 평행봉.quantity <= 0)) && }`. +- [ ] `components/pages/mypage/user-info.tsx:14,16,26`, `components/pages/search/around-search.tsx:498` — `{count && }` → count가 0이면 리터럴 "0" 렌더. 수정: `count > 0 &&`. +- [ ] `components/pages/search/marker-search-result.tsx:27` — fresh `data.error` 대신 stale state `marker?.error` 체크 → 에러 분기 도달 불가. `:66-67` `marker.photos ? marker.photos[0].photoUrl` → `photos: []`이면 throw. 수정: `data.error`, `marker.photos?.[0]?.photoUrl ?? "/metaimg.webp"`. +- [ ] `components/pages/register/set-description.tsx:66,70` — `description === ""` 비교하지만 초깃값 `null` → 비어 있어도 버튼 라벨 "다음". 수정: `!description`. +- [ ] `components/common/alert.tsx:144` vs `:157` — 액션 row는 `(onClick || cancel)`, 확인 버튼은 `(onClick || onClickAsync)` 게이트 → `onClickAsync`만 있고 `cancel` 없으면 확인 버튼 없음. 현재 호출부는 전부 `cancel: true`라 잠복. 수정: `(onClick || onClickAsync || cancel)`. +- [ ] `app/register/register-client.tsx:430-433` — `useUserStore.user`가 null(로그아웃/만료)이면 `!user`로 스켈레톤 영구, `user.error` 도달 불가. 수정: `isLoading` 기준 스켈레톤, `!user`는 `AuthError`. + +**채팅** +- [ ] `app/pullup/[id]/chat/pullup-chat-client.tsx:68` — `localStorage.getItem("cid")`를 raw로 사용. `ChatIdProvider`는 `JSON.stringify({ cid })`로 저장 → JSON 문자열이 `request-id`로 전송되고 `message.userid`와 비교(내 메시지 정렬). 소셜 채팅(`chat-detail-client.tsx:59-62`)은 `.cid` 파싱함. 수정: `JSON.parse(raw).cid`. +- [ ] `app/social/chat/[code]/chat-detail-client.tsx:110-119` — ping effect가 `[]` deps로 마운트 시 1회 실행, 그 시점 `ws.current`는 null(소켓은 `cid` 확정 후 생성) → early return → **keep-alive ping 영영 안 나감.** 수정: WebSocket 생성 effect(`:65`) 안에서 interval 생성/해제. +- [ ] `chat-detail-client.tsx:58-63,144`, `pullup-chat-client.tsx:67-70` — 자식 effect가 부모 `ChatIdProvider` effect보다 먼저 실행. 새 브라우저에서 채팅 URL 딥링크 시 `cid` null 유지 → 소셜 채팅 `null` 렌더, pullup 채팅 소켓 미생성. 수정: 없으면 로컬에서 같은 shape로 생성·저장. + +**레이아웃 / 라우팅** +- [ ] `app/admin/layout.tsx:48-59` — 루트 레이아웃이 이미 감싼 `ThemeProvider`/`UserProvider`/`Toaster`를 재래핑 → `/users/me` **2회 호출**, 토스트 **2중 렌더**(`useToast`는 모듈 전역 상태), `localFont` 두 번째 인스턴스. 수정: wrapper `
`만 남기고 프로바이더·Toaster·폰트 삭제. +- [ ] `app/admin/page.tsx:10-21` — user 확인 전에 `getAllReports` 먼저 fetch, `chulbong` 체크 없음(`report-admin/page.tsx:21`엔 있음), `noUser` 도달 불가 → 비관리자는 403 throw로 에러 페이지. 수정: `report-admin/page.tsx` 패턴 복사. +- [ ] `app/sitemap.ts:20` — `/home` 경로 존재하지 않음(홈은 `(home)` 그룹 → `/`) → sitemap이 404 URL 포함. 삭제. +- [ ] `app/mypage/locate/page.tsx:33` — `returnUrl="mypage/locate"` 앞 슬래시 누락 → signin의 `startsWith("/")` 검증 실패 → 로그인 후 `/`로 이동. 2.1 수정 후 실제 영향. +- [ ] `app/mypage/page.tsx:85` — 상대 `href="mypage/config"`. 현재 경로가 정확히 `/mypage`일 때만 우연히 동작. + +**입력 / 인코딩 / 안전성** +- [ ] `lib/api/search/search.ts:16` — `term=${query}` 미인코딩 → `&`, `#`, `%`, `+` 포함 시 쿼리 잘림/오염. `encodeURIComponent`. +- [ ] `lib/api/marker/user-marker.ts:34` — `${userName}` 경로 미인코딩. 호출부 `app/user-info/[user]/page.tsx:17`은 param을 decode한 뒤 raw로 재삽입. `encodeURIComponent`. +- [ ] `components/provider/geo-provider.tsx:40-41`, `components/layout/kakao-map.tsx:149-150` — 모든 window `message` 이벤트에 `JSON.parse(e.data)` try 없음. AdSense iframe/SDK도 message를 post → RN WebView 안에서 uncaught SyntaxError. 수정: `typeof e.data !== "string"` 가드 + try/catch. +- [ ] `lib/session-cache.ts:43` — `setSessionCache`만 try/catch 없음(getter는 있음). `sessionStorage.setItem` throw 시 `user-provider.tsx:35`의 non-401 catch로 떨어져 캐시 없으면 `setUser(null)` → **방금 로그인한 사용자 로그아웃 처리.** `signin-form.tsx:98`에선 redirect 중단. +- [ ] `components/pages/home/moment-list.tsx:197-205` — `decodeBlurhash`를 가드 없이 렌더에서 호출, 잘못된 해시에 throw(`lib/decode-hash.ts:15`) → 스토리 뷰 크래시. `:46-51` 목록 경로는 이미 try/catch. 재사용. +- [ ] `components/pages/pullup/comments.tsx:152-164,180-186` — `setComments` updater 안에서 `setProviderInfo` 사이드 이펙트(StrictMode 2회 실행). `handleDelete`가 목록을 raw page 1로 교체 → `:77-83`에서 걸러낸 "k-pullup" provider 행 다시 포함, `totalPages` 갱신 안 됨. +- [ ] `app/pullup/[id]/moment/moment-client.tsx:76,106` — `createObjectURL` 미해제, `clearSelect`가 URL만 버림 → blob 누수. +- [ ] `components/common/carousel.tsx:117-119` — cleanup이 `select`만 제거, `reInit` 리스너 남음. +- [ ] `components/pages/home/moment-list.tsx:55,93-97` — `let animationFrameId`가 렌더마다 재선언 → `cancelAnimationFrame` 무효. `useRef`. +- [ ] `components/pages/pullup/image-carousel.tsx:15,30-40`, `components/common/image-modal.tsx:39,94-107` — 슬라이드 전체가 boolean `loaded` 하나 공유 → 첫 이미지 로드 후 나머지 스켈레톤 사라짐. URL별 Set. +- [ ] `components/layout/overlay.tsx:24` — 클래스 오타 `absolut` → `-bottom-3 -left-10` 미적용 (수정 시 시각 확인 필요). + +### 2.4 설정 / 툴링 + +- [ ] `.storybook/main.ts:26` — `staticDirs: ["..\\public"]` 윈도우 백슬래시 → mac/linux에서 리터럴 경로, public 자산 누락. `"../public"`. +- [ ] `NEXT_PUBLIC_BASE_URL` — API 파일 16개가 fallback 없이 사용(`get-comments`, `convert-wgs`, `get-facilities`, `get-roadview-date`, `get-weather`, `marker-detail`, `marker-ranking`, `user-marker`, `get-all-moment`, `get-moment-for-marker`, `get-all-reports`, `my-suggested`, `report-for-mymarker`, `favorites`, `my-registered-location`, `myInfo`). `.env`는 gitignore, `.env.example` 없음, CLAUDE.md는 GA 변수만 문서화. `.github/workflows/playwright-test.yml:45`는 env 없이 `yarn dev:simple` → CI SSR이 `undefined/markers/...` 호출. `app/pullup/[id]/opengraph-image.tsx:10`만 fallback 있음 → 공통 상수 `API_BASE = process.env.NEXT_PUBLIC_BASE_URL ?? "https://api.k-pullup.com/api/v1"`로 통일하거나 workflow에 env 주입. `lib/api/marker/new-pictures.ts:14`는 prod URL 하드코딩. +- [ ] `tests/e2e/challenge/challenge.spec.ts:28,73-77` — `waitForTimeout(1000)` 하드 대기, `if (await cell.isVisible())`로 단언이 조건부(공허 통과). `expect.poll` + 무조건 단언. +- [ ] `tests/e2e/signin/signin.spec.ts:67,95` — 이름은 "마이 페이지로 이동", 단언은 `toHaveURL("/")`. 이름 정정. +- [ ] `package.json` — `@types/sharp@^0.32`는 deprecated stub(sharp ≥0.32 자체 타입). 제거. (`sharp`는 `opengraph-image.tsx`에서 런타임 사용 → deps 유지 맞음.) + +--- + +## 3. 사이드 이펙트 없는 개선 + +전부 런타임 동작 변화 0. A/B 결정과 무관하게 바로 진행 가능 (단, "dead `!response.ok` 분기 삭제"는 B 확정 후). + +### 3.1 미사용 파일 (import 0건, grep 확인) + +- [ ] `hooks/useEventPopup.ts` + `components/common/event-popup.tsx` +- [ ] `app/mypage/device-type.tsx` + 이것만 쓰는 `hooks/useDeviceType.ts` (34줄 주석 블록 포함, `lib/get-device-type.ts`와 중복) +- [ ] `components/common/scroll-to-top.tsx` +- [ ] `components/pages/home/slide-icons.tsx` (254줄) +- [ ] `components/ui/badge.tsx` +- [ ] `components/icons/bookmark-icon.tsx`, `checked-icon.tsx`, `config-icon.tsx`, `location-pin-icon.tsx` +- [ ] `types/kakao-location.type.ts` (파일 전체), `types/cluster.types.ts` (파일 전체) +- [ ] `components/layout/move-map-input.tsx` — 컴포넌트는 주석에서만 참조(`kakao-map.tsx:399`). `KakaoPlace` 타입만 `search-client.tsx:12`, `search-list.tsx:8`에서 사용 → 타입을 `types/`로 이동 후 삭제. + +### 3.2 미사용 dependencies (import 0건) + +- [ ] `@tanstack/react-query`, `remark-breaks`, `@radix-ui/react-slot` +- [ ] devDeps: `jsdom`, `@types/jsdom`, `@testing-library/jest-dom`, `@vitejs/plugin-react` (vitest는 `oxc.jsx` 사용), `webpack-cli`, `@types/sharp` +- `webpack`은 storybook builder peer라 유지. 제거 후 `yarn install` → `yarn build` + `yarn storybook` 확인. + +### 3.3 중복 테스트 + +- [ ] fetchData 테스트 **11파일**이 같은 4개 프로퍼티를 3중 복사: `__tests__/lib/fetchData-http-error` ≈ `fetchData.httpError`; `fetchData-http-success` ≈ `fetchData.httpSuccess` ≈ `lib/__tests__/fetchData-success`; `fetchData-response-body` ≈ `fetchData.responseBody` ≈ `lib/__tests__/fetchData-responseBody`; `fetchData.property` ≈ `lib/__tests__/fetchData.test`. 한 세트만 유지. +- [ ] `package.json` `test:e2e:ci` 스크립트 — CI는 `yarn playwright test` 직접 실행(`playwright-test.yml:51`). 제거 또는 사용. + +### 3.4 Dead state / dead 분기 + +- [ ] `components/layout/kakao-map.tsx:67,356-362` — `loading` state, `setLoading` 0회 → 오버레이 분기 dead +- [ ] `app/pullup/[id]/chat/pullup-chat-client.tsx:51,131,162-175,273` — `connection`이 false로 바뀌는 곳 없음 → 로딩 분기 + `disabled={!connection}` dead; `if (!ws)`는 ref 검사(항상 truthy) +- [ ] `components/common/side-main.tsx:312-328` — `hasBackButton ? (...)` 분기 안에서 `hasBackButton` 3중 체크 +- [ ] `components/layout/overlay.tsx:48-51` — `count < 5000 → "bg-red"`, `else → "bg-red"` 동일 +- [ ] `components/pages/challenge/celebration-motion.tsx:44-50` — 두 분기 모두 `return null` +- [ ] `store/useSearchStore.ts:26-36,44-48` — `addSearch` 두 분기 결과 동일, `removeItem`의 `if (newSearches)` 항상 true +- [ ] `lib/kakao-geocoder.ts:79-81` — 동일 분기 삼항; `:109-145` `batchCoordToAddress`, `clearGeocodeCache`, `getGeocacheCacheSize` 호출 0건 +- [ ] `lib/map-walker.ts:54-55` — `this.newPos; this.prevPos;` no-op 문 +- [ ] `lib/optimize-image.ts:259-261` — catch 안 `instanceof ImageValidationError` 도달 불가(검증은 try 밖에서 끝남) +- [ ] `hooks/useAddressResolver.ts:18,42-45,53,131-142` — `AbortController` 생성/abort하지만 `getAddress`에 전달 안 됨(signal 인자 없음); `cancel` 호출 0건 +- [ ] `store/useAlertStore.ts:34-38` — `async () => { await onClickAsync() }` identity 래퍼 +- [ ] `app/mypage/report/report-client.tsx:34` — `reports.length` 뒤의 `!reports` +- [ ] `app/register/register-client.tsx:67,218` — `setUser`는 effect deps에만 존재 +- [ ] `app/admin/admin-client.tsx:129,180` — try/catch 안의 `!response.ok` (B 확정 후 삭제) +- [ ] `app/pullup/[id]/facilities/page.tsx:17`, `moment/page.tsx:15` — `[id]` 세그먼트에서 `if (!id)` 도달 불가 +- [ ] `components/pages/pullup/image-list.tsx:114` — `errorData` 미사용 +- [ ] `components/layout/bottom-nav.tsx:52-56,47,177` — `w-[${width}px]` 동적 클래스(Tailwind JIT 미생성, 유일 호출부는 `"full"`), `useEffect` 안 `typeof window` 중복, `NavLink.animation?` 미사용 +- [ ] `components/notice/notice-list.tsx:27-36,48-52` — `noticeCopy`에 이미 `active:false`인데 재매핑 2회 +- [ ] `components/layout/image-carousel.tsx:37-49` — 상수 `"/placeholder_image.png"`만 붙이는 `useMemo` +- [ ] `lib/api/marker/close-marker.ts:38` — `n=${pageSize}`와 하드코딩 `pageSize=10` 동시 전송, `pageSize` 넘기는 호출부 없음 + +### 3.5 중복 구현 → 하나로 + +- [ ] `getToday` 4벌: `celebration-motion.tsx:9-15`, `goal-card.tsx:17-23`, `weekly-heatmap.tsx:31-37`, `store/useChallengeStore.ts:22-28` → `lib/challenge-streak.ts`에서 export +- [ ] haversine 2벌: `lib/admin-utils.ts:9-27`(m), `lib/find-nearby-markers.ts:3-20`(km) → 하나 export, 호출부에서 스케일 +- [ ] `lib/challenge-streak.ts:133-148,182-196,205-219` — "add one day" 3벌 → `addDays(dateStr, n)` (기존 테스트가 커버) +- [ ] `hooks/useMarkerControl.tsx:150-184` — `image`만 다른 두 `createMarker` 분기 → `image: options.selectId && m.markerId === options.selectId ? "selected" : "active"` +- [ ] `Device` 타입 — `app/mypage/page.tsx:15-20`(페이지 모듈)에서 **61파일**이 import(`lib/get-device-type.ts`, `side-main.tsx`, `bottom-nav.tsx` 등 컴포넌트 레이어 포함) → `types/device.ts`로 이동, 페이지에서 re-export +- [ ] `components/pages/pullup/image-list.tsx:3` — `ImageWrap`을 페이지 모듈 `app/article/2/image-wrap.tsx`에서 import(역방향 의존). `components/common/Image-wrap.tsx`와 근사 중복 → article 버전을 `components/common/`으로 +- [ ] `SheetHeight` 타입: `hooks/useDrawerGesture.ts:8-18` / `store/useSheetHeightStore.ts:3-13`; `Region` ≈ `CachedRegion`: `store/useGeolocationStore.ts:8-16` / `lib/address-cache.ts:8-17` +- [ ] `types/user.ts` vs `lib/api/user/myInfo.ts:14` (`User` ⊂ `MyInfo`); `get-all-reports.ts:13`이 `my-suggested.ts:3`의 `ReportStatus` 인라인; `CloseMarker`/`Favorite`/`RegisteredMarker`/`UserMarker` 동일 shape ×4; `favorites.ts:11`, `my-suggested.ts:20`의 로컬 `interface Response`가 전역 `Response` 섀도잉 + +### 3.6 미사용 export (참조 0건) + +- `store/useMapStore.ts:42,45` `replaceMarkers`/`replaceOverlays` (테스트만) · `store/useBottomSheetStore.ts:14` `setId` +- `lib/fetchData.ts:72` `getErrorMessage` · `lib/validate.ts:17` `validateNumeric` · `lib/admin-utils.ts:88` `getPriorityLevel` · `lib/cache/roadview-date-cache.ts:92` `clearRoadviewDateCache` +- `lib/optimize-image.ts:283,294` `optimizeImages`/`validateImages` (CLAUDE.md에 문서화됨 → 삭제 시 문서도 갱신, 또는 유지) +- `constant/index.ts` `CITIES`, `CITIES_BADGE_TITLE`, `REGION_CHAT`, `UPDATE_NOTICE`, `NOTICE` +- `types/kakao-map.types.ts` `Qa`, `KakaoLatLng` +- API 타입: `signup.ts` `SigninRes`, `signin.ts` `LoginRes`/`LoginReq`, `delete-marker-photo.ts` `DeleteMarkerPhotoResponse`/`DeleteMarkerPhotoError`, `set-new-marker.ts` `SetMarkerReq`, `get-roadview-date.ts` `RoadviewDateRes`, `search.ts` `SearchMarkers`/`SearchRes` + +### 3.7 React key (id 필드 있는데 미사용) + +- [ ] `app/pullup/[id]/moment/moment-client.tsx:223` `caption+createdAt` → `storyID` +- [ ] `app/social/chat/[code]/chat-detail-client.tsx:209` `timestamp+message+nickname` → `uid` +- [ ] `app/mypage/report-admin/report-admin-client.tsx:148` `reports[0].reportId-index` → `report.reportId` +- [ ] `app/search/search-client.tsx:265` `` `${search}-${index}` `` → `"[object Object]-i"` → `search.addr` +- [ ] `components/pages/pullup/upload-image.tsx:146`, `register/upload-image.tsx:142` data-URL + `[object File]` + index → `file.id` +- [ ] `components/pages/home/moment-list.tsx:244` → `storyID` + +### 3.8 접근성 (시각 변화 없음) + +- [ ] 아이콘 전용 컨트롤 `aria-label` 없음: GPS FAB(`components/common/tooltip.tsx:54`에 `aria-label={title}` 추가로 해결), `image-modal.tsx:76-83` 닫기, `input.tsx:104-110` 아이콘 버튼(`type="button"`도 없음), `search-header.tsx:52-61` 뒤로/홈, `pullup/upload-image.tsx:149-154` + `register/upload-image.tsx:145-153` 삭제 +- [ ] `components/pages/pullup/upload-image.tsx:204-212` `AddImageButton`이 `div onClick` (register 버전은 ` ) : (
diff --git a/components/layout/kakao-map.tsx b/components/layout/kakao-map.tsx index 0c87295..10f09b8 100644 --- a/components/layout/kakao-map.tsx +++ b/components/layout/kakao-map.tsx @@ -7,7 +7,6 @@ import useClientDeviceType from "@hooks/useClientDeviceType"; import useGpsTracking from "@hooks/useGpsTracking"; import useIsMounted from "@hooks/useIsMounted"; import { useToast } from "@hooks/useToast"; -import LoadingIcon from "@icons/loading-icon"; import cn from "@lib/cn"; import useGeolocationStore from "@store/useGeolocationStore"; import useImageCountStore from "@store/useImageCountStore"; @@ -64,7 +63,6 @@ const KakaoMap = () => { const { openRoadview } = useRoadviewStore(); const { toast } = useToast(); - const [loading, setLoading] = useState(false); const [shouldLoadMapSdk, setShouldLoadMapSdk] = useState(false); // Use GPS tracking hook @@ -369,13 +367,6 @@ const KakaoMap = () => { onLoad={handleLoadMap} /> )} - {loading && ( -
-
- -
-
- )}
{/* GPS FAB for Desktop only */} { - const now = new Date(); - const year = String(now.getFullYear()).padStart(4, "0"); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const day = String(now.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; -}; - const CelebrationMotion = () => { const records = useChallengeStore((s) => s.data.records); const goalSettings = useChallengeStore((s) => s.data.goalSettings); diff --git a/lib/__tests__/fetchData-responseBody.test.ts b/lib/__tests__/fetchData-responseBody.test.ts deleted file mode 100644 index d506721..0000000 --- a/lib/__tests__/fetchData-responseBody.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import * as fc from "fast-check"; -import fetchData, { FetchError } from "@lib/fetchData"; - -/** - * Property 4: Error response body preservation - * - * For any HTTP error response (status >= 400) with a readable body containing - * arbitrary text content, the thrown FetchError's responseBody property SHALL - * equal the response body text. If the body is unreadable or times out, - * responseBody SHALL be null. - * - * **Validates: Requirements 2.10** - */ -describe("Feature: fetchdata-error-handling, Property 4: Error response body preservation", () => { - beforeEach(() => { - vi.stubGlobal("fetch", vi.fn()); - }); - - afterEach(() => { - vi.restoreAllMocks(); - vi.useRealTimers(); - }); - - it("responseBody preserves the original body text for error responses", async () => { - await fc.assert( - fc.asyncProperty( - fc.integer({ min: 400, max: 599 }), - fc.webUrl(), - fc.string(), - async (status, url, bodyText) => { - const mockResponse = { - ok: false, - status, - text: () => Promise.resolve(bodyText), - } as unknown as Response; - - vi.mocked(fetch).mockResolvedValue(mockResponse); - - try { - await fetchData(url); - // Should not reach here - expect.fail("fetchData should have thrown a FetchError"); - } catch (error) { - expect(error).toBeInstanceOf(FetchError); - const fetchError = error as FetchError; - expect(fetchError.responseBody).toBe(bodyText); - } - } - ), - { numRuns: 20 } - ); - }); - - it("responseBody is null when body read times out", async () => { - vi.useFakeTimers(); - - const url = "https://example.com/timeout-test"; - - const mockResponse = { - ok: false, - status: 500, - text: () => new Promise(() => {}), // never resolves - } as unknown as Response; - - vi.mocked(fetch).mockResolvedValue(mockResponse); - - const fetchPromise = fetchData(url).catch((error) => error); - - // Advance time past the 5000ms timeout - await vi.advanceTimersByTimeAsync(5000); - - const error = await fetchPromise; - - expect(error).toBeInstanceOf(FetchError); - expect((error as FetchError).responseBody).toBe(null); - - vi.useRealTimers(); - }); -}); diff --git a/lib/__tests__/fetchData-success.test.ts b/lib/__tests__/fetchData-success.test.ts deleted file mode 100644 index 899a4cf..0000000 --- a/lib/__tests__/fetchData-success.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, it, expect, vi, afterEach } from "vitest"; -import * as fc from "fast-check"; -import fetchData from "@lib/fetchData"; - -/** - * Property 3: HTTP success status returns Response - * - * For any HTTP response with status code in the range [200, 299], - * calling fetchData SHALL return a Response object without throwing, - * where the returned Response's status equals the original status code. - * - * **Validates: Requirements 2.9** - */ -describe("Feature: fetchdata-error-handling, Property 3: HTTP success status returns Response", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("should return a Response object without throwing for any 2xx status", async () => { - await fc.assert( - fc.asyncProperty( - fc.integer({ min: 200, max: 299 }), - fc.webUrl(), - async (status, url) => { - const mockResponse = new Response(null, { - status, - statusText: "OK", - }); - - vi.stubGlobal( - "fetch", - vi.fn().mockResolvedValue(mockResponse) - ); - - const result = await fetchData(url); - - expect(result).toBeInstanceOf(Response); - expect(result.status).toBe(status); - } - ), - { numRuns: 20 } - ); - }); -}); diff --git a/lib/__tests__/fetchData.test.ts b/lib/__tests__/fetchData.test.ts deleted file mode 100644 index a09dfcc..0000000 --- a/lib/__tests__/fetchData.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect } from "vitest"; -import fc from "fast-check"; -import { FetchError } from "@/lib/fetchData"; - -/** - * Feature: fetchdata-error-handling, Property 1: FetchError constructor round-trip - * - * Validates: Requirements 6.1, 6.2, 6.3, 6.4 - */ -describe("Feature: fetchdata-error-handling, Property 1: FetchError constructor round-trip", () => { - it("should preserve all constructor arguments as instance properties", () => { - fc.assert( - fc.property( - fc.integer({ min: 0, max: 599 }), - fc.webUrl(), - fc.string({ minLength: 1 }), - (status, url, message) => { - const error = new FetchError(status, url, message); - - expect(error.status).toBe(status); - expect(error.url).toBe(url); - expect(error.message).toBe(message); - expect(error.name).toBe("FetchError"); - expect(error instanceof Error).toBe(true); - } - ), - { numRuns: 20 } - ); - }); -}); diff --git a/lib/challenge-streak.ts b/lib/challenge-streak.ts index 599f8da..85d4f06 100644 --- a/lib/challenge-streak.ts +++ b/lib/challenge-streak.ts @@ -57,6 +57,18 @@ const getIsoDayOfWeek = (dateStr: string): number => { // --- Exported pure functions --- +/** + * 오늘 날짜를 로컬 기준 "YYYY-MM-DD" 문자열로 반환한다. + * (챌린지 방문 기록 키로 사용 — celebration-motion, useChallengeStore 공용) + */ +const getToday = (): string => { + const now = new Date(); + const year = String(now.getFullYear()).padStart(4, "0"); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; +}; + /** * 연속 방문 일수 계산. * - 오늘 방문 기록이 있으면 오늘부터 역순 카운트 @@ -229,4 +241,5 @@ export { getCurrentWeekDays, getWeeklyAchievedCount, getHeatmapDates, + getToday, }; diff --git a/store/useChallengeStore.ts b/store/useChallengeStore.ts index 4b3689b..4b778f6 100644 --- a/store/useChallengeStore.ts +++ b/store/useChallengeStore.ts @@ -6,7 +6,7 @@ import { loadChallengeData, saveChallengeData, } from "@lib/challenge-storage"; -import { calculateStreak, getWeekIdentifier } from "@lib/challenge-streak"; +import { calculateStreak, getToday, getWeekIdentifier } from "@lib/challenge-streak"; interface ChallengeState { // State @@ -22,14 +22,6 @@ interface ChallengeState { markCelebrationShown: () => void; } -const getToday = (): string => { - const now = new Date(); - const year = String(now.getFullYear()).padStart(4, "0"); - const month = String(now.getMonth() + 1).padStart(2, "0"); - const day = String(now.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; -}; - const useChallengeStore = create()((set, get) => ({ data: getDefaultData(), isHydrated: false, diff --git a/store/useSearchStore.ts b/store/useSearchStore.ts index 6ae23a8..57d1218 100644 --- a/store/useSearchStore.ts +++ b/store/useSearchStore.ts @@ -23,17 +23,11 @@ const useSearchStore = create()( addSearch: (data: SearchData) => set((state) => { const MAX_SEARCHES = 50; - const item = state.searches.findIndex((search) => { - return search.addr === data.addr; + // 같은 주소가 있으면 제거 후 맨 앞으로 올린다. (중복 방지 + 최신 우선) + const deduped = state.searches.filter((search) => { + return search.addr !== data.addr; }); - - if (item !== -1) { - const newSearch = [...state.searches].filter((search) => { - return search.addr !== data.addr; - }); - return { searches: [data, ...newSearch].slice(0, MAX_SEARCHES) }; - } - return { searches: [data, ...state.searches].slice(0, MAX_SEARCHES) }; + return { searches: [data, ...deduped].slice(0, MAX_SEARCHES) }; }), removeItem: (addr: string) => set((state) => { @@ -41,11 +35,7 @@ const useSearchStore = create()( return addr !== search.addr; }); - if (newSearches) { - return { searches: newSearches }; - } - - return { searches: state.searches }; + return { searches: newSearches }; }), clearSearches: () => set({ searches: [] }), }), From bc359e42ed1a9bd243acd87c4e65ca570c518912 Mon Sep 17 00:00:00 2001 From: Yonghun Yi Date: Tue, 8 Sep 2026 10:36:20 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor:=20React=20key=EB=A5=BC=20?= =?UTF-8?q?=EA=B3=A0=EC=9C=A0=20id=EB=A1=9C=20=EA=B5=90=EC=B2=B4=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 인덱스·문자열 조합 key를 storyID/uid/reportId/addr/file.id 등 고유 값으로 바꾸고, tooltip 버튼 aria-label과 SectionTitle 빈 버튼 조건부 렌더로 접근성을 보완한다. (시각 변화 없음) --- app/mypage/report-admin/report-admin-client.tsx | 2 +- app/pullup/[id]/moment/moment-client.tsx | 2 +- app/search/search-client.tsx | 2 +- app/social/chat/[code]/chat-detail-client.tsx | 2 +- components/common/section.tsx | 14 ++++++++------ components/common/tooltip.tsx | 1 + components/pages/home/moment-list.tsx | 2 +- components/pages/pullup/upload-image.tsx | 2 +- components/pages/register/upload-image.tsx | 2 +- 9 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/mypage/report-admin/report-admin-client.tsx b/app/mypage/report-admin/report-admin-client.tsx index dc9a29d..d77844b 100644 --- a/app/mypage/report-admin/report-admin-client.tsx +++ b/app/mypage/report-admin/report-admin-client.tsx @@ -145,7 +145,7 @@ const MyreportClient = ({ {reports.reports.map((report, index) => ( { diff --git a/app/pullup/[id]/moment/moment-client.tsx b/app/pullup/[id]/moment/moment-client.tsx index 5b466d9..8d32e6a 100644 --- a/app/pullup/[id]/moment/moment-client.tsx +++ b/app/pullup/[id]/moment/moment-client.tsx @@ -226,7 +226,7 @@ const MomentClient = ({ {moments.map((moment, i) => { return (
diff --git a/app/search/search-client.tsx b/app/search/search-client.tsx index 25a1ae6..912ce3d 100644 --- a/app/search/search-client.tsx +++ b/app/search/search-client.tsx @@ -262,7 +262,7 @@ const SearchClient = ({ {searches.slice(0, visibleCount).map((search, index) => { return (
  • { return ( diff --git a/components/common/section.tsx b/components/common/section.tsx index 549df82..daf8805 100644 --- a/components/common/section.tsx +++ b/components/common/section.tsx @@ -39,12 +39,14 @@ export const SectionTitle = ({

    )} - + {buttonTitle && ( + + )} ); }; diff --git a/components/common/tooltip.tsx b/components/common/tooltip.tsx index 42c039c..fae64f4 100644 --- a/components/common/tooltip.tsx +++ b/components/common/tooltip.tsx @@ -52,6 +52,7 @@ const Tooltip = ({ return ( setIsVisible(true) : undefined} onMouseLeave={!isMobile ? () => setIsVisible(false) : undefined} onMouseDown={isMobile ? () => setIsVisible(true) : undefined} diff --git a/components/pages/home/moment-list.tsx b/components/pages/home/moment-list.tsx index 832359e..2e3886f 100644 --- a/components/pages/home/moment-list.tsx +++ b/components/pages/home/moment-list.tsx @@ -246,7 +246,7 @@ const MomentList = ({ data }: { data: Moment[] }) => { {data.map((moment, i) => (
    +
    +
  • + ); + } + if (!data || data.length === 0) { return (
    diff --git a/components/pages/mypage/locate/registered-locate-list.tsx b/components/pages/mypage/locate/registered-locate-list.tsx index a7f58db..0041844 100644 --- a/components/pages/mypage/locate/registered-locate-list.tsx +++ b/components/pages/mypage/locate/registered-locate-list.tsx @@ -26,12 +26,13 @@ const RegisteredLocateList = ({ data }: RegisteredListProps) => { const [currentPage, setCurrentPage] = useState(data.currentPage); const [isLoading, setIsLoading] = useState(false); + const [loadError, setLoadError] = useState(false); const observerRef = useRef(null); const loadMoreRef = useRef(null); const loadMoreMarkers = useCallback(async () => { - if (isLoading || currentPage >= data.totalPages) return; + if (isLoading || loadError || currentPage >= data.totalPages) return; setIsLoading(true); try { @@ -42,11 +43,17 @@ const RegisteredLocateList = ({ data }: RegisteredListProps) => { setMarkers((prevMarkers) => [...prevMarkers, ...newData.markers]); setCurrentPage(newData.currentPage); } catch { - // 추가 로딩 실패 시 조용히 중단 + // 실패 시 loadError 로 표시해 observer 의 자동 재요청 루프를 막고 + // 명시적 재시도 액션에서만 다시 시도한다. + setLoadError(true); } finally { setIsLoading(false); } - }, [currentPage, isLoading, data.totalPages]); + }, [currentPage, isLoading, loadError, data.totalPages]); + + const retryLoadMore = useCallback(() => { + setLoadError(false); + }, []); useEffect(() => { observerRef.current = new IntersectionObserver( @@ -119,7 +126,23 @@ const RegisteredLocateList = ({ data }: RegisteredListProps) => { ))} )} - {data.totalPages > currentPage &&
    } + {loadError && ( +
    + + 목록을 더 불러오지 못했습니다. + + +
    + )} + {!loadError && data.totalPages > currentPage && ( +
    + )}
    ); }; diff --git a/components/pages/pullup/comments.tsx b/components/pages/pullup/comments.tsx index bd1f152..f2142a4 100644 --- a/components/pages/pullup/comments.tsx +++ b/components/pages/pullup/comments.tsx @@ -178,7 +178,17 @@ const Comments = ({ markerId, initialComments }: CommentsProps) => { setDeleteLoading(true); try { await deleteComment(commentId); + } catch { + toast({ description: "잠시 후 다시 시도해주세요" }); + setDeleteLoading(false); + return; + } + + // 삭제 성공 시 로컬 목록에서 즉시 제거 (새로고침 실패와 무관하게 반영) + setComments((prev) => prev.filter((comment) => comment.commentId !== commentId)); + // 목록 새로고침은 별도 try/catch — 실패해도 삭제 성공 상태를 오염시키지 않는다. + try { const newComment = await getComments({ id: markerId, pageParam: 1, @@ -193,7 +203,7 @@ const Comments = ({ markerId, initialComments }: CommentsProps) => { setTotalPages(newComment.totalPages); setCurrentPage(1); } catch { - toast({ description: "잠시 후 다시 시도해주세요" }); + // 새로고침 실패: 위에서 로컬 제거는 이미 반영됨. 조용히 무시. } finally { setDeleteLoading(false); } diff --git a/components/pages/search/around-search.tsx b/components/pages/search/around-search.tsx index ee5174a..c957a62 100644 --- a/components/pages/search/around-search.tsx +++ b/components/pages/search/around-search.tsx @@ -44,6 +44,7 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { // Mini map references const miniMapRef = useRef(null); const miniMapInstanceRef = useRef(null); + const centerMarkerRef = useRef(null); const circleOverlayRef = useRef(null); // 전역 map 이 준비되면 kakao SDK 도 로드됐다는 신호로 사용한다. (P2-10) @@ -139,9 +140,10 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { const center = new window.kakao.maps.LatLng(Number(lat), Number(lng)); - // 이미 생성돼 있으면 재생성하지 않고 center 만 이동한다. (P2-10) + // 이미 생성돼 있으면 재생성하지 않고 center 와 마커 위치만 이동한다. (P2-10) if (miniMapInstanceRef.current) { miniMapInstanceRef.current.setCenter(center); + centerMarkerRef.current?.setPosition(center); return; } @@ -159,8 +161,8 @@ const AroundSearch = ({ address, lat, lng }: AroundSearchProps) => { const miniMap = new window.kakao.maps.Map(container, options); miniMapInstanceRef.current = miniMap; - // Add center marker - new window.kakao.maps.Marker({ + // Add center marker (위치 변경 시 재사용하기 위해 ref 에 보관) + centerMarkerRef.current = new window.kakao.maps.Marker({ position: center, map: miniMap, }); diff --git a/components/pages/search/marker-search-result.tsx b/components/pages/search/marker-search-result.tsx index 030d73b..1ca7066 100644 --- a/components/pages/search/marker-search-result.tsx +++ b/components/pages/search/marker-search-result.tsx @@ -22,17 +22,23 @@ const MarkerSearchResult = ({ address, markerId }: MarkerSearchResultProps) => { useEffect(() => { const fetch = async () => { setLoading(true); - const data = await markerDetail({ id: markerId }); + try { + const data = await markerDetail({ id: markerId }); - if (data.error) { + // 성공 응답에 error 필드가 오는 경우도 방어적으로 처리 + if (data.error) { + setError(true); + return; + } + + setMarker(data); + setError(false); + } catch { + // markerDetail 은 non-2xx 에서 throw → 에러 상태로 표시 setError(true); + } finally { setLoading(false); - return; } - - setMarker(data); - setError(false); - setLoading(false); }; fetch(); From 716bc7c82d9fd5fdd4b71f74e0ee32200ecf4149 Mon Sep 17 00:00:00 2001 From: Yonghun Yi Date: Tue, 8 Sep 2026 13:18:55 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20WebSocket=20request-id=EC=97=90=20?= =?UTF-8?q?cid=EB=A5=BC=20encodeURIComponent=EB=A1=9C=20=EC=9D=B8=EC=BD=94?= =?UTF-8?q?=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cid에 쿼리 구문 문자(&, #, % 등)가 포함돼도 URL이 깨지지 않도록 철봉/소셜 채팅 WebSocket URL의 request-id 값을 인코딩한다. --- app/pullup/[id]/chat/pullup-chat-client.tsx | 2 +- app/social/chat/[code]/chat-detail-client.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pullup/[id]/chat/pullup-chat-client.tsx b/app/pullup/[id]/chat/pullup-chat-client.tsx index 85ade88..a9bf529 100644 --- a/app/pullup/[id]/chat/pullup-chat-client.tsx +++ b/app/pullup/[id]/chat/pullup-chat-client.tsx @@ -101,7 +101,7 @@ const PullupChatClient = ({ if (!cid) return; ws.current = new WebSocket( - `wss://api.k-pullup.com/ws/${markerId}?request-id=${cid}` + `wss://api.k-pullup.com/ws/${markerId}?request-id=${encodeURIComponent(cid)}` ); ws.current.onopen = () => { diff --git a/app/social/chat/[code]/chat-detail-client.tsx b/app/social/chat/[code]/chat-detail-client.tsx index 2c37fe0..7c18796 100644 --- a/app/social/chat/[code]/chat-detail-client.tsx +++ b/app/social/chat/[code]/chat-detail-client.tsx @@ -84,7 +84,7 @@ const ChatDetailClient = ({ if (!cid) return; ws.current = new WebSocket( - `wss://api.k-pullup.com/ws/${code}?request-id=${cid}` + `wss://api.k-pullup.com/ws/${code}?request-id=${encodeURIComponent(cid)}` ); ws.current.onopen = () => {