diff --git a/src/components/Features/Sidebar/SidebarDetail/ShareMap/index.tsx b/src/components/Features/Sidebar/SidebarDetail/ShareMap/index.tsx
index ff12bb1..36a9628 100644
--- a/src/components/Features/Sidebar/SidebarDetail/ShareMap/index.tsx
+++ b/src/components/Features/Sidebar/SidebarDetail/ShareMap/index.tsx
@@ -1,11 +1,8 @@
import React from "react";
import ShareMapButton from "./ShareMapButton";
-type Props = {
- map: kakao.maps.Map;
-};
-function ShareMap({ map }: Props): React.ReactElement {
- return
;
+function ShareMap(): React.ReactElement {
+ return
;
}
export default ShareMap;
diff --git a/src/components/Features/Sidebar/SidebarDetail/index.tsx b/src/components/Features/Sidebar/SidebarDetail/index.tsx
index 747707c..a48bff6 100644
--- a/src/components/Features/Sidebar/SidebarDetail/index.tsx
+++ b/src/components/Features/Sidebar/SidebarDetail/index.tsx
@@ -4,10 +4,7 @@ import PlaceDetail from "./PlaceDetail";
import CreatePlaceButton from "./CreatePlace/CreatePlaceButton";
import Profile from "./Profile";
-type Props = {
- map: kakao.maps.Map;
-};
-function SidebarDetail({ map }: Props): React.ReactElement {
+function SidebarDetail(): React.ReactElement {
return (
);
}
diff --git a/src/components/Features/Sidebar/index.tsx b/src/components/Features/Sidebar/index.tsx
index 479a884..4abca05 100644
--- a/src/components/Features/Sidebar/index.tsx
+++ b/src/components/Features/Sidebar/index.tsx
@@ -4,15 +4,12 @@ import { sidebarIsOpenState } from "../../../states/sidebar/siteIsOpen";
import OpenSidebarButton from "./OpenSidebarButton";
import SidebarDetail from "./SidebarDetail";
-type Props = {
- map: kakao.maps.Map;
-};
-function Sidebar({ map }: Props): React.ReactElement {
+function Sidebar(): React.ReactElement {
const [isOpened] = useRecoilState(sidebarIsOpenState);
return (
- {isOpened && }
+ {isOpened && }
);
}
diff --git a/src/components/Features/index.tsx b/src/components/Features/index.tsx
index 019f8a6..23ffeb1 100644
--- a/src/components/Features/index.tsx
+++ b/src/components/Features/index.tsx
@@ -5,17 +5,13 @@ import BrandFilter from "./BrandFilter";
// import CreatePlaceButton from './Sidebar/SidebarDetail/CreatePlace/CreatePlaceButton';
import ShareMap from "./Sidebar/SidebarDetail/ShareMap";
-type Props = {
- map: kakao.maps.Map;
- clusterer: kakao.maps.MarkerClusterer;
-};
-function Features({ map, clusterer }: Props) {
+function Features() {
return (
-
-
-
-
+
+
+
+
{/**/}
{/**/}
diff --git a/src/components/Map/MapContent.tsx b/src/components/Map/MapContent.tsx
index c04288d..aa3e155 100644
--- a/src/components/Map/MapContent.tsx
+++ b/src/components/Map/MapContent.tsx
@@ -1,19 +1,17 @@
-import React from "react";
-import { SetterOrUpdater, useSetRecoilState } from "recoil";
-import { getPlaceMap, IPlace } from "../../states/places/placeMap";
-import { clickedPlaceState } from "../../states/places/clickedPlace";
-import { createPlaceModalDisplayState } from "../../states/buttons/createPlaceModalDisplayState";
-import { sidebarIsOpenState } from "../../states/sidebar/siteIsOpen";
-import { MapService } from "../../utils/kakaoMap/services/MapService";
import {
LoaderOptions,
Map,
MarkerClusterer,
useKakaoLoader,
} from "react-kakao-maps-sdk";
-import { env } from "../../env";
+import { env } from "@/env.ts";
import Features from "../Features";
-import useSupabase from "../../hooks/useSupabase.ts";
+import useMapPosition from "@/hooks/useMapPosition.ts";
+import MapController from "@/components/Map/MapController.tsx";
+import PlaceController from "@/components/Map/PlaceController.tsx";
+import MapEventController from "@/components/Map/MapEventController.ts";
+import { IPlace } from "@/hooks/usePlaceMap.ts";
+import useZoomLevel from "@/hooks/useZoomLevel.ts";
declare global {
interface Window {
@@ -31,22 +29,7 @@ declare global {
}
}
-export interface LatLng {
- latitude: number;
- longitude: number;
-}
-
function MapContent() {
- const [init, setInit] = React.useState
(false);
- const mapRef = React.useRef(null);
- const clustererRef = React.useRef(null);
- const setClickedPlace = useSetRecoilState(clickedPlaceState);
- const setCreatePlaceModalDisplay = useSetRecoilState(
- createPlaceModalDisplayState,
- );
- const setSidebarIsOpen = useSetRecoilState(sidebarIsOpenState);
- const supabaseClient = useSupabase();
-
useKakaoLoader({
appkey: env.kakao.mapApiKey,
libraries: ["services", "clusterer"],
@@ -54,210 +37,27 @@ function MapContent() {
retries: 3,
} as LoaderOptions);
- let debounce: NodeJS.Timeout;
- let isDebounced = false;
- let zoomChanged: boolean = false;
- const debounceTime: number = 500;
-
- const getAndAddPlace = (target: kakao.maps.Map): void => {
- const geoBound: [LatLng, LatLng] = MapService.getGeoBound(target);
- const minUnit = 0.25;
- const minLat = Math.floor(geoBound[0].latitude / minUnit) * minUnit;
- const minLon = Math.floor(geoBound[0].longitude / minUnit) * minUnit;
- const maxLat = Math.ceil(geoBound[1].latitude / minUnit) * minUnit;
- const maxLon = Math.ceil(geoBound[1].longitude / minUnit) * minUnit;
- for (let lat = minLat; lat < maxLat; lat++) {
- for (let lon = minLon; lon < maxLon; lon++) {
- getPlaceMap(
- supabaseClient,
- { latitude: lat, longitude: lon },
- { latitude: lat + 1, longitude: lon + 1 },
- ).then((newPlaceMap) => {
- setMarkerCluster(newPlaceMap, setClickedPlace, setSidebarIsOpen);
- });
- }
- }
- };
-
- function setMarkerCluster(
- newPlaceMap: { [p: string]: IPlace },
- setClickedPlace: SetterOrUpdater,
- setSidebarIsOpen: SetterOrUpdater,
- ): void {
- class PlaceMarker extends kakao.maps.Marker {
- id: string;
-
- constructor(props: { id: string } & kakao.maps.MarkerOptions) {
- super(props);
- this.id = props.id;
- }
- }
-
- const placeToMarker = (place: IPlace): PlaceMarker => {
- const marker = new PlaceMarker({
- position: new kakao.maps.LatLng(place.latitude, place.longitude),
- title: place.name,
- clickable: true,
- id: place.id,
- });
- kakao.maps.event.addListener(marker, "click", () => {
- setClickedPlace(marker.id);
- setSidebarIsOpen(true);
- });
- return marker;
- };
-
- const markers: PlaceMarker[] = [];
- Object.values(newPlaceMap).forEach((place: IPlace) => {
- if (!window.placeMap) window.placeMap = {};
- if (window.placeMap[place.id]) return;
- window.placeMap[place.id] = place;
- const marker: PlaceMarker = placeToMarker(place);
- const brandId = place.brand?.id || "no_brand";
- const brandName = place.brand?.name || "로컬";
- if (!window.brands) window.brands = {};
- if (!window.brands[brandId]) {
- window.brands[brandId] = {
- id: brandId,
- name: brandName,
- markers: [],
- nameOverlays: [],
- visible: true,
- };
- }
- window.brands[brandId].markers.push(marker);
- if (window.brands[brandId]?.visible) {
- markers.push(marker);
- }
-
- const nameOverlay = createNameOverlay(place);
- window.brands[brandId].nameOverlays.push(nameOverlay);
- if (
- MapService.clusterMinLevel > MapService.getZoom() &&
- window.brands[brandId]?.visible
- ) {
- nameOverlay.setMap(mapRef.current);
- }
- });
-
- const clusterer = clustererRef.current;
- if (!clusterer) throw new Error("clusterer is not initialized");
- clusterer.addMarkers(markers);
- }
-
- function createNameOverlay(place: IPlace) {
- return new kakao.maps.CustomOverlay({
- position: new kakao.maps.LatLng(place.latitude, place.longitude),
- content: `${
- place.brand?.name || place.name
- }
`,
- yAnchor: 0,
- clickable: false,
- });
- }
-
- const moveEventHandler = (target: kakao.maps.Map): void => {
- if (!isDebounced) {
- getAndAddPlace(target);
- } else {
- clearTimeout(debounce);
- }
- isDebounced = true;
- debounce = setTimeout(() => {
- isDebounced = false;
- }, debounceTime);
- };
-
- const onCenterChanged = (target: kakao.maps.Map) => {
- const center = target.getCenter();
- window.localStorage.setItem(
- "center",
- JSON.stringify({
- latitude: center.getLat(),
- longitude: center.getLng(),
- }),
- );
- if (!zoomChanged) moveEventHandler(target);
- };
-
- const onZoomChanged = (target: kakao.maps.Map) => {
- zoomChanged = true;
- const currentZoom: number = MapService.getZoom();
- // 추가되어있었다면 없애고, 없었으면 추가해주고
- if (MapService.isPassMinLevel(target)) {
- const visible: boolean = target.getLevel() < MapService.clusterMinLevel;
- Object.values(window.brands).map((brand) => {
- const isOverlayVisible = brand.visible && visible === brand.visible;
- brand.nameOverlays.map((nameOverlay) =>
- nameOverlay.setMap(isOverlayVisible ? target : null),
- );
- });
- }
-
- MapService.setZoom(target);
- if (currentZoom <= target.getLevel()) moveEventHandler(target);
- zoomChanged = false;
- };
-
- const mapOnCreate = (target: kakao.maps.Map) => {
- if (init || !target) return;
-
- MapService.setMapController(target);
- getAndAddPlace(target);
- document.onkeydown = (e: KeyboardEvent): void => {
- if (e.key === "Escape") {
- window.newPlace?.setMap(null);
- setClickedPlace(undefined);
- setCreatePlaceModalDisplay(false);
- }
- };
- mapRef.current = target;
-
- if (isAllRefInitialized()) {
- setInit(true);
- }
- };
-
- const clustererOnCreate = (target: kakao.maps.MarkerClusterer) => {
- if (init || !target) return;
-
- clustererRef.current = target;
- if (isAllRefInitialized()) {
- setInit(true);
- }
- };
-
- const isAllRefInitialized = (): boolean => {
- return !!mapRef.current && !!clustererRef.current;
- };
-
- const center = MapService.getCenter();
+ const { position } = useMapPosition();
+ const { level, minLevel, clusterMinLevel } = useZoomLevel();
return (
);
}
diff --git a/src/components/Map/MapController.tsx b/src/components/Map/MapController.tsx
new file mode 100644
index 0000000..554c643
--- /dev/null
+++ b/src/components/Map/MapController.tsx
@@ -0,0 +1,17 @@
+import { useMap } from "react-kakao-maps-sdk";
+import { useEffect } from "react";
+
+export default function MapController() {
+ const map = useMap();
+
+ useEffect(() => {
+ if (!map) return;
+
+ const mapTypeControl = new kakao.maps.MapTypeControl();
+ map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);
+ const zoomControl = new kakao.maps.ZoomControl();
+ map.addControl(zoomControl, kakao.maps.ControlPosition.BOTTOMRIGHT);
+ }, [map]);
+
+ return null;
+}
diff --git a/src/components/Map/MapEventController.ts b/src/components/Map/MapEventController.ts
new file mode 100644
index 0000000..afe05be
--- /dev/null
+++ b/src/components/Map/MapEventController.ts
@@ -0,0 +1,94 @@
+import { useEffect, useState } from "react";
+import useZoomLevel from "@/hooks/useZoomLevel.ts";
+import useMap from "@/hooks/useMap.ts";
+import { useSetRecoilState } from "recoil";
+import { clickedPlaceState } from "@/states/places/clickedPlace.ts";
+import { createPlaceModalDisplayState } from "@/states/buttons/createPlaceModalDisplayState.ts";
+import { requireLoadState } from "@/states/places/requireLoad.ts";
+
+export default function MapEventController() {
+ const map = useMap();
+ const setClickedPlace = useSetRecoilState(clickedPlaceState);
+ const setCreatePlaceModalDisplay = useSetRecoilState(
+ createPlaceModalDisplayState,
+ );
+ const setRequireLoad = useSetRecoilState(requireLoadState);
+ const { setZoomLevel, clusterMinLevel, getIsPassMinLevel, level } =
+ useZoomLevel();
+ const [zoomChanged, setZoomChanged] = useState(false);
+
+ let debounce: NodeJS.Timeout;
+ let isDebounced = false;
+ const debounceTime: number = 500;
+
+ const moveEventHandler = (): void => {
+ if (!isDebounced) {
+ setRequireLoad(true);
+ } else {
+ clearTimeout(debounce);
+ }
+ isDebounced = true;
+ debounce = setTimeout(() => {
+ isDebounced = false;
+ }, debounceTime);
+ };
+
+ const onCenterChanged = () => {
+ const center = map.getCenter();
+ window.localStorage.setItem(
+ "center",
+ JSON.stringify({
+ latitude: center.getLat(),
+ longitude: center.getLng(),
+ }),
+ );
+ if (!zoomChanged) moveEventHandler();
+ };
+
+ const onZoomChanged = () => {
+ setZoomChanged(true);
+ // 추가되어있었다면 없애고, 없었으면 추가해주고
+ if (getIsPassMinLevel()) {
+ const visible: boolean = map.getLevel() < clusterMinLevel;
+ Object.values(window.brands).map((brand) => {
+ const isOverlayVisible = brand.visible && visible === brand.visible;
+ brand.nameOverlays.map((nameOverlay) =>
+ nameOverlay.setMap(isOverlayVisible ? map : null),
+ );
+ });
+ }
+
+ setZoomLevel();
+ if (level <= map.getLevel()) moveEventHandler();
+ setZoomChanged(false);
+ };
+
+ useEffect(() => {
+ if (!map) return;
+
+ // useKakaoEvent(map, "bounds_changed", onBoundsChanged);
+ kakao.maps.event.addListener(map, "center_changed", onCenterChanged);
+ // useKakaoEvent(map, "click", onClick);
+ // useKakaoEvent(map, "dblclick", onDoubleClick);
+ kakao.maps.event.addListener(map, "drag", moveEventHandler);
+ // useKakaoEvent(map, "dragstart", onDragStart);
+ // useKakaoEvent(map, "dragend", onDragEnd);
+ // useKakaoEvent(map, "idle", onIdle);
+ // useKakaoEvent(map, "maptypeid_changed", onMaptypeidChanged);
+ // useKakaoEvent(map, "mousemove", onMouseMove);
+ // useKakaoEvent(map, "rightclick", onRightClick);
+ // useKakaoEvent(map, "tilesloaded", onTileLoaded);
+ kakao.maps.event.addListener(map, "zoom_changed", onZoomChanged);
+ // useKakaoEvent(map, "zoom_start", onZoomStart);
+
+ document.onkeydown = (e: KeyboardEvent): void => {
+ if (e.key === "Escape") {
+ window.newPlace?.setMap(null);
+ setClickedPlace(undefined);
+ setCreatePlaceModalDisplay(false);
+ }
+ };
+ }, []);
+
+ return null;
+}
diff --git a/src/components/Map/PlaceController.tsx b/src/components/Map/PlaceController.tsx
new file mode 100644
index 0000000..47047e4
--- /dev/null
+++ b/src/components/Map/PlaceController.tsx
@@ -0,0 +1,52 @@
+import { useRecoilState } from "recoil";
+import useMapClusterer from "@/hooks/useMapClusterer.ts";
+import useMap from "@/hooks/useMap.ts";
+import { requireLoadState } from "@/states/places/requireLoad.ts";
+import { useEffect, useState } from "react";
+import PlaceGrid from "@/components/Map/PlaceGrid.tsx";
+import useMapPosition, { LatLng } from "@/hooks/useMapPosition.ts";
+
+export default function PlaceController() {
+ const map = useMap();
+ const mapClusterer = useMapClusterer();
+ const [requireLoad, setRequireLoad] = useRecoilState(requireLoadState);
+ const [placeGridList, setPlaceGridList] = useState<
+ { bottomLeft: LatLng; topRight: LatLng }[]
+ >([]);
+ const { getGeoBound } = useMapPosition();
+
+ useEffect(() => {
+ if (!map || !mapClusterer || !requireLoad) return;
+
+ const geoBound: [LatLng, LatLng] = getGeoBound();
+ const minUnit = 0.25;
+ const minLat = Math.floor(geoBound[0].latitude / minUnit) * minUnit;
+ const minLon = Math.floor(geoBound[0].longitude / minUnit) * minUnit;
+ const maxLat = Math.ceil(geoBound[1].latitude / minUnit) * minUnit;
+ const maxLon = Math.ceil(geoBound[1].longitude / minUnit) * minUnit;
+
+ const newPlaceGridList = [];
+ for (let lat = minLat; lat < maxLat; lat++) {
+ for (let lon = minLon; lon < maxLon; lon++) {
+ const bottomLeft = { latitude: lat, longitude: lon };
+ const topRight = { latitude: lat + 1, longitude: lon + 1 };
+ newPlaceGridList.push({ topRight, bottomLeft });
+ }
+ }
+ setPlaceGridList(newPlaceGridList);
+
+ setRequireLoad(false);
+ }, [requireLoad, map, mapClusterer]);
+
+ return (
+ <>
+ {placeGridList.map((placeGrid) => (
+
+ ))}
+ >
+ );
+}
diff --git a/src/components/Map/PlaceGrid.tsx b/src/components/Map/PlaceGrid.tsx
new file mode 100644
index 0000000..6f09485
--- /dev/null
+++ b/src/components/Map/PlaceGrid.tsx
@@ -0,0 +1,106 @@
+import { LatLng } from "@/hooks/useMapPosition.ts";
+import usePlaceMap, { IPlace } from "@/hooks/usePlaceMap.ts";
+import { SetterOrUpdater, useSetRecoilState } from "recoil";
+import { sidebarIsOpenState } from "@/states/sidebar/siteIsOpen.ts";
+import useZoomLevel from "@/hooks/useZoomLevel.ts";
+import { clickedPlaceState } from "@/states/places/clickedPlace.ts";
+import useMapClusterer from "@/hooks/useMapClusterer.ts";
+import useMap from "@/hooks/useMap.ts";
+
+type Props = {
+ bottomLeft: LatLng;
+ topRight: LatLng;
+};
+
+export default function PlaceGrid(props: Props) {
+ const map = useMap();
+ const mapClusterer = useMapClusterer();
+ const setClickedPlace = useSetRecoilState(clickedPlaceState);
+ const setSidebarIsOpen = useSetRecoilState(sidebarIsOpenState);
+ const { level, clusterMinLevel } = useZoomLevel();
+ const { data, isLoading, error } = usePlaceMap({
+ bottomLeft: props.bottomLeft,
+ topRight: props.topRight,
+ });
+ function setMarkerCluster(
+ newPlaceMap: { [p: string]: IPlace },
+ setClickedPlace: SetterOrUpdater,
+ setSidebarIsOpen: SetterOrUpdater,
+ ): void {
+ class PlaceMarker extends kakao.maps.Marker {
+ id: string;
+
+ constructor(props: { id: string } & kakao.maps.MarkerOptions) {
+ super(props);
+ this.id = props.id;
+ }
+ }
+
+ const placeToMarker = (place: IPlace): PlaceMarker => {
+ const marker = new PlaceMarker({
+ position: new kakao.maps.LatLng(place.latitude, place.longitude),
+ title: place.name,
+ clickable: true,
+ id: place.id,
+ });
+ kakao.maps.event.addListener(marker, "click", () => {
+ setClickedPlace(marker.id);
+ setSidebarIsOpen(true);
+ });
+ return marker;
+ };
+
+ const markers: PlaceMarker[] = [];
+ Object.values(newPlaceMap).forEach((place: IPlace) => {
+ if (!window.placeMap) window.placeMap = {};
+ if (window.placeMap[place.id]) return;
+ window.placeMap[place.id] = place;
+ const marker: PlaceMarker = placeToMarker(place);
+ const brandId = place.brand?.id || "no_brand";
+ const brandName = place.brand?.name || "로컬";
+ if (!window.brands) window.brands = {};
+ if (!window.brands[brandId]) {
+ window.brands[brandId] = {
+ id: brandId,
+ name: brandName,
+ markers: [],
+ nameOverlays: [],
+ visible: true,
+ };
+ }
+ window.brands[brandId].markers.push(marker);
+ if (window.brands[brandId]?.visible) {
+ markers.push(marker);
+ }
+
+ const nameOverlay = createNameOverlay(place);
+ window.brands[brandId].nameOverlays.push(nameOverlay);
+ if (clusterMinLevel > level && window.brands[brandId]?.visible) {
+ nameOverlay.setMap(map);
+ }
+ });
+
+ if (!mapClusterer) throw new Error("clusterer is not initialized");
+ mapClusterer.addMarkers(markers);
+ }
+
+ function createNameOverlay(place: IPlace) {
+ return new kakao.maps.CustomOverlay({
+ position: new kakao.maps.LatLng(place.latitude, place.longitude),
+ content: `${
+ place.brand?.name || place.name
+ }
`,
+ yAnchor: 0,
+ clickable: false,
+ });
+ }
+ if (isLoading) {
+ return Loading...
;
+ }
+ if (!data || error) {
+ return Error
;
+ }
+
+ setMarkerCluster(data, setClickedPlace, setSidebarIsOpen);
+ return null;
+}
diff --git a/src/hooks/useMap.ts b/src/hooks/useMap.ts
new file mode 100644
index 0000000..ab09422
--- /dev/null
+++ b/src/hooks/useMap.ts
@@ -0,0 +1,6 @@
+import { useContext } from "react";
+import { KakaoMapContext } from "react-kakao-maps-sdk";
+
+export default function useMap() {
+ return useContext(KakaoMapContext);
+}
diff --git a/src/hooks/useMapClusterer.ts b/src/hooks/useMapClusterer.ts
new file mode 100644
index 0000000..6130a84
--- /dev/null
+++ b/src/hooks/useMapClusterer.ts
@@ -0,0 +1,6 @@
+import { useContext } from "react";
+import { KakaoMapMarkerClustererContext } from "react-kakao-maps-sdk";
+
+export default function useMapClusterer() {
+ return useContext(KakaoMapMarkerClustererContext);
+}
diff --git a/src/hooks/useMapPosition.ts b/src/hooks/useMapPosition.ts
new file mode 100644
index 0000000..9b3a688
--- /dev/null
+++ b/src/hooks/useMapPosition.ts
@@ -0,0 +1,66 @@
+import useMap from "@/hooks/useMap.ts";
+
+export interface LatLng {
+ latitude: number;
+ longitude: number;
+}
+
+export default function useMapPosition() {
+ const map = useMap();
+ const getPosition = (): LatLng => {
+ // search param 이 있는 경우
+ const centerFromSearchParams = new URLSearchParams(
+ window.location.search,
+ ).get("center");
+ if (centerFromSearchParams?.toString().split(",").length) {
+ const [lat, lng]: string[] = centerFromSearchParams
+ .toString()
+ .split(",", 2);
+ if (!isNaN(Number(lat)) && !isNaN(Number(lng))) {
+ return { latitude: Number(lat), longitude: Number(lng) };
+ }
+ }
+
+ // localstorage 가 있는 경우
+ const centerFromLocalStorage = window.localStorage.getItem("center");
+ if (centerFromLocalStorage) {
+ const result = JSON.parse(centerFromLocalStorage);
+ if (result.latitude && result.longitude) {
+ return { latitude: result.latitude, longitude: result.longitude };
+ }
+ }
+
+ // default
+ return { latitude: 37.53026789291489, longitude: 127.12380358542175 };
+ };
+
+ const position = getPosition();
+ return {
+ position,
+ setCenter: () => {
+ const center = map.getCenter();
+ window.localStorage.setItem(
+ "center",
+ JSON.stringify({
+ latitude: center.getLat(),
+ longitude: center.getLng(),
+ }),
+ );
+ },
+ getGeoBound: (): [LatLng, LatLng] => {
+ const bounds = map.getBounds();
+ const bottomLeft = bounds.getSouthWest();
+ const topRight = bounds.getNorthEast();
+ return [
+ {
+ latitude: bottomLeft.getLat(),
+ longitude: bottomLeft.getLng(),
+ },
+ {
+ latitude: topRight.getLat(),
+ longitude: topRight.getLng(),
+ },
+ ];
+ },
+ };
+}
diff --git a/src/hooks/usePlaceMap.ts b/src/hooks/usePlaceMap.ts
new file mode 100644
index 0000000..fba9c9c
--- /dev/null
+++ b/src/hooks/usePlaceMap.ts
@@ -0,0 +1,78 @@
+import { postError } from "@/utils/HttpRequestUtil.ts";
+import useSupabase from "@/hooks/useSupabase.ts";
+import { useQuery } from "@tanstack/react-query";
+import { LatLng } from "@/hooks/useMapPosition.ts";
+
+type Props = {
+ bottomLeft: LatLng;
+ topRight: LatLng;
+};
+
+export interface IHashtag {
+ hashtag_id: string;
+}
+
+export interface IBrand {
+ id: string;
+ name: string;
+ description: string;
+ hashtags: IHashtag[];
+}
+
+export interface IPlace {
+ id: string;
+ latitude: number;
+ longitude: number;
+ address?: string;
+ telephone?: string;
+ description?: string;
+ name: string;
+ hashtags: IHashtag[];
+ brand?: IBrand;
+}
+
+export default function usePlaceMap(props: Props) {
+ const supabaseClient = useSupabase();
+
+ const { topRight, bottomLeft } = props;
+
+ const searchParam = new URLSearchParams();
+ searchParam.set(
+ "bottom_left",
+ `${bottomLeft.latitude},${bottomLeft.longitude}`,
+ );
+ searchParam.set("top_right", `${topRight.latitude},${topRight.longitude}`);
+
+ return useQuery<{ [key: string]: IPlace }, Error, { [key: string]: IPlace }>({
+ queryKey: ["placeMap", searchParam.toString()],
+ queryFn: async () => {
+ return supabaseClient
+ .from("place")
+ .select(
+ `
+ *,
+ brand (*),
+ place_hashtags (*)
+ `,
+ )
+ .gte("latitude", bottomLeft.latitude)
+ .lte("latitude", topRight.latitude)
+ .gte("longitude", bottomLeft.longitude)
+ .lte("longitude", topRight.longitude)
+ .then(({ data, error }) => {
+ if (error) {
+ postError(error);
+ throw new Error(error.message);
+ }
+ return data;
+ })
+ .then((data) => {
+ const placeMap: { [key: string]: IPlace } = {};
+ Object.values(data).forEach((place) => {
+ placeMap[place.id] = place;
+ });
+ return placeMap;
+ });
+ },
+ });
+}
diff --git a/src/hooks/useZoomLevel.ts b/src/hooks/useZoomLevel.ts
new file mode 100644
index 0000000..5aeab27
--- /dev/null
+++ b/src/hooks/useZoomLevel.ts
@@ -0,0 +1,47 @@
+import useMap from "@/hooks/useMap.ts";
+import useMapPosition from "@/hooks/useMapPosition.ts";
+
+export default function useZoomLevel(): {
+ minLevel: number;
+ defaultLevel: number;
+ clusterMinLevel: number;
+ level: number;
+ setZoomLevel: () => void;
+ getIsPassMinLevel: () => boolean;
+} {
+ const { setCenter } = useMapPosition();
+ const map = useMap();
+ const zoomFromLocalStorage = window.localStorage.getItem("zoom");
+ const minLevel: number = 1;
+ const defaultLevel: number = 8;
+ const clusterMinLevel: number = 5;
+
+ const getLevel = () => {
+ const zoomFromSearchParams = new URLSearchParams(
+ window.location.search,
+ ).get("zoom");
+ if (zoomFromSearchParams && !isNaN(Number(zoomFromSearchParams))) {
+ return Number(zoomFromSearchParams);
+ }
+ return zoomFromLocalStorage ? Number(zoomFromLocalStorage) : defaultLevel;
+ };
+
+ const level = getLevel();
+
+ return {
+ minLevel: minLevel,
+ defaultLevel: defaultLevel,
+ clusterMinLevel: clusterMinLevel,
+ level: level,
+ setZoomLevel: () => {
+ window.localStorage.setItem("zoom", map.getLevel().toString());
+ setCenter();
+ },
+ getIsPassMinLevel: () => {
+ const isPrevLevelLargerThanMinLevel = level >= clusterMinLevel;
+ const isCurrentLevelLargerThanMinLevel =
+ map.getLevel() >= clusterMinLevel;
+ return isPrevLevelLargerThanMinLevel !== isCurrentLevelLargerThanMinLevel;
+ },
+ };
+}
diff --git a/src/states/buttons/createPlaceLatLngState.ts b/src/states/buttons/createPlaceLatLngState.ts
index d2efbc6..1eff8fb 100644
--- a/src/states/buttons/createPlaceLatLngState.ts
+++ b/src/states/buttons/createPlaceLatLngState.ts
@@ -1,5 +1,5 @@
import { atom } from "recoil";
-import { LatLng } from "../../components/Map/MapContent";
+import { LatLng } from "@/hooks/useMapPosition.ts";
export const createPlaceLatLngState = atom({
key: "createPlaceLatLngState",
diff --git a/src/states/places/placeMap.ts b/src/states/places/placeMap.ts
deleted file mode 100644
index 67d0f92..0000000
--- a/src/states/places/placeMap.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { postError } from "../../utils/HttpRequestUtil";
-import { LatLng } from "../../components/Map/MapContent";
-import { SupabaseClient } from "@supabase/supabase-js";
-
-export interface IHashtag {
- hashtag_id: string;
-}
-
-export interface IBrand {
- id: string;
- name: string;
- description: string;
- hashtags: IHashtag[];
-}
-
-export interface IPlace {
- id: string;
- latitude: number;
- longitude: number;
- address?: string;
- telephone?: string;
- description?: string;
- name: string;
- hashtags: IHashtag[];
- brand?: IBrand;
-}
-
-export async function getPlaceMap(
- supabaseClient: SupabaseClient,
- bottomLeft: LatLng,
- topRight: LatLng,
-): Promise<{
- [key: string]: IPlace;
-}> {
- const placeMap: { [key: string]: IPlace } = {};
-
- const searchParam = new URLSearchParams();
- searchParam.set(
- "bottom_left",
- `${bottomLeft.latitude},${bottomLeft.longitude}`,
- );
- searchParam.set("top_right", `${topRight.latitude},${topRight.longitude}`);
-
- const data = await supabaseClient
- .from("place")
- .select(
- `
- *,
- brand (*),
- place_hashtags (*)
- `,
- )
- .gte("latitude", bottomLeft.latitude)
- .lte("latitude", topRight.latitude)
- .gte("longitude", bottomLeft.longitude)
- .lte("longitude", topRight.longitude)
- .then(({ data, error }) => {
- if (error) {
- postError(error);
- throw new Error(error.message);
- }
- return data;
- });
-
- Object.values(data).forEach((place) => {
- placeMap[place.id] = place;
- });
- return placeMap;
-}
diff --git a/src/states/places/requireLoad.ts b/src/states/places/requireLoad.ts
new file mode 100644
index 0000000..904bc47
--- /dev/null
+++ b/src/states/places/requireLoad.ts
@@ -0,0 +1,6 @@
+import { atom } from "recoil";
+
+export const requireLoadState = atom({
+ key: "requireLoad",
+ default: true,
+});
diff --git a/src/utils/kakaoMap/services/MapService.ts b/src/utils/kakaoMap/services/MapService.ts
deleted file mode 100644
index f61f423..0000000
--- a/src/utils/kakaoMap/services/MapService.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { LatLng } from "../../../components/Map/MapContent";
-
-export class MapService {
- public static minLevel: number = 1;
- public static defaultLevel: number = 8;
- public static clusterMinLevel: number = 5;
-
- static setCenter(map: kakao.maps.Map): void {
- const center = map.getCenter();
- window.localStorage.setItem(
- "center",
- JSON.stringify({
- latitude: center.getLat(),
- longitude: center.getLng(),
- }),
- );
- }
-
- static getCenter(): LatLng {
- // search param 이 있는 경우
- const centerFromSearchParams = new URLSearchParams(
- window.location.search,
- ).get("center");
- if (centerFromSearchParams?.toString().split(",").length) {
- const [lat, lng]: string[] = centerFromSearchParams
- .toString()
- .split(",", 2);
- if (!isNaN(Number(lat)) && !isNaN(Number(lng))) {
- return { latitude: Number(lat), longitude: Number(lng) };
- }
- }
-
- // localstorage 가 있는 경우
- const centerFromLocalStorage = window.localStorage.getItem("center");
- if (centerFromLocalStorage) {
- return JSON.parse(centerFromLocalStorage);
- }
-
- // default
- return { latitude: 37.53026789291489, longitude: 127.12380358542175 };
- }
-
- static setZoom(map: kakao.maps.Map): void {
- window.localStorage.setItem("zoom", map.getLevel().toString());
- this.setCenter(map);
- }
-
- static getZoom(): number {
- const zoomFromSearchParams = new URLSearchParams(
- window.location.search,
- ).get("zoom");
- if (zoomFromSearchParams && !isNaN(Number(zoomFromSearchParams))) {
- return Number(zoomFromSearchParams);
- }
-
- const zoomFromLocalStorage = window.localStorage.getItem("zoom");
- return zoomFromLocalStorage
- ? Number(zoomFromLocalStorage)
- : this.defaultLevel;
- }
-
- static getGeoBound(map: kakao.maps.Map): [LatLng, LatLng] {
- const bounds = map.getBounds();
- const bottomLeft = bounds.getSouthWest();
- const topRight = bounds.getNorthEast();
- return [
- {
- latitude: bottomLeft.getLat(),
- longitude: bottomLeft.getLng(),
- },
- {
- latitude: topRight.getLat(),
- longitude: topRight.getLng(),
- },
- ];
- }
-
- static setMapController(map: kakao.maps.Map): void {
- const mapTypeControl = new kakao.maps.MapTypeControl();
- map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);
- const zoomControl = new kakao.maps.ZoomControl();
- map.addControl(zoomControl, kakao.maps.ControlPosition.BOTTOMRIGHT);
- }
-
- static isPassMinLevel(map: kakao.maps.Map): boolean {
- const isPrevLevelLargerThanMinLevel =
- this.getZoom() >= MapService.clusterMinLevel;
- const isCurrentLevelLargerThanMinLevel =
- map.getLevel() >= MapService.clusterMinLevel;
- return isPrevLevelLargerThanMinLevel !== isCurrentLevelLargerThanMinLevel;
- }
-}
diff --git a/src/utils/kakaoMap/services/MarkerService.ts b/src/utils/kakaoMap/services/MarkerService.ts
index f9bd891..f99fece 100644
--- a/src/utils/kakaoMap/services/MarkerService.ts
+++ b/src/utils/kakaoMap/services/MarkerService.ts
@@ -1,5 +1,4 @@
-import { IPlace } from "../../../states/places/placeMap";
-import { MapService } from "./MapService";
+import { IPlace } from "@/hooks/usePlaceMap.ts";
export class MarkerService {
static createNameOverlay(place: IPlace) {
@@ -12,29 +11,4 @@ export class MarkerService {
clickable: false,
});
}
-
- static applyClusterFilter(
- brandHashList: string[],
- status: boolean,
- map: kakao.maps.Map,
- clusterer: kakao.maps.MarkerClusterer,
- ): void {
- const markers: kakao.maps.Marker[] = [];
- brandHashList.forEach((brandHash: string) => {
- if (window.brands[brandHash])
- markers.push(...window.brands[brandHash].markers);
- });
- brandHashList.forEach(
- (brandHash: string) =>
- MapService.clusterMinLevel > MapService.getZoom() &&
- window.brands[brandHash]?.nameOverlays.map((nameOverlay) =>
- nameOverlay.setMap(status ? map : null),
- ),
- );
- if (status) {
- clusterer.addMarkers(markers);
- } else {
- clusterer.removeMarkers(markers);
- }
- }
}
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..8751af8
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,8 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: ["./src/**/*.{js,jsx,ts,tsx}"],
+ plugins: [],
+};
+
+export default config;
diff --git a/tsconfig.json b/tsconfig.json
index fa715e4..e4ba94b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,9 +1,9 @@
{
"compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "target": "ESNext",
"module": "ESNext",
+ "useDefineForClassFields": true,
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"esModuleInterop": true,
@@ -22,8 +22,14 @@
"noFallthroughCasesInSwitch": true,
"types": [
"kakao.maps.d.ts",
- ]
+ ],
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
},
"include": ["src"],
+ "exclude": ["node_modules", "dist", "build"],
"references": [{ "path": "./tsconfig.node.json" }]
}
diff --git a/vite.config.ts b/vite.config.ts
index f0078e4..cbcd5f8 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -7,4 +7,9 @@ export default defineConfig({
server: {
port: 3000,
},
+ resolve: {
+ alias: {
+ "@": "/src",
+ },
+ },
});