diff --git a/app/compare/page.tsx b/app/compare/page.tsx index f9fa17c..e558f96 100644 --- a/app/compare/page.tsx +++ b/app/compare/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import CompareSidebar from "../components/mypage/CompareSidebar"; import CompareMain from "../components/mypage/CompareMain"; import { useSearchParams, useRouter } from "next/navigation"; @@ -12,6 +12,7 @@ import { getCurateQueryOptions } from "@/shared/api/generated/personalized-curat import { useQueries } from "@tanstack/react-query"; import { DealInfoDealType } from "@/shared/api/generated/model/dealInfoDealType"; import { formatNumberToKoreanPrice } from "@/app/utils/format"; +import { useUserStore } from "@/app/store/userStore"; import { Suspense } from "react"; @@ -19,8 +20,17 @@ import { Suspense } from "react"; // ... House interface was here ... // ... MOCK_HOUSES, MOCK_CURATION, MOCK_BASIC_INFO were here ... +const CONDITION_MAP: Record = { + 1: "소음", + 2: "환경", + 3: "안전", + 4: "접근성", + 5: "편의", +}; + const ComparePageContent = () => { const router = useRouter(); + const { user } = useUserStore(); const searchParams = useSearchParams(); const idsParam = searchParams.get("ids"); const initialIds = idsParam ? idsParam.split(",") : []; @@ -116,15 +126,33 @@ const ComparePageContent = () => { ); const curationInfo = curationQueries[index]?.data?.data; + // 점수 정보를 배열로 변환하여 정렬 + const allScores = [ + { label: "소음", score: compareInfo?.noiseScore || 0 }, + { label: "환경", score: compareInfo?.environmentScore || 0 }, + { label: "안전", score: compareInfo?.safetyScore || 0 }, + { label: "접근성", score: compareInfo?.accessibilityScore || 0 }, + { label: "편의", score: compareInfo?.convenienceScore || 0 }, + ]; + + // 점수 높은 순으로 상위 3개 추출 + const top3Strengths = allScores + .sort((a, b) => b.score - a.score) + .slice(0, 3) + .map((s) => s.label); + + // 사용자의 선호 조건 (한글 명칭으로 변환) + const userPrefs = + user?.preferredConditions?.map((c) => CONDITION_MAP[c]) || []; + + // 매물의 강점(상위 3개)과 사용자의 선호 조건의 교집합 필터링 + const personalizedTags = top3Strengths.filter((tag) => + userPrefs.includes(tag), + ) as ("소음" | "안전" | "접근성" | "편의" | "환경")[]; + return { summary: curationInfo?.description || "큐레이션 분석 중입니다...", - tags: ["안전", "편의"] as ( - | "소음" - | "안전" - | "접근성" - | "편의" - | "환경" - )[], // 백엔드에서 제공하지 않으므로 기본태그 + tags: personalizedTags, scores: { 소음: compareInfo?.noiseScore || 0, 환경: compareInfo?.environmentScore || 0, diff --git a/app/components/layout/Navbar.tsx b/app/components/layout/Navbar.tsx index 6719cb5..6ac77e8 100644 --- a/app/components/layout/Navbar.tsx +++ b/app/components/layout/Navbar.tsx @@ -10,19 +10,28 @@ import { logoutAction } from "@/app/login/actions"; import { useRouter, usePathname } from "next/navigation"; import { usePropertyStore } from "@/app/store/propertyStore"; +import { useModuleStore } from "@/app/store/moduleStore"; import { GetMainPropertyPropertyType } from "@/shared/api/generated/model/getMainPropertyPropertyType"; export default function Navbar() { const router = useRouter(); const pathname = usePathname(); const { user, clearUser } = useUserStore(); - const { selectedPropertyType, setPropertyType, syncWithUserPreference } = - usePropertyStore(); + const { + selectedPropertyType, + setPropertyType, + syncWithUserPreference, + resetFilters, + } = usePropertyStore(); + const { resetToUserPreference } = useModuleStore(); const [mounted, setMounted] = useState(false); // hydration 에러 방지 useEffect(() => { - setMounted(true); + // Cascading render 방지를 위해 비동기 처리 + setTimeout(() => { + setMounted(true); + }, 0); }, []); // 초기 유저 선호도 동기화 @@ -65,7 +74,13 @@ export default function Navbar() { ]; const handleMenuClick = (type: GetMainPropertyPropertyType) => { + // 1. 주거 형태 변경 setPropertyType(type); + // 2. 검색 필터 초기화 + resetFilters(); + // 3. 우선순위 모듈 초기화 (유저 선호도로 복구) + resetToUserPreference(user?.preferredConditions || []); + // 4. 지도 페이지로 이동 router.push("/map"); }; @@ -109,7 +124,10 @@ export default function Navbar() { {mounted && user ? ( <> {/* 찜한 목록 */} -
diff --git a/app/components/list/List.tsx b/app/components/list/List.tsx index a3fa047..47c7861 100644 --- a/app/components/list/List.tsx +++ b/app/components/list/List.tsx @@ -8,6 +8,7 @@ import ListDetail from "./ListDetail"; import PriorityToggle from "../ui/PriorityToggle"; import { useUserStore } from "@/app/store/userStore"; import { useModuleStore, ModuleId } from "@/app/store/moduleStore"; +import { useMapModeStore } from "@/app/store/mapModeStore"; import { useGetMainProperty } from "@/shared/api/generated/main-property-controller/main-property-controller"; import { DealInfoDealType } from "@/shared/api/generated/model/dealInfoDealType"; import { formatNumberToKoreanPrice } from "@/app/utils/format"; @@ -31,6 +32,10 @@ const PROPERTY_TYPE_MAP: Record = { import { usePropertyStore } from "@/app/store/propertyStore"; +import { useSearchProperty } from "@/shared/api/generated/main-property-controller/main-property-controller"; +import { PropertySearchRequestDTODealType } from "@/shared/api/generated/model/propertySearchRequestDTODealType"; +import { PropertySearchRequestDTOPropertyType } from "@/shared/api/generated/model/propertySearchRequestDTOPropertyType"; + const List = () => { const [selectedId, setSelectedId] = useState(null); const [hasMounted, setHasMounted] = useState(false); @@ -39,7 +44,17 @@ const List = () => { const { activeModules, toggleModule } = useModuleStore(); const { toggleZzim } = usePropertyZzim(); const { addViewedId } = useRecentViewStore(); - const { selectedPropertyType } = usePropertyStore(); + const { + selectedPropertyType, + keyword, + selectedTypes, + depositRange, + monthlyRentRange, + salePriceRange, + spaceRange, + } = usePropertyStore(); + + const { setPropertiesOnMap } = useMapModeStore(); useEffect(() => { const timer = setTimeout(() => { @@ -55,24 +70,145 @@ const List = () => { if (hasMounted) { const propertyIdParam = searchParams.get("propertyId"); if (propertyIdParam) { - setSelectedId(Number(propertyIdParam)); + // Cascading render 방지를 위해 비동기 처리 + setTimeout(() => { + setSelectedId(Number(propertyIdParam)); + }, 0); } } }, [hasMounted, searchParams]); - const { data: apiResponse, isLoading } = useGetMainProperty( + // 필터가 하나라도 적용되었는지 확인 (주거 형태 필터 제외, 우선순위 바 활성화 포함) + const isFilterActive = + keyword.trim() !== "" || + selectedTypes.length > 0 || + spaceRange[0] !== 0 || + spaceRange[1] !== 60 || + activeModules.length > 0; + + // 공통 ID 맵 + const mIdMap: Record = { + noise: 1, + environment: 2, + safety: 3, + accessibility: 4, + convenience: 5, + }; + + // 요청할 우선순위 조건 생성 (활성 모듈 우선 + 프로필 선호도 보완 + 최대 3개 제한) + const getSelectConditions = () => { + const activeIds = activeModules.map((m) => mIdMap[m]); + const preferredIds = user?.preferredConditions || []; + // 활성 모듈을 앞에 두고 중복 제거 후 최대 3개 추출 + return Array.from(new Set([...activeIds, ...preferredIds])).slice(0, 3); + }; + + // 메인 매물 조회 (기본 추천) + const mainPropertyQuery = useGetMainProperty( { - select: user?.preferredConditions || [], + select: Array.from(new Set(user?.preferredConditions || [])).slice(0, 3), propertyType: selectedPropertyType, - size: 10, + size: 30, + }, + { + query: { + enabled: !!user && hasMounted && !isFilterActive, + }, + }, + ); + + // 매물 검색 조회 (필터/키워드 적용 시) + const searchPropertyQuery = useSearchProperty( + { + requestDTO: { + keyword: keyword || undefined, + propertyType: + selectedPropertyType as PropertySearchRequestDTOPropertyType, + dealType: selectedTypes.includes("매매") + ? PropertySearchRequestDTODealType.SALE + : selectedTypes.includes("전세") + ? PropertySearchRequestDTODealType.LEASE + : selectedTypes.includes("월세") + ? PropertySearchRequestDTODealType.RENT + : undefined, + minDeposit: selectedTypes.some((t) => ["월세", "전세"].includes(t)) + ? depositRange[0] + : undefined, + maxDeposit: selectedTypes.some((t) => ["월세", "전세"].includes(t)) + ? depositRange[1] >= 100000 + ? undefined + : depositRange[1] + : undefined, + minMonthlyRent: selectedTypes.includes("월세") + ? monthlyRentRange[0] + : undefined, + maxMonthlyRent: selectedTypes.includes("월세") + ? monthlyRentRange[1] >= 500 + ? undefined + : monthlyRentRange[1] + : undefined, + minPrice: selectedTypes.includes("매매") + ? salePriceRange[0] + : undefined, + maxPrice: selectedTypes.includes("매매") + ? salePriceRange[1] >= 1000000 + ? undefined + : salePriceRange[1] + : undefined, + minExclusiveArea: spaceRange[0] * 3.3058, // 평 -> m2 변환 + maxExclusiveArea: + spaceRange[1] >= 60 ? undefined : spaceRange[1] * 3.3058, + selectConditions: getSelectConditions(), + size: 30, + }, }, { query: { - enabled: !!user && hasMounted, + enabled: !!user && hasMounted && isFilterActive, }, }, ); + const apiResponse = isFilterActive + ? searchPropertyQuery.data + : mainPropertyQuery.data; + const isLoading = isFilterActive + ? searchPropertyQuery.isLoading + : mainPropertyQuery.isLoading; + + // 현재 리스트의 매물들을 지도에 표시하기 위해 동기화 + useEffect(() => { + if (apiResponse?.data?.items) { + const markers = apiResponse.data.items + .filter((item) => item.latitude && item.longitude) + .map((item) => { + let priceStr = ""; + let dealLabel = ""; + const deal = item.dealInfo; + if (deal?.dealType === DealInfoDealType.RENT) { + priceStr = `${deal.deposit}/${deal.monthlyRent}`; + dealLabel = "월세"; + } else if (deal?.dealType === DealInfoDealType.LEASE) { + priceStr = formatNumberToKoreanPrice(deal.price || 0); + dealLabel = "전세"; + } else if (deal?.dealType === DealInfoDealType.SALE) { + priceStr = formatNumberToKoreanPrice(deal.price || 0); + dealLabel = "매매"; + } + + return { + id: item.propertyId || 0, + lat: item.latitude!, + lng: item.longitude!, + title: item.apartmentName || "", + price: priceStr, + dealType: dealLabel, + }; + }); + setPropertiesOnMap(markers); + } + }, [apiResponse, setPropertiesOnMap]); + const houses = apiResponse?.data?.items?.map((item, index) => { let priceStr = ""; @@ -85,6 +221,7 @@ const List = () => { priceStr = `매매 ${formatNumberToKoreanPrice(deal.price || 0)}`; } + const conditions = item.conditions || []; return { id: item.propertyId || 0, rank: index + 1, @@ -94,7 +231,8 @@ const List = () => { type: PROPERTY_TYPE_MAP[item.propertyType || ""] || "아파트", area: `${item.supplyArea || 0}/${item.exclusiveArea || 0}m²`, floor: `${item.floor || 0}/${item.totalFloor || 0}층`, - tags: (item.conditions || []).map((c) => CONDITION_MAP[c] || ""), + tags: conditions.map((c) => CONDITION_MAP[c] || ""), + conditionIds: conditions, isLiked: item.liked, }; }) || []; @@ -104,6 +242,20 @@ const List = () => { activeModules.forEach((id) => toggleModule(id)); }; + const handleTagClick = (conditionId: number) => { + const idMap: Record = { + 1: "noise", + 2: "environment", + 3: "safety", + 4: "accessibility", + 5: "convenience", + }; + const moduleId = idMap[conditionId]; + if (moduleId) { + toggleModule(moduleId); + } + }; + // Hydration을 방지하기 위해 마운트 전에는 뼈대만 렌더링하거나 기본값을 렌더링 const username = hasMounted ? user?.username : ""; const displayActiveModules = hasMounted ? activeModules : []; @@ -182,6 +334,7 @@ const List = () => { setSelectedId(id); addViewedId(id); }} + onTagClick={handleTagClick} onToggleZzim={toggleZzim} /> )) diff --git a/app/components/list/ListDetail.tsx b/app/components/list/ListDetail.tsx index d7439bb..3d9c259 100644 --- a/app/components/list/ListDetail.tsx +++ b/app/components/list/ListDetail.tsx @@ -20,6 +20,7 @@ import { useCurate } from "@/shared/api/generated/personalized-curation-controll import { useMapModeStore } from "@/app/store/mapModeStore"; import { usePropertyZzim } from "@/app/hooks/usePropertyZzim"; +import { formatNumberToKoreanPrice } from "@/app/utils/format"; const sections = [ { id: "curation", label: "맞춤 큐레이션 정보" }, @@ -48,29 +49,68 @@ const ListDetail = ({ const isManualScrolling = useRef(false); const scrollTimeoutRef = useRef(null); + const tabsBoxRef = useRef(null); + const tabRefs = useRef>(new Map()); const { data: apiResponse, isLoading } = useGetPropertyDetail(propertyId); const { data: curationResponse, isLoading: isAiLoading } = useCurate(propertyId); const detailData = apiResponse?.data; + // 활성 탭이 바뀔 때마다 탭바를 자동으로 스크롤하여 활성 탭을 왼쪽 처음에 맞춤 + useEffect(() => { + const activeTabElement = tabRefs.current.get(activeTab); + if (activeTabElement && tabsBoxRef.current) { + const container = tabsBoxRef.current; + const scrollLeft = activeTabElement.offsetLeft; + container.scrollTo({ + left: scrollLeft, + behavior: "smooth", + }); + } + }, [activeTab]); + // 매물 정보를 불러오면 지도에 좌표 설정 useEffect(() => { if (detailData?.propertyInfo) { const { latitude, longitude, apartmentName } = detailData.propertyInfo; + const deal = detailData.deal; + let priceStr = ""; + let dealLabel = ""; + + if (deal) { + if (deal.dealType === "RENT") { + priceStr = `${deal.deposit}/${deal.monthlyRent}`; + dealLabel = "월세"; + } else if (deal.dealType === "LEASE") { + priceStr = formatNumberToKoreanPrice(deal.price || 0); + dealLabel = "전세"; + } else if (deal.dealType === "SALE") { + priceStr = formatNumberToKoreanPrice(deal.price || 0); + dealLabel = "매매"; + } + } + if (latitude && longitude) { setSelectedProperty({ + id: propertyId, lat: latitude, lng: longitude, title: apartmentName || "매물 위치", + price: priceStr, + dealType: dealLabel, + propertyScores: detailData.propertyScore, }); } } return () => { - clearSelectedProperty(); + // 렌더링 사이클에 의존하지 않고 전역 스토어의 최신 상태를 직접 확인 + if (!useMapModeStore.getState().isMapMode) { + clearSelectedProperty(); + } }; - }, [detailData, setSelectedProperty, clearSelectedProperty]); + }, [detailData, setSelectedProperty, clearSelectedProperty, propertyId]); // 스크롤 감지 및 탭 포커스 자동 전환 useEffect(() => { @@ -174,10 +214,17 @@ const ListDetail = ({
{/* Tab Bar (Sticky) */} -
diff --git a/app/components/list/list_section/CurationSection.tsx b/app/components/list/list_section/CurationSection.tsx index 6919338..768f0a7 100644 --- a/app/components/list/list_section/CurationSection.tsx +++ b/app/components/list/list_section/CurationSection.tsx @@ -2,6 +2,8 @@ import Image from "next/image"; import React, { useState } from "react"; +import ReactMarkdown from "react-markdown"; +import rehypeRaw from "rehype-raw"; import { useMapModeStore } from "../../../store/mapModeStore"; const CATEGORY_TOOLTIPS: Record = { @@ -33,12 +35,48 @@ const CurationSection = ({ isAiLoading, }: CurationSectionProps) => { const [hoveredCategory, setHoveredCategory] = useState(null); - const { setMapMode } = useMapModeStore(); + const { setMapMode, selectedProperty } = useMapModeStore(); const sortedMatchedConditions = (conditions || []) .map((cId) => CONDITION_MAP[cId]) .filter(Boolean); + // 실제 점수 데이터 가져오기 (기본값 0) + const scores = { + 안전: selectedProperty?.propertyScores?.safetyScore ?? 0, + 환경: selectedProperty?.propertyScores?.environmentScore ?? 0, + 접근성: selectedProperty?.propertyScores?.accessibilityScore ?? 0, + 편의: selectedProperty?.propertyScores?.convenienceScore ?? 0, + 소음: selectedProperty?.propertyScores?.noiseScore ?? 0, + }; + + // 레이더 차트 좌표 계산 (중심: 80, 80 / 최대 반지름: 80) + const getPoint = (score: number, angle: number) => { + const radius = 80 * (score / 100); + const rad = ((angle - 90) * Math.PI) / 180; + return { + x: 80 + radius * Math.cos(rad), + y: 80 + radius * Math.sin(rad), + }; + }; + + // 5가지 요소의 각도 (안전, 환경, 접근성, 편의, 소음) + const angles = [0, 72, 144, 216, 288]; + const radarScores = [ + scores.안전, + scores.환경, + scores.접근성, + scores.편의, + scores.소음, + ]; + + const points = angles + .map((angle, i) => { + const p = getPoint(radarScores[i], angle); + return `${p.x},${p.y}`; + }) + .join(" "); + return (

@@ -58,35 +96,48 @@ const CurationSection = ({ D.HOME 요약 -
-

- {isAiLoading ? ( - - AI가 매물을 분석하고 있습니다... - - ) : aiSummary ? ( - aiSummary - ) : sortedMatchedConditions.length > 0 ? ( - <> - 해당 지역의{" "} - {sortedMatchedConditions.slice(0, 2).map((label, i) => ( - - - {label} 점수 - - {i === 0 && sortedMatchedConditions.length > 1 - ? " 및 " - : ""} - - ))}{" "} - 높아 거주 만족도가 기대되는 추천 매물입니다! - - ) : ( - - AI가 매물을 분석하고 있습니다... - - )} -

+
+ {isAiLoading ? ( + + AI가 매물을 분석하고 있습니다... + + ) : aiSummary ? ( + ( +

{children}

+ ), + strong: ({ children }) => ( + + {children} + + ), + ul: ({ children }) => ( +
    {children}
+ ), + li: ({ children }) =>
  • {children}
  • , + div: ({ children }) =>
    {children}
    , + }} + > + {aiSummary} +
    + ) : sortedMatchedConditions.length > 0 ? ( +

    + 해당 지역의{" "} + {sortedMatchedConditions.slice(0, 2).map((label, i) => ( + + {label} 점수 + {i === 0 && sortedMatchedConditions.length > 1 ? " 및 " : ""} + + ))}{" "} + 높아 거주 만족도가 기대되는 추천 매물입니다! +

    + ) : ( + + AI가 매물을 분석하고 있습니다... + + )}
    @@ -136,14 +187,11 @@ const CurationSection = ({ - + - {/* Labels & Scores with Tooltips (Mock values as agreed) */} - {/* 1. 안전 (Top - 83) */} + {/* Labels & Scores with Tooltips */} + {/* 1. 안전 */}
    setHoveredCategory("안전")} @@ -167,7 +215,7 @@ const CurationSection = ({
    - 83 + {scores.안전} {hoveredCategory === "안전" && (
    @@ -178,7 +226,7 @@ const CurationSection = ({ )}
    - {/* 2. 환경 (Right-ish - 67) */} + {/* 2. 환경 */}
    setHoveredCategory("환경")} @@ -202,7 +250,7 @@ const CurationSection = ({
    - 67 + {scores.환경} {hoveredCategory === "환경" && (
    @@ -213,7 +261,7 @@ const CurationSection = ({ )}
    - {/* 3. 접근성 (Bottom-Right - 45) */} + {/* 3. 접근성 */}
    setHoveredCategory("접근성")} @@ -237,7 +285,7 @@ const CurationSection = ({
    - 45 + {scores.접근성} {hoveredCategory === "접근성" && (
    @@ -248,7 +296,7 @@ const CurationSection = ({ )}
    - {/* 4. 편의 (Bottom-Left - 60) */} + {/* 4. 편의 */}
    setHoveredCategory("편의")} @@ -272,7 +320,7 @@ const CurationSection = ({
    - 60 + {scores.편의} {hoveredCategory === "편의" && (
    @@ -283,7 +331,7 @@ const CurationSection = ({ )}
    - {/* 5. 소음 (Left-ish - 10) */} + {/* 5. 소음 */}
    setHoveredCategory("소음")} @@ -307,7 +355,7 @@ const CurationSection = ({
    - 10 + {scores.소음} {hoveredCategory === "소음" && (
    diff --git a/app/components/list/list_section/SchoolItem.tsx b/app/components/list/list_section/SchoolItem.tsx index 8662fe4..70a770e 100644 --- a/app/components/list/list_section/SchoolItem.tsx +++ b/app/components/list/list_section/SchoolItem.tsx @@ -11,15 +11,9 @@ interface SchoolItemProps { onClick?: () => void; } -const SchoolItem = ({ - name, - type, - distance, - time, - onClick, -}: SchoolItemProps) => { +const SchoolItem = ({ name, type, distance, onClick }: SchoolItemProps) => { return ( -
    +
    diff --git a/app/components/list/list_section/SchoolSection.tsx b/app/components/list/list_section/SchoolSection.tsx index 8463e6b..0c4bb22 100644 --- a/app/components/list/list_section/SchoolSection.tsx +++ b/app/components/list/list_section/SchoolSection.tsx @@ -74,9 +74,9 @@ const SchoolSection = ({ 서울청구초통학구역
    -
    +
    {currentSchools.map((school, idx) => ( -
    + onOpenSchoolDetail?.(school.name)} /> {idx !== currentSchools.length - 1 && ( -
    +
    )} -
    + ))}
    {/* Exception School Section */}
    -
    +
    학군배정 예외학교
    -
    - {/* Mocked for demonstration mirroring Figma */} +
    onOpenSchoolDetail?.("서울청구초등학교")} /> -
    - {/* Figma Design Indicator (Backdrop Blur + Arrows) */} -
    +
    - + {currentIndex + 1} / {imageUrls.length}
    diff --git a/app/components/list/list_section/detail/ContactModal.tsx b/app/components/list/list_section/detail/ContactModal.tsx index 2917b21..d4a5b46 100644 --- a/app/components/list/list_section/detail/ContactModal.tsx +++ b/app/components/list/list_section/detail/ContactModal.tsx @@ -48,6 +48,7 @@ const ContactModal = ({

    {apartmentName ? `${apartmentName} ` : ""}중개사무소에 연락하여 +
    방문일을 예약하세요

    @@ -88,7 +89,11 @@ const ContactModal = ({
    - {realtorInfo?.phone || realtorInfo?.officePhone || "-"} + {( + realtorInfo?.phone || + realtorInfo?.officePhone || + "-" + ).replace("-", "")}
    diff --git a/app/components/map/FilterBar.tsx b/app/components/map/FilterBar.tsx index 4832e31..8f0313d 100644 --- a/app/components/map/FilterBar.tsx +++ b/app/components/map/FilterBar.tsx @@ -3,11 +3,12 @@ import React, { useState } from "react"; import Image from "next/image"; import TransactionTypeDropdown from "./dropdown/TransactionTypeDropdown"; -import ResidenceTypeDropdown from "./dropdown/ResidenceTypeDropdown"; import SpaceDropdown from "./dropdown/SpaceDropdown"; import { useEffect } from "react"; import { useRef } from "react"; +import { usePropertyStore } from "../../store/propertyStore"; + /** * FilterBar Component * 검색어 입력, 거래 유형, 주거 형태, 평수 필터 및 초기화 버튼을 포함하는 바 @@ -16,23 +17,21 @@ export default function FilterBar() { const dropdownRef = useRef(null); const [openFilter, setOpenFilter] = useState(null); - //(거래 유형 드랍박스 상태값들) - const [selectedTypes, setSelectedTypes] = useState([]); - const [depositRange, setDepositRange] = useState<[number, number]>([0, 0]); // in 10k KRW (만원) - const [monthlyRentRange, setMonthlyRentRange] = useState<[number, number]>([ - 0, 0, - ]); - const [salePriceRange, setSalePriceRange] = useState<[number, number]>([ - 0, 0, - ]); - - //(주거 형태 드랍박스 상태값) - const [selectedResidenceTypes, setSelectedResidenceTypes] = useState< - string[] - >([]); - - //(평수 드랍박스 상태값) - const [spaceRange, setSpaceRange] = useState<[number, number]>([0, 60]); + const { + keyword, + setKeyword, + selectedTypes, + setSelectedTypes, + depositRange, + setDepositRange, + monthlyRentRange, + setMonthlyRentRange, + salePriceRange, + setSalePriceRange, + spaceRange, + setSpaceRange, + resetFilters, + } = usePropertyStore(); const toggleFilter = (filterName: string) => { setOpenFilter(openFilter === filterName ? null : filterName); @@ -95,14 +94,6 @@ export default function FilterBar() { return `${baseLabel}${priceLabel}`; }; - /** - * 주거 형태 버튼 라벨 생성 함수 - */ - const getResidenceTypeLabel = () => { - if (selectedResidenceTypes.length === 0) return "주거 형태"; - return selectedResidenceTypes.join(", "); - }; - /** * 평수 버튼 라벨 생성 함수 */ @@ -119,23 +110,20 @@ export default function FilterBar() { }; const handleReset = () => { - setSelectedTypes([]); - setDepositRange([0, 0]); - setMonthlyRentRange([0, 0]); - setSalePriceRange([0, 0]); - setSelectedResidenceTypes([]); - setSpaceRange([0, 60]); + resetFilters(); setOpenFilter(null); }; return (
    {/* 검색 입력 영역 */} -
    +
    setKeyword(e.target.value)} placeholder="지역, 단지, 지하철역 등을 입력하세요" - className="w-[366px] h-12 pl-5 pr-12 bg-[#F8FAFB] border border-[#E4E4E4] rounded-lg text-[16px] text-[#707070] placeholder:text-[#C4C4C4] focus:outline-none focus:border-[#30CEA1] transition-colors" + className="w-91.5 h-12 pl-5 pr-12 bg-[#F8FAFB] border border-[#E4E4E4] rounded-lg text-[16px] text-[#707070] placeholder:text-[#C4C4C4] focus:outline-none focus:border-[#30CEA1] transition-colors" />
    { + const size = isSelected ? 80 : 70; + const iconSrc = isSelected + ? "/map_marker/clicked_marker.svg" + : "/map_marker/normal_marker.svg"; + + return ( + +
    +
    + + {dealType} + + + {price} + +
    +
    +
    + ); +}; const KakaoMap = () => { - const { selectedProperty } = useMapModeStore(); + const { + isMapMode, + selectedProperty, + propertiesOnMap, + convenienceInfras, + selectedInfra, + setSelectedInfra, + clearSelectedInfra, + environmentInfras, + selectedEnvironment, + setSelectedEnvironment, + clearSelectedEnvironment, + noiseInfras, + populationInfras, + districtAvgNoise, + districtAvgPopulation, + selectedNoise, + setSelectedNoise, + clearSelectedNoise, + selectedPopulation, + setSelectedPopulation, + clearSelectedPopulation, + nearbyNoiseInfras, + selectedNearbyNoise, + setSelectedNearbyNoise, + clearSelectedNearbyNoise, + } = useMapModeStore(); const [map, setMap] = useState(null); + // 편의 시설 마커 아이콘 매핑 + const infraIconMap: Record = { + [FacilityItemInfraType.HOSPITAL]: + "/map_marker/convenience/hospital_marker.svg", + [FacilityItemInfraType.EMERGENCY_MEDICAL_CENTER]: + "/map_marker/convenience/hospital_marker.svg", + [FacilityItemInfraType.MARKET]: "/map_marker/convenience/mart_marker.svg", + [FacilityItemInfraType.CONVENIENCE_STORE]: + "/map_marker/convenience/store_marker.svg", + }; + + const envIconMap: Record = { + WALK: "/map_marker/environment/mountain_marker.svg", + PARK: "/map_marker/environment/park_marker.svg", + }; + + const noiseMarkerIcon = "/map_marker/noise/noise_marker.svg"; + const populationMarkerIcon = "/map_marker/noise/population_marker.svg"; + + const nearbyNoiseIconMap: Record = { + FIRE_STATION: "/map_marker/noise/firestation_marker.svg", + EMERGENCY_CENTER: "/map_marker/noise/hospital_marker.svg", + CONSTRUCTION: "/map_marker/noise/construction_marker.svg", + }; + // 기본 중심값: 약수역 const defaultCenter = { lat: 37.5545, lng: 127.0112 }; @@ -20,7 +140,7 @@ const KakaoMap = () => { appkey: process.env.NEXT_PUBLIC_KAKAO_MAP_API_KEY || "", }); - // 매물이 선택되었을 때 오프셋 적용 (왼쪽 800px 패널 고려) + // 매물이 선택되었을 때 오프셋 적용 useEffect(() => { if (map && selectedProperty) { // 1. 먼저 해당 좌표로 중심 이동 @@ -30,11 +150,13 @@ const KakaoMap = () => { ); map.setCenter(moveLatLng); - // 2. 우측 가시 영역 중앙으로 오게 하기 위해 왼쪽(이미지상 좌측이동 = 지도뷰 우측이동)으로 -400px 패닝 - // Sidebar(800px) / 2 = 400px 만큼 우측으로 밀어줌 - map.panBy(-400, 0); + // 2. 일반 모드일 때만 우측 가시 영역 중앙으로 오게 하기 위해 왼쪽으로 -400px 패닝 + // 종합데이터 모드(isMapMode = true)에서는 사이드바가 없으므로 정중앙 유지 + if (!isMapMode) { + map.panBy(-400, 0); + } } - }, [map, selectedProperty]); + }, [map, selectedProperty, isMapMode]); if (loading) return
    ; @@ -51,12 +173,267 @@ const KakaoMap = () => { style={{ width: "100%", height: "100%" }} level={selectedProperty ? 2 : 3} onCreate={setMap} + onClick={() => { + clearSelectedInfra(); + clearSelectedEnvironment(); + clearSelectedNoise(); + clearSelectedPopulation(); + }} > - {selectedProperty && ( - + {isMapMode ? ( + <> + {/* 종합데이터 모드: 편의 시설 마커 */} + {convenienceInfras.map((infra: FacilityItem) => { + const iconSrc = infra.infraType + ? infraIconMap[infra.infraType] + : null; + if (!iconSrc || !infra.latitude || !infra.longitude) return null; + + return ( + + setSelectedInfra(infra)} + /> + + ); + })} + + {/* 편의 시설 커스텀 오버레이 */} + {selectedInfra && + selectedInfra.latitude && + selectedInfra.longitude && ( + + clearSelectedInfra()} + /> + + )} + + {/* 환경 정보 마커 */} + {environmentInfras.map((item: MapItem) => { + const iconSrc = item.natureType + ? envIconMap[item.natureType] + : null; + if (!iconSrc || !item.latitude || !item.longitude) return null; + + return ( + setSelectedEnvironment(item)} + /> + ); + })} + + {/* 환경 정보 커스텀 오버레이 */} + {selectedEnvironment && + selectedEnvironment.latitude && + selectedEnvironment.longitude && ( + + clearSelectedEnvironment()} + /> + + )} + + {/* 종합데이터 모드: 선택된 매물만 map_marker.svg로 표시 */} + {selectedProperty && ( + + )} + + {noiseInfras.map((item: MapItem) => { + if (!item.latitude || !item.longitude) return null; + return ( + setSelectedNoise(item)} + image={{ + src: noiseMarkerIcon, + size: { width: 300, height: 300 }, + options: { + offset: { x: 150, y: 150 }, + }, + }} + /> + ); + })} + + {selectedNoise && + selectedNoise.latitude && + selectedNoise.longitude && ( + + + + )} + + {/* 유동인구 정보 마커 */} + {populationInfras.map((item: MapItem) => { + if (!item.latitude || !item.longitude) return null; + return ( + setSelectedPopulation(item)} + image={{ + src: populationMarkerIcon, + size: { width: 300, height: 300 }, + options: { + offset: { x: 150, y: 150 }, + }, + }} + /> + ); + })} + + {selectedPopulation && + selectedPopulation.latitude && + selectedPopulation.longitude && ( + + + + )} + + {/* 소음 발생 예상 구간 마커 */} + {nearbyNoiseInfras.map((item: NearbyNoiseItem) => { + const iconSrc = nearbyNoiseIconMap[item.noiseType]; + if (!iconSrc) return null; + + return ( + setSelectedNearbyNoise(item)} + image={{ + src: iconSrc, + size: { width: 44, height: 44 }, + options: { + offset: { x: 22, y: 44 }, + }, + }} + /> + ); + })} + + {selectedNearbyNoise && ( + + + + )} + + ) : ( + <> + {/* 일반 모드: 리스트상의 모든 매물 렌더링 */} + {propertiesOnMap.map((prop) => { + const isSelected = + selectedProperty?.lat === prop.lat && + selectedProperty?.lng === prop.lng; + return ( + + ); + })} + + {/* 선택된 매물이 리스트에 없을 수도 있으므로 별도 렌더링 */} + {selectedProperty && + !propertiesOnMap.find( + (p) => + p.lat === selectedProperty.lat && + p.lng === selectedProperty.lng, + ) && ( + + )} + )} ); diff --git a/app/components/map/MapHeader.tsx b/app/components/map/MapHeader.tsx index 21869bd..d4aff41 100644 --- a/app/components/map/MapHeader.tsx +++ b/app/components/map/MapHeader.tsx @@ -8,8 +8,9 @@ interface MapHeaderProps { title?: string; } -const MapHeader = ({ title = "약수하이츠아파트" }: MapHeaderProps) => { - const { setMapMode } = useMapModeStore(); +const MapHeader = () => { + const { setMapMode, selectedProperty } = useMapModeStore(); + const title = selectedProperty?.title || "매물 위치"; return (
    diff --git a/app/components/map/MapOverlays.tsx b/app/components/map/MapOverlays.tsx new file mode 100644 index 0000000..e95d831 --- /dev/null +++ b/app/components/map/MapOverlays.tsx @@ -0,0 +1,175 @@ +import React from "react"; +import { FacilityItem } from "@/shared/api/generated/model/facilityItem"; +import { NearbyNoiseItem } from "@/app/types/nearby-noise"; + +export interface MapItem { + natureType?: "WALK" | "PARK"; + distanceMeters?: number; + id?: number; + name?: string; + latitude?: number; + longitude?: number; + population?: number; + noise?: number; +} + +export const FacilityOverlay = ({ + infra, + onClose, +}: { + infra: FacilityItem & { roadAddress?: string }; + onClose: () => void; +}) => { + return ( +
    +
    +

    + {infra.name} +

    +
    +
    +

    + {infra.roadAddress || "주소 정보가 없습니다."} +

    +
    +
    + ); +}; + +export const EnvironmentOverlay = ({ + item, + onClose, +}: { + item: MapItem; + onClose: () => void; +}) => { + return ( +
    +
    +

    + {item.name} +

    +
    +
    +

    + {item.distanceMeters && + `${(item.distanceMeters / 1000).toFixed(1)}km`} +

    +

    + 거리에 있습니다. +

    +
    +
    + ); +}; + +export const NoiseOverlay = ({ + item, + districtAvg, + onClose, +}: { + item: MapItem; + districtAvg: number; + onClose: () => void; +}) => { + return ( +
    +
    +

    현재 소음도:

    +

    + {Math.round(item.noise || 0)}dB +

    +
    +
    +
    + 평균 소음도 + + {Math.round(districtAvg)}dB + +
    +
    +
    + ); +}; + +export const PopulationOverlay = ({ + item, + districtAvg, + onClose, +}: { + item: MapItem; + districtAvg: number; + onClose: () => void; +}) => { + return ( +
    +
    +

    현재 유동인구:

    +

    + {item.population?.toLocaleString()}명 +

    +
    +
    +
    + 평균 유동인구 + + {Math.round(districtAvg).toLocaleString()}명 + +
    +
    +
    + ); +}; + +export const NearbyNoiseOverlay = ({ + item, + onClose, +}: { + item: NearbyNoiseItem; + onClose: () => void; +}) => { + const isConstruction = item.noiseType === "CONSTRUCTION"; + const headerColor = "#EA8B2B"; + + return ( +
    +
    +

    {item.name}

    +
    +
    +
    +
    +

    + {(item.distanceMeter / 1000).toFixed(1)}km +

    +

    거리에 있습니다

    +
    + {isConstruction && item.endDate && ( +

    + 공사 종료 예정: {item.endDate} +

    + )} +
    +
    +
    + ); +}; diff --git a/app/components/module/AccessibilityModule.tsx b/app/components/module/AccessibilityModule.tsx index 363913c..6e3623b 100644 --- a/app/components/module/AccessibilityModule.tsx +++ b/app/components/module/AccessibilityModule.tsx @@ -1,6 +1,7 @@ import React from "react"; import Image from "next/image"; import { useModuleStore } from "@/app/store/moduleStore"; +import { useMapModeStore } from "@/app/store/mapModeStore"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -10,7 +11,9 @@ function cn(...inputs: ClassValue[]) { const AccessibilityModule = () => { const { activeModules, toggleModule } = useModuleStore(); + const { selectedProperty } = useMapModeStore(); const isActive = activeModules.includes("accessibility"); + const score = selectedProperty?.propertyScores?.accessibilityScore || 0; return (
    { isActive ? "text-[#0192C8]" : "text-[#555555]", )} > - 82점 + {score}점
    diff --git a/app/components/module/ConvenienceModule.tsx b/app/components/module/ConvenienceModule.tsx index b4c820b..b5b122b 100644 --- a/app/components/module/ConvenienceModule.tsx +++ b/app/components/module/ConvenienceModule.tsx @@ -2,6 +2,9 @@ import React, { useState } from "react"; import Image from "next/image"; import { Check } from "lucide-react"; import { useModuleStore } from "@/app/store/moduleStore"; +import { useMapModeStore } from "@/app/store/mapModeStore"; +import { useGetConvenienceInfra } from "@/shared/api/generated/infra-controller/infra-controller"; +import { useEffect } from "react"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -11,16 +14,69 @@ function cn(...inputs: ClassValue[]) { const ConvenienceModule = () => { const { activeModules, toggleModule } = useModuleStore(); + const { + selectedProperty, + setConvenienceInfras, + clearConvenienceInfras, + clearSelectedInfra, + } = useMapModeStore(); const isActive = activeModules.includes("convenience"); const [selectedTime, setSelectedTime] = useState(5); // 5, 10, 20 + const score = selectedProperty?.propertyScores?.convenienceScore || 0; // Checkbox states for the facilities const [checkedItems, setCheckedItems] = useState({ - store: false, + store: true, // 기본값 true로 설정 (사용자 요청에 따라 마커가 찍혀야 하므로) market: true, - hospital: false, + hospital: true, }); + // 편의 정보 데이터 호출 + const infraTypes: string[] = []; + if (checkedItems.store) infraTypes.push("CONVENIENCE_STORE"); + if (checkedItems.market) infraTypes.push("MARKET"); + if (checkedItems.hospital) infraTypes.push("HOSPITAL"); + + const infraTypesJoined = infraTypes.length > 0 ? infraTypes.join(", ") : ""; + + const { data: infraData } = useGetConvenienceInfra( + { + request: { + propertyId: selectedProperty?.id, + walkMinutes: selectedTime, + infraTypes: (infraTypesJoined as any) || undefined, + }, + } as any, + { + query: { + enabled: isActive && !!selectedProperty?.id, + }, + request: { + params: { + propertyId: selectedProperty?.id, + walkMinutes: selectedTime, + infraTypes: infraTypesJoined || undefined, + }, + }, + }, + ); + + // 데이터가 오면 스토어에 동기화 + useEffect(() => { + if (isActive && infraData?.data?.facilities) { + setConvenienceInfras(infraData.data.facilities); + } else if (!isActive) { + clearConvenienceInfras(); + clearSelectedInfra(); + } + }, [ + isActive, + infraData, + setConvenienceInfras, + clearConvenienceInfras, + clearSelectedInfra, + ]); + const toggleItem = (key: keyof typeof checkedItems) => { setCheckedItems((prev) => ({ ...prev, [key]: !prev[key] })); }; @@ -65,7 +121,7 @@ const ConvenienceModule = () => { isActive ? "text-[#745BCD]" : "text-[#555555]", )} > - 82점 + {score}점
    diff --git a/app/components/module/EnvironmentModule.tsx b/app/components/module/EnvironmentModule.tsx index 46969ff..1324973 100644 --- a/app/components/module/EnvironmentModule.tsx +++ b/app/components/module/EnvironmentModule.tsx @@ -1,6 +1,9 @@ import React from "react"; import Image from "next/image"; import { useModuleStore } from "@/app/store/moduleStore"; +import { useMapModeStore } from "@/app/store/mapModeStore"; +import { useGetEnvironmentTotal } from "@/shared/api/generated/environment-controller/environment-controller"; +import { useEffect } from "react"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -10,8 +13,66 @@ function cn(...inputs: ClassValue[]) { const EnvironmentModule = () => { const { activeModules, toggleModule } = useModuleStore(); + const { + selectedProperty, + setEnvironmentInfras, + clearEnvironmentInfras, + clearSelectedEnvironment, + environmentInfras, + } = useMapModeStore(); const isActive = activeModules.includes("environment"); + const { data: envData } = useGetEnvironmentTotal( + { + propertyId: selectedProperty?.id as number, + distance: 3000, + }, + { + query: { + enabled: isActive && !!selectedProperty?.id, + }, + }, + ); + + useEffect(() => { + if (isActive && envData?.data?.items) { + setEnvironmentInfras(envData.data.items); + } else if (!isActive) { + clearEnvironmentInfras(); + clearSelectedEnvironment(); + } + }, [ + isActive, + envData, + setEnvironmentInfras, + clearEnvironmentInfras, + clearSelectedEnvironment, + ]); + + const score = + envData?.data?.environmentScore || + selectedProperty?.propertyScores?.environmentScore || + 0; + const pmData = envData?.data?.particulateMatter; + + // 도보 시간 계산 (표준: 80m/min) + const calculateWalkTime = (meters?: number) => { + if (!meters) return "-"; + const minutes = Math.ceil(meters / 80); + return `도보 ${minutes}분`; + }; + + const nearestPark = environmentInfras + .filter((item) => item.natureType === "PARK") + .sort((a, b) => (a.distanceMeters || 0) - (b.distanceMeters || 0))[0]; + + const nearestTrail = environmentInfras + .filter((item) => item.natureType === "WALK") + .sort((a, b) => (a.distanceMeters || 0) - (b.distanceMeters || 0))[0]; + + const parkTime = calculateWalkTime(nearestPark?.distanceMeters); + const trailTime = calculateWalkTime(nearestTrail?.distanceMeters); + return (
    { isActive ? "text-[#29AD29]" : "text-[#555555]", )} > - 82점 + {score}점
    @@ -123,7 +184,7 @@ const EnvironmentModule = () => {
    - 도보 3분 + {parkTime}
    @@ -141,7 +202,7 @@ const EnvironmentModule = () => {
    - 도보 10분 + {trailTime}
    @@ -177,21 +238,21 @@ const EnvironmentModule = () => { {[ { name: "폐기물 수집", - time: "도보 8분", + time: "-", icon: "/icons/module/environment/factory.svg", width: 15, height: 15, }, { name: "하수처리장", - time: "도보 10분", + time: "-", icon: "/icons/module/environment/factory.svg", width: 15, height: 15, }, { name: "장례시설", - time: "도보 15분", + time: "-", icon: "/icons/module/environment/funeral.svg", width: 14.571, height: 14.571, @@ -245,7 +306,7 @@ const EnvironmentModule = () => {
    - 15 + {pmData?.pm10 ?? 15} ㎍/㎥ @@ -273,7 +334,7 @@ const EnvironmentModule = () => {
    - 21 + {pmData?.pm25 ?? 21} ㎍/㎥ diff --git a/app/components/module/NoiseModule.tsx b/app/components/module/NoiseModule.tsx index aaed938..a7955a7 100644 --- a/app/components/module/NoiseModule.tsx +++ b/app/components/module/NoiseModule.tsx @@ -1,6 +1,13 @@ -import React from "react"; +import React, { useEffect } from "react"; import Image from "next/image"; import { useModuleStore } from "@/app/store/moduleStore"; +import { useMapModeStore } from "@/app/store/mapModeStore"; +import { + useGetSmartPole, + useGetNearbyNoise, +} from "@/shared/api/generated/noise-controller/noise-controller"; +import { NearbyNoiseData } from "@/app/types/nearby-noise"; +import { MapItem } from "@/app/components/map/MapOverlays"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -10,12 +17,148 @@ function cn(...inputs: ClassValue[]) { const NoiseModule = () => { const { activeModules, toggleModule } = useModuleStore(); + const { + selectedProperty, + debouncedTime, + setNoiseInfras, + setPopulationInfras, + clearNoiseInfras, + clearPopulationInfras, + setDistrictAvgNoise, + setDistrictAvgPopulation, + setNearbyNoiseInfras, + clearNearbyNoiseInfras, + } = useMapModeStore(); const isActive = activeModules.includes("noise"); + const score = selectedProperty?.propertyScores?.noiseScore || 0; + + const { data: smartPoleData } = useGetSmartPole( + { + propertyId: selectedProperty?.id, + time: Math.floor(debouncedTime), + distance: 3000, + weekend: true, + } as unknown as Parameters[0], + { + query: { + enabled: !!selectedProperty?.id && isActive, + }, + }, + ); + + const { data: nearbyNoiseData } = useGetNearbyNoise( + { + propertyId: Number(selectedProperty?.id), + distance: 2000, + }, + { + query: { + enabled: !!selectedProperty?.id && isActive, + }, + }, + ); + + useEffect(() => { + if (isActive && smartPoleData?.data?.noise?.items) { + setNoiseInfras(smartPoleData.data.noise.items); + if (smartPoleData.data.noise.districtAvgNoise !== undefined) { + setDistrictAvgNoise(smartPoleData.data.noise.districtAvgNoise); + } + } else if (!isActive) { + clearNoiseInfras(); + } + }, [ + isActive, + smartPoleData, + setNoiseInfras, + clearNoiseInfras, + setDistrictAvgNoise, + ]); + + useEffect(() => { + if (isActive && smartPoleData?.data?.population?.items) { + setPopulationInfras(smartPoleData.data.population.items); + if (smartPoleData.data.population.districtAvgPopulation !== undefined) { + setDistrictAvgPopulation( + smartPoleData.data.population.districtAvgPopulation, + ); + } + } else if (!isActive) { + clearPopulationInfras(); + } + }, [ + isActive, + smartPoleData, + setPopulationInfras, + clearPopulationInfras, + setDistrictAvgPopulation, + ]); + + useEffect(() => { + const data = nearbyNoiseData?.data as unknown as NearbyNoiseData; + if (isActive && data?.items) { + setNearbyNoiseInfras(data.items); + } else if (!isActive) { + clearNearbyNoiseInfras(); + } + }, [isActive, nearbyNoiseData, setNearbyNoiseInfras, clearNearbyNoiseInfras]); + + const noiseInfo = smartPoleData?.data?.noise; + const populationInfo = smartPoleData?.data?.population; + + // 도보 시간 계산 (표준: 80m/min) + const calculateWalkTime = (meters?: number) => { + if (meters === undefined) return "-"; + const minutes = Math.ceil(meters / 80); + return `도보 ${minutes}분`; + }; + + const { nearbyNoiseInfras } = useMapModeStore(); + + const nearestFireStation = nearbyNoiseInfras + .filter((item) => item.noiseType === "FIRE_STATION") + .sort((a, b) => a.distanceMeter - b.distanceMeter)[0]; + + const nearestHospital = nearbyNoiseInfras + .filter((item) => item.noiseType === "EMERGENCY_CENTER") + .sort((a, b) => a.distanceMeter - b.distanceMeter)[0]; + + const nearestConstruction = nearbyNoiseInfras + .filter((item) => item.noiseType === "CONSTRUCTION") + .sort((a, b) => a.distanceMeter - b.distanceMeter)[0]; + + const nearbyMapItems = [ + { + name: "소방서", + time: calculateWalkTime(nearestFireStation?.distanceMeter), + icon: "/icons/module/noise/fire.svg", + width: 14.75, + height: 14.585, + }, + { + name: "대학병원", + time: calculateWalkTime(nearestHospital?.distanceMeter), + icon: "/icons/module/noise/hospital.svg", + width: 16.2, + height: 14.4, + }, + { + name: nearestConstruction?.name || "공사현장", + time: calculateWalkTime(nearestConstruction?.distanceMeter), + icon: "/icons/module/noise/construction.svg", + sub: nearestConstruction?.endDate + ? `${nearestConstruction.endDate} 종료예정` + : undefined, + width: 15.6, + height: 15.63, + truncate: true, + }, + ]; return (
    { {/* Header Section */}
    @@ -32,7 +175,7 @@ const NoiseModule = () => { {/* Icon Badge */}
    @@ -52,7 +195,7 @@ const NoiseModule = () => { isActive ? "text-[#EA8B2B]" : "text-[#555555]", )} > - 82점 + {score}점
    @@ -60,7 +203,7 @@ const NoiseModule = () => {

    {/* Profile and Quick Stats */} -
    +
    {/* User Profile Card */}

    @@ -112,7 +229,7 @@ const UserInfo = () => {

    {/* Jjim Count Card */} -
    +
    찜한 집 @@ -124,13 +241,19 @@ const UserInfo = () => {
    - like - 12개 + {isLikeLoading ? ( +
    + ) : ( + <> + white heart + {likedCount}개 + + )}
    @@ -155,7 +278,7 @@ const UserInfo = () => { className="flex flex-col items-center gap-2 relative" >
    {idx + 1}
    @@ -273,55 +396,25 @@ const UserInfo = () => {
    -
    - {[1, 2, 3].map((i) => ( -
    -
    -
    - marker -

    서울시 중구

    -
    - house -
    - -
    -

    - 전세 6억 9,000 -

    -
    -

    - 약수 하이츠 104동 -

    -

    - 아파트, 2/20층, 142.65m² -

    -
    -
    - - 소음 - - - 접근성 - -
    -
    -
    - ))} -
    + {isRecentLoading || isCompareLoading ? ( +
    +
    +
    + ) : recentHouses.length > 0 ? ( +
    + {recentHouses.map((house) => ( + router.push(`/map?propertyId=${id}`)} + /> + ))} +
    + ) : ( +
    +

    최근에 본 집이 없습니다.

    +
    + )}
    ); diff --git a/app/components/onboarding/step2/RegionSelectionStep.tsx b/app/components/onboarding/step2/RegionSelectionStep.tsx index a13cd64..cc80992 100644 --- a/app/components/onboarding/step2/RegionSelectionStep.tsx +++ b/app/components/onboarding/step2/RegionSelectionStep.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { useState } from "react"; +import React from "react"; import { ChevronLeft } from "lucide-react"; import { OnboardingProgress } from "../OnboardingProgress"; -import { RegionBadge } from "./RegionBadge"; +import { RegionBadge } from "@/app/components/ui/RegionBadge"; import SeoulMap from "@/app/components/onboarding/step2/SeoulMap"; import { AnimatePresence } from "framer-motion"; import { cn } from "@/app/lib/utils"; diff --git a/app/components/signup/SignupForm.tsx b/app/components/signup/SignupForm.tsx index e8109b0..7c47e89 100644 --- a/app/components/signup/SignupForm.tsx +++ b/app/components/signup/SignupForm.tsx @@ -219,7 +219,7 @@ export const SignupForm = () => { setValue("termsService", val, { shouldValidate: true }) } label={ - + [필수] 디닷홈{" "} 개인 정보 처리 방침을 @@ -227,6 +227,7 @@ export const SignupForm = () => { } /> +
    { setValue("termsPrivacy", val, { shouldValidate: true }) } label={ - + [필수] 디닷홈 약관을 읽고 이를 동의합니다. diff --git a/app/components/signup/TermsCheckbox.tsx b/app/components/signup/TermsCheckbox.tsx index 3e07a3d..8197fa2 100644 --- a/app/components/signup/TermsCheckbox.tsx +++ b/app/components/signup/TermsCheckbox.tsx @@ -25,7 +25,7 @@ export const TermsCheckbox = ({ required = false, }: TermsCheckboxProps) => { return ( -
    +
    onChange(!checked)} diff --git a/app/components/start/InfoBadge.tsx b/app/components/start/InfoBadge.tsx index 67555e1..d9cb582 100644 --- a/app/components/start/InfoBadge.tsx +++ b/app/components/start/InfoBadge.tsx @@ -2,6 +2,7 @@ import React from "react"; import Image from "next/image"; +import { motion } from "framer-motion"; import { cn } from "@/app/lib/utils"; interface InfoBadgeProps { @@ -25,12 +26,20 @@ export default function InfoBadge({ width, }: InfoBadgeProps) { return ( -
    {label} -
    + ); } diff --git a/app/components/start/MainVisual.tsx b/app/components/start/MainVisual.tsx index fcbf74a..7165a0a 100644 --- a/app/components/start/MainVisual.tsx +++ b/app/components/start/MainVisual.tsx @@ -2,9 +2,26 @@ import React from "react"; import Image from "next/image"; -import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useUserStore } from "@/app/store/userStore"; export default function MainVisual() { + const { user } = useUserStore(); + const router = useRouter(); + + const handleStart = () => { + if (!user) { + router.push("/login"); + return; + } + + if (user.onboardingStatus) { + router.push("/map"); + } else { + router.push("/onboarding"); + } + }; + return (
    {/* 로고 (디자인 좌표 반영) */} @@ -32,12 +49,12 @@ export default function MainVisual() {
    - 시작하기 - +
    ); } diff --git a/app/components/ui/RangeSlider.tsx b/app/components/ui/RangeSlider.tsx index 3f1c55e..70f7381 100644 --- a/app/components/ui/RangeSlider.tsx +++ b/app/components/ui/RangeSlider.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useState, useEffect } from "react"; import * as Slider from "@radix-ui/react-slider"; interface RangeSliderProps { @@ -20,23 +20,33 @@ export default function RangeSlider({ formatValue, showUnit = true, }: RangeSliderProps) { + // 드래그 중 실시간 수치 표시를 위한 로컬 상태 + const [internalValue, setInternalValue] = useState<[number, number]>(range); + + // 외부(부모)에서 range가 변경될 경우(예: 초기화) 로컬 상태 동기화 + useEffect(() => { + setInternalValue(range); + }, [range]); + return (
    {title} - {formatValue(range[0])} - {showUnit && title !== "매매가" ? "원" : ""} ~ {formatValue(range[1])} + {formatValue(internalValue[0])} + {showUnit && title !== "매매가" ? "원" : ""} ~{" "} + {formatValue(internalValue[1])}
    setRange(value as [number, number])} + onValueChange={(value) => setInternalValue(value as [number, number])} + onValueCommit={(value) => setRange(value as [number, number])} > diff --git a/app/components/onboarding/step2/RegionBadge.tsx b/app/components/ui/RegionBadge.tsx similarity index 52% rename from app/components/onboarding/step2/RegionBadge.tsx rename to app/components/ui/RegionBadge.tsx index ac15427..fc00d5b 100644 --- a/app/components/onboarding/step2/RegionBadge.tsx +++ b/app/components/ui/RegionBadge.tsx @@ -3,26 +3,37 @@ import React from "react"; import { X } from "lucide-react"; import { motion } from "framer-motion"; +import { cn } from "@/app/lib/utils"; interface RegionBadgeProps { name: string; onRemove: () => void; + size?: "sm" | "md"; } -export const RegionBadge = ({ name, onRemove }: RegionBadgeProps) => { +export const RegionBadge = ({ + name, + onRemove, + size = "md", +}: RegionBadgeProps) => { + const isSmall = size === "sm"; + return ( - {name} + {name} ); diff --git a/app/globals.css b/app/globals.css index 3742c91..3bd33db 100644 --- a/app/globals.css +++ b/app/globals.css @@ -18,12 +18,14 @@ --color-border-1: #d9d9d9; } -@layer utilities { - .no-scrollbar::-webkit-scrollbar { - display: none; - } - .no-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ - } +/* 스크롤바 숨기기 (기능은 유지) */ +.no-scrollbar { + -ms-overflow-style: none; /* IE or Edge */ + scrollbar-width: none; /* Firefox */ +} + +.no-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ + width: 0; + height: 0; } diff --git a/app/layout.tsx b/app/layout.tsx index c6dda3e..27aaa54 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -18,7 +18,7 @@ export default function RootLayout({ }>) { return ( - + {children} diff --git a/app/login/actions.ts b/app/login/actions.ts index 467e5b5..bae22e6 100644 --- a/app/login/actions.ts +++ b/app/login/actions.ts @@ -47,6 +47,7 @@ export async function loginAction(data: LoginFormValues) { username: response.data.user.userName, preferredType: response.data.user.propertyType, preferredConditions: response.data.user.preferredConditions, + onboardingStatus: response.data.user.onboardingStatus, }, }; } catch (error: unknown) { diff --git a/app/login/page.tsx b/app/login/page.tsx index 1d1a7b9..6595532 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -41,10 +41,15 @@ const LoginPage = () => { username: res.user.username, preferredType: res.user.preferredType, preferredConditions: res.user.preferredConditions, + onboardingStatus: res.user.onboardingStatus, }); - // 로그인 성공 시 메인 또는 지도 페이지로 이동 - router.push("/map"); + // 온보딩 상태에 따른 리다이렉션 + if (res.user.onboardingStatus) { + router.push("/map"); + } else { + router.push("/onboarding"); + } router.refresh(); } else { alert(res.message); diff --git a/app/map/page.tsx b/app/map/page.tsx index 5928643..409fbf7 100644 --- a/app/map/page.tsx +++ b/app/map/page.tsx @@ -20,43 +20,58 @@ import { Toast } from "../components/ui/Toast"; const MapPage = () => { const { isMapMode } = useMapModeStore(); const { user } = useUserStore(); - const { - activeModules, - toggleModule, - toastMessage, - setToastMessage, - getDisplayOrder, - } = useModuleStore(); + const { resetModules, toastMessage, setToastMessage, getDisplayOrder } = + useModuleStore(); const initialActivatedRef = useRef(false); + const listInitialActivatedRef = useRef(false); - // 초기 진입 시 온보딩 우선순위 활성화 로직 + // 초기 진입 및 모드 전환 시 우선순위 활성화 로직 useEffect(() => { - if ( - user?.preferredConditions && - user.preferredConditions.length > 0 && - !initialActivatedRef.current && - activeModules.length === 0 - ) { - const idMap: Record = { - 1: "noise", - 2: "environment", - 3: "safety", - 4: "accessibility", - 5: "convenience", - }; - - // 온보딩에서 선택한 우선순위들을 활성화 (최대 3개 권장이나 온보딩 데이터 전체 반영) - user.preferredConditions.forEach((priorityId) => { - const moduleId = idMap[priorityId]; - if (moduleId && !activeModules.includes(moduleId)) { - toggleModule(moduleId); + if (!user?.preferredConditions || user.preferredConditions.length === 0) + return; + + const idMap: Record = { + 1: "noise", + 2: "environment", + 3: "safety", + 4: "accessibility", + 5: "convenience", + }; + + if (isMapMode) { + // 종합데이터 모드 진입 시: 첫 번째 우선순위 모듈만 활성화 + if (!initialActivatedRef.current) { + const firstPriorityId = user.preferredConditions[0]; + const moduleId = idMap[firstPriorityId]; + + if (moduleId) { + resetModules([moduleId]); } - }); + initialActivatedRef.current = true; + } + + // 리스트 뷰 초기화 플래그 리셋 (다시 돌아갈 때 다시 켜지도록) + if (listInitialActivatedRef.current) { + listInitialActivatedRef.current = false; + } + } else { + // 리스트 뷰 (초기 혹은 복귀): 온보딩 모든 우선순위 활성화 + if (!listInitialActivatedRef.current) { + const preferredModuleIds = user.preferredConditions + .map((id) => idMap[id]) + .filter(Boolean) as ModuleId[]; + + resetModules(preferredModuleIds); + listInitialActivatedRef.current = true; + } - initialActivatedRef.current = true; + // 종합데이터 모드 초기화 플래그 리셋 + if (initialActivatedRef.current) { + initialActivatedRef.current = false; + } } - }, [user, activeModules, toggleModule]); + }, [isMapMode, user, resetModules]); const renderModule = (id: ModuleId) => { switch (id) { diff --git a/app/mypage/layout.tsx b/app/mypage/layout.tsx index 6216b34..66bfb53 100644 --- a/app/mypage/layout.tsx +++ b/app/mypage/layout.tsx @@ -1,11 +1,22 @@ "use client"; -import React from "react"; +import React, { useEffect, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; import MyPageSidebar from "../components/mypage/MyPageSidebar"; import { MyPageProvider, useMyPage } from "./MyPageContext"; const MyPageLayoutContent = ({ children }: { children: React.ReactNode }) => { const { currentMenu, setCurrentMenu } = useMyPage(); + const searchParams = useSearchParams(); + const menuParam = searchParams.get("menu"); + + useEffect(() => { + if (menuParam === "liked") { + setCurrentMenu("찜한 집"); + // URL 파라미터 처리 후 깔끔하게 제거하고 싶을 수 있으나, + // 일단은 동기화에 집중합니다. + } + }, [menuParam, setCurrentMenu]); return (
    @@ -29,10 +40,12 @@ const MyPageLayoutContent = ({ children }: { children: React.ReactNode }) => { const Layout = ({ children }: { children: React.ReactNode }) => { return ( -
    +
    - {children} + + {children} +
    diff --git a/app/store/mapModeStore.ts b/app/store/mapModeStore.ts index cbc551d..b22efb4 100644 --- a/app/store/mapModeStore.ts +++ b/app/store/mapModeStore.ts @@ -1,9 +1,17 @@ import { create } from "zustand"; +import { PropertyScore } from "@/shared/api/generated/model/propertyScore"; +import { FacilityItem } from "@/shared/api/generated/model/facilityItem"; +import { MapItem } from "../components/map/MapOverlays"; +import { NearbyNoiseItem } from "../types/nearby-noise"; interface SelectedProperty { + id: number; lat: number; lng: number; title: string; + price?: string; + dealType?: string; + propertyScores?: PropertyScore; } interface MapModeState { @@ -12,12 +20,106 @@ interface MapModeState { selectedProperty: SelectedProperty | null; setSelectedProperty: (property: SelectedProperty) => void; clearSelectedProperty: () => void; + propertiesOnMap: (SelectedProperty & { id: number })[]; + setPropertiesOnMap: ( + properties: (SelectedProperty & { id: number })[], + ) => void; + clearPropertiesOnMap: () => void; + convenienceInfras: FacilityItem[]; + setConvenienceInfras: (infras: FacilityItem[]) => void; + clearConvenienceInfras: () => void; + selectedInfra: FacilityItem | null; + setSelectedInfra: (infra: FacilityItem | null) => void; + clearSelectedInfra: () => void; + environmentInfras: MapItem[]; + setEnvironmentInfras: (infras: MapItem[]) => void; + clearEnvironmentInfras: () => void; + selectedEnvironment: MapItem | null; + setSelectedEnvironment: (item: MapItem | null) => void; + clearSelectedEnvironment: () => void; + currentTime: number; + setCurrentTime: (time: number) => void; + debouncedTime: number; + setDebouncedTime: (time: number) => void; + noiseInfras: MapItem[]; + setNoiseInfras: (infras: MapItem[]) => void; + clearNoiseInfras: () => void; + populationInfras: MapItem[]; + setPopulationInfras: (infras: MapItem[]) => void; + clearPopulationInfras: () => void; + districtAvgNoise: number; + setDistrictAvgNoise: (avg: number) => void; + districtAvgPopulation: number; + setDistrictAvgPopulation: (avg: number) => void; + selectedNoise: MapItem | null; + setSelectedNoise: (item: MapItem | null) => void; + clearSelectedNoise: () => void; + selectedPopulation: MapItem | null; + setSelectedPopulation: (item: MapItem | null) => void; + clearSelectedPopulation: () => void; + + // 소음 발생 예상 구간 정보 + nearbyNoiseInfras: NearbyNoiseItem[]; + setNearbyNoiseInfras: (items: NearbyNoiseItem[]) => void; + clearNearbyNoiseInfras: () => void; + selectedNearbyNoise: NearbyNoiseItem | null; + setSelectedNearbyNoise: (item: NearbyNoiseItem) => void; + clearSelectedNearbyNoise: () => void; } export const useMapModeStore = create((set) => ({ isMapMode: false, - setMapMode: (isMapMode) => set({ isMapMode }), + setMapMode: (isMapMode: boolean) => set({ isMapMode }), selectedProperty: null, - setSelectedProperty: (selectedProperty) => set({ selectedProperty }), + setSelectedProperty: (selectedProperty: SelectedProperty) => + set({ selectedProperty }), clearSelectedProperty: () => set({ selectedProperty: null }), + propertiesOnMap: [], + setPropertiesOnMap: (properties: (SelectedProperty & { id: number })[]) => + set({ propertiesOnMap: properties }), + clearPropertiesOnMap: () => set({ propertiesOnMap: [] }), + convenienceInfras: [], + setConvenienceInfras: (infras: FacilityItem[]) => + set({ convenienceInfras: infras }), + clearConvenienceInfras: () => set({ convenienceInfras: [] }), + selectedInfra: null, + setSelectedInfra: (infra: FacilityItem | null) => + set({ selectedInfra: infra }), + clearSelectedInfra: () => set({ selectedInfra: null }), + environmentInfras: [], + setEnvironmentInfras: (infras: MapItem[]) => + set({ environmentInfras: infras }), + clearEnvironmentInfras: () => set({ environmentInfras: [] }), + selectedEnvironment: null, + setSelectedEnvironment: (item: MapItem | null) => + set({ selectedEnvironment: item }), + clearSelectedEnvironment: () => set({ selectedEnvironment: null }), + currentTime: 12, + setCurrentTime: (currentTime: number) => set({ currentTime }), + debouncedTime: 12, + setDebouncedTime: (debouncedTime: number) => set({ debouncedTime }), + noiseInfras: [], + setNoiseInfras: (infras: MapItem[]) => set({ noiseInfras: infras }), + clearNoiseInfras: () => set({ noiseInfras: [] }), + populationInfras: [], + setPopulationInfras: (infras: MapItem[]) => set({ populationInfras: infras }), + clearPopulationInfras: () => set({ populationInfras: [] }), + districtAvgNoise: 0, + setDistrictAvgNoise: (districtAvgNoise: number) => set({ districtAvgNoise }), + districtAvgPopulation: 0, + setDistrictAvgPopulation: (districtAvgPopulation: number) => + set({ districtAvgPopulation }), + selectedNoise: null, + setSelectedNoise: (selectedNoise: MapItem | null) => set({ selectedNoise }), + clearSelectedNoise: () => set({ selectedNoise: null }), + selectedPopulation: null, + setSelectedPopulation: (item) => set({ selectedPopulation: item }), + clearSelectedPopulation: () => set({ selectedPopulation: null }), + + nearbyNoiseInfras: [], + setNearbyNoiseInfras: (items) => set({ nearbyNoiseInfras: items }), + clearNearbyNoiseInfras: () => set({ nearbyNoiseInfras: [] }), + selectedNearbyNoise: null, + setSelectedNearbyNoise: (item) => set({ selectedNearbyNoise: item }), + clearSelectedNearbyNoise: () => set({ selectedNearbyNoise: null }), })); diff --git a/app/store/moduleStore.ts b/app/store/moduleStore.ts index 9bccddf..8a2a248 100644 --- a/app/store/moduleStore.ts +++ b/app/store/moduleStore.ts @@ -27,6 +27,8 @@ interface ModuleState { toastMessage: string | null; toggleModule: (id: ModuleId) => void; + resetModules: (ids?: ModuleId[]) => void; + resetToUserPreference: (preferredConditions: number[]) => void; // 사용자 선호도 기반 초기화 setToastMessage: (message: string | null) => void; getDisplayOrder: () => ModuleId[]; // 렌더링 순서 계산 (활성 모듈 우선 + 미활성 모듈) } @@ -45,21 +47,34 @@ export const useModuleStore = create((set, get) => ({ activeModules: activeModules.filter((m) => m !== id), }); } else { - // 켜는 경우: 맨 앞에 추가 - const newActiveModules = [id, ...activeModules]; - - // 정책: 4번째 정보를 켤 시 토스트 알림 - if (newActiveModules.length === 4) { + // 켜는 경우: 이미 3개인 경우 추가 차단 + if (activeModules.length >= 3) { set({ - toastMessage: - "필요한 정보에 집중할 수 있도록 정보는 최대 3개 선택을 권장합니다.", + toastMessage: "필요한 정보는 최대 3개까지만 선택할 수 있습니다.", }); + return; } - - set({ activeModules: newActiveModules }); + // 3개 미만인 경우만 추가 + set({ activeModules: [id, ...activeModules] }); } }, + resetModules: (ids = []) => set({ activeModules: ids }), + + resetToUserPreference: (preferredConditions) => { + const idMap: Record = { + 1: "noise", + 2: "environment", + 3: "safety", + 4: "accessibility", + 5: "convenience", + }; + const mappedModules = preferredConditions + .map((id) => idMap[id]) + .filter((m): m is ModuleId => !!m); + set({ activeModules: mappedModules }); + }, + setToastMessage: (message) => set({ toastMessage: message }), getDisplayOrder: () => { diff --git a/app/store/propertyStore.ts b/app/store/propertyStore.ts index 3cbc67c..dbc2644 100644 --- a/app/store/propertyStore.ts +++ b/app/store/propertyStore.ts @@ -3,9 +3,39 @@ import { GetMainPropertyPropertyType } from "@/shared/api/generated/model/getMai import { useUserStore } from "./userStore"; interface PropertyState { + // 기존 상태 selectedPropertyType: GetMainPropertyPropertyType; setPropertyType: (type: GetMainPropertyPropertyType) => void; syncWithUserPreference: () => void; + + // 검색 및 필터 상태 추가 + keyword: string; + setKeyword: (keyword: string) => void; + + selectedTypes: string[]; // ['월세', '전세', '매매'] + setSelectedTypes: (types: string[] | ((prev: string[]) => string[])) => void; + + depositRange: [number, number]; + setDepositRange: ( + range: [number, number] | ((prev: [number, number]) => [number, number]), + ) => void; + + monthlyRentRange: [number, number]; + setMonthlyRentRange: ( + range: [number, number] | ((prev: [number, number]) => [number, number]), + ) => void; + + salePriceRange: [number, number]; + setSalePriceRange: ( + range: [number, number] | ((prev: [number, number]) => [number, number]), + ) => void; + + spaceRange: [number, number]; + setSpaceRange: ( + range: [number, number] | ((prev: [number, number]) => [number, number]), + ) => void; + + resetFilters: () => void; } export const usePropertyStore = create()((set) => ({ @@ -19,4 +49,55 @@ export const usePropertyStore = create()((set) => ({ }); } }, + + // 검색 및 필터 초기값 + keyword: "", + setKeyword: (keyword) => set({ keyword }), + + selectedTypes: [], + setSelectedTypes: (payload) => + set((state) => ({ + selectedTypes: + typeof payload === "function" ? payload(state.selectedTypes) : payload, + })), + + depositRange: [0, 100000], + setDepositRange: (payload) => + set((state) => ({ + depositRange: + typeof payload === "function" ? payload(state.depositRange) : payload, + })), + + monthlyRentRange: [0, 500], + setMonthlyRentRange: (payload) => + set((state) => ({ + monthlyRentRange: + typeof payload === "function" + ? payload(state.monthlyRentRange) + : payload, + })), + + salePriceRange: [0, 1000000], + setSalePriceRange: (payload) => + set((state) => ({ + salePriceRange: + typeof payload === "function" ? payload(state.salePriceRange) : payload, + })), + + spaceRange: [0, 60], + setSpaceRange: (payload) => + set((state) => ({ + spaceRange: + typeof payload === "function" ? payload(state.spaceRange) : payload, + })), + + resetFilters: () => + set({ + keyword: "", + selectedTypes: [], + depositRange: [0, 100000], + monthlyRentRange: [0, 500], + salePriceRange: [0, 1000000], + spaceRange: [0, 60], + }), })); diff --git a/app/store/userStore.ts b/app/store/userStore.ts index 18edc0a..4c0800a 100644 --- a/app/store/userStore.ts +++ b/app/store/userStore.ts @@ -11,6 +11,7 @@ interface User { username: string; preferredType: string; preferredConditions: number[]; + onboardingStatus: boolean; } /** diff --git a/app/types/nearby-noise.ts b/app/types/nearby-noise.ts new file mode 100644 index 0000000..3e134a2 --- /dev/null +++ b/app/types/nearby-noise.ts @@ -0,0 +1,23 @@ +//DTO가 동기화되지 않음에 따라 임시 선언 + +export type NearbyNoiseType = + | "FIRE_STATION" + | "EMERGENCY_CENTER" + | "CONSTRUCTION"; + +export interface NearbyNoiseItem { + noiseType: NearbyNoiseType; + id: number; + name: string; + latitude: number; + longitude: number; + distanceMeter: number; + endDate: string | null; +} + +export interface NearbyNoiseData { + enabled: boolean; + noiseScore: number; + radiusMeters: number; + items: NearbyNoiseItem[]; +} diff --git a/package.json b/package.json index 9529a27..64faeaf 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "react-dom": "19.2.3", "react-hook-form": "^7.71.1", "react-kakao-maps-sdk": "^1.2.0", + "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", "tailwind-merge": "^3.4.0", "zod": "^4.3.6", "zustand": "^5.0.11" diff --git a/public/icons/common/like.svg b/public/icons/common/like.svg new file mode 100644 index 0000000..761d035 --- /dev/null +++ b/public/icons/common/like.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/common/unlike_border.svg b/public/icons/common/unlike_border.svg new file mode 100644 index 0000000..7ea69d0 --- /dev/null +++ b/public/icons/common/unlike_border.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/common/white_heart.svg b/public/icons/common/white_heart.svg new file mode 100644 index 0000000..c45d098 --- /dev/null +++ b/public/icons/common/white_heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/map_marker/clicked_marker.svg b/public/map_marker/clicked_marker.svg new file mode 100644 index 0000000..ca0cd86 --- /dev/null +++ b/public/map_marker/clicked_marker.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/convenience/hospital_marker.svg b/public/map_marker/convenience/hospital_marker.svg new file mode 100644 index 0000000..2ac5925 --- /dev/null +++ b/public/map_marker/convenience/hospital_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/convenience/mart_marker.svg b/public/map_marker/convenience/mart_marker.svg new file mode 100644 index 0000000..139b932 --- /dev/null +++ b/public/map_marker/convenience/mart_marker.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/convenience/store_marker.svg b/public/map_marker/convenience/store_marker.svg new file mode 100644 index 0000000..929edf2 --- /dev/null +++ b/public/map_marker/convenience/store_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/environment/funeral_marker.svg b/public/map_marker/environment/funeral_marker.svg new file mode 100644 index 0000000..936a99a --- /dev/null +++ b/public/map_marker/environment/funeral_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/environment/mountain_marker.svg b/public/map_marker/environment/mountain_marker.svg new file mode 100644 index 0000000..9d9ec96 --- /dev/null +++ b/public/map_marker/environment/mountain_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/environment/park_marker.svg b/public/map_marker/environment/park_marker.svg new file mode 100644 index 0000000..97ab888 --- /dev/null +++ b/public/map_marker/environment/park_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/environment/waste_marker.svg b/public/map_marker/environment/waste_marker.svg new file mode 100644 index 0000000..ccc1c6b --- /dev/null +++ b/public/map_marker/environment/waste_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/map_marker.svg b/public/map_marker/map_marker.svg new file mode 100644 index 0000000..d70b905 --- /dev/null +++ b/public/map_marker/map_marker.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/noise/construction_marker.svg b/public/map_marker/noise/construction_marker.svg new file mode 100644 index 0000000..5a84ac7 --- /dev/null +++ b/public/map_marker/noise/construction_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/noise/firestation_marker.svg b/public/map_marker/noise/firestation_marker.svg new file mode 100644 index 0000000..c1cc914 --- /dev/null +++ b/public/map_marker/noise/firestation_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/noise/hospital_marker.svg b/public/map_marker/noise/hospital_marker.svg new file mode 100644 index 0000000..87723bc --- /dev/null +++ b/public/map_marker/noise/hospital_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/noise/noise_marker.svg b/public/map_marker/noise/noise_marker.svg new file mode 100644 index 0000000..a318406 --- /dev/null +++ b/public/map_marker/noise/noise_marker.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/noise/population_marker.svg b/public/map_marker/noise/population_marker.svg new file mode 100644 index 0000000..919d0e1 --- /dev/null +++ b/public/map_marker/noise/population_marker.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/noise/road_marker.svg b/public/map_marker/noise/road_marker.svg new file mode 100644 index 0000000..063d62e --- /dev/null +++ b/public/map_marker/noise/road_marker.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/map_marker/normal_marker.svg b/public/map_marker/normal_marker.svg new file mode 100644 index 0000000..167c230 --- /dev/null +++ b/public/map_marker/normal_marker.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/shared/api/auth.ts b/shared/api/auth.ts index 165e7ce..ec44724 100644 --- a/shared/api/auth.ts +++ b/shared/api/auth.ts @@ -21,6 +21,7 @@ export interface LoginResponse { userName: string; propertyType: string; preferredConditions: number[]; + onboardingStatus: boolean; }; }; error?: { diff --git a/shared/api/axios-instance.ts b/shared/api/axios-instance.ts index cc9bb22..28c9e3d 100644 --- a/shared/api/axios-instance.ts +++ b/shared/api/axios-instance.ts @@ -33,6 +33,28 @@ export const customInstance = ( options?: AxiosRequestConfig, ): Promise => { const source = axios.CancelToken.source(); + + // GET 요청 시 파라미터 가공 + if (config.method?.toLowerCase() === "get" && config.params) { + let flattenedParams = { ...config.params }; + + // 1. requestDTO 중첩 제거 (Flattening) + if (flattenedParams.requestDTO) { + const { requestDTO, ...rest } = flattenedParams; + flattenedParams = { ...rest, ...requestDTO }; + } + + // 2. 배열 직렬화 변경 (Comma-separated) + Object.keys(flattenedParams).forEach((key) => { + const value = flattenedParams[key]; + if (Array.isArray(value)) { + flattenedParams[key] = value.join(","); + } + }); + + config.params = flattenedParams; + } + const promise = AXIOS_INSTANCE({ ...config, ...options, diff --git a/shared/api/generated/environment-controller/environment-controller.ts b/shared/api/generated/environment-controller/environment-controller.ts index da57345..1fb1dcd 100644 --- a/shared/api/generated/environment-controller/environment-controller.ts +++ b/shared/api/generated/environment-controller/environment-controller.ts @@ -6,14 +6,21 @@ * OpenAPI spec version: v1.0.0 */ import { - useMutation + useMutation, + useQuery } from '@tanstack/react-query' import type { MutationFunction, + QueryFunction, + QueryKey, UseMutationOptions, - UseMutationResult + UseMutationResult, + UseQueryOptions, + UseQueryResult } from '@tanstack/react-query' import type { + GetEnvironmentTotalParams, + ResponseDTOEnvironmentTotalDTO, SaveWasteFacilityParams } from '.././model' import { customInstance } from '../../axios-instance'; @@ -73,4 +80,59 @@ const {mutation: mutationOptions, request: requestOptions} = options ?? {}; return useMutation(mutationOptions); } - \ No newline at end of file + export const getEnvironmentTotal = ( + params: GetEnvironmentTotalParams, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customInstance( + {url: `/api/v1/environment/total`, method: 'GET', + params, signal + }, + options); + } + + +export const getGetEnvironmentTotalQueryKey = (params: GetEnvironmentTotalParams,) => { + return [`/api/v1/environment/total`, ...(params ? [params]: [])] as const; + } + + +export const getGetEnvironmentTotalQueryOptions = >, TError = ErrorType>(params: GetEnvironmentTotalParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetEnvironmentTotalQueryKey(params); + + + + const queryFn: QueryFunction>> = ({ signal }) => getEnvironmentTotal(params, requestOptions, signal); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } +} + +export type GetEnvironmentTotalQueryResult = NonNullable>> +export type GetEnvironmentTotalQueryError = ErrorType + +export const useGetEnvironmentTotal = >, TError = ErrorType>( + params: GetEnvironmentTotalParams, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + + ): UseQueryResult & { queryKey: QueryKey } => { + + const queryOptions = getGetEnvironmentTotalQueryOptions(params,options) + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + diff --git a/shared/api/generated/model/detailPropertyDTO.ts b/shared/api/generated/model/detailPropertyDTO.ts index d41d27a..8d24ecb 100644 --- a/shared/api/generated/model/detailPropertyDTO.ts +++ b/shared/api/generated/model/detailPropertyDTO.ts @@ -10,6 +10,7 @@ import type { Facility } from './facility'; import type { PropertyImages } from './propertyImages'; import type { Option } from './option'; import type { PropertyInfo } from './propertyInfo'; +import type { PropertyScore } from './propertyScore'; import type { RealtorInfo } from './realtorInfo'; export interface DetailPropertyDTO { @@ -19,5 +20,6 @@ export interface DetailPropertyDTO { images?: PropertyImages; option?: Option; propertyInfo?: PropertyInfo; + propertyScore?: PropertyScore; realtorInfo?: RealtorInfo; } diff --git a/shared/api/generated/model/environmentTotalDTO.ts b/shared/api/generated/model/environmentTotalDTO.ts new file mode 100644 index 0000000..b3d3673 --- /dev/null +++ b/shared/api/generated/model/environmentTotalDTO.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ +import type { Item } from './item'; +import type { ParticulateMatter } from './particulateMatter'; + +export interface EnvironmentTotalDTO { + enabled?: boolean; + environmentScore?: number; + items?: Item[]; + particulateMatter?: ParticulateMatter; + radiusMeters?: number; +} diff --git a/shared/api/generated/model/getEnvironmentTotalParams.ts b/shared/api/generated/model/getEnvironmentTotalParams.ts new file mode 100644 index 0000000..b487fe3 --- /dev/null +++ b/shared/api/generated/model/getEnvironmentTotalParams.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ + +export type GetEnvironmentTotalParams = { +propertyId: number; +distance: number; +}; diff --git a/shared/api/generated/model/index.ts b/shared/api/generated/model/index.ts index ff3aa57..b6bbcb4 100644 --- a/shared/api/generated/model/index.ts +++ b/shared/api/generated/model/index.ts @@ -16,6 +16,7 @@ export * from './dealRequestDTOTradeType'; export * from './destinationDTO'; export * from './detailPropertyDTO'; export * from './districtRegisterDTO'; +export * from './environmentTotalDTO'; export * from './errorDTO'; export * from './facility'; export * from './facilityItem'; @@ -23,6 +24,7 @@ export * from './facilityItemInfraType'; export * from './facilityList'; export * from './facilityListFacilityType'; export * from './getConvenienceInfraParams'; +export * from './getEnvironmentTotalParams'; export * from './getMainPropertyParams'; export * from './getMainPropertyPropertyType'; export * from './getNearbyNoiseParams'; @@ -41,6 +43,8 @@ export * from './onboardRequestDTOPropertyType'; export * from './option'; export * from './optionList'; export * from './optionListOptionType'; +export * from './particulateMatter'; +export * from './policeModalResponse'; export * from './propertyCompareDTO'; export * from './propertyCompareDTOOrientation'; export * from './propertyImages'; @@ -56,6 +60,7 @@ export * from './propertyRegisterRequestDTOFacilitiesItem'; export * from './propertyRegisterRequestDTOOptionsItem'; export * from './propertyRegisterRequestDTOOrientation'; export * from './propertyRegisterRequestDTOPropertyType'; +export * from './propertyScore'; export * from './propertySearchRequestDTO'; export * from './propertySearchRequestDTODealType'; export * from './propertySearchRequestDTOPropertyType'; @@ -64,15 +69,18 @@ export * from './recentViewParams'; export * from './responseDTOCurationResponse'; export * from './responseDTODestinationDTO'; export * from './responseDTODetailPropertyDTO'; +export * from './responseDTOEnvironmentTotalDTO'; export * from './responseDTOListPropertyCompareDTO'; export * from './responseDTOListPropertyListDTO'; export * from './responseDTOMainPropertyResponseDTO'; export * from './responseDTONearbyInfraDTO'; export * from './responseDTONearbyNoiseDTO'; +export * from './responseDTOSafetyModalResponse'; export * from './responseDTOSmartPoleResponseDTO'; export * from './responseDTOTestResponseDTO'; export * from './responseDTOVoid'; export * from './responseDTOVoidData'; +export * from './safetyModalResponse'; export * from './saveCctvInfraParams'; export * from './saveConstructionInfraParams'; export * from './saveConvenienceStoreInfraParams'; diff --git a/shared/api/generated/model/item.ts b/shared/api/generated/model/item.ts index b91d7a3..87a093e 100644 --- a/shared/api/generated/model/item.ts +++ b/shared/api/generated/model/item.ts @@ -7,8 +7,10 @@ */ export interface Item { - distanceMeter?: number; + natureType?: "WALK" | "PARK"; + distanceMeters?: number; id?: number; + name?: string; latitude?: number; longitude?: number; population?: number; diff --git a/shared/api/generated/model/particulateMatter.ts b/shared/api/generated/model/particulateMatter.ts new file mode 100644 index 0000000..3dcf2bc --- /dev/null +++ b/shared/api/generated/model/particulateMatter.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ + +export interface ParticulateMatter { + pm10?: number; + pm25?: number; +} diff --git a/shared/api/generated/model/policeModalResponse.ts b/shared/api/generated/model/policeModalResponse.ts new file mode 100644 index 0000000..b731346 --- /dev/null +++ b/shared/api/generated/model/policeModalResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ + +export interface PoliceModalResponse { + durationTime?: number; + latitude?: number; + longitude?: number; + policeOfficeName?: string; +} diff --git a/shared/api/generated/model/propertyListDTO.ts b/shared/api/generated/model/propertyListDTO.ts index 8506bea..f43dae5 100644 --- a/shared/api/generated/model/propertyListDTO.ts +++ b/shared/api/generated/model/propertyListDTO.ts @@ -5,8 +5,8 @@ * Dingle API * OpenAPI spec version: v1.0.0 */ -import type { DealInfo } from "./dealInfo"; -import type { PropertyListDTOPropertyType } from "./propertyListDTOPropertyType"; +import type { DealInfo } from './dealInfo'; +import type { PropertyListDTOPropertyType } from './propertyListDTOPropertyType'; export interface PropertyListDTO { apartmentName?: string; @@ -18,5 +18,4 @@ export interface PropertyListDTO { propertyType?: PropertyListDTOPropertyType; supplyArea?: number; totalFloor?: number; - address?: string; } diff --git a/shared/api/generated/model/propertyScore.ts b/shared/api/generated/model/propertyScore.ts new file mode 100644 index 0000000..6a78f7f --- /dev/null +++ b/shared/api/generated/model/propertyScore.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ + +export interface PropertyScore { + accessibilityScore?: number; + convenienceScore?: number; + environmentScore?: number; + noiseScore?: number; + safetyScore?: number; +} diff --git a/shared/api/generated/model/responseDTOEnvironmentTotalDTO.ts b/shared/api/generated/model/responseDTOEnvironmentTotalDTO.ts new file mode 100644 index 0000000..573ff79 --- /dev/null +++ b/shared/api/generated/model/responseDTOEnvironmentTotalDTO.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ +import type { EnvironmentTotalDTO } from './environmentTotalDTO'; +import type { ErrorDTO } from './errorDTO'; + +export interface ResponseDTOEnvironmentTotalDTO { + data?: EnvironmentTotalDTO; + error?: ErrorDTO; + success?: boolean; +} diff --git a/shared/api/generated/model/responseDTOSafetyModalResponse.ts b/shared/api/generated/model/responseDTOSafetyModalResponse.ts new file mode 100644 index 0000000..1895d78 --- /dev/null +++ b/shared/api/generated/model/responseDTOSafetyModalResponse.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ +import type { SafetyModalResponse } from './safetyModalResponse'; +import type { ErrorDTO } from './errorDTO'; + +export interface ResponseDTOSafetyModalResponse { + data?: SafetyModalResponse; + error?: ErrorDTO; + success?: boolean; +} diff --git a/shared/api/generated/model/safetyModalResponse.ts b/shared/api/generated/model/safetyModalResponse.ts new file mode 100644 index 0000000..b79e737 --- /dev/null +++ b/shared/api/generated/model/safetyModalResponse.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * Dingle Swagger API + * Dingle API + * OpenAPI spec version: v1.0.0 + */ +import type { PoliceModalResponse } from './policeModalResponse'; + +export interface SafetyModalResponse { + passedCrimeZone?: boolean; + pathCctvCount?: number; + pathLightCount?: number; + polices?: PoliceModalResponse[]; +} diff --git a/shared/api/generated/model/smartPoleNoiseResponseDTO.ts b/shared/api/generated/model/smartPoleNoiseResponseDTO.ts index b2272c3..e9a52e3 100644 --- a/shared/api/generated/model/smartPoleNoiseResponseDTO.ts +++ b/shared/api/generated/model/smartPoleNoiseResponseDTO.ts @@ -10,5 +10,7 @@ import type { Item } from './item'; export interface SmartPoleNoiseResponseDTO { avgNoise?: number; count?: number; + districtAvgNoise?: number; items?: Item[]; + overCount?: number; } diff --git a/shared/api/generated/model/smartPolePopulationResponseDTO.ts b/shared/api/generated/model/smartPolePopulationResponseDTO.ts index 8dcaa37..abf1391 100644 --- a/shared/api/generated/model/smartPolePopulationResponseDTO.ts +++ b/shared/api/generated/model/smartPolePopulationResponseDTO.ts @@ -10,5 +10,7 @@ import type { Item } from './item'; export interface SmartPolePopulationResponseDTO { avgPopulation?: number; count?: number; + districtAvgPopulation?: number; items?: Item[]; + overCount?: number; } diff --git a/shared/api/generated/model/smartPoleResponseDTO.ts b/shared/api/generated/model/smartPoleResponseDTO.ts index d885509..d932285 100644 --- a/shared/api/generated/model/smartPoleResponseDTO.ts +++ b/shared/api/generated/model/smartPoleResponseDTO.ts @@ -9,6 +9,7 @@ import type { SmartPoleNoiseResponseDTO } from './smartPoleNoiseResponseDTO'; import type { SmartPolePopulationResponseDTO } from './smartPolePopulationResponseDTO'; export interface SmartPoleResponseDTO { + districtId?: number; noise?: SmartPoleNoiseResponseDTO; population?: SmartPolePopulationResponseDTO; propertyId?: number; diff --git a/shared/api/generated/safety-controller/safety-controller.ts b/shared/api/generated/safety-controller/safety-controller.ts index 8b74001..2906ead 100644 --- a/shared/api/generated/safety-controller/safety-controller.ts +++ b/shared/api/generated/safety-controller/safety-controller.ts @@ -6,13 +6,21 @@ * OpenAPI spec version: v1.0.0 */ import { - useMutation + useMutation, + useQuery } from '@tanstack/react-query' import type { MutationFunction, + QueryFunction, + QueryKey, UseMutationOptions, - UseMutationResult + UseMutationResult, + UseQueryOptions, + UseQueryResult } from '@tanstack/react-query' +import type { + ResponseDTOSafetyModalResponse +} from '.././model' import { customInstance } from '../../axios-instance'; import type { ErrorType } from '../../axios-instance'; @@ -69,4 +77,65 @@ const {mutation: mutationOptions, request: requestOptions} = options ?? {}; return useMutation(mutationOptions); } - \ No newline at end of file + /** + * 안전 모달을 조회합니다. + * @summary 지도 안전 모달 조회 API (이거 아직 완성 아님) + */ +export const getSafetyModal = ( + propertyId: number, + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customInstance( + {url: `/api/v1/safety/${propertyId}/modal`, method: 'GET', signal + }, + options); + } + + +export const getGetSafetyModalQueryKey = (propertyId: number,) => { + return [`/api/v1/safety/${propertyId}/modal`] as const; + } + + +export const getGetSafetyModalQueryOptions = >, TError = ErrorType>(propertyId: number, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSafetyModalQueryKey(propertyId); + + + + const queryFn: QueryFunction>> = ({ signal }) => getSafetyModal(propertyId, requestOptions, signal); + + + + + + return { queryKey, queryFn, enabled: !!(propertyId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: QueryKey } +} + +export type GetSafetyModalQueryResult = NonNullable>> +export type GetSafetyModalQueryError = ErrorType + +/** + * @summary 지도 안전 모달 조회 API (이거 아직 완성 아님) + */ +export const useGetSafetyModal = >, TError = ErrorType>( + propertyId: number, options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + + ): UseQueryResult & { queryKey: QueryKey } => { + + const queryOptions = getGetSafetyModalQueryOptions(propertyId,options) + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + diff --git a/yarn.lock b/yarn.lock index 63d0d59..3780892 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1506,6 +1506,13 @@ "@types/d3-transition" "*" "@types/d3-zoom" "*" +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/es-aggregate-error@^1.0.2": version "1.0.6" resolved "https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz" @@ -1513,7 +1520,14 @@ dependencies: "@types/node" "*" -"@types/estree@^1.0.6": +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== + dependencies: + "@types/estree" "*" + +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": version "1.0.8" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -1523,6 +1537,13 @@ resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz" integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + "@types/json-schema@^7.0.11", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.7": version "7.0.15" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" @@ -1533,6 +1554,18 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/node@*": version "25.0.10" resolved "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz" @@ -1559,6 +1592,16 @@ dependencies: csstype "^3.2.2" +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + +"@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + "@types/urijs@^1.19.19": version "1.19.26" resolved "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.26.tgz" @@ -1660,6 +1703,11 @@ "@typescript-eslint/types" "8.53.1" eslint-visitor-keys "^4.2.1" +"@ungap/structured-clone@^1.0.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + "@unrs/resolver-binding-android-arm-eabi@1.11.1": version "1.11.1" resolved "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81" @@ -1988,6 +2036,11 @@ axobject-query@^4.1.0: resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz" integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -2082,6 +2135,11 @@ caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001759: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz" integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" @@ -2090,6 +2148,26 @@ chalk@^4.0.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + chokidar@^3.6.0: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" @@ -2143,6 +2221,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + commander@7: version "7.2.0" resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" @@ -2460,13 +2543,20 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3: +debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" +decode-named-character-reference@^1.0.0: + version "1.3.0" + resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz#3e40603760874c2e5867691b599d73a7da25b53f" + integrity sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q== + dependencies: + character-entities "^2.0.0" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -2507,11 +2597,23 @@ dependency-graph@0.11.0, dependency-graph@~0.11.0: resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz" integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-libc@^2.0.3, detect-libc@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -2566,6 +2668,11 @@ enquirer@^2.4.1: ansi-colors "^4.1.1" strip-ansi "^6.0.1" +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0, es-abstract@^1.24.1: version "1.24.1" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz" @@ -2960,6 +3067,11 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-util-is-identifier-name@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" + integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== + esutils@2.0.3, esutils@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" @@ -2985,6 +3097,11 @@ execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -3309,6 +3426,98 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hast-util-from-parse5@^8.0.0: + version "8.0.3" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e" + integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + devlop "^1.0.0" + hastscript "^9.0.0" + property-information "^7.0.0" + vfile "^6.0.0" + vfile-location "^5.0.0" + web-namespaces "^2.0.0" + +hast-util-parse-selector@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" + integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-raw@^9.0.0: + version "9.1.0" + resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz#79b66b26f6f68fb50dfb4716b2cdca90d92adf2e" + integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw== + dependencies: + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-from-parse5 "^8.0.0" + hast-util-to-parse5 "^8.0.0" + html-void-elements "^3.0.0" + mdast-util-to-hast "^13.0.0" + parse5 "^7.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-to-jsx-runtime@^2.0.0: + version "2.3.6" + resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98" + integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/unist" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + estree-util-is-identifier-name "^3.0.0" + hast-util-whitespace "^3.0.0" + mdast-util-mdx-expression "^2.0.0" + mdast-util-mdx-jsx "^3.0.0" + mdast-util-mdxjs-esm "^2.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + style-to-js "^1.0.0" + unist-util-position "^5.0.0" + vfile-message "^4.0.0" + +hast-util-to-parse5@^8.0.0: + version "8.0.1" + resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz#95aa391cc0514b4951418d01c883d1038af42f5d" + integrity sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + devlop "^1.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + web-namespaces "^2.0.0" + zwitch "^2.0.0" + +hast-util-whitespace@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" + integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== + dependencies: + "@types/hast" "^3.0.0" + +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== + dependencies: + "@types/hast" "^3.0.0" + comma-separated-tokens "^2.0.0" + hast-util-parse-selector "^4.0.0" + property-information "^7.0.0" + space-separated-tokens "^2.0.0" + hermes-estree@0.25.1: version "0.25.1" resolved "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz" @@ -3321,6 +3530,16 @@ hermes-parser@^0.25.1: dependencies: hermes-estree "0.25.1" +html-url-attributes@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87" + integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ== + +html-void-elements@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" + integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== + http2-client@^1.2.5: version "1.3.5" resolved "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz" @@ -3371,6 +3590,11 @@ inflected@^2.1.0: resolved "https://registry.npmjs.org/inflected/-/inflected-2.1.0.tgz" integrity sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w== +inline-style-parser@0.2.7: + version "0.2.7" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz#b1fc68bfc0313b8685745e4464e37f9376b9c909" + integrity sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA== + internal-slot@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz" @@ -3385,6 +3609,19 @@ internal-slot@^1.1.0: resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: version "3.0.5" resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz" @@ -3463,6 +3700,11 @@ is-date-object@^1.0.5, is-date-object@^1.1.0: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" @@ -3498,6 +3740,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + is-map@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz" @@ -3521,6 +3768,11 @@ is-number@^7.0.0: resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + is-regex@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz" @@ -3894,6 +4146,11 @@ loglevel@^1.9.2: resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz" integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" @@ -3925,6 +4182,111 @@ math-intrinsics@^1.1.0: resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +mdast-util-from-markdown@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-mdx-expression@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz#43f0abac9adc756e2086f63822a38c8d3c3a5096" + integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-mdx-jsx@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz#fd04c67a2a7499efb905a8a5c578dddc9fdada0d" + integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + ccount "^2.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-stringify-position "^4.0.0" + vfile-message "^4.0.0" + +mdast-util-mdxjs-esm@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" + integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-hast@^13.0.0: + version "13.2.1" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz#d7ff84ca499a57e2c060ae67548ad950e689a053" + integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + "@ungap/structured-clone" "^1.0.0" + devlop "^1.0.0" + micromark-util-sanitize-uri "^2.0.0" + trim-lines "^3.0.0" + unist-util-position "^5.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.2" + resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -3935,6 +4297,200 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +micromark-core-commonmark@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +micromark@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" @@ -4294,6 +4850,26 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-entities@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== + dependencies: + "@types/unist" "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse5@^7.0.0: + version "7.3.0" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== + dependencies: + entities "^6.0.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -4371,6 +4947,11 @@ prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" @@ -4411,6 +4992,23 @@ react-kakao-maps-sdk@^1.2.0: "@babel/runtime" "^7.22.15" kakao.maps.d.ts "^0.1.39" +react-markdown@^10.1.0: + version "10.1.0" + resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz#e22bc20faddbc07605c15284255653c0f3bad5ca" + integrity sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + hast-util-to-jsx-runtime "^2.0.0" + html-url-attributes "^3.0.0" + mdast-util-to-hast "^13.0.0" + remark-parse "^11.0.0" + remark-rehype "^11.0.0" + unified "^11.0.0" + unist-util-visit "^5.0.0" + vfile "^6.0.0" + react@19.2.3: version "19.2.3" resolved "https://registry.npmjs.org/react/-/react-19.2.3.tgz" @@ -4454,6 +5052,36 @@ regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: gopd "^1.2.0" set-function-name "^2.0.2" +rehype-raw@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" + integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== + dependencies: + "@types/hast" "^3.0.0" + hast-util-raw "^9.0.0" + vfile "^6.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-rehype@^11.0.0: + version "11.1.2" + resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz#2addaadda80ca9bd9aa0da763e74d16327683b37" + integrity sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw== + dependencies: + "@types/hast" "^3.0.0" + "@types/mdast" "^4.0.0" + mdast-util-to-hast "^13.0.0" + unified "^11.0.0" + vfile "^6.0.0" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" @@ -4750,6 +5378,11 @@ source-map-js@^1.0.2, source-map-js@^1.2.1: resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + stable-hash@^0.0.5: version "0.0.5" resolved "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz" @@ -4845,6 +5478,14 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -4867,6 +5508,20 @@ strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +style-to-js@^1.0.0: + version "1.1.21" + resolved "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz#2908941187f857e79e28e9cd78008b9a0b3e0e8d" + integrity sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ== + dependencies: + style-to-object "1.0.14" + +style-to-object@1.0.14: + version "1.0.14" + resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz#1d22f0e7266bb8c6d8cae5caf4ec4f005e08f611" + integrity sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw== + dependencies: + inline-style-parser "0.2.7" + styled-jsx@5.1.6: version "5.1.6" resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz" @@ -4938,6 +5593,16 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + ts-api-utils@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz" @@ -5055,6 +5720,57 @@ undici-types@~7.16.0: resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== +unified@^11.0.0: + version "11.0.5" + resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + +unist-util-is@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz#d0a3f86f2dd0db7acd7d8c2478080b5c67f9c6a9" + integrity sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-position@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" + integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.2" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz#777df7fb98652ce16b4b7cd999d0a1a40efa3a02" + integrity sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz#9a2a28b0aa76a15e0da70a08a5863a2f060e2468" + integrity sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universalify@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" @@ -5117,6 +5833,35 @@ validator@^13.15.23: resolved "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz" integrity sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA== +vfile-location@^5.0.0: + version "5.0.3" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== + dependencies: + "@types/unist" "^3.0.0" + vfile "^6.0.0" + +vfile-message@^4.0.0: + version "4.0.3" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +web-namespaces@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" + integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" @@ -5261,3 +6006,8 @@ zustand@^5.0.11: version "5.0.11" resolved "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz#99f912e590de1ca9ce6c6d1cab6cdb1f034ab494" integrity sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==