diff --git a/README.md b/README.md index 5c4b5cadf..742fdb861 100644 --- a/README.md +++ b/README.md @@ -1,296 +1,131 @@ -# NVIDIA NemoClaw: Reference Stack for Running OpenClaw in OpenShell - - -[![License](https://img.shields.io/badge/License-Apache_2.0-blue)](https://github.com/NVIDIA/NemoClaw/blob/main/LICENSE) -[![Security Policy](https://img.shields.io/badge/Security-Report%20a%20Vulnerability-red)](https://github.com/NVIDIA/NemoClaw/blob/main/SECURITY.md) -[![Project Status](https://img.shields.io/badge/status-alpha-orange)](https://github.com/NVIDIA/NemoClaw/blob/main/docs/about/release-notes.md) - - - -NVIDIA NemoClaw is an open source reference stack that simplifies running [OpenClaw](https://openclaw.ai) always-on assistants more safely. -It installs the [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) runtime, part of NVIDIA Agent Toolkit, which provides additional security for running autonomous agents. -It also includes open source models such as [NVIDIA Nemotron](https://build.nvidia.com). - - -> **Alpha software** -> -> NemoClaw is available in early preview starting March 16, 2026. -> This software is not production-ready. -> Interfaces, APIs, and behavior may change without notice as we iterate on the design. -> The project is shared to gather feedback and enable early experimentation. -> We welcome issues and discussion from the community while the project evolves. +# ๐Ÿ’ฐ ์ตœ์ €๊ฐ€ ์ง€๋„ (Lowest Price Map) ---- - -## Quick Start - -Follow these steps to get started with NemoClaw and your first sandboxed OpenClaw agent. - -> **โ„น๏ธ Note** -> -> NemoClaw creates a fresh OpenClaw instance inside the sandbox during onboarding. - - - -### Prerequisites - -Check the prerequisites before you start to ensure you have the necessary software and hardware to run NemoClaw. - -#### Hardware - -| Resource | Minimum | Recommended | -|----------|----------------|------------------| -| CPU | 4 vCPU | 4+ vCPU | -| RAM | 8 GB | 16 GB | -| Disk | 20 GB free | 40 GB free | - -The sandbox image is approximately 2.4 GB compressed. During image push, the Docker daemon, k3s, and the OpenShell gateway run alongside the export pipeline, which buffers decompressed layers in memory. On machines with less than 8 GB of RAM, this combined usage can trigger the OOM killer. If you cannot add memory, configuring at least 8 GB of swap can work around the issue at the cost of slower performance. +> ์„œ์šธ ๊ด€๊ด‘๊ฐ์„ ์œ„ํ•œ **์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์ตœ์ €๊ฐ€ ์ง€๋„** โ€” ํ™˜์ „์†Œยท์ฃผ์œ ์†Œยท๋ง›์ง‘ยท์นดํŽ˜ยทํŽธ์˜์ ยท์ฐœ์งˆ๋ฐฉยท๋…ธ๋ž˜๋ฐฉยท์ „ํ†ต์‹œ์žฅยท๊ด€๊ด‘๋ช…์†Œ๋ฅผ ํ•œ ๋ˆˆ์—. -#### Software +[![React](https://img.shields.io/badge/React-18-61dafb?logo=react)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript)](https://www.typescriptlang.org) +[![Vite](https://img.shields.io/badge/Vite-5-646cff?logo=vite)](https://vitejs.dev) +[![Leaflet](https://img.shields.io/badge/Leaflet-1.9-199900?logo=leaflet)](https://leafletjs.com) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind-3-06b6d4?logo=tailwindcss)](https://tailwindcss.com) -| Dependency | Version | -|------------|----------------------------------| -| Linux | Ubuntu 22.04 LTS or later | -| Node.js | 20 or later | -| npm | 10 or later | -| Container runtime | Supported runtime installed and running | -| [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed | - -#### Container Runtime Support +--- -| Platform | Supported runtimes | Notes | -|----------|--------------------|-------| -| Linux | Docker | Primary supported path today | -| macOS (Apple Silicon) | Colima, Docker Desktop | Recommended runtimes for supported macOS setups | -| macOS | Podman | Not supported yet. NemoClaw currently depends on OpenShell support for Podman on macOS. | -| Windows WSL | Docker Desktop (WSL backend) | Supported target path | +## ์Šคํฌ๋ฆฐ์ƒท -#### macOS first-run checklist +| ์ง€๋„ ๋ฉ”์ธ | ์ƒ์„ธ ํŒจ๋„ | ์‚ฌ์ด๋“œ๋ฐ” ํ•„ํ„ฐ | +|-----------|-----------|---------------| +| ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ปฌ๋Ÿฌ ํ•€ + ๊ฐ€๊ฒฉ ํˆดํŒ | ์ฆ๊ฒจ์ฐพ๊ธฐ ยท ์นด์นด์˜ค๋งต ๋งํฌ | ์ตœ์ €๊ฐ€ ํ† ๊ธ€ ยท ๊ตญ์  ํ•„ํ„ฐ | -On a fresh macOS machine, install the prerequisites in this order: +--- -1. Install Xcode Command Line Tools: +## ์ฃผ์š” ๊ธฐ๋Šฅ - ```bash - xcode-select --install - ``` +### ๐Ÿ—บ๏ธ ์ง€๋„ & ํ•€ +- **10๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ** ์ปฌ๋Ÿฌ ๋‹ค์ด์•„๋ชฌ๋“œ ํ•€ (์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๊ณ ์œ  ์ƒ‰์ƒ + ์ด๋ชจ์ง€) +- **๐Ÿ‘‘ ์ตœ์ €๊ฐ€ ์ˆœ์œ„** โ€” ์นดํ…Œ๊ณ ๋ฆฌ ๋‚ด 1ยท2ยท3์œ„ ํ•€์— ๊ธˆยท์€ยท๋™ ๋ฐฐ์ง€ +- **์„ ํƒ๋œ ํ•€ ๊ฐ•์กฐ** โ€” ํด๋ฆญ ์‹œ scale 1.25 + ํฐ์ƒ‰ ํ…Œ๋‘๋ฆฌ ๋ง +- **ํˆดํŒ** โ€” ํ˜ธ๋ฒ„ ์‹œ ์ด๋ฆ„ + ํ•ต์‹ฌ ๊ฐ€๊ฒฉ ์ •๋ณด +- **์ ‘ํžˆ๋Š” ๋ฒ”๋ก€** (์ขŒ์ธก ํ•˜๋‹จ) โ€” ์นดํ…Œ๊ณ ๋ฆฌ ์ƒ‰์ƒ/์ด๋ชจ์ง€ ์„ค๋ช… -2. Install and start a supported container runtime: - - Docker Desktop - - Colima -3. Run the NemoClaw installer. +### ๐Ÿ” ๊ฒ€์ƒ‰ & ํ•„ํ„ฐ +- **์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰** โ€” ์ด๋ฆ„ยท์˜๋ฌธ๋ช…ยท์ฃผ์†Œ (TopBar ๐Ÿ” ๋ฒ„ํŠผ, Escape๋กœ ๋‹ซ๊ธฐ) +- **์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ** โ€” 10๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ ๋‹ค์ค‘ ์„ ํƒ +- **๊ตญ์  ํ•„ํ„ฐ** โ€” ํ•œ์‹ / ์ผ์‹ / ์ค‘์‹ / ์„œ์–‘์‹ / ์ธ๋„์‹ / ํ• ๋ž„ / ๋น„๊ฑด +- **๊ฐ€๊ฒฉ ๋ฒ”์œ„ ์Šฌ๋ผ์ด๋”** โ€” ์ตœ์†Œ~์ตœ๋Œ€ ๊ฐ€๊ฒฉ ์„ค์ • +- **์ตœ์ €๊ฐ€๋งŒ ๋ณด๊ธฐ** โ€” ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ 1๊ฐœ ์ตœ์ €๊ฐ€ ํ•€๋งŒ ํ‘œ์‹œ +- **ํ•„ํ„ฐ ์˜์†์„ฑ** โ€” ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ localStorage์—์„œ ๋ณต์› -This avoids the two most common first-run failures on macOS: +### โค๏ธ ์ฆ๊ฒจ์ฐพ๊ธฐ +- ์ƒ์„ธ ํŒจ๋„์—์„œ โค๏ธ ๋ฒ„ํŠผ์œผ๋กœ ๋ถ๋งˆํฌ +- Sidebar ์ฆ๊ฒจ์ฐพ๊ธฐ ์„น์…˜์—์„œ ๋ชจ์•„๋ณด๊ธฐ +- localStorage ๊ธฐ๋ฐ˜ ์˜๊ตฌ ์ €์žฅ -- missing developer tools needed by the installer and Node.js toolchain -- Docker connection errors when no supported container runtime is installed or running +### ๐Ÿ“ ๋‚ด ์œ„์น˜ +- ๋ธŒ๋ผ์šฐ์ € Geolocation API โ†’ ํ˜„์žฌ ์œ„์น˜๋กœ ์ง€๋„ flyTo +- ์˜ค๋ฅธ์ชฝ ์ƒ๋‹จ ๐Ÿ“ ๋ฒ„ํŠผ (Leaflet ๋„ค์ดํ‹ฐ๋ธŒ ์ปจํŠธ๋กค) -> **๐Ÿ’ก Tip** -> -> For DGX Spark, follow the [DGX Spark setup guide](https://github.com/NVIDIA/NemoClaw/blob/main/spark-install.md). It covers Spark-specific prerequisites, such as cgroup v2 and Docker configuration, before running the standard installer. +### ๐Ÿ“ฑ ์ƒ์„ธ ํŒจ๋„ +- ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋งž์ถค ์ •๋ณด (๊ฐ€๊ฒฉํ‘œ ยท ๋ฉ”๋‰ด ยท ํ™˜์œจ ยท ํŽธ์˜์‹œ์„ค ๋“ฑ) +- ์นด์นด์˜ค๋งต / Google Maps ๋”ฅ๋งํฌ ๋ฒ„ํŠผ +- ํŒจ๋„ ์—ด๋ฆด ๋•Œ ์Šคํฌ๋กค ์ž๋™ ์ดˆ๊ธฐํ™” -### Install NemoClaw and Onboard OpenClaw Agent +### ๐ŸŒ ๋‹ค๊ตญ์–ด +- ํ•œ๊ตญ์–ด / English ํ† ๊ธ€ (localStorage ์ €์žฅ) -Download and run the installer script. -The script installs Node.js if it is not already present, then runs the guided onboard wizard to create a sandbox, configure inference, and apply security policies. +### ๐ŸŒ‘ ๊ฒฐ๊ณผ ์—†์Œ ์ฒ˜๋ฆฌ +- ํ•€ 0๊ฐœ์ผ ๋•Œ ์˜ค๋ฒ„๋ ˆ์ด ์•ˆ๋‚ด + ํ•„ํ„ฐ ์ดˆ๊ธฐํ™” ๋ฒ„ํŠผ -```bash -curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash -``` +--- -If you use nvm or fnm to manage Node.js, the installer may not update your current shell's PATH. -If `nemoclaw` is not found after install, run `source ~/.bashrc` (or `source ~/.zshrc` for zsh) or open a new terminal. +## ๋ฐ์ดํ„ฐ (151๊ฐœ ์žฅ์†Œ) + +| ์นดํ…Œ๊ณ ๋ฆฌ | ์ˆ˜ | ์ฃผ์š” ์ •๋ณด | +|---|---|---| +| ๐Ÿ’ฑ ์‚ฌ์„คํ™˜์ „์†Œ | 15 | ํ†ตํ™”๋ณ„ ๋งค์ž…/ํŒ๋งค ํ™˜์œจ, ์ˆ˜์ˆ˜๋ฃŒ ์—ฌ๋ถ€ | +| โ›ฝ ์ฃผ์œ /์ถฉ์ „์†Œ | 22 | ํœ˜๋ฐœ์œ ยท๊ฒฝ์œ ยทLPGยท์ „๊ธฐยท์ˆ˜์†Œ ๊ฐ€๊ฒฉ | +| ๐Ÿœ ์‹๋‹น | 28 | 1์ธ ๊ฐ€๊ฒฉ, ๊ตญ์  ํƒœ๊ทธ (ํ• ๋ž„ยท๋น„๊ฑด ํฌํ•จ) | +| โ˜• ์นดํŽ˜ | 18 | ์•„๋ฉ”๋ฆฌ์นด๋…ธ ๊ฐ€๊ฒฉ, WiFiยท์•ผ์™ธ์„ ์—ฌ๋ถ€ | +| ๐Ÿช ํŽธ์˜์  | 12 | ๋ธŒ๋žœ๋“œ, ํ‰๊ท  ๋‹จ๊ฐ€, ATMยท24์‹œ ์—ฌ๋ถ€ | +| ๐Ÿ› ์ฐœ์งˆ๋ฐฉ | 10 | ์ž…์žฅ๋ฃŒ, ์ˆ™๋ฐ•๋ฃŒ, ํŽธ์˜์‹œ์„ค ๋ชฉ๋ก | +| ๐ŸŽค ๋…ธ๋ž˜๋ฐฉ | 10 | ๋ฃธ ํฌ๊ธฐ๋ณ„ ์‹œ๊ฐ„๋‹น ์š”๊ธˆ, ํ• ์ธ ์‹œ๊ฐ„๋Œ€ | +| ๐Ÿฎ ์ „ํ†ต์‹œ์žฅ | 8 | ์ธ๊ธฐ ๋ฉ”๋‰ดยท์ƒํ’ˆ๋ณ„ ๊ฐ€๊ฒฉ | +| ๐Ÿ›๏ธ ๊ด€๊ด‘๋ช…์†Œ | 12 | ์ž…์žฅ๊ถŒ ์ข…๋ฅ˜๋ณ„ ๊ฐ€๊ฒฉ, ๋ฌด๋ฃŒ ์ž…์žฅ ์กฐ๊ฑด | +| โœจ ๊ธฐํƒ€ | 16 | ํฌ์žฅ๋งˆ์ฐจยทPC๋ฐฉยท๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šคยท๋นจ๋ž˜๋ฐฉ ๋“ฑ | -When the install completes, a summary confirms the running environment: +--- -```text -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -Sandbox my-assistant (Landlock + seccomp + netns) -Model nvidia/nemotron-3-super-120b-a12b (NVIDIA Endpoints) -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -Run: nemoclaw my-assistant connect -Status: nemoclaw my-assistant status -Logs: nemoclaw my-assistant logs --follow -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +## ๊ธฐ์ˆ  ์Šคํƒ -[INFO] === Installation complete === ``` - -### Chat with the Agent - -Connect to the sandbox, then chat with the agent through the TUI or the CLI. - -#### Connect to the Sandbox - -Run the following command to connect to the sandbox: - -```bash -nemoclaw my-assistant connect +Frontend React 18 + TypeScript 5 + Vite 5 +์ง€๋„ react-leaflet 4.x + Leaflet 1.9.x + OpenStreetMap (CARTO Voyager) +์ƒํƒœ๊ด€๋ฆฌ Zustand 4.x (filterStore ยท uiStore ยท favoritesStore) +์Šคํƒ€์ผ Tailwind CSS v3 + Noto Sans KR ``` -This connects you to the sandbox shell `sandbox@my-assistant:~$` where you can run `openclaw` commands. - -#### OpenClaw TUI +--- -In the sandbox shell, run the following command to open the OpenClaw TUI, which opens an interactive chat interface. +## ๋กœ์ปฌ ์‹คํ–‰ ```bash -openclaw tui +cd map-app +npm install +npm run dev +# โ†’ http://localhost:5173 ``` -Send a test message to the agent and verify you receive a response. - -> **โ„น๏ธ Note** -> -> The TUI is best for interactive back-and-forth. If you need the full text of a long response such as a large code generation output, use the CLI instead. - -#### OpenClaw CLI - -In the sandbox shell, run the following command to send a single message and print the response: +๋นŒ๋“œ: ```bash -openclaw agent --agent main --local -m "hello" --session-id test +npm run build ``` -This prints the complete response directly in the terminal and avoids relying on the TUI view for long output. - -### Uninstall +--- -To remove NemoClaw and all resources created during setup, in the terminal outside the sandbox, run: +## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ -```bash -curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh | bash ``` - -The script removes sandboxes, the NemoClaw gateway and providers, related Docker images and containers, local state directories, and the global `nemoclaw` npm package. It does not remove shared system tooling such as Docker, Node.js, npm, or Ollama. - -| Flag | Effect | -|--------------------|-----------------------------------------------------| -| `--yes` | Skip the confirmation prompt. | -| `--keep-openshell` | Leave the `openshell` binary installed. | -| `--delete-models` | Also remove NemoClaw-pulled Ollama models. | - -For example, to skip the confirmation prompt: - -```bash -curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh | bash -s -- --yes +map-app/src/ +โ”œโ”€โ”€ components/ +โ”‚ โ”œโ”€โ”€ filters/ # CategoryFilter, NationalityDropdown, PriceRangeSlider +โ”‚ โ”œโ”€โ”€ layout/ # TopBar (๊ฒ€์ƒ‰), Sidebar (ํ•„ํ„ฐ+์ฆ๊ฒจ์ฐพ๊ธฐ) +โ”‚ โ”œโ”€โ”€ map/ # MapContainer, MarkerLayer, CategoryPin +โ”‚ โ”‚ # MyLocationButton, EmptyState, MapLegend +โ”‚ โ””โ”€โ”€ panels/ # DetailPanel (์ƒ์„ธ ์ •๋ณด) +โ”œโ”€โ”€ data/ # 10๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ JSON (151๊ฐœ ์žฅ์†Œ) +โ”œโ”€โ”€ hooks/ # useFilteredMarkers, useAllLocations, useAllPriceRanks +โ”œโ”€โ”€ store/ # filterStore, uiStore, favoritesStore +โ””โ”€โ”€ types/ # AnyLocation union type, CATEGORY_META ``` - - ---- - -## How It Works - -NemoClaw installs the NVIDIA OpenShell runtime, then creates a sandboxed OpenClaw environment where every network request, file access, and inference call is governed by declarative policy. The `nemoclaw` CLI orchestrates the full stack: OpenShell gateway, sandbox, inference provider, and network policy. - -| Component | Role | -|------------------|-------------------------------------------------------------------------------------------| -| **Plugin** | TypeScript CLI commands for launch, connect, status, and logs. | -| **Blueprint** | Versioned Python artifact that orchestrates sandbox creation, policy, and inference setup. | -| **Sandbox** | Isolated OpenShell container running OpenClaw with policy-enforced egress and filesystem. | -| **Inference** | Provider-routed model calls, routed through the OpenShell gateway, transparent to the agent. | - -The blueprint lifecycle follows four stages: resolve the artifact, verify its digest, plan the resources, and apply through the OpenShell CLI. - -When something goes wrong, errors may originate from either NemoClaw or the OpenShell layer underneath. Run `nemoclaw status` for NemoClaw-level health and `openshell sandbox list` to check the underlying sandbox state. - ---- - -## Inference - -Inference requests from the agent never leave the sandbox directly. OpenShell intercepts every call and routes it to the provider you selected during onboarding. - -Supported non-experimental onboarding paths: - -| Provider | Notes | -|---|---| -| NVIDIA Endpoints | Curated hosted models on `integrate.api.nvidia.com`. | -| OpenAI | Curated GPT models plus `Other...` for manual model entry. | -| Other OpenAI-compatible endpoint | For proxies and compatible gateways. | -| Anthropic | Curated Claude models plus `Other...` for manual model entry. | -| Other Anthropic-compatible endpoint | For Claude proxies and compatible gateways. | -| Google Gemini | Google's OpenAI-compatible endpoint. | - -During onboarding, NemoClaw validates the selected provider and model before it creates the sandbox: - -- OpenAI-compatible providers: tries `/responses` first, then `/chat/completions` -- Anthropic-compatible providers: tries `/v1/messages` -- If validation fails, the wizard prompts you to fix the selection before continuing - -Credentials stay on the host in `~/.nemoclaw/credentials.json`. The sandbox only sees the routed `inference.local` endpoint, not your raw provider key. - -Local Ollama is supported in the standard onboarding flow. Local vLLM remains experimental, and local host-routed inference on macOS still depends on OpenShell host-routing support in addition to the local service itself being reachable on the host. - ---- - -## Protection Layers - -The sandbox starts with a default policy that controls network egress and filesystem access: - -| Layer | What it protects | When it applies | -|------------|-----------------------------------------------------|-----------------------------| -| Network | Blocks unauthorized outbound connections. | Hot-reloadable at runtime. | -| Filesystem | Prevents reads/writes outside `/sandbox` and `/tmp`.| Locked at sandbox creation. | -| Process | Blocks privilege escalation and dangerous syscalls. | Locked at sandbox creation. | -| Inference | Reroutes model API calls to controlled backends. | Hot-reloadable at runtime. | - -When the agent tries to reach an unlisted host, OpenShell blocks the request and surfaces it in the TUI for operator approval. - --- -## Configuring Sandbox Policy - -The sandbox policy is defined in a declarative YAML file and enforced by the OpenShell runtime. -NemoClaw ships a default policy in [`nemoclaw-blueprint/policies/openclaw-sandbox.yaml`](https://github.com/NVIDIA/NemoClaw/blob/main/nemoclaw-blueprint/policies/openclaw-sandbox.yaml) that denies all network egress except explicitly listed endpoints. - -Operators can customize the policy in two ways: +## ๊ฐœ๋ฐœ ๋ธŒ๋žœ์น˜ -| Method | How | Scope | -|--------|-----|-------| -| **Static** | Edit `openclaw-sandbox.yaml` and re-run `nemoclaw onboard`. | Persists across restarts. | -| **Dynamic** | Run `openshell policy set ` on a running sandbox. | Session only; resets on restart. | - -NemoClaw includes preset policy files for common integrations such as PyPI, Docker Hub, Slack, and Jira in `nemoclaw-blueprint/policies/presets/`. Apply a preset as-is or use it as a starting template. - -NemoClaw is an open project โ€” we are still determining which presets to ship by default. If you have suggestions, please open an [issue](https://github.com/NVIDIA/NemoClaw/issues) or [discussion](https://github.com/NVIDIA/NemoClaw/discussions). - -When the agent attempts to reach an endpoint not covered by the policy, OpenShell blocks the request and surfaces it in the TUI (`openshell term`) for the operator to approve or deny in real time. Approved endpoints persist for the current session only. - -For step-by-step instructions, see [Customize Network Policy](https://docs.nvidia.com/nemoclaw/latest/network-policy/customize-network-policy.html). For the underlying enforcement details, see the OpenShell [Policy Schema](https://docs.nvidia.com/openshell/latest/reference/policy-schema.html) and [Sandbox Policies](https://docs.nvidia.com/openshell/latest/sandboxes/policies.html) documentation. +`claude/lowest-price-map-app-aj4LR` --- -## Key Commands - -### Host commands (`nemoclaw`) - -Run these on the host to set up, connect to, and manage sandboxes. - -| Command | Description | -|--------------------------------------|--------------------------------------------------------| -| `nemoclaw onboard` | Interactive setup wizard: gateway, providers, sandbox. | -| `nemoclaw connect` | Open an interactive shell inside the sandbox. | -| `openshell term` | Launch the OpenShell TUI for monitoring and approvals. | -| `nemoclaw start` / `stop` / `status` | Manage auxiliary services (Telegram bridge, tunnel). | - -See the full [CLI reference](https://docs.nvidia.com/nemoclaw/latest/reference/commands.html) for all commands, flags, and options. - ---- - -## Learn More - -Refer to the documentation for more information on NemoClaw. - -- [Overview](https://docs.nvidia.com/nemoclaw/latest/about/overview.html): Learn what NemoClaw does and how it fits together. -- [How It Works](https://docs.nvidia.com/nemoclaw/latest/about/how-it-works.html): Learn about the plugin, blueprint, and sandbox lifecycle. -- [Architecture](https://docs.nvidia.com/nemoclaw/latest/reference/architecture.html): Learn about the plugin structure, blueprint lifecycle, and sandbox environment. -- [Inference Profiles](https://docs.nvidia.com/nemoclaw/latest/reference/inference-profiles.html): Learn how NemoClaw configures routed inference providers. -- [Network Policies](https://docs.nvidia.com/nemoclaw/latest/reference/network-policies.html): Learn about egress control and policy customization. -- [CLI Commands](https://docs.nvidia.com/nemoclaw/latest/reference/commands.html): Learn about the full command reference. -- [Troubleshooting](https://docs.nvidia.com/nemoclaw/latest/reference/troubleshooting.html): Troubleshoot common issues and resolution steps. -- [Discord](https://discord.gg/XFpfPv9Uvx): Join the community for questions and discussion. - -## License +## ๋ผ์ด์„ ์Šค -This project is licensed under the [Apache License 2.0](LICENSE). +[Apache License 2.0](LICENSE) diff --git a/map-app/index.html b/map-app/index.html new file mode 100644 index 000000000..0fcdf5a5d --- /dev/null +++ b/map-app/index.html @@ -0,0 +1,16 @@ + + + + + + + ์ตœ์ €๊ฐ€ ์ง€๋„ | Korea Lowest Price Map + + + + + +
+ + + diff --git a/map-app/package-lock.json b/map-app/package-lock.json new file mode 100644 index 000000000..f22106020 --- /dev/null +++ b/map-app/package-lock.json @@ -0,0 +1,2704 @@ +{ + "name": "map-app", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "map-app", + "version": "0.1.0", + "dependencies": { + "@seed-design/css": "^1.2.7", + "@seed-design/design-token": "^1.0.4", + "@seed-design/tailwind3-plugin": "^1.1.18", + "leaflet": "^1.9.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@types/leaflet": "^1.9.12", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.3", + "vite": "^5.3.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@seed-design/css": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@seed-design/css/-/css-1.2.7.tgz", + "integrity": "sha512-Tgr8SKEfi1Awx+UyajmpZw0ekpYPsor6S/8R/RHQGCIUx9UpLtlsDsUkgBvkyB2SrzPTnXXnKioLGpb0EF4HyA==" + }, + "node_modules/@seed-design/design-token": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@seed-design/design-token/-/design-token-1.0.4.tgz", + "integrity": "sha512-XSrnUZ0xVLHiMxxHSJQ8YARlAluSt5CgiKFFeEkbVAfcYh66jQWdM1N/RuJosORuXyidf+tZ2zMYRcwRetldPw==" + }, + "node_modules/@seed-design/tailwind3-plugin": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@seed-design/tailwind3-plugin/-/tailwind3-plugin-1.1.18.tgz", + "integrity": "sha512-oySeVTaAni/+ajFqNsBb+rmRkIw5VKfwnmo7rwGzCO263vHhU/lTo07bIhvSizmSelD8YwaGLQ0M5f12oKzRug==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": "3" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.21", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", + "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", + "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001785", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", + "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/map-app/package.json b/map-app/package.json new file mode 100644 index 000000000..d87525f09 --- /dev/null +++ b/map-app/package.json @@ -0,0 +1,33 @@ +{ + "name": "map-app", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "@seed-design/css": "^1.2.7", + "@seed-design/design-token": "^1.0.4", + "@seed-design/tailwind3-plugin": "^1.1.18", + "leaflet": "^1.9.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-leaflet": "^4.2.1", + "zustand": "^4.5.2" + }, + "devDependencies": { + "@types/leaflet": "^1.9.12", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.40", + "tailwindcss": "^3.4.7", + "typescript": "^5.5.3", + "vite": "^5.3.5" + } +} diff --git a/map-app/postcss.config.cjs b/map-app/postcss.config.cjs new file mode 100644 index 000000000..33ad091d2 --- /dev/null +++ b/map-app/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/map-app/src/App.tsx b/map-app/src/App.tsx new file mode 100644 index 000000000..7c49667ea --- /dev/null +++ b/map-app/src/App.tsx @@ -0,0 +1,52 @@ +import { useEffect } from 'react' +import { TopBar } from './components/layout/TopBar' +import { Sidebar } from './components/layout/Sidebar' +import { MapContainer } from './components/map/MapContainer' +import { CategoryFilterBar } from './components/filters/CategoryFilterBar' +import { DesktopDetailPanel, MobileBottomSheet } from './components/panels/DetailPanel' +import { useUiStore } from './store' + +export default function App() { + const { setIsMobile } = useUiStore() + + useEffect(() => { + const handle = () => setIsMobile(window.innerWidth < 768) + window.addEventListener('resize', handle) + return () => window.removeEventListener('resize', handle) + }, [setIsMobile]) + + return ( +
+ {/* Top navigation bar */} + + + {/* Main content area */} +
+ {/* Desktop sidebar */} + + + {/* Map + filter bar + detail panel */} +
+ {/* Mobile category filter bar */} +
+ +
+ + {/* Map area + right detail panel */} +
+ {/* Map */} +
+ +
+ + {/* Desktop detail panel (slides in from right) */} + +
+
+
+ + {/* Mobile bottom sheet */} + +
+ ) +} diff --git a/map-app/src/components/common/CategoryIcon.tsx b/map-app/src/components/common/CategoryIcon.tsx new file mode 100644 index 000000000..d4b379193 --- /dev/null +++ b/map-app/src/components/common/CategoryIcon.tsx @@ -0,0 +1,18 @@ +import { CATEGORY_META } from '@/types' +import type { PriceCategory } from '@/types' + +interface Props { + category: PriceCategory + size?: 'sm' | 'md' | 'lg' + className?: string +} + +export function CategoryIcon({ category, size = 'md', className = '' }: Props) { + const meta = CATEGORY_META[category] + const sizeClass = size === 'sm' ? 'text-lg' : size === 'lg' ? 'text-3xl' : 'text-xl' + return ( + + {meta.emoji} + + ) +} diff --git a/map-app/src/components/common/PriceBadge.tsx b/map-app/src/components/common/PriceBadge.tsx new file mode 100644 index 000000000..88dfd887b --- /dev/null +++ b/map-app/src/components/common/PriceBadge.tsx @@ -0,0 +1,22 @@ +interface Props { + price: number + unit?: string + highlight?: boolean + className?: string +} + +export function PriceBadge({ price, unit = 'โ‚ฉ', highlight = false, className = '' }: Props) { + const formatted = price.toLocaleString('ko-KR') + return ( + + {highlight && ๐Ÿ‘‘} + {unit}{formatted} + + ) +} diff --git a/map-app/src/components/filters/CategoryFilterBar.tsx b/map-app/src/components/filters/CategoryFilterBar.tsx new file mode 100644 index 000000000..cff0c61ff --- /dev/null +++ b/map-app/src/components/filters/CategoryFilterBar.tsx @@ -0,0 +1,52 @@ +import { useFilterStore } from '@/store' +import { CATEGORY_META } from '@/types' +import type { PriceCategory } from '@/types' +import { useUiStore } from '@/store' + +const CATEGORIES: PriceCategory[] = [ + 'exchange', 'fuel', 'restaurant', 'cafe', 'convenience', + 'jjimjilbang', 'karaoke', 'market', 'attraction', 'extra', +] + +export function CategoryFilterBar() { + const { activeCategories, toggleCategory, clearCategories } = useFilterStore() + const { language } = useUiStore() + + return ( +
+ {/* All button */} + + + {CATEGORIES.map((cat) => { + const meta = CATEGORY_META[cat] + const isActive = activeCategories.includes(cat) + return ( + + ) + })} +
+ ) +} diff --git a/map-app/src/components/filters/NationalityDropdown.tsx b/map-app/src/components/filters/NationalityDropdown.tsx new file mode 100644 index 000000000..60ceb157d --- /dev/null +++ b/map-app/src/components/filters/NationalityDropdown.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react' +import { useFilterStore } from '@/store' +import { useUiStore } from '@/store' +import { NATIONALITY_META } from '@/types' +import type { Nationality } from '@/types' + +const NATIONALITIES: Nationality[] = [ + 'all', 'korean', 'japanese', 'chinese', 'western', 'indian', 'halal', 'vegan', +] + +export function NationalityDropdown() { + const [open, setOpen] = useState(false) + const { nationality, setNationality } = useFilterStore() + const { language } = useUiStore() + const current = NATIONALITY_META[nationality] + + return ( +
+ + + {open && ( +
+
+ {NATIONALITIES.map((nat) => { + const meta = NATIONALITY_META[nat] + const isSelected = nationality === nat + return ( + + ) + })} +
+
+ )} + + {open && ( +
setOpen(false)} /> + )} +
+ ) +} diff --git a/map-app/src/components/filters/PriceRangeSlider.tsx b/map-app/src/components/filters/PriceRangeSlider.tsx new file mode 100644 index 000000000..420a2ddc2 --- /dev/null +++ b/map-app/src/components/filters/PriceRangeSlider.tsx @@ -0,0 +1,82 @@ +import { useFilterStore } from '@/store' +import { useUiStore } from '@/store' + +export function PriceRangeSlider() { + const { priceRange, setPriceRange } = useFilterStore() + const { language } = useUiStore() + const MAX = 200000 + + const formatPrice = (v: number) => { + if (v >= 100000) return `${(v / 10000).toFixed(0)}๋งŒ์›` + if (v >= 10000) return `${(v / 10000).toFixed(1)}๋งŒ` + return `โ‚ฉ${v.toLocaleString()}` + } + + const brandColor = 'var(--seed-color-bg-brand-solid)' + const trackColor = 'var(--seed-color-bg-neutral-weak)' + + return ( +
+
+

+ {language === 'ko' ? '๐Ÿ’ฐ ๊ฐ€๊ฒฉ ๋ฒ”์œ„' : '๐Ÿ’ฐ Price Range'} +

+ + {formatPrice(priceRange[0])} ~ {formatPrice(priceRange[1])} + +
+
+
+
+ {language === 'ko' ? '์ตœ์†Œ' : 'Min'} + {formatPrice(priceRange[0])} +
+ { + const val = Number(e.target.value) + if (val < priceRange[1]) setPriceRange([val, priceRange[1]]) + }} + className="w-full seed-range" + style={{ + background: `linear-gradient(to right, ${trackColor} 0%, ${trackColor} ${(priceRange[0]/MAX)*100}%, ${brandColor} ${(priceRange[0]/MAX)*100}%, ${brandColor} 100%)` + }} + /> +
+
+
+ {language === 'ko' ? '์ตœ๋Œ€' : 'Max'} + + {priceRange[1] >= MAX ? (language === 'ko' ? '์ œํ•œ์—†์Œ' : 'No limit') : formatPrice(priceRange[1])} + +
+ { + const val = Number(e.target.value) + if (val > priceRange[0]) setPriceRange([priceRange[0], val]) + }} + className="w-full seed-range" + style={{ + background: `linear-gradient(to right, ${brandColor} 0%, ${brandColor} ${(priceRange[1]/MAX)*100}%, ${trackColor} ${(priceRange[1]/MAX)*100}%, ${trackColor} 100%)` + }} + /> +
+
+ +
+ ) +} diff --git a/map-app/src/components/layout/Sidebar.tsx b/map-app/src/components/layout/Sidebar.tsx new file mode 100644 index 000000000..f5410c897 --- /dev/null +++ b/map-app/src/components/layout/Sidebar.tsx @@ -0,0 +1,170 @@ +import { PriceRangeSlider } from '@/components/filters/PriceRangeSlider' +import { useUiStore, useFilterStore, useFavoritesStore } from '@/store' +import { useAllLocations } from '@/hooks/useAllLocations' +import { useUiStore as useUi } from '@/store' +import { CATEGORY_META } from '@/types' +import type { PriceCategory } from '@/types' + +const CATEGORIES: PriceCategory[] = [ + 'exchange', 'fuel', 'restaurant', 'cafe', 'convenience', + 'jjimjilbang', 'karaoke', 'market', 'attraction', 'extra', +] + +const STAT_CATEGORIES: { emoji: string; cat: PriceCategory; labelKo: string; labelEn: string }[] = [ + { emoji: '๐Ÿ’ฑ', cat: 'exchange', labelKo: 'ํ™˜์ „์†Œ', labelEn: 'Exchange' }, + { emoji: 'โ›ฝ', cat: 'fuel', labelKo: '์ฃผ์œ ์†Œ', labelEn: 'Fuel' }, + { emoji: '๐Ÿœ', cat: 'restaurant', labelKo: '์‹๋‹น', labelEn: 'Restaurants' }, + { emoji: 'โ˜•', cat: 'cafe', labelKo: '์นดํŽ˜', labelEn: 'Cafes' }, +] + +export function Sidebar() { + const { language } = useUiStore() + const { activeCategories, toggleCategory, clearCategories, showCheapestOnly, toggleCheapestOnly } = useFilterStore() + const { favoriteIds, clear: clearFavs } = useFavoritesStore() + const { setSelectedPin } = useUi() + const all = useAllLocations() + + const countByCategory = CATEGORIES.reduce>((acc, cat) => { + acc[cat] = all.filter((l) => l.category === cat).length + return acc + }, {}) + + return ( + + ) +} diff --git a/map-app/src/components/layout/TopBar.tsx b/map-app/src/components/layout/TopBar.tsx new file mode 100644 index 000000000..f9c13e84f --- /dev/null +++ b/map-app/src/components/layout/TopBar.tsx @@ -0,0 +1,95 @@ +import { useState, useRef, useEffect } from 'react' +import { useUiStore } from '@/store' +import { useFilteredMarkers } from '@/hooks/useFilteredMarkers' +import { NationalityDropdown } from '@/components/filters/NationalityDropdown' +import { useFilterStore } from '@/store' + +export function TopBar() { + const { language, toggleLanguage } = useUiStore() + const filteredCount = useFilteredMarkers().length + const { searchQuery, setSearchQuery } = useFilterStore() + const [searchOpen, setSearchOpen] = useState(false) + const inputRef = useRef(null) + + useEffect(() => { + if (searchOpen && inputRef.current) { + inputRef.current.focus() + } + }, [searchOpen]) + + function handleSearchToggle() { + if (searchOpen && searchQuery) { + setSearchQuery('') + } else { + setSearchOpen((v) => !v) + } + } + + return ( +
+
+ {/* Logo */} +
+
+ ๐Ÿ’ฐ +
+
+

+ {language === 'ko' ? '์ตœ์ €๊ฐ€ ์ง€๋„' : 'Lowest Price Map'} +

+

+ {language === 'ko' ? `${filteredCount}๊ฐœ ์žฅ์†Œ` : `${filteredCount} places`} +

+
+
+ + {/* Search bar (expanded state) */} + {searchOpen && ( +
+ setSearchQuery(e.target.value)} + placeholder={language === 'ko' ? '์žฅ์†Œ ๊ฒ€์ƒ‰...' : 'Search places...'} + className="w-full bg-bg-layer-fill text-fg-neutral placeholder:text-fg-neutral-subtle text-sm px-3 py-1.5 + rounded-lg border border-stroke-neutral-subtle focus:outline-none focus:border-stroke-brand-solid transition-colors" + onKeyDown={(e) => { + if (e.key === 'Escape') { + setSearchQuery('') + setSearchOpen(false) + } + }} + /> +
+ )} + + {/* Right side controls */} +
+ {/* Search button */} + + + {/* Nationality filter (hide when search open on small screens) */} + {!searchOpen && } + + {/* Language toggle */} + +
+
+
+ ) +} diff --git a/map-app/src/components/map/CategoryPin.tsx b/map-app/src/components/map/CategoryPin.tsx new file mode 100644 index 000000000..9c5e3d129 --- /dev/null +++ b/map-app/src/components/map/CategoryPin.tsx @@ -0,0 +1,50 @@ +import L from 'leaflet' +import { CATEGORY_META } from '@/types' +import type { PriceCategory } from '@/types' + +// Create custom Leaflet DivIcon for each category +export function createCategoryIcon( + category: PriceCategory, + rank?: 1 | 2 | 3, +): L.DivIcon { + const meta = CATEGORY_META[category] + + const rankBadge = + rank === 1 + ? '
๐Ÿ‘‘
' + : rank === 2 + ? '
2
' + : rank === 3 + ? '
3
' + : '' + + const html = ` +
+ ${rankBadge} +
+ ${meta.emoji} +
+
+ ` + + return L.divIcon({ + html, + className: '', + iconSize: [40, 48], + iconAnchor: [20, 48], + popupAnchor: [0, -48], + }) +} diff --git a/map-app/src/components/map/EmptyState.tsx b/map-app/src/components/map/EmptyState.tsx new file mode 100644 index 000000000..1c303878c --- /dev/null +++ b/map-app/src/components/map/EmptyState.tsx @@ -0,0 +1,45 @@ +import { useFilterStore } from '@/store' +import { useUiStore } from '@/store' + +export function EmptyState() { + const { resetFilters } = useFilterStore() + const { language } = useUiStore() + + return ( +
+
+
๐Ÿ—บ๏ธ
+

+ {language === 'ko' ? '๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ' : 'No results found'} +

+

+ {language === 'ko' + ? 'ํ•„ํ„ฐ ์กฐ๊ฑด์„ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ์ดˆ๊ธฐํ™”ํ•ด ๋ณด์„ธ์š”' + : 'Try adjusting or resetting your filters'} +

+ +
+
+ ) +} diff --git a/map-app/src/components/map/MapContainer.tsx b/map-app/src/components/map/MapContainer.tsx new file mode 100644 index 000000000..bd6df4521 --- /dev/null +++ b/map-app/src/components/map/MapContainer.tsx @@ -0,0 +1,41 @@ +import { MapContainer as LeafletMap, TileLayer } from 'react-leaflet' +import { MarkerLayer } from './MarkerLayer' +import { MyLocationButton } from './MyLocationButton' +import { MapLegend } from './MapLegend' +import 'leaflet/dist/leaflet.css' + +// Fix for default marker icons in leaflet + webpack/vite +import L from 'leaflet' +import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png' +import markerIcon from 'leaflet/dist/images/marker-icon.png' +import markerShadow from 'leaflet/dist/images/marker-shadow.png' + +delete (L.Icon.Default.prototype as unknown as Record)._getIconUrl +L.Icon.Default.mergeOptions({ + iconRetinaUrl: markerIcon2x, + iconUrl: markerIcon, + shadowUrl: markerShadow, +}) + +const SEOUL_CENTER: [number, number] = [37.5665, 126.978] + +export function MapContainer() { + return ( + + + + + + + ) +} diff --git a/map-app/src/components/map/MapLegend.tsx b/map-app/src/components/map/MapLegend.tsx new file mode 100644 index 000000000..2bb479cf6 --- /dev/null +++ b/map-app/src/components/map/MapLegend.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from 'react' +import { useMap } from 'react-leaflet' +import L from 'leaflet' +import { createRoot, Root } from 'react-dom/client' +import { CATEGORY_META } from '@/types' +import { useUiStore } from '@/store' +import type { PriceCategory } from '@/types' + +const CATEGORIES: PriceCategory[] = [ + 'exchange', 'fuel', 'restaurant', 'cafe', 'convenience', + 'jjimjilbang', 'karaoke', 'market', 'attraction', 'extra', +] + +function LegendPanel() { + const [open, setOpen] = useState(false) + const { language } = useUiStore() + + return ( +
+ + + {open && ( +
+
+ ๐Ÿ‘‘ 1์œ„ + ๐Ÿฅˆ 2์œ„ + ๐Ÿฅ‰ 3์œ„ +
+ {CATEGORIES.map((cat) => { + const meta = CATEGORY_META[cat] + return ( +
+ + {meta.emoji} + + {language === 'ko' ? meta.label : meta.labelEn} +
+ ) + })} +
+ )} +
+ ) +} + +export function MapLegend() { + const map = useMap() + + useEffect(() => { + let root: Root | null = null + + const control = new (L.Control.extend({ + options: { position: 'bottomleft' }, + onAdd() { + const container = L.DomUtil.create('div') + L.DomEvent.disableClickPropagation(container) + root = createRoot(container) + root.render() + return container + }, + }))() + + control.addTo(map) + return () => { + root?.unmount() + control.remove() + } + }, [map]) + + return null +} diff --git a/map-app/src/components/map/MarkerLayer.tsx b/map-app/src/components/map/MarkerLayer.tsx new file mode 100644 index 000000000..fb59c8e95 --- /dev/null +++ b/map-app/src/components/map/MarkerLayer.tsx @@ -0,0 +1,109 @@ +import { Marker, Tooltip } from 'react-leaflet' +import L from 'leaflet' +import { useFilteredMarkers } from '@/hooks/useFilteredMarkers' +import { useAllPriceRanks } from '@/hooks/useAllPriceRanks' +import { useUiStore } from '@/store' +import { createCategoryIcon } from './CategoryPin' +import { CATEGORY_META } from '@/types' +import { EmptyState } from './EmptyState' +import type { AnyLocation } from '@/types' + +function getDisplayPrice(loc: AnyLocation): string { + switch (loc.category) { + case 'exchange': { + const usd = loc.rates.find((r) => r.currency === 'USD') + return usd ? `$1 = โ‚ฉ${usd.buyRate.toLocaleString()}` : '' + } + case 'fuel': { + if (loc.prices.gasoline) return `ํœ˜๋ฐœ์œ  โ‚ฉ${loc.prices.gasoline.toLocaleString()}/L` + if (loc.prices.electric) return `์ „๊ธฐ โ‚ฉ${loc.prices.electric}/kWh` + if (loc.prices.hydrogen) return `์ˆ˜์†Œ โ‚ฉ${loc.prices.hydrogen?.toLocaleString()}/kg` + return '' + } + case 'restaurant': return `โ‚ฉ${loc.pricePerPerson.toLocaleString()}/์ธ` + case 'cafe': return `์•„๋ฉ”๋ฆฌ์นด๋…ธ โ‚ฉ${loc.americanoPrice.toLocaleString()}` + case 'convenience': return `ํ‰๊ท  โ‚ฉ${loc.avgItemPrice.toLocaleString()}` + case 'jjimjilbang': return `์ž…์žฅ โ‚ฉ${loc.entryFee.toLocaleString()}` + case 'karaoke': return `โ‚ฉ${Math.min(...loc.rates.map((r) => r.pricePerHour)).toLocaleString()}/์‹œ๊ฐ„` + case 'market': return `โ‚ฉ${Math.min(...loc.popularItems.map((i) => i.price)).toLocaleString()}~` + case 'attraction': { + const paid = loc.tickets.filter((t) => t.price > 0) + if (loc.freeEntry || paid.length === 0) return '๋ฌด๋ฃŒ ์ž…์žฅ' + return `โ‚ฉ${Math.min(...paid.map((t) => t.price)).toLocaleString()}~` + } + case 'extra': return `โ‚ฉ${loc.price.toLocaleString()}/${loc.priceUnit}` + default: return '' + } +} + +function createSelectedIcon(loc: AnyLocation, rank: 1 | 2 | 3 | undefined) { + const meta = CATEGORY_META[loc.category] + const rankStyle = rank === 1 + ? 'background:#fbbf24;border-color:#f59e0b' + : rank === 2 + ? 'background:#94a3b8;border-color:#64748b' + : rank === 3 + ? 'background:#cd7c4a;border-color:#a16207' + : `background:${meta.color};border-color:${meta.color}` + + const html = ` +
${meta.emoji}
+ ` + return L.divIcon({ html, className: '', iconSize: [44, 44], iconAnchor: [22, 22] }) +} + +export function MarkerLayer() { + const markers = useFilteredMarkers() + const { setSelectedPin, selectedPin } = useUiStore() + const rankMap = useAllPriceRanks() + + return ( + <> + {markers.length === 0 && } + {markers.map((loc) => { + const rank = rankMap[loc.id] as 1 | 2 | 3 | undefined + const isSelected = selectedPin?.id === loc.id + const icon = isSelected + ? createSelectedIcon(loc, rank) + : createCategoryIcon(loc.category, rank) + const meta = CATEGORY_META[loc.category] + const priceLabel = getDisplayPrice(loc) + + return ( + setSelectedPin(loc), + }} + > + +
+
+ {meta.emoji} + {loc.name} + {rank === 1 && ๐Ÿ‘‘} +
+ {priceLabel && ( +
{priceLabel}
+ )} +
+
+
+ ) + })} + + ) +} diff --git a/map-app/src/components/map/MyLocationButton.tsx b/map-app/src/components/map/MyLocationButton.tsx new file mode 100644 index 000000000..22785a8f8 --- /dev/null +++ b/map-app/src/components/map/MyLocationButton.tsx @@ -0,0 +1,58 @@ +import { useEffect } from 'react' +import { useMap } from 'react-leaflet' +import L from 'leaflet' + +export function MyLocationButton() { + const map = useMap() + + useEffect(() => { + const btn = L.DomUtil.create('button') as HTMLButtonElement + btn.innerHTML = '๐Ÿ“' + btn.title = '๋‚ด ์œ„์น˜๋กœ ์ด๋™' + btn.style.cssText = ` + width:40px;height:40px;border-radius:50%;background:#fff; + border:2px solid #e2e8f0;box-shadow:0 2px 8px rgba(0,0,0,0.15); + font-size:18px;cursor:pointer;display:flex;align-items:center; + justify-content:center;transition:background 0.15s; + ` + btn.onmouseenter = () => { btn.style.background = '#f8fafc' } + btn.onmouseleave = () => { btn.style.background = '#fff' } + + const control = new (L.Control.extend({ + options: { position: 'topright' }, + onAdd() { + const container = L.DomUtil.create('div') + L.DomEvent.disableClickPropagation(container) + let loading = false + + btn.onclick = () => { + if (loading || !navigator.geolocation) return + loading = true + btn.innerHTML = 'โŸณ' + btn.style.animation = 'spin 1s linear infinite' + navigator.geolocation.getCurrentPosition( + (pos) => { + map.flyTo([pos.coords.latitude, pos.coords.longitude], 15, { duration: 1.2 }) + loading = false + btn.innerHTML = '๐Ÿ“' + btn.style.animation = '' + }, + () => { + loading = false + btn.innerHTML = '๐Ÿ“' + btn.style.animation = '' + }, + { timeout: 8000 } + ) + } + container.appendChild(btn) + return container + }, + }))() + + control.addTo(map) + return () => { control.remove() } + }, [map]) + + return null +} diff --git a/map-app/src/components/panels/DetailPanel.tsx b/map-app/src/components/panels/DetailPanel.tsx new file mode 100644 index 000000000..41aa7cf0b --- /dev/null +++ b/map-app/src/components/panels/DetailPanel.tsx @@ -0,0 +1,177 @@ +import { useRef, useEffect } from 'react' +import { useUiStore } from '@/store' +import { useFavoritesStore } from '@/store/favoritesStore' +import { CATEGORY_META } from '@/types' +import { ExchangeDetail } from './ExchangeDetail' +import { FuelDetail } from './FuelDetail' +import { RestaurantDetail } from './RestaurantDetail' +import { GenericDetail } from './GenericDetail' +import type { ExchangeLocation, FuelStation, Restaurant } from '@/types' + +interface PanelContentProps { + onClose: () => void +} + +export function PanelContent({ onClose }: PanelContentProps) { + const { selectedPin, language } = useUiStore() + const { isFavorite, toggle } = useFavoritesStore() + const scrollRef = useRef(null) + + // Reset scroll when pin changes + useEffect(() => { + if (scrollRef.current) scrollRef.current.scrollTop = 0 + }, [selectedPin?.id]) + + if (!selectedPin) return null + + const meta = CATEGORY_META[selectedPin.category] + const fav = isFavorite(selectedPin.id) + + function renderDetail() { + if (!selectedPin) return null + switch (selectedPin.category) { + case 'exchange': return + case 'fuel': return + case 'restaurant': return + default: return + } + } + + const kakaoUrl = `https://map.kakao.com/link/map/${encodeURIComponent(selectedPin.name)},${selectedPin.lat},${selectedPin.lng}` + const googleUrl = `https://www.google.com/maps/search/?api=1&query=${selectedPin.lat},${selectedPin.lng}` + + return ( +
+ {/* Header */} +
+
+
+ {meta.emoji} +
+
+
+

+ {language === 'ko' ? selectedPin.name : (selectedPin.nameEn || selectedPin.name)} +

+
+
+ + {language === 'ko' ? meta.label : meta.labelEn} + + {selectedPin.rating && ( + โญ {selectedPin.rating} + )} +
+ {selectedPin.address && ( +

{selectedPin.address}

+ )} +
+
+
+ {/* Favorites button */} + + {/* Close button */} + +
+
+ + {/* Scrollable content */} +
+ {renderDetail()} + + {/* Phone */} + {selectedPin.phone && ( +
+
+ ๐Ÿ“ž + {selectedPin.phone} +
+
+ )} + + {/* Open in Maps */} + +
+
+ ) +} + +// Desktop sidebar panel +export function DesktopDetailPanel() { + const { isPanelOpen, closePanel } = useUiStore() + + return ( +
+ {isPanelOpen && } +
+ ) +} + +// Mobile bottom sheet with drag support +export function MobileBottomSheet() { + const { isPanelOpen, closePanel } = useUiStore() + + return ( + <> + {isPanelOpen && ( +
+ )} +
+
+
+
+
+ {isPanelOpen && } +
+
+ + ) +} diff --git a/map-app/src/components/panels/ExchangeDetail.tsx b/map-app/src/components/panels/ExchangeDetail.tsx new file mode 100644 index 000000000..00c5e29c8 --- /dev/null +++ b/map-app/src/components/panels/ExchangeDetail.tsx @@ -0,0 +1,73 @@ +import type { ExchangeLocation } from '@/types' +import { useUiStore } from '@/store' + +const CURRENCY_FLAGS: Record = { + USD: '๐Ÿ‡บ๐Ÿ‡ธ', JPY: '๐Ÿ‡ฏ๐Ÿ‡ต', CNY: '๐Ÿ‡จ๐Ÿ‡ณ', EUR: '๐Ÿ‡ช๐Ÿ‡บ', GBP: '๐Ÿ‡ฌ๐Ÿ‡ง', THB: '๐Ÿ‡น๐Ÿ‡ญ', +} + +interface Props { location: ExchangeLocation } + +export function ExchangeDetail({ location }: Props) { + const { language } = useUiStore() + const sortedRates = [...location.rates].sort((a, b) => b.buyRate - a.buyRate) + + return ( +
+
+ {location.noCommission && ( + + โœ… {language === 'ko' ? '์ˆ˜์ˆ˜๋ฃŒ ์—†์Œ' : 'No Commission'} + + )} + {location.minAmount !== undefined && location.minAmount === 0 && ( + + ๐Ÿ’ก {language === 'ko' ? '์ตœ์†Œ๊ธˆ์•ก ์—†์Œ' : 'No Minimum'} + + )} +
+ +
+

+ {language === 'ko' ? 'ํ™˜์œจ (๋งค๋งค๊ธฐ์ค€์œจ)' : 'Exchange Rates'} +

+
+ {sortedRates.map((rate) => ( +
+
+ {CURRENCY_FLAGS[rate.currency] ?? '๐Ÿ’ฑ'} +
+
1 {rate.currency}
+
+ {language === 'ko' ? '์‚ด ๋•Œ' : 'Buy'} / {language === 'ko' ? 'ํŒ” ๋•Œ' : 'Sell'} +
+
+
+
+
โ‚ฉ{rate.buyRate.toLocaleString()}
+
โ‚ฉ{rate.sellRate.toLocaleString()}
+
+
+ ))} +
+
+ + {location.openHours && ( +
+ ๐Ÿ• + {location.openHours} +
+ )} + + {location.minAmount !== undefined && location.minAmount > 0 && ( +
+ ๐Ÿ’ต + + {language === 'ko' ? `์ตœ์†Œ ํ™˜์ „: $${location.minAmount}` : `Min amount: $${location.minAmount}`} + +
+ )} +
+ ) +} diff --git a/map-app/src/components/panels/FuelDetail.tsx b/map-app/src/components/panels/FuelDetail.tsx new file mode 100644 index 000000000..bad8db65c --- /dev/null +++ b/map-app/src/components/panels/FuelDetail.tsx @@ -0,0 +1,64 @@ +import type { FuelStation } from '@/types' +import { useUiStore } from '@/store' + +const FUEL_META = { + gasoline: { label: 'ํœ˜๋ฐœ์œ ', labelEn: 'Gasoline', unit: '/L', emoji: 'โ›ฝ', color: 'text-green-700', bg: 'bg-green-50' }, + diesel: { label: '๊ฒฝ์œ ', labelEn: 'Diesel', unit: '/L', emoji: '๐Ÿ›ข๏ธ', color: 'text-blue-700', bg: 'bg-blue-50' }, + lpg: { label: 'LPG', labelEn: 'LPG', unit: '/L', emoji: '๐Ÿ”ต', color: 'text-purple-700', bg: 'bg-purple-50' }, + electric: { label: '์ „๊ธฐ', labelEn: 'Electric', unit: '/kWh', emoji: 'โšก', color: 'text-yellow-700', bg: 'bg-yellow-50' }, + hydrogen: { label: '์ˆ˜์†Œ', labelEn: 'Hydrogen', unit: '/kg', emoji: '๐Ÿ’ง', color: 'text-cyan-700', bg: 'bg-cyan-50' }, +} + +interface Props { location: FuelStation } + +export function FuelDetail({ location }: Props) { + const { language } = useUiStore() + const entries = Object.entries(location.prices).filter(([, v]) => v !== undefined) as [keyof typeof FUEL_META, number][] + + return ( +
+
+ {location.selfService && ( + + ๐Ÿ”ง {language === 'ko' ? '์…€ํ”„์ฃผ์œ ' : 'Self-service'} + + )} + {location.carWash && ( + + ๐Ÿšฟ {language === 'ko' ? '์„ธ์ฐจ์žฅ' : 'Car wash'} + + )} +
+ +
+

+ {language === 'ko' ? '์—ฐ๋ฃŒ ๊ฐ€๊ฒฉ' : 'Fuel Prices'} +

+
+ {entries.map(([type, price]) => { + const meta = FUEL_META[type] + return ( +
+
+ {meta.emoji} + + {language === 'ko' ? meta.label : meta.labelEn} + +
+
+ โ‚ฉ{price.toLocaleString()} + {meta.unit} +
+
+ ) + })} +
+
+ +
+
{location.brand}
+
{location.address}
+
+
+ ) +} diff --git a/map-app/src/components/panels/GenericDetail.tsx b/map-app/src/components/panels/GenericDetail.tsx new file mode 100644 index 000000000..108359136 --- /dev/null +++ b/map-app/src/components/panels/GenericDetail.tsx @@ -0,0 +1,226 @@ +import type { + AnyLocation, Cafe, ConvenienceStore, Jjimjilbang, + Karaoke, TraditionalMarket, TouristAttraction, Extra +} from '@/types' +import { useUiStore } from '@/store' + +interface Props { location: AnyLocation } + +export function GenericDetail({ location }: Props) { + const { language } = useUiStore() + + switch (location.category) { + case 'cafe': { + const loc = location as Cafe + return ( +
+
+ {loc.hasWifi && ๐Ÿ“ถ WiFi} + {loc.hasOutdoorSeating && ๐ŸŒฟ {language === 'ko' ? '์•ผ์™ธ์„' : 'Outdoor'}} +
+
+ {language === 'ko' ? '์•„๋ฉ”๋ฆฌ์นด๋…ธ' : 'Americano'} + โ‚ฉ{loc.americanoPrice.toLocaleString()} +
+
+ {loc.items.map((item, i) => ( +
+
+ {language === 'ko' ? item.name : item.nameEn} + {item.isBestValue && ๐Ÿ‘‘ BEST} +
+ โ‚ฉ{item.price.toLocaleString()} +
+ ))} +
+
+ ) + } + + case 'convenience': { + const loc = location as ConvenienceStore + const brandColors: Record = { + CU: 'bg-purple-100 text-purple-700', + GS25: 'bg-blue-100 text-blue-700', + '7-Eleven': 'bg-red-100 text-red-700', + emart24: 'bg-yellow-100 text-yellow-700', + Ministop: 'bg-green-100 text-green-700', + } + return ( +
+
+ + {loc.brand} + + {loc.open24Hours && 24H} + {loc.hasAtm && ๐Ÿง ATM} +
+

{language === 'ko' ? '์ธ๊ธฐ ์ƒํ’ˆ' : 'Popular Items'}

+
+ {loc.popularItems.map((item, i) => ( +
+ {language === 'ko' ? item.name : item.nameEn} + โ‚ฉ{item.price.toLocaleString()} +
+ ))} +
+
+ ) + } + + case 'jjimjilbang': { + const loc = location as Jjimjilbang + return ( +
+
+
+
{language === 'ko' ? '์ž…์žฅ๋ฃŒ' : 'Entry'}
+
โ‚ฉ{loc.entryFee.toLocaleString()}
+
+ {loc.overnightFee && ( +
+
{language === 'ko' ? '์ˆ™๋ฐ• ์ถ”๊ฐ€' : 'Overnight'}
+
+โ‚ฉ{loc.overnightFee.toLocaleString()}
+
+ )} +
+
+ {loc.towelIncluded && ๐Ÿ› {language === 'ko' ? 'ํƒ€์›” ํฌํ•จ' : 'Towel incl.'}} + {loc.separateGenders && ๐Ÿ‘ฅ {language === 'ko' ? '๋‚จ๋…€ ๊ตฌ๋ถ„' : 'Separate genders'}} +
+
+

{language === 'ko' ? '์‹œ์„ค' : 'Amenities'}

+
+ {loc.amenities.map((a, i) => ( + {a} + ))} +
+
+
+ ) + } + + case 'karaoke': { + const loc = location as Karaoke + const sizeLabel: Record = { small: '์†Œ (2-3์ธ)', medium: '์ค‘ (4-6์ธ)', large: '๋Œ€ (10์ธ+)' } + const sizeLabelEn: Record = { small: 'Small (2-3p)', medium: 'Medium (4-6p)', large: 'Large (10p+)' } + return ( +
+
+ {loc.hasForeignSongs && ๐ŸŽต {language === 'ko' ? '์™ธ๊ตญ๊ณก ์žˆ์Œ' : 'Foreign songs'}} + {loc.hasTambourine && ๐Ÿฅ {language === 'ko' ? 'ํƒฌ๋ฒ„๋ฆฐ ์žˆ์Œ' : 'Tambourine'}} +
+
+ {loc.rates.map((rate, i) => ( +
+ + {language === 'ko' ? sizeLabel[rate.roomSize] : sizeLabelEn[rate.roomSize]} + + โ‚ฉ{rate.pricePerHour.toLocaleString()}/h +
+ ))} +
+ {loc.discountHours && ( +
+ โฐ + {loc.discountHours} +
+ )} +
+ ) + } + + case 'market': { + const loc = location as TraditionalMarket + return ( +
+
+ + ๐Ÿฎ {language === 'ko' ? loc.marketType : loc.marketTypeEn} + + {loc.closedDay && ( + + ๐Ÿšซ {language === 'ko' ? `${loc.closedDay} ํœด๋ฌด` : `Closed: ${loc.closedDay}`} + + )} +
+
+

{language === 'ko' ? '์ธ๊ธฐ ์ƒํ’ˆ' : 'Popular Items'}

+
+ {loc.popularItems.map((item, i) => ( +
+ {language === 'ko' ? item.name : item.nameEn} + โ‚ฉ{item.price.toLocaleString()} /{item.unit} +
+ ))} +
+
+
+ ๐Ÿ“…{loc.operatingDays} + {loc.openHours && <>ยท๐Ÿ• {loc.openHours}} +
+
+ ) + } + + case 'attraction': { + const loc = location as TouristAttraction + return ( +
+ {loc.discountInfo && ( +
+ ๐Ÿ’ก + + {language === 'ko' ? loc.discountInfo : (loc.discountInfoEn ?? loc.discountInfo)} + +
+ )} +
+

{language === 'ko' ? '์ž…์žฅ๊ถŒ' : 'Tickets'}

+
+ {loc.tickets.map((ticket, i) => ( +
+ {language === 'ko' ? ticket.type : ticket.typeEn} + + {ticket.price === 0 ? (language === 'ko' ? '๋ฌด๋ฃŒ' : 'FREE') : `โ‚ฉ${ticket.price.toLocaleString()}`} + +
+ ))} +
+
+ {loc.openHours && ( +
+ ๐Ÿ•{loc.openHours} +
+ )} +
+ ) + } + + case 'extra': { + const loc = location as Extra + return ( +
+
+ {loc.emoji} + + {language === 'ko' ? loc.extraTypeLabel : loc.extraTypeLabelEn} + +
+
+ + {language === 'ko' ? `โ‚ฉ/1${loc.priceUnit}` : `โ‚ฉ/${loc.priceUnitEn}`} + + โ‚ฉ{loc.price.toLocaleString()} +
+

+ {language === 'ko' ? loc.description : loc.descriptionEn} +

+
+ ) + } + + default: + return null + } +} diff --git a/map-app/src/components/panels/RestaurantDetail.tsx b/map-app/src/components/panels/RestaurantDetail.tsx new file mode 100644 index 000000000..e7f7a39e6 --- /dev/null +++ b/map-app/src/components/panels/RestaurantDetail.tsx @@ -0,0 +1,124 @@ +import type { Restaurant } from '@/types' +import { useUiStore } from '@/store' +import { useFilterStore } from '@/store' +import { NATIONALITY_META } from '@/types' + +const SPICY_LABELS = ['', '๐ŸŒถ', '๐ŸŒถ๐ŸŒถ', '๐ŸŒถ๐ŸŒถ๐ŸŒถ'] + +interface Props { location: Restaurant } + +export function RestaurantDetail({ location }: Props) { + const { language } = useUiStore() + const { nationality } = useFilterStore() + + // Highlight menu items relevant to selected nationality + const highlightedItems = location.menuItems.filter((item) => { + if (nationality === 'vegan' || nationality === 'indian') return item.isVegetarian + if (nationality === 'halal') return item.isHalal + return true + }) + + const displayItems = highlightedItems.length > 0 ? highlightedItems : location.menuItems + + return ( +
+ {/* Tags */} +
+ + ๐Ÿฝ๏ธ {language === 'ko' ? location.cuisineType : location.cuisineTypeEn} + + {location.isHalal && ( + + ๐Ÿ•Œ Halal + + )} + {location.isVegetarianFriendly && ( + + ๐ŸŒฑ {language === 'ko' ? '์ฑ„์‹ ๊ฐ€๋Šฅ' : 'Vegetarian OK'} + + )} + {location.hasEnglishMenu && ( + + ๐Ÿ‡บ๐Ÿ‡ธ {language === 'ko' ? '์˜๋ฌธ๋ฉ”๋‰ด' : 'Eng Menu'} + + )} + {location.hasChineseMenu && ( + + ๐Ÿ‡จ๐Ÿ‡ณ {language === 'ko' ? '์ค‘๋ฌธ๋ฉ”๋‰ด' : 'CN Menu'} + + )} + {location.hasJapaneseMenu && ( + + ๐Ÿ‡ฏ๐Ÿ‡ต {language === 'ko' ? '์ผ๋ฌธ๋ฉ”๋‰ด' : 'JP Menu'} + + )} +
+ + {/* Nationality match indicator */} + {nationality !== 'all' && ( +
+ {NATIONALITY_META[nationality].flag} + + {language === 'ko' + ? `${NATIONALITY_META[nationality].label} ์ทจํ–ฅ ๋ฉ”๋‰ด๋ฅผ ๊ฐ•์กฐํ–ˆ์–ด์š”` + : `Showing items matched for ${NATIONALITY_META[nationality].labelEn} taste` + } + +
+ )} + + {/* Average price */} +
+ + {language === 'ko' ? '1์ธ ํ‰๊ท ' : 'Avg per person'} + + + โ‚ฉ{location.pricePerPerson.toLocaleString()} + +
+ + {/* Menu items */} +
+

+ {language === 'ko' ? '๋ฉ”๋‰ด' : 'Menu'} +

+
+ {displayItems.map((item, i) => ( +
+
+
+ + {language === 'ko' ? item.name : item.nameEn} + + {item.spicyLevel !== undefined && item.spicyLevel > 0 && ( + {SPICY_LABELS[item.spicyLevel]} + )} + {item.isVegetarian && ๐ŸŒฑ} + {item.isHalal && ๐Ÿ•Œ} +
+ {language === 'ko' && item.nameEn && ( + {item.nameEn} + )} +
+ โ‚ฉ{item.price.toLocaleString()} +
+ ))} +
+
+ + {/* Nationality tags */} +
+

+ {language === 'ko' ? '์ถ”์ฒœ ๋ฐฉ๋ฌธ๊ฐ' : 'Recommended for'} +

+
+ {location.nationalityTags.map((nat) => ( + + {NATIONALITY_META[nat].flag} {language === 'ko' ? NATIONALITY_META[nat].label : NATIONALITY_META[nat].labelEn} + + ))} +
+
+
+ ) +} diff --git a/map-app/src/data/attractions.json b/map-app/src/data/attractions.json new file mode 100644 index 000000000..0401e5a43 --- /dev/null +++ b/map-app/src/data/attractions.json @@ -0,0 +1,231 @@ +[ + { + "id": "att-001", + "name": "๊ฒฝ๋ณต๊ถ", + "nameEn": "Gyeongbokgung Palace", + "lat": 37.5796, + "lng": 126.9770, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์‚ฌ์ง๋กœ 161", + "category": "attraction", + "rating": 4.7, + "attractionType": "๊ถ๊ถ", + "freeEntry": false, + "discountInfo": "ํ•œ๋ณต ์ฐฉ์šฉ ์‹œ ๋ฌด๋ฃŒ", + "discountInfoEn": "Free with Hanbok rental", + "openHours": "09:00-18:00 (ํ™”์š”์ผ ํœด๊ด€)", + "tickets": [ + { "type": "์ผ๋ฐ˜", "typeEn": "Adult", "price": 3000 }, + { "type": "์–ด๋ฆฐ์ด", "typeEn": "Child (7-18)", "price": 1500 }, + { "type": "6์„ธ ์ดํ•˜", "typeEn": "Under 6", "price": 0 }, + { "type": "65์„ธ ์ด์ƒ", "typeEn": "Senior 65+", "price": 0 } + ] + }, + { + "id": "att-002", + "name": "๋‚จ์‚ฐ์„œ์šธํƒ€์›Œ", + "nameEn": "N Seoul Tower", + "lat": 37.5512, + "lng": 126.9882, + "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ๋‚จ์‚ฐ๊ณต์›๊ธธ 105", + "category": "attraction", + "rating": 4.5, + "attractionType": "์ „๋ง๋Œ€", + "freeEntry": false, + "openHours": "10:00-23:00", + "tickets": [ + { "type": "์ „๋ง๋Œ€ ์ผ๋ฐ˜", "typeEn": "Observatory Adult", "price": 21000 }, + { "type": "์ „๋ง๋Œ€ ์–ด๋ฆฐ์ด", "typeEn": "Observatory Child", "price": 14000 }, + { "type": "๋‚จ์‚ฐ ๋“ฑ์‚ฐ (๋ฌด๋ฃŒ)", "typeEn": "Hiking (Free)", "price": 0 } + ] + }, + { + "id": "att-003", + "name": "์ฐฝ๋•๊ถ", + "nameEn": "Changdeokgung Palace", + "lat": 37.5794, + "lng": 126.9910, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์œจ๊ณก๋กœ 99", + "category": "attraction", + "rating": 4.8, + "attractionType": "๊ถ๊ถ (์œ ๋„ค์Šค์ฝ”)", + "freeEntry": false, + "discountInfo": "ํ•œ๋ณต ์ฐฉ์šฉ ์‹œ ๋ฌด๋ฃŒ, ๋ฌธํ™”๊ฐ€ ์žˆ๋Š” ๋‚  ํ• ์ธ", + "discountInfoEn": "Free with Hanbok; discount on Culture Day", + "openHours": "09:00-17:30", + "tickets": [ + { "type": "์ผ๋ฐ˜ (ํ›„์› ํฌํ•จ)", "typeEn": "Adult + Secret Garden", "price": 8000 }, + { "type": "์ผ๋ฐ˜ (ํ›„์› ๋ฏธํฌํ•จ)", "typeEn": "Adult Only", "price": 3000 }, + { "type": "์–ด๋ฆฐ์ด", "typeEn": "Child", "price": 1500 } + ] + }, + { + "id": "att-004", + "name": "๊ตญ๋ฆฝ์ค‘์•™๋ฐ•๋ฌผ๊ด€", + "nameEn": "National Museum of Korea", + "lat": 37.5238, + "lng": 126.9806, + "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์„œ๋น™๊ณ ๋กœ 137", + "category": "attraction", + "rating": 4.6, + "attractionType": "๋ฐ•๋ฌผ๊ด€", + "freeEntry": true, + "discountInfo": "์ƒ์„ค์ „์‹œ ๋ฌด๋ฃŒ, ํŠน๋ณ„์ „์‹œ ์œ ๋ฃŒ", + "discountInfoEn": "Permanent exhibits free, special exhibits paid", + "openHours": "10:00-18:00 (ํ†  21:00๊นŒ์ง€)", + "tickets": [ + { "type": "์ƒ์„ค์ „์‹œ", "typeEn": "Permanent Exhibition", "price": 0 }, + { "type": "ํŠน๋ณ„์ „์‹œ", "typeEn": "Special Exhibition", "price": 10000 } + ] + }, + { + "id": "att-005", + "name": "๋กฏ๋ฐ์›”๋“œ ์–ด๋“œ๋ฒค์ฒ˜", + "nameEn": "Lotte World Adventure", + "lat": 37.5114, + "lng": 127.0980, + "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 240", + "category": "attraction", + "rating": 4.4, + "attractionType": "ํ…Œ๋งˆํŒŒํฌ", + "freeEntry": false, + "discountInfo": "ํ†ตํ•ฉ์ด์šฉ๊ถŒ ์ธํ„ฐ๋„ท ๊ตฌ๋งค ์‹œ ์ตœ๋Œ€ 15% ํ• ์ธ", + "discountInfoEn": "Up to 15% off online booking", + "openHours": "10:00-21:00", + "tickets": [ + { "type": "์ข…์ผ๊ถŒ (์–ด๋ฅธ)", "typeEn": "Day Pass Adult", "price": 62000 }, + { "type": "์ข…์ผ๊ถŒ (์–ด๋ฆฐ์ด)", "typeEn": "Day Pass Child", "price": 52000 }, + { "type": "์•ผ๊ฐ„๊ถŒ", "typeEn": "Evening Pass (after 4pm)", "price": 48000 } + ] + }, + { + "id": "att-006", + "name": "๋ถ์ดŒํ•œ์˜ฅ๋งˆ์„", + "nameEn": "Bukchon Hanok Village", + "lat": 37.5826, + "lng": 126.9852, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ๊ณ„๋™๊ธธ 37", + "category": "attraction", + "rating": 4.3, + "attractionType": "๋ฌธํ™”๋งˆ์„", + "freeEntry": true, + "discountInfo": "๋ฌด๋ฃŒ ์ž…์žฅ, ๊ด€๊ด‘ ์•ˆ๋‚ด์†Œ ์šด์˜", + "discountInfoEn": "Free entry, tourist center available", + "openHours": "24์‹œ๊ฐ„ (์ฃผ๋ฏผ ๊ฑฐ์ฃผ์ง€์—ญ - ์กฐ์šฉํžˆ)", + "tickets": [ + { "type": "๊ด€๋žŒ (๋ฌด๋ฃŒ)", "typeEn": "Visit (Free)", "price": 0 } + ] + }, + { + "id": "att-007", + "name": "๋•์ˆ˜๊ถ", + "nameEn": "Deoksugung Palace", + "lat": 37.5658, + "lng": 126.9748, + "address": "์„œ์šธ ์ค‘๊ตฌ ์„ธ์ข…๋Œ€๋กœ 99", + "category": "attraction", + "rating": 4.5, + "attractionType": "๊ถ๊ถ", + "freeEntry": false, + "discountInfo": "ํ•œ๋ณต ์ฐฉ์šฉ ์‹œ ๋ฌด๋ฃŒ, ์ˆ˜๋ฌธ์žฅ ๊ต๋Œ€์‹ ๋ฌด๋ฃŒ ๊ด€๋žŒ", + "discountInfoEn": "Free with Hanbok; free Royal Guard ceremony", + "openHours": "09:00-21:00 (์›”์š”์ผ ํœด๊ด€)", + "tickets": [ + { "type": "์ผ๋ฐ˜", "typeEn": "Adult", "price": 1000 }, + { "type": "์–ด๋ฆฐ์ด", "typeEn": "Child", "price": 0 } + ] + }, + { + "id": "att-008", + "name": "์„œ์šธ์ˆฒ", + "nameEn": "Seoul Forest", + "lat": 37.5449, + "lng": 127.0374, + "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ๋š์„ฌ๋กœ 273", + "category": "attraction", + "rating": 4.6, + "attractionType": "๊ณต์›", + "freeEntry": true, + "discountInfo": "์ž…์žฅ ๋ฌด๋ฃŒ, ์ฃผ์ฐจ ์œ ๋ฃŒ", + "discountInfoEn": "Free entry, paid parking", + "openHours": "24์‹œ๊ฐ„", + "tickets": [ + { "type": "๊ณต์› ์ž…์žฅ", "typeEn": "Park Entry", "price": 0 } + ] + }, + { + "id": "att-009", + "name": "ํ•œ๊ฐ•๊ณต์› ์—ฌ์˜๋„", + "nameEn": "Han River Park Yeouido", + "lat": 37.5283, + "lng": 126.9332, + "address": "์„œ์šธ ์˜๋“ฑํฌ๊ตฌ ์—ฌ์˜๋™๋กœ 330", + "category": "attraction", + "rating": 4.5, + "attractionType": "ํ•œ๊ฐ•๊ณต์›", + "freeEntry": true, + "discountInfo": "๋ฌด๋ฃŒ ์ž…์žฅ", + "discountInfoEn": "Free entry", + "openHours": "24์‹œ๊ฐ„", + "tickets": [ + { "type": "๊ณต์› ์ž…์žฅ", "typeEn": "Park Entry", "price": 0 }, + { "type": "์ž์ „๊ฑฐ ๋Œ€์—ฌ", "typeEn": "Bike Rental/hr", "price": 3000 } + ] + }, + { + "id": "att-010", + "name": "๊ตญ๋ฆฝ๋ฏผ์†๋ฐ•๋ฌผ๊ด€", + "nameEn": "National Folk Museum", + "lat": 37.5808, + "lng": 126.9789, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์‚ผ์ฒญ๋กœ 37", + "category": "attraction", + "rating": 4.4, + "attractionType": "๋ฐ•๋ฌผ๊ด€", + "freeEntry": true, + "discountInfo": "์ƒ์„ค์ „์‹œ ๋ฌด๋ฃŒ (๊ฒฝ๋ณต๊ถ ์ž…์žฅ ํ›„)", + "discountInfoEn": "Free with Gyeongbokgung ticket", + "openHours": "09:00-18:00 (ํ™”์š”์ผ ํœด๊ด€)", + "tickets": [ + { "type": "์ƒ์„ค์ „์‹œ", "typeEn": "Permanent Exhibition", "price": 0 } + ] + }, + { + "id": "att-011", + "name": "์„œ์šธ๋Œ€๊ณต์›", + "nameEn": "Seoul Grand Park", + "lat": 37.4270, + "lng": 127.0102, + "address": "๊ฒฝ๊ธฐ๋„ ๊ณผ์ฒœ์‹œ ๋Œ€๊ณต์›๊ด‘์žฅ๋กœ 102", + "category": "attraction", + "rating": 4.3, + "attractionType": "๋™๋ฌผ์›ยท๋†€์ด๊ณต์›", + "freeEntry": false, + "discountInfo": "๊ธฐํ›„๋™ํ–‰์นด๋“œ ์†Œ์ง€์ž 20% ํ• ์ธ", + "discountInfoEn": "20% off with Climate Card", + "openHours": "09:00-19:00", + "tickets": [ + { "type": "๋™๋ฌผ์› (์–ด๋ฅธ)", "typeEn": "Zoo Adult", "price": 5000 }, + { "type": "๋™๋ฌผ์› (์–ด๋ฆฐ์ด)", "typeEn": "Zoo Child", "price": 2000 }, + { "type": "์‹๋ฌผ์›", "typeEn": "Botanical Garden", "price": 1000 } + ] + }, + { + "id": "att-012", + "name": "DDP ๋™๋Œ€๋ฌธ๋””์ž์ธํ”Œ๋ผ์ž", + "nameEn": "Dongdaemun Design Plaza", + "lat": 37.5666, + "lng": 127.0093, + "address": "์„œ์šธ ์ค‘๊ตฌ ์„์ง€๋กœ 281", + "category": "attraction", + "rating": 4.4, + "attractionType": "๋ณตํ•ฉ๋ฌธํ™”๊ณต๊ฐ„", + "freeEntry": true, + "discountInfo": "์™ธ๋ถ€ ๊ด€๋žŒ ๋ฌด๋ฃŒ, ์ „์‹œ๊ด€ ์œ ๋ฃŒ", + "discountInfoEn": "Free to visit exterior; exhibitions paid", + "openHours": "10:00-20:00", + "tickets": [ + { "type": "์™ธ๋ถ€ ๊ด€๋žŒ", "typeEn": "Exterior View", "price": 0 }, + { "type": "๊ธฐํš ์ „์‹œ", "typeEn": "Special Exhibition", "price": 8000 } + ] + } +] diff --git a/map-app/src/data/cafes.json b/map-app/src/data/cafes.json new file mode 100644 index 000000000..23948fb12 --- /dev/null +++ b/map-app/src/data/cafes.json @@ -0,0 +1,182 @@ +[ + { + "id": "cafe-001", "name": "๋ฉ”๊ฐ€MGC์ปคํ”ผ ๋ช…๋™์ ", "nameEn": "Mega MGC Coffee Myeongdong", + "lat": 37.5629, "lng": 126.9820, "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™๊ธธ 48", "category": "cafe", + "rating": 4.2, "brand": "Mega MGC", "americanoPrice": 2000, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 2000, "isBestValue": true }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 2500 }, + { "name": "๋ฐ”๋‹๋ผ๋ผ๋–ผ", "nameEn": "Vanilla Latte", "price": 3000 }, + { "name": "์ผ€์ดํฌ", "nameEn": "Cake slice", "price": 3000 } + ] + }, + { + "id": "cafe-002", "name": "์ปดํฌ์ฆˆ์ปคํ”ผ ํ™๋Œ€์ ", "nameEn": "Compose Coffee Hongdae", + "lat": 37.5572, "lng": 126.9222, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์™€์šฐ์‚ฐ๋กœ 20", "category": "cafe", + "rating": 4.1, "brand": "Compose", "americanoPrice": 1500, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 1500, "isBestValue": true }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 2000 }, + { "name": "๋‹ฌ๊ณ ๋‚˜๋ผ๋–ผ", "nameEn": "Dalgona Latte", "price": 2500 } + ] + }, + { + "id": "cafe-003", "name": "์Šคํƒ€๋ฒ…์Šค ๊ฐ•๋‚จ์ ", "nameEn": "Starbucks Gangnam", + "lat": 37.4980, "lng": 127.0272, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 400", "category": "cafe", + "rating": 4.4, "brand": "Starbucks", "americanoPrice": 4500, "hasWifi": true, "hasOutdoorSeating": true, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 4500 }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 5500 }, + { "name": "์นด๋ผ๋ฉœ๋งˆํ‚ค์•„ํ† ", "nameEn": "Caramel Macchiato", "price": 6000 }, + { "name": "ํ”„๋ผํ‘ธ์น˜๋…ธ", "nameEn": "Frappuccino", "price": 6500 } + ] + }, + { + "id": "cafe-004", "name": "๋นฝ๋‹ค๋ฐฉ ์ข…๋กœ์ ", "nameEn": "Paik's Coffee Jongno", + "lat": 37.5713, "lng": 126.9806, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ข…๋กœ 112", "category": "cafe", + "rating": 4.0, "brand": "Paik's Coffee", "americanoPrice": 1500, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 1500, "isBestValue": true }, + { "name": "์›์กฐ์ปคํ”ผ", "nameEn": "Original Coffee", "price": 2000 }, + { "name": "๋”ธ๊ธฐ๋ผ๋–ผ", "nameEn": "Strawberry Latte", "price": 3000 } + ] + }, + { + "id": "cafe-005", "name": "์ด๋””์•ผ์ปคํ”ผ ์ดํƒœ์›์ ", "nameEn": "Ediya Coffee Itaewon", + "lat": 37.5344, "lng": 126.9949, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 183", "category": "cafe", + "rating": 3.9, "brand": "Ediya", "americanoPrice": 2300, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 2300 }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 3200 }, + { "name": "์Šค๋ฌด๋””", "nameEn": "Smoothie", "price": 4000 } + ] + }, + { + "id": "cafe-006", "name": "ํ…Œ๋ผ๋กœ์‚ฌ ํ™๋Œ€์ ", "nameEn": "Terarosa Hongdae", + "lat": 37.5592, "lng": 126.9268, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ๋™๊ต๋กœ 225", "category": "cafe", + "rating": 4.8, "brand": "Terarosa", "americanoPrice": 5500, "hasWifi": true, "hasOutdoorSeating": true, + "items": [ + { "name": "์‹ฑ๊ธ€์˜ค๋ฆฌ์ง„ ๋“œ๋ฆฝ์ปคํ”ผ", "nameEn": "Single Origin Drip", "price": 7000, "isBestValue": true }, + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 5500 }, + { "name": "ํ”Œ๋žซํ™”์ดํŠธ", "nameEn": "Flat White", "price": 6000 } + ] + }, + { + "id": "cafe-007", "name": "ํˆฌ์ธํ”Œ๋ ˆ์ด์Šค ๊ฐ•๋‚จ", "nameEn": "A Twosome Place Gangnam", + "lat": 37.5015, "lng": 127.0231, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋…ผํ˜„๋กœ 432", "category": "cafe", + "rating": 4.3, "brand": "A Twosome Place", "americanoPrice": 3900, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 3900 }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 4700 }, + { "name": "๋”ธ๊ธฐ ์ผ€์ดํฌ", "nameEn": "Strawberry Cake", "price": 7500 } + ] + }, + { + "id": "cafe-008", "name": "๋‚˜๋ฌด์‚ฌ์ด๋กœ ์ธ์‚ฌ๋™", "nameEn": "Between Trees Insadong", + "lat": 37.5747, "lng": 126.9847, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ธ์‚ฌ๋™๊ธธ 35", "category": "cafe", + "rating": 4.6, "brand": "Independent", "americanoPrice": 4000, "hasWifi": false, "hasOutdoorSeating": true, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 4000 }, + { "name": "์Œํ™”์ฐจ", "nameEn": "Traditional Herbal Tea", "price": 6000 }, + { "name": "์‹ํ˜œ", "nameEn": "Sweet Rice Drink", "price": 5000 } + ] + }, + { + "id": "cafe-009", "name": "๋”๋ฒคํ‹ฐ ๊ฑด๋Œ€์ ", "nameEn": "The Venti Konkuk", + "lat": 37.5410, "lng": 127.0706, "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ์•„์ฐจ์‚ฐ๋กœ 258", "category": "cafe", + "rating": 4.1, "brand": "The Venti", "americanoPrice": 1800, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 1800, "isBestValue": true }, + { "name": "์˜ค๋ ˆ์˜ค๋ผ๋–ผ", "nameEn": "Oreo Latte", "price": 3000 }, + { "name": "์ƒํฌ๋ฆผ๋น™์ˆ˜", "nameEn": "Cream Shaved Ice", "price": 5500 } + ] + }, + { + "id": "cafe-010", "name": "์„ฑ์ˆ˜ ์–ด๋‹ˆ์–ธ", "nameEn": "Onion Seongsu", + "lat": 37.5448, "lng": 127.0557, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์•„์ฐจ์‚ฐ๋กœ9๊ธธ 8", "category": "cafe", + "rating": 4.9, "brand": "Onion", "americanoPrice": 5000, "hasWifi": false, "hasOutdoorSeating": true, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 5000 }, + { "name": "์–ด๋‹ˆ์–ธ ์†Œ๊ธˆ๋นต", "nameEn": "Onion Salt Bread", "price": 4200, "isBestValue": true }, + { "name": "์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ผ€์ดํฌ", "nameEn": "Signature Cake", "price": 7500 } + ] + }, + { + "id": "cafe-011", "name": "์ปดํฌ์ฆˆ์ปคํ”ผ ์ž ์‹ค์ ", "nameEn": "Compose Coffee Jamsil", + "lat": 37.5122, "lng": 127.1014, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 300", "category": "cafe", + "rating": 4.0, "brand": "Compose", "americanoPrice": 1500, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 1500, "isBestValue": true }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 2000 }, + { "name": "์ดˆ์ฝ”๋ผ๋–ผ", "nameEn": "Choco Latte", "price": 2500 } + ] + }, + { + "id": "cafe-012", "name": "๋ฉ”๊ฐ€์ปคํ”ผ ์™•์‹ญ๋ฆฌ์ ", "nameEn": "Mega Coffee Wangsimni", + "lat": 37.5617, "lng": 127.0368, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๋กœ 95", "category": "cafe", + "rating": 4.0, "brand": "Mega MGC", "americanoPrice": 2000, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 2000, "isBestValue": true }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 2500 }, + { "name": "์•„์ด์Šคํ‹ฐ", "nameEn": "Iced Tea", "price": 2000 } + ] + }, + { + "id": "cafe-013", "name": "์Šคํƒ€๋ฒ…์Šค ํ™๋Œ€์ž…๊ตฌ์—ญ", "nameEn": "Starbucks Hongik Univ.", + "lat": 37.5566, "lng": 126.9240, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์–‘ํ™”๋กœ 177", "category": "cafe", + "rating": 4.3, "brand": "Starbucks", "americanoPrice": 4500, "hasWifi": true, "hasOutdoorSeating": true, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 4500 }, + { "name": "์ฝœ๋“œ๋ธŒ๋ฃจ", "nameEn": "Cold Brew", "price": 5500 }, + { "name": "๋งˆ์นด๋กฑ", "nameEn": "Macaron", "price": 2400 } + ] + }, + { + "id": "cafe-014", "name": "๋นฝ๋‹ค๋ฐฉ ๊ฐ•๋‚จ์—ญ์ ", "nameEn": "Paik's Coffee Gangnam Station", + "lat": 37.4977, "lng": 127.0282, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 364", "category": "cafe", + "rating": 4.1, "brand": "Paik's Coffee", "americanoPrice": 1500, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 1500, "isBestValue": true }, + { "name": "์›์กฐ์ปคํ”ผ", "nameEn": "Original Coffee", "price": 2000 }, + { "name": "ํŒฅ๋น™์ˆ˜", "nameEn": "Red Bean Shaved Ice", "price": 5500 } + ] + }, + { + "id": "cafe-015", "name": "๋ธ”๋ฃจ๋ณดํ‹€ ์„ฑ์ˆ˜", "nameEn": "Blue Bottle Seongsu", + "lat": 37.5440, "lng": 127.0558, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๋กœ2๊ธธ 20-1", "category": "cafe", + "rating": 4.7, "brand": "Blue Bottle", "americanoPrice": 6000, "hasWifi": false, "hasOutdoorSeating": true, + "items": [ + { "name": "๋‰ด์˜ฌ๋ฆฌ์–ธ์ฆˆ ์•„์ด์Šค์ปคํ”ผ", "nameEn": "New Orleans Iced", "price": 7500, "isBestValue": true }, + { "name": "์นดํŽ˜๋ผ๋–ผ", "nameEn": "Cafe Latte", "price": 7000 }, + { "name": "์‹ฑ๊ธ€์˜ค๋ฆฌ์ง„ ๋“œ๋ฆฝ", "nameEn": "Single Origin Drip", "price": 8000 } + ] + }, + { + "id": "cafe-016", "name": "์ด๋””์•ผ ์ƒ์•”์ ", "nameEn": "Ediya Coffee Sangam", + "lat": 37.5680, "lng": 126.8935, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์›”๋“œ์ปต๋ถ๋กœ 366", "category": "cafe", + "rating": 3.8, "brand": "Ediya", "americanoPrice": 2300, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 2300 }, + { "name": "์ฟจ ๋ธ”๋ฃจ๋ฒ ๋ฆฌ", "nameEn": "Cool Blueberry", "price": 3800 } + ] + }, + { + "id": "cafe-017", "name": "๋ฉ”๊ฐ€์ปคํ”ผ ์‹ ์ดŒ์ ", "nameEn": "Mega Coffee Sinchon", + "lat": 37.5592, "lng": 126.9366, "address": "์„œ์šธ ์„œ๋Œ€๋ฌธ๊ตฌ ์‹ ์ดŒ๋กœ 75", "category": "cafe", + "rating": 4.0, "brand": "Mega MGC", "americanoPrice": 2000, "hasWifi": true, "hasOutdoorSeating": false, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 2000, "isBestValue": true }, + { "name": "๋”ธ๊ธฐ๋ผ๋–ผ", "nameEn": "Strawberry Latte", "price": 3000 } + ] + }, + { + "id": "cafe-018", "name": "ํŠธ๋ฆฌํ•˜์šฐ์Šค ์นดํŽ˜ ๋ถ์ดŒ", "nameEn": "Treehouse Cafe Bukchon", + "lat": 37.5822, "lng": 126.9848, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ๋ถ์ดŒ๋กœ 112", "category": "cafe", + "rating": 4.5, "brand": "Independent", "americanoPrice": 5000, "hasWifi": false, "hasOutdoorSeating": true, + "items": [ + { "name": "์•„์ด์Šค ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "Iced Americano", "price": 5000 }, + { "name": "ํ™์‚ผ๋ผ๋–ผ", "nameEn": "Red Ginseng Latte", "price": 7000, "isBestValue": true }, + { "name": "์ „ํ†ต ์ˆ˜์ •๊ณผ", "nameEn": "Persimmon Punch", "price": 6000 } + ] + } +] diff --git a/map-app/src/data/convenience.json b/map-app/src/data/convenience.json new file mode 100644 index 000000000..2eb9b668f --- /dev/null +++ b/map-app/src/data/convenience.json @@ -0,0 +1,112 @@ +[ + { + "id": "conv-001", "name": "CU ๋ช…๋™์ ", "nameEn": "CU Myeongdong", + "lat": 37.5630, "lng": 126.9815, "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™๊ธธ 50", "category": "convenience", + "rating": 4.0, "brand": "CU", "avgItemPrice": 1800, "open24Hours": true, "hasAtm": true, + "popularItems": [ + { "name": "์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "Onigiri", "price": 1200, "category": "food" }, + { "name": "์ปต๋ผ๋ฉด", "nameEn": "Cup Noodles", "price": 1000, "category": "food" }, + { "name": "ํŽธ์˜์  ๋„์‹œ๋ฝ", "nameEn": "Bento Box", "price": 4500, "category": "food" } + ] + }, + { + "id": "conv-002", "name": "GS25 ํ™๋Œ€์ ", "nameEn": "GS25 Hongdae", + "lat": 37.5574, "lng": 126.9234, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์™€์šฐ์‚ฐ๋กœ 31", "category": "convenience", + "rating": 4.1, "brand": "GS25", "avgItemPrice": 1850, "open24Hours": true, "hasAtm": true, + "popularItems": [ + { "name": "์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "Onigiri", "price": 1100, "category": "food" }, + { "name": "ํ•ซ๋„๊ทธ", "nameEn": "Corn Dog", "price": 1500, "category": "food" }, + { "name": "์ปต์–ผ์Œ", "nameEn": "Cup Ice", "price": 600, "category": "drink" } + ] + }, + { + "id": "conv-003", "name": "7-Eleven ๊ฐ•๋‚จ์—ญ์ ", "nameEn": "7-Eleven Gangnam Station", + "lat": 37.4979, "lng": 127.0262, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 396", "category": "convenience", + "rating": 3.9, "brand": "7-Eleven", "avgItemPrice": 1950, "open24Hours": true, "hasAtm": false, + "popularItems": [ + { "name": "์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "Onigiri", "price": 1300, "category": "food" }, + { "name": "์น˜ํ‚จ ์ŠคํŠธ๋ฆฝ", "nameEn": "Chicken Strips", "price": 2000, "category": "food" } + ] + }, + { + "id": "conv-004", "name": "emart24 ์ข…๋กœ์ ", "nameEn": "emart24 Jongno", + "lat": 37.5706, "lng": 126.9820, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ข…๋กœ 88", "category": "convenience", + "rating": 4.0, "brand": "emart24", "avgItemPrice": 1750, "open24Hours": true, "hasAtm": true, + "popularItems": [ + { "name": "์ž์ฒด๋ธŒ๋žœ๋“œ ์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "PB Onigiri", "price": 900, "category": "food" }, + { "name": "๋…ธ๋ธŒ๋žœ๋“œ ์Œ๋ฃŒ", "nameEn": "No Brand Drink", "price": 800, "category": "drink" } + ] + }, + { + "id": "conv-005", "name": "CU ์ดํƒœ์›์ ", "nameEn": "CU Itaewon", + "lat": 37.5356, "lng": 126.9928, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 200", "category": "convenience", + "rating": 4.2, "brand": "CU", "avgItemPrice": 1800, "open24Hours": true, "hasAtm": false, + "popularItems": [ + { "name": "์ˆ˜์ž… ๋งฅ์ฃผ 4์บ”", "nameEn": "Imported Beer 4-pack", "price": 10800, "category": "drink" }, + { "name": "์•ผ์ฑ„์ƒ๋Ÿฌ๋“œ", "nameEn": "Veggie Salad", "price": 2500, "category": "food" } + ] + }, + { + "id": "conv-006", "name": "GS25 ๊ฑด๋Œ€์ž…๊ตฌ์ ", "nameEn": "GS25 Konkuk", + "lat": 37.5404, "lng": 127.0714, "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ์•„์ฐจ์‚ฐ๋กœ 262", "category": "convenience", + "rating": 4.0, "brand": "GS25", "avgItemPrice": 1850, "open24Hours": true, "hasAtm": true, + "popularItems": [ + { "name": "์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "Onigiri", "price": 1100, "category": "food" }, + { "name": "1+1 ๊ณผ์ž", "nameEn": "1+1 Snack Deal", "price": 1500, "category": "food" } + ] + }, + { + "id": "conv-007", "name": "CU ์„ฑ์ˆ˜์ ", "nameEn": "CU Seongsu", + "lat": 37.5443, "lng": 127.0551, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ๋š์„ฌ๋กœ 399", "category": "convenience", + "rating": 4.1, "brand": "CU", "avgItemPrice": 1800, "open24Hours": true, "hasAtm": false, + "popularItems": [ + { "name": "CU ์ˆ˜์ œ๋งฅ์ฃผ", "nameEn": "CU Craft Beer", "price": 3500, "category": "drink" }, + { "name": "์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "Onigiri", "price": 1200, "category": "food" } + ] + }, + { + "id": "conv-008", "name": "7-Eleven ์ž ์‹ค์—ญ์ ", "nameEn": "7-Eleven Jamsil Station", + "lat": 37.5133, "lng": 127.1003, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์ž ์‹ค์—ญ ์ง€ํ•˜", "category": "convenience", + "rating": 3.8, "brand": "7-Eleven", "avgItemPrice": 1950, "open24Hours": true, "hasAtm": true, + "popularItems": [ + { "name": "๋„์‹œ๋ฝ", "nameEn": "Bento Box", "price": 4800, "category": "food" }, + { "name": "๊ฒŒํ† ๋ ˆ์ด", "nameEn": "Gatorade", "price": 1800, "category": "drink" } + ] + }, + { + "id": "conv-009", "name": "emart24 ํ™๋Œ€์ ", "nameEn": "emart24 Hongdae", + "lat": 37.5571, "lng": 126.9226, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ํ™์ต๋กœ 18", "category": "convenience", + "rating": 4.0, "brand": "emart24", "avgItemPrice": 1750, "open24Hours": true, "hasAtm": false, + "popularItems": [ + { "name": "ํ”ผ์ฝ”ํฌ ๋„์‹œ๋ฝ", "nameEn": "Peacock Bento", "price": 4200, "category": "food" }, + { "name": "PB ์•„๋ฉ”๋ฆฌ์นด๋…ธ", "nameEn": "PB Americano", "price": 1000, "category": "drink" } + ] + }, + { + "id": "conv-010", "name": "GS25 ๊ฐ•๋‚จ์—ญ์ ", "nameEn": "GS25 Gangnam Station", + "lat": 37.4983, "lng": 127.0270, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 400 ์ง€ํ•˜", "category": "convenience", + "rating": 4.2, "brand": "GS25", "avgItemPrice": 1850, "open24Hours": true, "hasAtm": true, + "popularItems": [ + { "name": "GS25 ์šฐ๋ฆฌ๋™๋„ค GS", "nameEn": "GS25 Brand Gimbap", "price": 1800, "category": "food" }, + { "name": "์˜ค๋ชจ๋ฆฌ ๊น€์น˜์ฐŒ๊ฐœ ๋ผ๋ฉด", "nameEn": "Omori Kimchi Ramen", "price": 1700, "category": "food" } + ] + }, + { + "id": "conv-011", "name": "CU ๋™๋Œ€๋ฌธ์ ", "nameEn": "CU Dongdaemun", + "lat": 37.5668, "lng": 127.0094, "address": "์„œ์šธ ์ค‘๊ตฌ ์„์ง€๋กœ 298", "category": "convenience", + "rating": 3.9, "brand": "CU", "avgItemPrice": 1800, "open24Hours": true, "hasAtm": true, + "popularItems": [ + { "name": "์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "Onigiri", "price": 1200, "category": "food" }, + { "name": "๋ฐ”๋‚˜๋‚˜์šฐ์œ ", "nameEn": "Banana Milk", "price": 1500, "category": "drink" } + ] + }, + { + "id": "conv-012", "name": "GS25 ์™•์‹ญ๋ฆฌ์ ", "nameEn": "GS25 Wangsimni", + "lat": 37.5614, "lng": 127.0380, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๊ด‘์žฅ๋กœ 14", "category": "convenience", + "rating": 4.0, "brand": "GS25", "avgItemPrice": 1850, "open24Hours": true, "hasAtm": false, + "popularItems": [ + { "name": "์ฐธ์น˜๋งˆ์š” ์‚ผ๊ฐ๊น€๋ฐฅ", "nameEn": "Tuna Mayo Onigiri", "price": 1200, "category": "food" }, + { "name": "๋ถˆ๋‹ญ๋ณถ์Œ๋ฉด", "nameEn": "Fire Noodles", "price": 1400, "category": "food" } + ] + } +] diff --git a/map-app/src/data/exchange.json b/map-app/src/data/exchange.json new file mode 100644 index 000000000..4e3a894d0 --- /dev/null +++ b/map-app/src/data/exchange.json @@ -0,0 +1,195 @@ +[ + { + "id": "ex-001", "name": "๋ช…๋™ ์šฐ๋ฆฌํ™˜์ „์†Œ", "nameEn": "Myeongdong Woori Exchange", + "lat": 37.5635, "lng": 126.9832, "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™๊ธธ 14", "category": "exchange", + "rating": 4.5, "phone": "02-1234-5678", "openHours": "09:00-20:00", + "noCommission": true, "minAmount": 10, + "rates": [ + { "currency": "USD", "buyRate": 1347, "sellRate": 1389 }, + { "currency": "JPY", "buyRate": 908, "sellRate": 936 }, + { "currency": "CNY", "buyRate": 185, "sellRate": 191 }, + { "currency": "EUR", "buyRate": 1481, "sellRate": 1527 }, + { "currency": "GBP", "buyRate": 1724, "sellRate": 1776 } + ] + }, + { + "id": "ex-002", "name": "๋™๋Œ€๋ฌธ ํ™˜์ „์„ผํ„ฐ", "nameEn": "Dongdaemun Exchange Center", + "lat": 37.5665, "lng": 127.0088, "address": "์„œ์šธ ์ค‘๊ตฌ ์„์ง€๋กœ 281", "category": "exchange", + "rating": 4.2, "phone": "02-2345-6789", "openHours": "10:00-22:00", + "noCommission": false, "minAmount": 50, + "rates": [ + { "currency": "USD", "buyRate": 1340, "sellRate": 1396 }, + { "currency": "JPY", "buyRate": 902, "sellRate": 942 }, + { "currency": "CNY", "buyRate": 183, "sellRate": 194 }, + { "currency": "EUR", "buyRate": 1475, "sellRate": 1532 } + ] + }, + { + "id": "ex-003", "name": "ํ™๋Œ€ ๋ฒ ์ŠคํŠธํ™˜์ „", "nameEn": "Hongdae Best Exchange", + "lat": 37.5578, "lng": 126.9238, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์™€์šฐ์‚ฐ๋กœ 94", "category": "exchange", + "rating": 4.7, "phone": "02-3456-7890", "openHours": "10:00-21:00", + "noCommission": true, "minAmount": 0, + "rates": [ + { "currency": "USD", "buyRate": 1352, "sellRate": 1384 }, + { "currency": "JPY", "buyRate": 912, "sellRate": 932 }, + { "currency": "CNY", "buyRate": 187, "sellRate": 189 }, + { "currency": "EUR", "buyRate": 1488, "sellRate": 1521 }, + { "currency": "GBP", "buyRate": 1731, "sellRate": 1770 } + ] + }, + { + "id": "ex-004", "name": "๊ฐ•๋‚จ ํ”„๋ฆฌ๋ฏธ์—„ํ™˜์ „", "nameEn": "Gangnam Premium Exchange", + "lat": 37.4979, "lng": 127.0276, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 396", "category": "exchange", + "rating": 4.3, "phone": "02-4567-8901", "openHours": "09:30-19:30", + "noCommission": false, "minAmount": 100, + "rates": [ + { "currency": "USD", "buyRate": 1335, "sellRate": 1400 }, + { "currency": "JPY", "buyRate": 899, "sellRate": 945 }, + { "currency": "CNY", "buyRate": 181, "sellRate": 196 }, + { "currency": "EUR", "buyRate": 1470, "sellRate": 1538 } + ] + }, + { + "id": "ex-005", "name": "์ดํƒœ์› ๊ธ€๋กœ๋ฒŒํ™˜์ „", "nameEn": "Itaewon Global Exchange", + "lat": 37.5345, "lng": 126.9945, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 177", "category": "exchange", + "rating": 4.6, "phone": "02-5678-9012", "openHours": "09:00-21:00", + "noCommission": true, "minAmount": 20, + "rates": [ + { "currency": "USD", "buyRate": 1350, "sellRate": 1386 }, + { "currency": "JPY", "buyRate": 910, "sellRate": 934 }, + { "currency": "CNY", "buyRate": 186, "sellRate": 190 }, + { "currency": "EUR", "buyRate": 1485, "sellRate": 1524 }, + { "currency": "GBP", "buyRate": 1728, "sellRate": 1773 }, + { "currency": "THB", "buyRate": 37, "sellRate": 40 } + ] + }, + { + "id": "ex-006", "name": "์ธ์‚ฌ๋™ ํŠธ๋ž˜๋ธ”ํ™˜์ „", "nameEn": "Insadong Travel Exchange", + "lat": 37.5742, "lng": 126.9853, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ธ์‚ฌ๋™๊ธธ 42", "category": "exchange", + "rating": 4.4, "phone": "02-6789-0123", "openHours": "10:00-19:00", + "noCommission": true, "minAmount": 10, + "rates": [ + { "currency": "USD", "buyRate": 1348, "sellRate": 1388 }, + { "currency": "JPY", "buyRate": 909, "sellRate": 935 }, + { "currency": "CNY", "buyRate": 185, "sellRate": 191 }, + { "currency": "EUR", "buyRate": 1482, "sellRate": 1526 }, + { "currency": "THB", "buyRate": 36, "sellRate": 41 } + ] + }, + { + "id": "ex-007", "name": "์‹ ์ดŒ ๋Œ€ํ•™๋กœํ™˜์ „", "nameEn": "Sinchon Campus Exchange", + "lat": 37.5596, "lng": 126.9364, "address": "์„œ์šธ ์„œ๋Œ€๋ฌธ๊ตฌ ์‹ ์ดŒ๋กœ 83", "category": "exchange", + "rating": 4.1, "phone": "02-7890-1234", "openHours": "09:00-18:00", + "noCommission": false, "minAmount": 30, + "rates": [ + { "currency": "USD", "buyRate": 1344, "sellRate": 1392 }, + { "currency": "JPY", "buyRate": 905, "sellRate": 939 }, + { "currency": "CNY", "buyRate": 184, "sellRate": 193 }, + { "currency": "EUR", "buyRate": 1478, "sellRate": 1530 } + ] + }, + { + "id": "ex-008", "name": "์ข…๋กœ ํ™ฉ๊ธˆํ™˜์ „", "nameEn": "Jongno Golden Exchange", + "lat": 37.5701, "lng": 126.9827, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ข…๋กœ 99", "category": "exchange", + "rating": 4.8, "phone": "02-8901-2345", "openHours": "08:30-20:30", + "noCommission": true, "minAmount": 0, + "rates": [ + { "currency": "USD", "buyRate": 1354, "sellRate": 1382 }, + { "currency": "JPY", "buyRate": 914, "sellRate": 930 }, + { "currency": "CNY", "buyRate": 188, "sellRate": 188 }, + { "currency": "EUR", "buyRate": 1490, "sellRate": 1519 }, + { "currency": "GBP", "buyRate": 1735, "sellRate": 1765 }, + { "currency": "THB", "buyRate": 38, "sellRate": 39 } + ] + }, + { + "id": "ex-009", "name": "๊ฑด๋Œ€ ์Šคํƒ€์‹œํ‹ฐํ™˜์ „", "nameEn": "Konkuk Star City Exchange", + "lat": 37.5404, "lng": 127.0710, "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ๋Šฅ๋™๋กœ 120", "category": "exchange", + "rating": 4.2, "phone": "02-9012-3456", "openHours": "10:00-21:00", + "noCommission": true, "minAmount": 0, + "rates": [ + { "currency": "USD", "buyRate": 1349, "sellRate": 1387 }, + { "currency": "JPY", "buyRate": 907, "sellRate": 937 }, + { "currency": "CNY", "buyRate": 185, "sellRate": 192 }, + { "currency": "EUR", "buyRate": 1483, "sellRate": 1523 } + ] + }, + { + "id": "ex-010", "name": "์„ฑ์ˆ˜ ํŠธ๋ Œ๋””ํ™˜์ „", "nameEn": "Seongsu Trendy Exchange", + "lat": 37.5445, "lng": 127.0564, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์„ฑ์ˆ˜์ด๋กœ 78", "category": "exchange", + "rating": 4.5, "phone": "02-0123-4567", "openHours": "10:00-20:00", + "noCommission": true, "minAmount": 10, + "rates": [ + { "currency": "USD", "buyRate": 1351, "sellRate": 1385 }, + { "currency": "JPY", "buyRate": 911, "sellRate": 933 }, + { "currency": "CNY", "buyRate": 186, "sellRate": 190 }, + { "currency": "EUR", "buyRate": 1486, "sellRate": 1522 } + ] + }, + { + "id": "ex-011", "name": "์ž ์‹ค ๋กฏ๋ฐํ™˜์ „", "nameEn": "Jamsil Lotte Exchange", + "lat": 37.5107, "lng": 127.0980, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 240 ๋กฏ๋ฐ์›”๋“œํƒ€์›Œ", "category": "exchange", + "rating": 4.0, "phone": "02-1122-3344", "openHours": "10:00-20:00", + "noCommission": false, "minAmount": 50, + "rates": [ + { "currency": "USD", "buyRate": 1339, "sellRate": 1397 }, + { "currency": "JPY", "buyRate": 901, "sellRate": 943 }, + { "currency": "CNY", "buyRate": 182, "sellRate": 195 }, + { "currency": "EUR", "buyRate": 1473, "sellRate": 1534 }, + { "currency": "GBP", "buyRate": 1715, "sellRate": 1786 } + ] + }, + { + "id": "ex-012", "name": "์ธ์ฒœ๊ณตํ•ญ 1ํ„ฐ๋ฏธ๋„ ํ™˜์ „", "nameEn": "ICN Airport T1 Exchange", + "lat": 37.4491, "lng": 126.4509, "address": "์ธ์ฒœ ์ค‘๊ตฌ ๊ณตํ•ญ๋กœ 272 ์ธ์ฒœ๊ตญ์ œ๊ณตํ•ญ 1ํ„ฐ๋ฏธ๋„", "category": "exchange", + "rating": 3.8, "phone": "032-741-1234", "openHours": "06:00-22:00", + "noCommission": false, "minAmount": 0, + "rates": [ + { "currency": "USD", "buyRate": 1328, "sellRate": 1408 }, + { "currency": "JPY", "buyRate": 895, "sellRate": 949 }, + { "currency": "CNY", "buyRate": 179, "sellRate": 199 }, + { "currency": "EUR", "buyRate": 1462, "sellRate": 1542 }, + { "currency": "GBP", "buyRate": 1702, "sellRate": 1798 }, + { "currency": "THB", "buyRate": 34, "sellRate": 43 } + ] + }, + { + "id": "ex-013", "name": "์™•์‹ญ๋ฆฌ ๋ฏผํ™˜์ „", "nameEn": "Wangsimni Min Exchange", + "lat": 37.5613, "lng": 127.0367, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๊ด‘์žฅ๋กœ 9", "category": "exchange", + "rating": 4.3, "phone": "02-2233-4455", "openHours": "09:00-20:00", + "noCommission": true, "minAmount": 20, + "rates": [ + { "currency": "USD", "buyRate": 1346, "sellRate": 1390 }, + { "currency": "JPY", "buyRate": 907, "sellRate": 937 }, + { "currency": "CNY", "buyRate": 184, "sellRate": 192 }, + { "currency": "EUR", "buyRate": 1480, "sellRate": 1527 } + ] + }, + { + "id": "ex-014", "name": "๊ฐ•๋‚จ์—ญ 24์‹œ ํ™˜์ „", "nameEn": "Gangnam Station 24H Exchange", + "lat": 37.4975, "lng": 127.0287, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 426", "category": "exchange", + "rating": 4.4, "phone": "02-3344-5566", "openHours": "24์‹œ๊ฐ„", + "noCommission": true, "minAmount": 10, + "rates": [ + { "currency": "USD", "buyRate": 1350, "sellRate": 1386 }, + { "currency": "JPY", "buyRate": 909, "sellRate": 935 }, + { "currency": "CNY", "buyRate": 185, "sellRate": 191 }, + { "currency": "EUR", "buyRate": 1484, "sellRate": 1524 }, + { "currency": "GBP", "buyRate": 1727, "sellRate": 1773 } + ] + }, + { + "id": "ex-015", "name": "๋ช…๋™ KEBํ•˜๋‚˜์€ํ–‰ ๊ณต์‹ํ™˜์ „", "nameEn": "Myeongdong KEB Hana Bank", + "lat": 37.5624, "lng": 126.9820, "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™11๊ธธ 35", "category": "exchange", + "rating": 4.6, "phone": "02-1588-1111", "openHours": "09:00-17:00 (ํ‰์ผ)", + "noCommission": false, "minAmount": 0, + "rates": [ + { "currency": "USD", "buyRate": 1345, "sellRate": 1391 }, + { "currency": "JPY", "buyRate": 906, "sellRate": 938 }, + { "currency": "CNY", "buyRate": 184, "sellRate": 193 }, + { "currency": "EUR", "buyRate": 1479, "sellRate": 1529 }, + { "currency": "GBP", "buyRate": 1722, "sellRate": 1778 }, + { "currency": "THB", "buyRate": 36, "sellRate": 41 } + ] + } +] diff --git a/map-app/src/data/extras.json b/map-app/src/data/extras.json new file mode 100644 index 000000000..1013eff32 --- /dev/null +++ b/map-app/src/data/extras.json @@ -0,0 +1,306 @@ +[ + { + "id": "ext-001", + "name": "๋ช…๋™ ๊ธธ๊ฑฐ๋ฆฌ ํ˜ธ๋–ก", + "nameEn": "Myeongdong Street Hotteok", + "lat": 37.5641, + "lng": 126.9836, + "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™8๋‚˜๊ธธ (ํฌ์žฅ๋งˆ์ฐจ)", + "category": "extra", + "rating": 4.6, + "extraType": "streetfood", + "extraTypeLabel": "ํฌ์žฅ๋งˆ์ฐจ", + "extraTypeLabelEn": "Street Food", + "price": 1500, + "priceUnit": "๊ฐœ", + "priceUnitEn": "per piece", + "description": "๊ฐ“ ๊ตฌ์šด ๋‹ฌ์ฝคํ•œ ํ˜ธ๋–ก, ์”จ์•— ํ˜ธ๋–ก๋„ ์žˆ์–ด์š”", + "descriptionEn": "Freshly grilled sweet pancake with seeds", + "emoji": "๐Ÿฅž" + }, + { + "id": "ext-002", + "name": "์ข…๋กœ3๊ฐ€ ํฌ์žฅ๋งˆ์ฐจ", + "nameEn": "Jongno Street Food Stalls", + "lat": 37.5702, + "lng": 126.9884, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ข…๋กœ 80 (ํฌ์žฅ๋งˆ์ฐจ ๊ฑฐ๋ฆฌ)", + "category": "extra", + "rating": 4.3, + "extraType": "streetfood", + "extraTypeLabel": "ํฌ์žฅ๋งˆ์ฐจ", + "extraTypeLabelEn": "Street Food", + "price": 3000, + "priceUnit": "์ธ๋ถ„", + "priceUnitEn": "per serving", + "description": "๋–ก๋ณถ์ด, ํŠ€๊น€, ์˜ค๋Ž…๊ตญ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ถ„์‹", + "descriptionEn": "Tteokbokki, fried items, fish cake soup", + "emoji": "๐Ÿข" + }, + { + "id": "ext-003", + "name": "ํ™๋Œ€ PC๋ฐฉ ๋„ท๋งˆ๋ฃจ", + "nameEn": "Hongdae PC Bang Netmaru", + "lat": 37.5568, + "lng": 126.9230, + "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ํ™์ต๋กœ 5๊ธธ 20", + "category": "extra", + "rating": 4.5, + "extraType": "pcbang", + "extraTypeLabel": "PC๋ฐฉ", + "extraTypeLabelEn": "Internet Cafe", + "price": 1500, + "priceUnit": "์‹œ๊ฐ„", + "priceUnitEn": "per hour", + "description": "์ตœ์‹  ๊ฒŒ์ž„ PC, ๊ณ ์‚ฌ์–‘ ์ขŒ์„, ์‹์Œ๋ฃŒ ์„œ๋น„์Šค", + "descriptionEn": "High-spec gaming PCs, food/drinks served at seat", + "emoji": "๐Ÿ–ฅ๏ธ" + }, + { + "id": "ext-004", + "name": "๊ฐ•๋‚จ PC๋ฐฉ ํ”Œ๋ ˆ์ด", + "nameEn": "Gangnam PC Bang Play", + "lat": 37.5003, + "lng": 127.0264, + "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 410 3์ธต", + "category": "extra", + "rating": 4.4, + "extraType": "pcbang", + "extraTypeLabel": "PC๋ฐฉ", + "extraTypeLabelEn": "Internet Cafe", + "price": 1800, + "priceUnit": "์‹œ๊ฐ„", + "priceUnitEn": "per hour", + "description": "RTX 4080 ๊ฒŒ์ด๋ฐ PC, ํ”„๋ฆฌ๋ฏธ์—„ ์ขŒ์„", + "descriptionEn": "RTX 4080 gaming PCs, premium seating", + "emoji": "๐Ÿ–ฅ๏ธ" + }, + { + "id": "ext-005", + "name": "ํ™๋Œ€ ๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šค ์šฐ๋ฆฌ์ง‘", + "nameEn": "Hongdae Guesthouse Urijip", + "lat": 37.5584, + "lng": 126.9244, + "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์™€์šฐ์‚ฐ๋กœ 25", + "category": "extra", + "rating": 4.4, + "extraType": "accommodation", + "extraTypeLabel": "๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šค", + "extraTypeLabelEn": "Guesthouse", + "price": 25000, + "priceUnit": "๋ฐ• (๋„๋ฏธํ† ๋ฆฌ)", + "priceUnitEn": "per night (dorm)", + "description": "ํ™๋Œ€ 5๋ถ„ ๊ฑฐ๋ฆฌ, ๋„๋ฏธํ† ๋ฆฌ/๊ฐœ์ธ์‹ค, ์กฐ์‹ ํฌํ•จ", + "descriptionEn": "5min to Hongdae, dorm & private rooms, breakfast included", + "emoji": "๐Ÿ " + }, + { + "id": "ext-006", + "name": "๋ช…๋™ YHA ํ˜ธ์Šคํ…”", + "nameEn": "Myeongdong YHA Hostel", + "lat": 37.5622, + "lng": 126.9829, + "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™1๊ฐ€ 58-3", + "category": "extra", + "rating": 4.2, + "extraType": "accommodation", + "extraTypeLabel": "ํ˜ธ์Šคํ…”", + "extraTypeLabelEn": "Hostel", + "price": 22000, + "priceUnit": "๋ฐ• (๋„๋ฏธํ† ๋ฆฌ)", + "priceUnitEn": "per night (dorm)", + "description": "๋ช…๋™ ์ค‘์‹ฌ, ๊ณต์šฉ ์ฃผ๋ฐฉ, ๋ฃจํ”„ํƒ‘ ํ…Œ๋ผ์Šค", + "descriptionEn": "Central Myeongdong, shared kitchen, rooftop terrace", + "emoji": "๐Ÿจ" + }, + { + "id": "ext-007", + "name": "์ดํƒœ์› ์Œ์‹ ํฌ์žฅ๋งˆ์ฐจ", + "nameEn": "Itaewon International Street Food", + "lat": 37.5359, + "lng": 126.9953, + "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 190", + "category": "extra", + "rating": 4.1, + "extraType": "streetfood", + "extraTypeLabel": "ํฌ์žฅ๋งˆ์ฐจ", + "extraTypeLabelEn": "Street Food", + "price": 5000, + "priceUnit": "์ธ๋ถ„", + "priceUnitEn": "per serving", + "description": "์ผ€๋ฐฅ, ํŒ”๋ผํŽ , ๊ฐ๊ตญ ์Œ์‹ ํฌ์žฅ๋งˆ์ฐจ", + "descriptionEn": "Kebab, falafel, international street food", + "emoji": "๐ŸŒฏ" + }, + { + "id": "ext-008", + "name": "๋™๋Œ€๋ฌธ 24H PC๋ฐฉ", + "nameEn": "Dongdaemun 24H PC Bang", + "lat": 37.5671, + "lng": 127.0094, + "address": "์„œ์šธ ์ค‘๊ตฌ ์„์ง€๋กœ 294", + "category": "extra", + "rating": 3.8, + "extraType": "pcbang", + "extraTypeLabel": "PC๋ฐฉ", + "extraTypeLabelEn": "Internet Cafe", + "price": 1200, + "priceUnit": "์‹œ๊ฐ„", + "priceUnitEn": "per hour", + "description": "24์‹œ๊ฐ„ ์šด์˜, ์ตœ์ €๊ฐ€ PC๋ฐฉ, ๋ผ๋ฉด 1000์›", + "descriptionEn": "24h, cheapest PC Bang, ramen โ‚ฉ1000", + "emoji": "๐Ÿ–ฅ๏ธ" + }, + { + "id": "ext-009", + "name": "๊ฑด๋Œ€ ๊ธธ๊ฑฐ๋ฆฌ ๋‹ญ๊ผฌ์น˜", + "nameEn": "Konkuk Street Yakitori", + "lat": 37.5400, + "lng": 127.0720, + "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ํ™”์–‘๋™ (์•ผ์‹œ์žฅ)", + "category": "extra", + "rating": 4.4, + "extraType": "streetfood", + "extraTypeLabel": "ํฌ์žฅ๋งˆ์ฐจ", + "extraTypeLabelEn": "Street Food", + "price": 2000, + "priceUnit": "๊ผฌ์น˜", + "priceUnitEn": "per skewer", + "description": "๋ถˆ๋ง› ๋‹ญ๊ผฌ์น˜, ์†Œ๋–ก์†Œ๋–ก, ๋ฌธ์–ด๊ผฌ์น˜", + "descriptionEn": "Charcoal chicken skewer, sausage-rice cake, octopus", + "emoji": "๐Ÿก" + }, + { + "id": "ext-010", + "name": "์„ฑ์ˆ˜ ํŒ์—…์Šคํ† ์–ด ๊ฑฐ๋ฆฌ", + "nameEn": "Seongsu Pop-Up Store Street", + "lat": 37.5440, + "lng": 127.0560, + "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์„ฑ์ˆ˜์ด๋กœ 80", + "category": "extra", + "rating": 4.3, + "extraType": "streetfood", + "extraTypeLabel": "ํŒ์—…์Šคํ† ์–ด", + "extraTypeLabelEn": "Pop-up Stores", + "price": 0, + "priceUnit": "์ž…์žฅ", + "priceUnitEn": "entry", + "description": "๋ธŒ๋žœ๋“œ ํŒ์—…์Šคํ† ์–ด, ๋ฌด๋ฃŒ ๊ตฟ์ฆˆ, ํฌํ† ๋ถ€์Šค", + "descriptionEn": "Brand pop-ups, free merch, photo booths", + "emoji": "๐Ÿ›๏ธ" + }, + { + "id": "ext-011", + "name": "๊ฐ•๋‚จ PC๋ฐฉ ๋ฐฐํ‹€๊ทธ๋ผ์šด๋“œ", + "nameEn": "Gangnam PC Bang Battlegrounds", + "lat": 37.4990, + "lng": 127.0280, + "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 394 2์ธต", + "category": "extra", + "rating": 4.2, + "extraType": "pcbang", + "extraTypeLabel": "PC๋ฐฉ", + "extraTypeLabelEn": "Internet Cafe", + "price": 1600, + "priceUnit": "์‹œ๊ฐ„", + "priceUnitEn": "per hour", + "description": "360Hz ๋ชจ๋‹ˆํ„ฐ, PC๋ฐฉ ์ „์šฉ ์ขŒ์„, ๊ฒŒ์ด๋ฐ ์Œ๋ฃŒ", + "descriptionEn": "360Hz monitors, pro gaming chairs, energy drinks", + "emoji": "๐Ÿ–ฅ๏ธ" + }, + { + "id": "ext-012", + "name": "์ž ์‹ค ๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šค ์˜ฌ๋ฆผํ”ฝ", + "nameEn": "Jamsil Olympic Guesthouse", + "lat": 37.5120, + "lng": 127.1000, + "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์ž ์‹ค๋™ 150", + "category": "extra", + "rating": 4.0, + "extraType": "accommodation", + "extraTypeLabel": "๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šค", + "extraTypeLabelEn": "Guesthouse", + "price": 28000, + "priceUnit": "๋ฐ• (๋„๋ฏธํ† ๋ฆฌ)", + "priceUnitEn": "per night (dorm)", + "description": "์ž ์‹ค์—ญ ๋„๋ณด 5๋ถ„, 8์ธ ๋„๋ฏธํ† ๋ฆฌ, ๊ณต์šฉ ์ฃผ๋ฐฉ", + "descriptionEn": "5min from Jamsil Station, 8-bed dorm, shared kitchen", + "emoji": "๐Ÿ " + }, + { + "id": "ext-013", + "name": "ํ™๋Œ€ ๋ฒ„์Šคํ‚น ๊ด‘์žฅ", + "nameEn": "Hongdae Busking Square", + "lat": 37.5574, + "lng": 126.9240, + "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์–ด์šธ๋งˆ๋‹น๋กœ (KT&G ์•ž)", + "category": "extra", + "rating": 4.5, + "extraType": "streetfood", + "extraTypeLabel": "์•ผ์™ธ๊ณต์—ฐ", + "extraTypeLabelEn": "Outdoor Performance", + "price": 0, + "priceUnit": "์ž…์žฅ", + "priceUnitEn": "entry", + "description": "๋งค์ฃผ ์ฃผ๋ง ๋ฒ„์Šคํ‚น, ์ธ๋””๋ฐด๋“œ, ๋Œ„์Šคํผํฌ๋จผ์Šค", + "descriptionEn": "Weekend busking, indie bands, dance performances", + "emoji": "๐ŸŽค" + }, + { + "id": "ext-014", + "name": "์ดํƒœ์› ๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šค ์ฝ”๋ฆฌ์•„๋‚˜", + "nameEn": "Itaewon Guesthouse Koreana", + "lat": 37.5350, + "lng": 126.9935, + "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 178", + "category": "extra", + "rating": 4.3, + "extraType": "accommodation", + "extraTypeLabel": "๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šค", + "extraTypeLabelEn": "Guesthouse", + "price": 30000, + "priceUnit": "๋ฐ• (๋„๋ฏธํ† ๋ฆฌ)", + "priceUnitEn": "per night (dorm)", + "description": "์ดํƒœ์› ์ค‘์‹ฌ, ๋‹ค๊ตญ์  ์—ฌํ–‰์ž ๋ชจ์ž„, ์˜ฅ์ƒ ๋ฐ”", + "descriptionEn": "Central Itaewon, international crowd, rooftop bar", + "emoji": "๐Ÿจ" + }, + { + "id": "ext-015", + "name": "์ข…๋กœ ๋‚™์›์ƒ๊ฐ€ ์•…๊ธฐ๊ฑฐ๋ฆฌ", + "nameEn": "Jongno Nakwon Arcade Music Street", + "lat": 37.5720, + "lng": 126.9882, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์‚ผ์ผ๋Œ€๋กœ 428", + "category": "extra", + "rating": 4.2, + "extraType": "streetfood", + "extraTypeLabel": "ํŠนํ™”๊ฑฐ๋ฆฌ", + "extraTypeLabelEn": "Specialty Street", + "price": 0, + "priceUnit": "๋ฐฉ๋ฌธ", + "priceUnitEn": "visit", + "description": "์•…๊ธฐ ์ „๋ฌธ ์ƒ๊ฐ€, ์ค‘๊ณ  ์•…๊ธฐ ์ตœ์ €๊ฐ€, ์•…๋ณด ํŒ๋งค", + "descriptionEn": "Musical instrument shops, secondhand deals, sheet music", + "emoji": "๐ŸŽธ" + }, + { + "id": "ext-016", + "name": "์‹ ์ดŒ ์ฝ”์ธ ๋นจ๋ž˜๋ฐฉ", + "nameEn": "Sinchon Coin Laundromat", + "lat": 37.5590, + "lng": 126.9360, + "address": "์„œ์šธ ์„œ๋Œ€๋ฌธ๊ตฌ ์‹ ์ดŒ๋กœ 77", + "category": "extra", + "rating": 4.1, + "extraType": "accommodation", + "extraTypeLabel": "์ฝ”์ธ ๋นจ๋ž˜๋ฐฉ", + "extraTypeLabelEn": "Coin Laundromat", + "price": 3000, + "priceUnit": "ํšŒ (์„ธํƒ)", + "priceUnitEn": "per wash", + "description": "24์‹œ๊ฐ„ ๋ฌด์ธ ์šด์˜, ์„ธํƒ+๊ฑด์กฐ 6000์›, ์™ธ๊ตญ์–ด ์•ˆ๋‚ด", + "descriptionEn": "24h unmanned, wash+dry โ‚ฉ6000, multilingual guide", + "emoji": "๐Ÿงบ" + } +] diff --git a/map-app/src/data/fuel.json b/map-app/src/data/fuel.json new file mode 100644 index 000000000..d1eba828d --- /dev/null +++ b/map-app/src/data/fuel.json @@ -0,0 +1,134 @@ +[ + { + "id": "fuel-001", "name": "GS์นผํ…์Šค ๊ฐ•๋‚จ์ ", "nameEn": "GS Caltex Gangnam", + "lat": 37.5010, "lng": 127.0252, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123", "category": "fuel", + "rating": 4.2, "brand": "GS์นผํ…์Šค", "selfService": false, "carWash": true, + "prices": { "gasoline": 1687, "diesel": 1512, "lpg": 987 } + }, + { + "id": "fuel-002", "name": "SK์—๋„ˆ์ง€ ํ™๋Œ€์ ", "nameEn": "SK Energy Hongdae", + "lat": 37.5560, "lng": 126.9248, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์–‘ํ™”๋กœ 56", "category": "fuel", + "rating": 4.0, "brand": "SK์—๋„ˆ์ง€", "selfService": true, "carWash": true, + "prices": { "gasoline": 1672, "diesel": 1498, "lpg": 975 } + }, + { + "id": "fuel-003", "name": "ํ˜„๋Œ€์˜ค์ผ๋ฑ…ํฌ ์ข…๋กœ์ ", "nameEn": "Hyundai Oilbank Jongno", + "lat": 37.5720, "lng": 126.9788, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ข…๋กœ 180", "category": "fuel", + "rating": 3.9, "brand": "ํ˜„๋Œ€์˜ค์ผ๋ฑ…ํฌ", "selfService": false, "carWash": false, + "prices": { "gasoline": 1695, "diesel": 1521, "lpg": 992 } + }, + { + "id": "fuel-004", "name": "S-OIL ์ดํƒœ์›์ ", "nameEn": "S-OIL Itaewon", + "lat": 37.5353, "lng": 126.9888, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 210", "category": "fuel", + "rating": 4.1, "brand": "S-OIL", "selfService": true, "carWash": false, + "prices": { "gasoline": 1668, "diesel": 1489, "lpg": 971 } + }, + { + "id": "fuel-005", "name": "GS์นผํ…์Šค ์—ฌ์˜๋„์ ", "nameEn": "GS Caltex Yeouido", + "lat": 37.5219, "lng": 126.9246, "address": "์„œ์šธ ์˜๋“ฑํฌ๊ตฌ ์—ฌ์˜๋Œ€๋ฐฉ๋กœ 100", "category": "fuel", + "rating": 4.3, "brand": "GS์นผํ…์Šค", "selfService": false, "carWash": true, + "prices": { "gasoline": 1682, "diesel": 1507, "lpg": 983, "electric": 327 } + }, + { + "id": "fuel-006", "name": "ํ•œ๊ตญ์ „๋ ฅ EV์ถฉ์ „์†Œ ๊ฐ•๋‚จ", "nameEn": "KEPCO EV Gangnam", + "lat": 37.4970, "lng": 127.0290, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋ด‰์€์‚ฌ๋กœ 524", "category": "fuel", + "rating": 4.5, "brand": "ํ•œ๊ตญ์ „๋ ฅ", "selfService": true, "carWash": false, + "prices": { "electric": 292 } + }, + { + "id": "fuel-007", "name": "Tesla ์ˆ˜ํผ์ฐจ์ € ์ฝ”์—‘์Šค", "nameEn": "Tesla Supercharger COEX", + "lat": 37.5122, "lng": 127.0586, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋ด‰์€์‚ฌ๋กœ 524 ์ฝ”์—‘์Šค", "category": "fuel", + "rating": 4.7, "brand": "Tesla", "selfService": true, "carWash": false, + "prices": { "electric": 344 } + }, + { + "id": "fuel-008", "name": "์ˆ˜์†Œ์ถฉ์ „์†Œ ์ƒ์•”", "nameEn": "Hydrogen Station Sangam", + "lat": 37.5680, "lng": 126.8943, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์ƒ์•”์‚ฐ๋กœ 75", "category": "fuel", + "rating": 4.4, "brand": "H2Korea", "selfService": false, "carWash": false, + "prices": { "hydrogen": 9200 } + }, + { + "id": "fuel-009", "name": "SK์—๋„ˆ์ง€ ์ž๊ฐ€์ฃผ์œ  ์†กํŒŒ", "nameEn": "SK Self-Service Songpa", + "lat": 37.5047, "lng": 127.1144, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์ž ์‹ค๋กœ 50", "category": "fuel", + "rating": 4.2, "brand": "SK์—๋„ˆ์ง€", "selfService": true, "carWash": false, + "prices": { "gasoline": 1659, "diesel": 1481, "lpg": 965 } + }, + { + "id": "fuel-010", "name": "GS์นผํ…์Šค EV๋ณตํ•ฉ ๋งˆํฌ", "nameEn": "GS Caltex EV Mapo", + "lat": 37.5558, "lng": 126.9046, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์„ฑ์‚ฐ๋กœ 128", "category": "fuel", + "rating": 4.3, "brand": "GS์นผํ…์Šค", "selfService": true, "carWash": false, + "prices": { "gasoline": 1675, "diesel": 1501, "electric": 315 } + }, + { + "id": "fuel-011", "name": "์•Œ๋œฐ์ฃผ์œ ์†Œ ๋…ธ์›์ ", "nameEn": "Budget Fuel Nowon", + "lat": 37.6547, "lng": 127.0567, "address": "์„œ์šธ ๋…ธ์›๊ตฌ ๋™์ผ๋กœ 1234", "category": "fuel", + "rating": 3.8, "brand": "์•Œ๋œฐ์ฃผ์œ ์†Œ", "selfService": true, "carWash": false, + "prices": { "gasoline": 1648, "diesel": 1472, "lpg": 961 } + }, + { + "id": "fuel-012", "name": "ํ˜„๋Œ€ ์ˆ˜์†Œ์ถฉ์ „์†Œ ์–‘์žฌ", "nameEn": "Hyundai Hydrogen Yangjae", + "lat": 37.4840, "lng": 127.0356, "address": "์„œ์šธ ์„œ์ดˆ๊ตฌ ์–‘์žฌ๋Œ€๋กœ 339", "category": "fuel", + "rating": 4.6, "brand": "Hyundai", "selfService": false, "carWash": false, + "prices": { "hydrogen": 8900, "electric": 310 } + }, + { + "id": "fuel-013", "name": "S-OIL ๊ฑด๋Œ€์ ", "nameEn": "S-OIL Konkuk", + "lat": 37.5398, "lng": 127.0705, "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ์•„์ฐจ์‚ฐ๋กœ 254", "category": "fuel", + "rating": 4.0, "brand": "S-OIL", "selfService": true, "carWash": true, + "prices": { "gasoline": 1662, "diesel": 1487, "lpg": 968 } + }, + { + "id": "fuel-014", "name": "SK์—๋„ˆ์ง€ ์„ฑ์ˆ˜์ ", "nameEn": "SK Energy Seongsu", + "lat": 37.5442, "lng": 127.0546, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ๋š์„ฌ๋กœ 124", "category": "fuel", + "rating": 4.1, "brand": "SK์—๋„ˆ์ง€", "selfService": false, "carWash": false, + "prices": { "gasoline": 1671, "diesel": 1495, "electric": 318 } + }, + { + "id": "fuel-015", "name": "์ฐจ์ง€๋น„ EV์ถฉ์ „์†Œ ์ž ์‹ค", "nameEn": "ChargeV EV Jamsil", + "lat": 37.5140, "lng": 127.1002, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์ž ์‹ค๋™ 40-1", "category": "fuel", + "rating": 4.2, "brand": "ChargeV", "selfService": true, "carWash": false, + "prices": { "electric": 285 } + }, + { + "id": "fuel-016", "name": "ํ˜„๋Œ€์˜ค์ผ๋ฑ…ํฌ ์™•์‹ญ๋ฆฌ", "nameEn": "Hyundai Oilbank Wangsimni", + "lat": 37.5610, "lng": 127.0375, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๋กœ 160", "category": "fuel", + "rating": 3.9, "brand": "ํ˜„๋Œ€์˜ค์ผ๋ฑ…ํฌ", "selfService": false, "carWash": true, + "prices": { "gasoline": 1691, "diesel": 1516, "lpg": 989 } + }, + { + "id": "fuel-017", "name": "์•Œ๋œฐ์ฃผ์œ ์†Œ ๊ฐ•๋™์ ", "nameEn": "Budget Fuel Gangdong", + "lat": 37.5303, "lng": 127.1233, "address": "์„œ์šธ ๊ฐ•๋™๊ตฌ ์ฒœํ˜ธ๋Œ€๋กœ 1246", "category": "fuel", + "rating": 3.7, "brand": "์•Œ๋œฐ์ฃผ์œ ์†Œ", "selfService": true, "carWash": false, + "prices": { "gasoline": 1645, "diesel": 1469, "lpg": 958 } + }, + { + "id": "fuel-018", "name": "GS์นผํ…์Šค ์‹ ์ดŒ์ ", "nameEn": "GS Caltex Sinchon", + "lat": 37.5585, "lng": 126.9355, "address": "์„œ์šธ ์„œ๋Œ€๋ฌธ๊ตฌ ์‹ ์ดŒ๋กœ 88", "category": "fuel", + "rating": 4.1, "brand": "GS์นผํ…์Šค", "selfService": false, "carWash": false, + "prices": { "gasoline": 1680, "diesel": 1505, "lpg": 981 } + }, + { + "id": "fuel-019", "name": "ํ™˜๊ฒฝ๋ถ€ EV์ถฉ์ „์†Œ ์ข…๋กœ", "nameEn": "MOE EV Charging Jongno", + "lat": 37.5703, "lng": 126.9842, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์„ธ์ข…๋Œ€๋กœ 209", "category": "fuel", + "rating": 4.3, "brand": "ํ™˜๊ฒฝ๋ถ€", "selfService": true, "carWash": false, + "prices": { "electric": 275 } + }, + { + "id": "fuel-020", "name": "SK์—๋„ˆ์ง€ ์ƒ์•”DMC์ ", "nameEn": "SK Energy Sangam DMC", + "lat": 37.5676, "lng": 126.8910, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์›”๋“œ์ปต๋ถ๋กœ 400", "category": "fuel", + "rating": 4.2, "brand": "SK์—๋„ˆ์ง€", "selfService": true, "carWash": true, + "prices": { "gasoline": 1666, "diesel": 1492, "lpg": 970, "electric": 312 } + }, + { + "id": "fuel-021", "name": "์ˆ˜์†Œ์ถฉ์ „์†Œ ์–‘์žฌ ํ˜„๋Œ€๋ชจ๋น„์Šค", "nameEn": "Hyundai Mobis Hydrogen Yangjae", + "lat": 37.4822, "lng": 127.0340, "address": "์„œ์šธ ์„œ์ดˆ๊ตฌ ํ—Œ๋ฆ‰๋กœ 12", "category": "fuel", + "rating": 4.5, "brand": "ํ˜„๋Œ€๋ชจ๋น„์Šค", "selfService": false, "carWash": false, + "prices": { "hydrogen": 8700 } + }, + { + "id": "fuel-022", "name": "GS์นผํ…์Šค ์ž ์‹ค์ ", "nameEn": "GS Caltex Jamsil", + "lat": 37.5082, "lng": 127.0887, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ๋ฐฑ์ œ๊ณ ๋ถ„๋กœ 388", "category": "fuel", + "rating": 4.0, "brand": "GS์นผํ…์Šค", "selfService": false, "carWash": true, + "prices": { "gasoline": 1684, "diesel": 1510, "lpg": 985 } + } +] diff --git a/map-app/src/data/jjimjilbang.json b/map-app/src/data/jjimjilbang.json new file mode 100644 index 000000000..967e8bab0 --- /dev/null +++ b/map-app/src/data/jjimjilbang.json @@ -0,0 +1,172 @@ +[ + { + "id": "jjim-001", + "name": "์šฉ์‚ฐ ๋“œ๋ž˜๊ณคํž์ŠคํŒŒ", + "nameEn": "Dragon Hill Spa Yongsan", + "lat": 37.5283, + "lng": 126.9641, + "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ํ•œ๊ฐ•๋Œ€๋กœ 40๋‚˜๊ธธ 24", + "category": "jjimjilbang", + "rating": 4.5, + "phone": "02-792-0001", + "openHours": "24์‹œ๊ฐ„", + "entryFee": 16000, + "overnightFee": 10000, + "towelIncluded": true, + "separateGenders": true, + "amenities": ["์‚ฌ์šฐ๋‚˜", "์ˆ˜์˜์žฅ", "์ฐœ์งˆ๋ฐฉ", "์ˆ˜๋ฉด์‹ค", "์‹๋‹น", "์˜ํ™”๊ด€", "๊ฒŒ์ž„๋ฃธ", "์ธํ„ฐ๋„ท๋ฃธ"] + }, + { + "id": "jjim-002", + "name": "๊ฐ•๋‚จ ๋„์‹ฌ๊ณต์ค‘๋ชฉ์š•ํƒ•", + "nameEn": "Gangnam City Bathhouse", + "lat": 37.5023, + "lng": 127.0248, + "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋กœ 211", + "category": "jjimjilbang", + "rating": 4.2, + "phone": "02-555-1234", + "openHours": "05:00-01:00", + "entryFee": 9000, + "overnightFee": null, + "towelIncluded": false, + "separateGenders": true, + "amenities": ["์‚ฌ์šฐ๋‚˜", "ํƒ•", "์ฐœ์งˆ๋ฐฉ"] + }, + { + "id": "jjim-003", + "name": "ํ™๋Œ€ ์‹ ์ดŒ์ฐœ์งˆ", + "nameEn": "Hongdae Sinchon Jjimjil", + "lat": 37.5558, + "lng": 126.9278, + "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์‹ ์ดŒ๋กœ 82", + "category": "jjimjilbang", + "rating": 4.0, + "phone": "02-333-5678", + "openHours": "24์‹œ๊ฐ„", + "entryFee": 8000, + "overnightFee": 8000, + "towelIncluded": true, + "separateGenders": true, + "amenities": ["์‚ฌ์šฐ๋‚˜", "์ฐœ์งˆ๋ฐฉ", "์ˆ˜๋ฉด์‹ค", "์‹๋‹น"] + }, + { + "id": "jjim-004", + "name": "์ŠคํŒŒ๋žœ๋“œ ์‹ ๋ผํ˜ธํ…”", + "nameEn": "Spa Land Shilla Hotel", + "lat": 37.5565, + "lng": 126.9987, + "address": "์„œ์šธ ์ค‘๊ตฌ ๋™ํ˜ธ๋กœ 249", + "category": "jjimjilbang", + "rating": 4.9, + "phone": "02-2230-3348", + "openHours": "06:00-22:00", + "entryFee": 130000, + "overnightFee": null, + "towelIncluded": true, + "separateGenders": false, + "amenities": ["ํ”„๋ฆฌ๋ฏธ์—„ ์‚ฌ์šฐ๋‚˜", "์•„์ฟ ์•„ ํ’€", "์š”๊ฐ€ ์ŠคํŠœ๋””์˜ค", "๋ ˆ์Šคํ† ๋ž‘", "ํŠธ๋ฆฌํŠธ๋จผํŠธ๋ฃธ"] + }, + { + "id": "jjim-005", + "name": "๋™๋Œ€๋ฌธ ์ฐœ์งˆํŒŒํฌ", + "nameEn": "Dongdaemun Jjimjil Park", + "lat": 37.5668, + "lng": 127.0087, + "address": "์„œ์šธ ์ค‘๊ตฌ ๋™๋Œ€๋ฌธ๋กœ 7", + "category": "jjimjilbang", + "rating": 3.8, + "phone": "02-2266-9999", + "openHours": "24์‹œ๊ฐ„", + "entryFee": 7000, + "overnightFee": 6000, + "towelIncluded": false, + "separateGenders": true, + "amenities": ["์‚ฌ์šฐ๋‚˜", "์ฐœ์งˆ๋ฐฉ", "์ˆ˜๋ฉด์‹ค"] + }, + { + "id": "jjim-006", + "name": "๊ฑด๋Œ€ ์›Œํ„ฐํŒŒํฌ ์ŠคํŒŒ", + "nameEn": "Konkuk Waterpark Spa", + "lat": 37.5412, + "lng": 127.0688, + "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ํ™”์–‘๋™ 50", + "category": "jjimjilbang", + "rating": 4.1, + "phone": "02-450-3000", + "openHours": "24์‹œ๊ฐ„", + "entryFee": 10000, + "overnightFee": 9000, + "towelIncluded": true, + "separateGenders": true, + "amenities": ["์‚ฌ์šฐ๋‚˜", "์ฐœ์งˆ๋ฐฉ", "์ˆ˜๋ฉด์‹ค", "์‹๋‹น", "๊ฒŒ์ž„์ฝ”๋„ˆ"] + }, + { + "id": "jjim-007", + "name": "์ž ์‹ค ๋กฏ๋ฐ์ŠคํŒŒ", + "nameEn": "Jamsil Lotte Spa", + "lat": 37.5115, + "lng": 127.0990, + "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 240 ๋กฏ๋ฐํ˜ธํ…”", + "category": "jjimjilbang", + "rating": 4.6, + "phone": "02-2213-1234", + "openHours": "06:00-22:00", + "entryFee": 80000, + "overnightFee": null, + "towelIncluded": true, + "separateGenders": false, + "amenities": ["๋Ÿญ์…”๋ฆฌ ์‚ฌ์šฐ๋‚˜", "์‹ค๋‚ด ํ’€", "๋งˆ์‚ฌ์ง€๋ฃธ", "๋ ˆ์Šคํ† ๋ž‘"] + }, + { + "id": "jjim-008", + "name": "์™•์‹ญ๋ฆฌ ํ•ดํ”ผ๋ฐ์ด ์ฐœ์งˆ๋ฐฉ", + "nameEn": "Wangsimni Happy Day Jjimjilbang", + "lat": 37.5618, + "lng": 127.0382, + "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๋กœ 112", + "category": "jjimjilbang", + "rating": 3.9, + "phone": "02-2290-8800", + "openHours": "24์‹œ๊ฐ„", + "entryFee": 8000, + "overnightFee": 7000, + "towelIncluded": true, + "separateGenders": true, + "amenities": ["์‚ฌ์šฐ๋‚˜", "์ฐœ์งˆ๋ฐฉ", "์ˆ˜๋ฉด์‹ค", "์กฑ์š•"] + }, + { + "id": "jjim-009", + "name": "๋…ธ์› ํ™ฉํ† ๋ฐฉ ์ฐœ์งˆ", + "nameEn": "Nowon Hwangto Jjimjilbang", + "lat": 37.6545, + "lng": 127.0570, + "address": "์„œ์šธ ๋…ธ์›๊ตฌ ๋™์ผ๋กœ 1248", + "category": "jjimjilbang", + "rating": 4.0, + "phone": "02-930-0000", + "openHours": "05:00-24:00", + "entryFee": 7000, + "overnightFee": 6000, + "towelIncluded": false, + "separateGenders": true, + "amenities": ["ํ™ฉํ† ๋ฐฉ", "์‚ฌ์šฐ๋‚˜", "์ฐœ์งˆ๋ฐฉ", "์ˆ˜๋ฉด์‹ค"] + }, + { + "id": "jjim-010", + "name": "์„ฑ์ˆ˜ ์•„์ฟ ์•„ ์ŠคํŒŒ", + "nameEn": "Seongsu Aqua Spa", + "lat": 37.5450, + "lng": 127.0540, + "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์„ฑ์ˆ˜์ด๋กœ 60", + "category": "jjimjilbang", + "rating": 4.3, + "phone": "02-469-5500", + "openHours": "24์‹œ๊ฐ„", + "entryFee": 12000, + "overnightFee": 10000, + "towelIncluded": true, + "separateGenders": true, + "amenities": ["๋…ธ์ฒœํƒ•", "์‚ฌ์šฐ๋‚˜", "์ฐœ์งˆ๋ฐฉ", "์ˆ˜๋ฉด์‹ค", "์นดํŽ˜"] + } +] diff --git a/map-app/src/data/karaoke.json b/map-app/src/data/karaoke.json new file mode 100644 index 000000000..dccaee50c --- /dev/null +++ b/map-app/src/data/karaoke.json @@ -0,0 +1,180 @@ +[ + { + "id": "kara-001", + "name": "์ฝ”์ธ๋…ธ๋ž˜๋ฐฉ ํ™๋Œ€์ ", + "nameEn": "Coin Noraebang Hongdae", + "lat": 37.5576, + "lng": 126.9243, + "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์–ด์šธ๋งˆ๋‹น๋กœ 67", + "category": "karaoke", + "rating": 4.3, + "openHours": "24์‹œ๊ฐ„", + "hasForeignSongs": true, + "hasTambourine": true, + "discountHours": "ํ‰์ผ ๋‚ฎ 12์‹œ-18์‹œ 30% ํ• ์ธ", + "rates": [ + { "roomSize": "small", "pricePerHour": 6000, "maxPeople": 2 } + ] + }, + { + "id": "kara-002", + "name": "๋งฅ๋…ธ๋ž˜๋ฐฉ ๊ฐ•๋‚จ์ ", + "nameEn": "Mac Noraebang Gangnam", + "lat": 37.4995, + "lng": 127.0267, + "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 380", + "category": "karaoke", + "rating": 4.5, + "openHours": "11:00-04:00", + "hasForeignSongs": true, + "hasTambourine": true, + "discountHours": "๋‚ฎ ํ• ์ธ 11:00-18:00", + "rates": [ + { "roomSize": "small", "pricePerHour": 15000, "maxPeople": 3 }, + { "roomSize": "medium", "pricePerHour": 20000, "maxPeople": 6 }, + { "roomSize": "large", "pricePerHour": 30000, "maxPeople": 12 } + ] + }, + { + "id": "kara-003", + "name": "์ˆ˜๋…ธ๋ž˜๋ฐฉ ๋ช…๋™์ ", + "nameEn": "Su Noraebang Myeongdong", + "lat": 37.5638, + "lng": 126.9826, + "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™๊ธธ 55", + "category": "karaoke", + "rating": 4.1, + "openHours": "12:00-03:00", + "hasForeignSongs": true, + "hasTambourine": false, + "rates": [ + { "roomSize": "small", "pricePerHour": 18000, "maxPeople": 4 }, + { "roomSize": "medium", "pricePerHour": 25000, "maxPeople": 8 }, + { "roomSize": "large", "pricePerHour": 35000, "maxPeople": 15 } + ] + }, + { + "id": "kara-004", + "name": "์ฝ”์ธ๋…ธ๋ž˜๋ฐฉ ์‹ ์ดŒ์ ", + "nameEn": "Coin Noraebang Sinchon", + "lat": 37.5588, + "lng": 126.9368, + "address": "์„œ์šธ ์„œ๋Œ€๋ฌธ๊ตฌ ์‹ ์ดŒ๋กœ 62", + "category": "karaoke", + "rating": 4.0, + "openHours": "24์‹œ๊ฐ„", + "hasForeignSongs": true, + "hasTambourine": true, + "discountHours": "์ƒˆ๋ฒฝ 02:00-10:00 50% ํ• ์ธ", + "rates": [ + { "roomSize": "small", "pricePerHour": 6000, "maxPeople": 2 } + ] + }, + { + "id": "kara-005", + "name": "๋Ÿญ์…”๋ฆฌ ๋…ธ๋ž˜ํด๋Ÿฝ ์ดํƒœ์›", + "nameEn": "Luxury Norae Club Itaewon", + "lat": 37.5348, + "lng": 126.9938, + "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 180", + "category": "karaoke", + "rating": 4.7, + "openHours": "17:00-05:00", + "hasForeignSongs": true, + "hasTambourine": true, + "rates": [ + { "roomSize": "small", "pricePerHour": 35000, "maxPeople": 4 }, + { "roomSize": "medium", "pricePerHour": 50000, "maxPeople": 8 }, + { "roomSize": "large", "pricePerHour": 80000, "maxPeople": 20 } + ] + }, + { + "id": "kara-006", + "name": "์ฝ”์ธ๋…ธ๋ž˜๋ฐฉ ๊ฑด๋Œ€์ ", + "nameEn": "Coin Noraebang Konkuk", + "lat": 37.5407, + "lng": 127.0708, + "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ์•„์ฐจ์‚ฐ๋กœ 270", + "category": "karaoke", + "rating": 4.2, + "openHours": "24์‹œ๊ฐ„", + "hasForeignSongs": true, + "hasTambourine": true, + "discountHours": "์˜ค์ „ 10:00-13:00 40% ํ• ์ธ", + "rates": [ + { "roomSize": "small", "pricePerHour": 6000, "maxPeople": 2 } + ] + }, + { + "id": "kara-007", + "name": "์—์Šค๋…ธ๋ž˜๋ฐฉ ์„ฑ์ˆ˜์ ", + "nameEn": "S Noraebang Seongsu", + "lat": 37.5447, + "lng": 127.0553, + "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ๋š์„ฌ๋กœ 413", + "category": "karaoke", + "rating": 4.0, + "openHours": "11:00-03:00", + "hasForeignSongs": true, + "hasTambourine": true, + "rates": [ + { "roomSize": "small", "pricePerHour": 12000, "maxPeople": 3 }, + { "roomSize": "medium", "pricePerHour": 18000, "maxPeople": 6 }, + { "roomSize": "large", "pricePerHour": 25000, "maxPeople": 10 } + ] + }, + { + "id": "kara-008", + "name": "์ฝ”์ธ๋…ธ๋ž˜๋ฐฉ ์ž ์‹ค์ ", + "nameEn": "Coin Noraebang Jamsil", + "lat": 37.5130, + "lng": 127.1005, + "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์ž ์‹ค๋กœ 21", + "category": "karaoke", + "rating": 4.1, + "openHours": "24์‹œ๊ฐ„", + "hasForeignSongs": true, + "hasTambourine": false, + "discountHours": "ํ‰์ผ ์˜ค์ „ 9:00-14:00 50% ํ• ์ธ", + "rates": [ + { "roomSize": "small", "pricePerHour": 6000, "maxPeople": 2 } + ] + }, + { + "id": "kara-009", + "name": "์™•์‹ญ๋ฆฌ ํฌ๋ ˆ์ŠคํŠธ ๋…ธ๋ž˜๋ฐฉ", + "nameEn": "Wangsimni Forest Noraebang", + "lat": 37.5615, + "lng": 127.0370, + "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๊ด‘์žฅ๋กœ 20", + "category": "karaoke", + "rating": 3.9, + "openHours": "12:00-04:00", + "hasForeignSongs": true, + "hasTambourine": true, + "rates": [ + { "roomSize": "small", "pricePerHour": 13000, "maxPeople": 3 }, + { "roomSize": "medium", "pricePerHour": 20000, "maxPeople": 7 }, + { "roomSize": "large", "pricePerHour": 28000, "maxPeople": 12 } + ] + }, + { + "id": "kara-010", + "name": "์ƒ์•” MBC ๋…ธ๋ž˜๋ฐฉ", + "nameEn": "Sangam MBC Noraebang", + "lat": 37.5678, + "lng": 126.8930, + "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์ƒ์•”๋™ 1589", + "category": "karaoke", + "rating": 4.3, + "openHours": "10:00-02:00", + "hasForeignSongs": true, + "hasTambourine": true, + "discountHours": "๋‚ฎ 12:00-18:00 20% ํ• ์ธ", + "rates": [ + { "roomSize": "small", "pricePerHour": 10000, "maxPeople": 3 }, + { "roomSize": "medium", "pricePerHour": 16000, "maxPeople": 6 }, + { "roomSize": "large", "pricePerHour": 22000, "maxPeople": 10 } + ] + } +] diff --git a/map-app/src/data/markets.json b/map-app/src/data/markets.json new file mode 100644 index 000000000..fc688254e --- /dev/null +++ b/map-app/src/data/markets.json @@ -0,0 +1,166 @@ +[ + { + "id": "mkt-001", + "name": "๊ด‘์žฅ์‹œ์žฅ", + "nameEn": "Gwangjang Market", + "lat": 37.5700, + "lng": 126.9993, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ฐฝ๊ฒฝ๊ถ๋กœ 88", + "category": "market", + "rating": 4.6, + "marketType": "์ „ํ†ต ์žฌ๋ž˜์‹œ์žฅ", + "marketTypeEn": "Traditional Market", + "operatingDays": "๋งค์ผ ์šด์˜", + "closedDay": "์—†์Œ", + "openHours": "09:00-21:00", + "popularItems": [ + { "name": "๋นˆ๋Œ€๋–ก", "nameEn": "Mung Bean Pancake", "price": 5000, "unit": "1๊ฐœ" }, + { "name": "๋งˆ์•ฝ๊น€๋ฐฅ", "nameEn": "Mini Kimbap", "price": 3000, "unit": "1์ค„" }, + { "name": "์œกํšŒ", "nameEn": "Beef Tartare", "price": 15000, "unit": "1์ธ๋ถ„" }, + { "name": "์ˆœ๋Œ€", "nameEn": "Sundae (blood sausage)", "price": 5000, "unit": "1์ธ๋ถ„" }, + { "name": "๋–ก", "nameEn": "Rice Cake", "price": 2000, "unit": "100g" } + ] + }, + { + "id": "mkt-002", + "name": "๋‚จ๋Œ€๋ฌธ์‹œ์žฅ", + "nameEn": "Namdaemun Market", + "lat": 37.5594, + "lng": 126.9764, + "address": "์„œ์šธ ์ค‘๊ตฌ ๋‚จ๋Œ€๋ฌธ์‹œ์žฅ4๊ธธ 21", + "category": "market", + "rating": 4.4, + "marketType": "์ข…ํ•ฉ ์žฌ๋ž˜์‹œ์žฅ", + "marketTypeEn": "General Traditional Market", + "operatingDays": "์›”~ํ† ", + "closedDay": "์ผ์š”์ผ", + "openHours": "05:00-18:00", + "popularItems": [ + { "name": "ํ˜ธ๋–ก", "nameEn": "Hotteok", "price": 1000, "unit": "1๊ฐœ" }, + { "name": "๊ฐˆ์น˜์กฐ๋ฆผ", "nameEn": "Braised Hairtail Fish", "price": 12000, "unit": "1์ธ๋ถ„" }, + { "name": "์นผ๊ตญ์ˆ˜", "nameEn": "Knife-cut Noodles", "price": 8000, "unit": "1๊ทธ๋ฆ‡" }, + { "name": "ํ•ธ๋“œ๋ฉ”์ด๋“œ ๊ฐ€๋ฐฉ", "nameEn": "Handmade Bag", "price": 15000, "unit": "1๊ฐœ" } + ] + }, + { + "id": "mkt-003", + "name": "ํ†ต์ธ์‹œ์žฅ", + "nameEn": "Tongin Market", + "lat": 37.5792, + "lng": 126.9706, + "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ํšจ์ž๋กœ7๊ธธ 34", + "category": "market", + "rating": 4.5, + "marketType": "์—ฝ์ „ ๋„์‹œ๋ฝ ์‹œ์žฅ", + "marketTypeEn": "Traditional Coin Lunchbox Market", + "operatingDays": "ํ™”~์ผ", + "closedDay": "์›”์š”์ผ", + "openHours": "10:00-18:00", + "popularItems": [ + { "name": "์—ฝ์ „ ๋„์‹œ๋ฝ ์„ธํŠธ", "nameEn": "Traditional Coin Lunchbox", "price": 5000, "unit": "50์—ฝ์ „" }, + { "name": "๋–ก๋ณถ์ด", "nameEn": "Tteokbokki", "price": 3000, "unit": "1์ ‘์‹œ" }, + { "name": "์ „", "nameEn": "Korean Pancake", "price": 3000, "unit": "1์ ‘์‹œ" } + ] + }, + { + "id": "mkt-004", + "name": "๋ง์›์‹œ์žฅ", + "nameEn": "Mangwon Market", + "lat": 37.5556, + "lng": 126.9080, + "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ํฌ์€๋กœ8๊ธธ 13", + "category": "market", + "rating": 4.3, + "marketType": "๋™๋„ค ์žฌ๋ž˜์‹œ์žฅ", + "marketTypeEn": "Neighborhood Market", + "operatingDays": "๋งค์ผ ์šด์˜", + "closedDay": "์—†์Œ", + "openHours": "08:00-21:00", + "popularItems": [ + { "name": "์‹ ์„  ์ฑ„์†Œ ๋ชจ๋‘ ", "nameEn": "Fresh Veggie Assortment", "price": 3000, "unit": "1๋ด‰" }, + { "name": "์ˆ˜์ œ ์–ด๋ฌต", "nameEn": "Handmade Fish Cake", "price": 5000, "unit": "200g" }, + { "name": "๊ตญ๋ฌผ๋–ก๋ณถ์ด", "nameEn": "Soupy Tteokbokki", "price": 3500, "unit": "1์ธ๋ถ„" } + ] + }, + { + "id": "mkt-005", + "name": "๋™๋Œ€๋ฌธ ํ’๋ฌผ์‹œ์žฅ", + "nameEn": "Dongdaemun Flea Market", + "lat": 37.5676, + "lng": 127.0118, + "address": "์„œ์šธ ์ค‘๊ตฌ ์žฅ์ถฉ๋‹จ๋กœ 247", + "category": "market", + "rating": 4.2, + "marketType": "๋ฒผ๋ฃฉ์‹œ์žฅยท๋นˆํ‹ฐ์ง€", + "marketTypeEn": "Flea & Vintage Market", + "operatingDays": "๊ธˆ~์ผ", + "closedDay": "์›”~๋ชฉ", + "openHours": "10:00-20:00", + "popularItems": [ + { "name": "๋นˆํ‹ฐ์ง€ ์˜๋ฅ˜", "nameEn": "Vintage Clothing", "price": 5000, "unit": "1๋ฒŒ" }, + { "name": "๊ณจ๋™ํ’ˆ ์†Œํ’ˆ", "nameEn": "Antique Decor", "price": 3000, "unit": "1์ " }, + { "name": "์ˆ˜์ œ ์•ก์„ธ์„œ๋ฆฌ", "nameEn": "Handmade Accessories", "price": 4000, "unit": "1๊ฐœ" } + ] + }, + { + "id": "mkt-006", + "name": "๋…ธ๋Ÿ‰์ง„ ์ˆ˜์‚ฐ์‹œ์žฅ", + "nameEn": "Noryangjin Fish Market", + "lat": 37.5137, + "lng": 126.9424, + "address": "์„œ์šธ ๋™์ž‘๊ตฌ ๋…ธ๋Ÿ‰์ง„๋กœ 12", + "category": "market", + "rating": 4.5, + "marketType": "์ˆ˜์‚ฐ์‹œ์žฅ", + "marketTypeEn": "Fish Market", + "operatingDays": "๋งค์ผ ์šด์˜", + "closedDay": "์—†์Œ", + "openHours": "01:00-22:00", + "popularItems": [ + { "name": "ํšŒ (๊ด‘์–ด)", "nameEn": "Sashimi (Flatfish)", "price": 30000, "unit": "1์ธ๋ถ„" }, + { "name": "๊ฝƒ๊ฒŒ", "nameEn": "Blue Crab", "price": 15000, "unit": "500g" }, + { "name": "๋‚™์ง€", "nameEn": "Octopus", "price": 10000, "unit": "๋งˆ๋ฆฌ" }, + { "name": "ํ•ด๋ฌผํƒ•", "nameEn": "Seafood Stew", "price": 25000, "unit": "2์ธ๋ถ„" } + ] + }, + { + "id": "mkt-007", + "name": "๊ฐ•๋™ ๊ธธ๋™ ์ž์—ฐ์ƒํƒœ๊ณต์› ์ฃผ๋ง์žฅ", + "nameEn": "Gildong Weekend Market", + "lat": 37.5298, + "lng": 127.1244, + "address": "์„œ์šธ ๊ฐ•๋™๊ตฌ ์ฒœํ˜ธ๋Œ€๋กœ 1258", + "category": "market", + "rating": 4.1, + "marketType": "์ฃผ๋ง ๋กœ์ปฌ๋งˆ์ผ“", + "marketTypeEn": "Weekend Local Market", + "operatingDays": "ํ† ~์ผ", + "closedDay": "ํ‰์ผ", + "openHours": "10:00-17:00", + "popularItems": [ + { "name": "๋กœ์ปฌ ๋ฒ ์ด์ปค๋ฆฌ", "nameEn": "Local Bakery Items", "price": 3500, "unit": "1๊ฐœ" }, + { "name": "์ˆ˜์ œ์ฒญ ์žผ", "nameEn": "Handmade Jam", "price": 8000, "unit": "1๋ณ‘" }, + { "name": "๋†์‚ฐ๋ฌผ ์ง๊ฑฐ๋ž˜", "nameEn": "Farm Direct Produce", "price": 5000, "unit": "1๋ฌถ์Œ" } + ] + }, + { + "id": "mkt-008", + "name": "์„ฑ์ˆ˜ ์ˆ˜์ œํ™”๊ฑฐ๋ฆฌ ๋งˆ์ผ“", + "nameEn": "Seongsu Handmade Shoes Market", + "lat": 37.5441, + "lng": 127.0569, + "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์„ฑ์ˆ˜์ด๋กœ 78", + "category": "market", + "rating": 4.4, + "marketType": "ํŠนํ™” ์ˆ˜์ œํ™” ๋งˆ์ผ“", + "marketTypeEn": "Artisan Shoe Market", + "operatingDays": "ํ™”~์ผ", + "closedDay": "์›”์š”์ผ", + "openHours": "11:00-20:00", + "popularItems": [ + { "name": "์ˆ˜์ œ ์Šค๋‹ˆ์ปค์ฆˆ", "nameEn": "Handmade Sneakers", "price": 80000, "unit": "1์ผค๋ ˆ" }, + { "name": "์ˆ˜์ œ ๋กœํผ", "nameEn": "Handmade Loafers", "price": 120000, "unit": "1์ผค๋ ˆ" }, + { "name": "๊ฐ€์ฃฝ ํŒŒ์šฐ์น˜", "nameEn": "Leather Pouch", "price": 25000, "unit": "1๊ฐœ" } + ] + } +] diff --git a/map-app/src/data/restaurants.json b/map-app/src/data/restaurants.json new file mode 100644 index 000000000..4e840cbf6 --- /dev/null +++ b/map-app/src/data/restaurants.json @@ -0,0 +1,328 @@ +[ + { + "id": "rest-001", "name": "๋ช…๋™ ๊ต์ž", "nameEn": "Myeongdong Kyoja", + "lat": 37.5634, "lng": 126.9844, "address": "์„œ์šธ ์ค‘๊ตฌ ๋ช…๋™10๊ธธ 29", "category": "restaurant", + "rating": 4.7, "nationalityTags": ["all","korean","japanese","chinese","western"], + "pricePerPerson": 11000, "cuisineType": "์นผ๊ตญ์ˆ˜/๋งŒ๋‘", "cuisineTypeEn": "Kalguksu & Dumplings", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": true, "hasChineseMenu": true, "hasJapaneseMenu": true, + "menuItems": [ + { "name": "์นผ๊ตญ์ˆ˜", "nameEn": "Knife-cut Noodles", "price": 11000, "spicyLevel": 1 }, + { "name": "๋น„๋น”๊ตญ์ˆ˜", "nameEn": "Bibim Noodles", "price": 11000, "spicyLevel": 2 }, + { "name": "๋งŒ๋‘", "nameEn": "Dumplings", "price": 9000, "spicyLevel": 0 } + ] + }, + { + "id": "rest-002", "name": "๊ด‘์žฅ์‹œ์žฅ ๋นˆ๋Œ€๋–ก", "nameEn": "Gwangjang Market Bindaetteok", + "lat": 37.5699, "lng": 126.9997, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ฐฝ๊ฒฝ๊ถ๋กœ 88", "category": "restaurant", + "rating": 4.5, "nationalityTags": ["all","korean","western","vegan"], + "pricePerPerson": 8000, "cuisineType": "์ „ํ†ต ๋ถ„์‹", "cuisineTypeEn": "Traditional Street Food", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "๋นˆ๋Œ€๋–ก", "nameEn": "Mung Bean Pancake", "price": 5000, "spicyLevel": 0, "isVegetarian": true }, + { "name": "๋งˆ์•ฝ๊น€๋ฐฅ", "nameEn": "Mini Kimbap", "price": 3000, "spicyLevel": 0, "isVegetarian": true }, + { "name": "์œกํšŒ", "nameEn": "Beef Tartare", "price": 15000, "spicyLevel": 1 } + ] + }, + { + "id": "rest-003", "name": "์ดํƒœ์› ํ• ๋ž„ ์ธ๋„์š”๋ฆฌ", "nameEn": "Itaewon Halal Indian Kitchen", + "lat": 37.5341, "lng": 126.9911, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 171", "category": "restaurant", + "rating": 4.6, "nationalityTags": ["indian","halal","vegan"], + "pricePerPerson": 18000, "cuisineType": "์ธ๋„ ์š”๋ฆฌ", "cuisineTypeEn": "Indian Cuisine", + "isHalal": true, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "๋ฒ„ํ„ฐ ์น˜ํ‚จ ์ปค๋ฆฌ", "nameEn": "Butter Chicken Curry", "price": 18000, "spicyLevel": 2, "isHalal": true }, + { "name": "ํŒ”๋ฝ ํŒŒ๋‹ˆ๋ฅด", "nameEn": "Palak Paneer", "price": 16000, "spicyLevel": 1, "isVegetarian": true, "isHalal": true }, + { "name": "๋‹ฌ ๋งˆํฌ๋‹ˆ", "nameEn": "Dal Makhni", "price": 14000, "spicyLevel": 1, "isVegetarian": true, "isHalal": true }, + { "name": "๋น„๋ฆฌ์•ผ๋‹ˆ", "nameEn": "Chicken Biryani", "price": 20000, "spicyLevel": 2, "isHalal": true } + ] + }, + { + "id": "rest-004", "name": "ํ™๋Œ€ ์‚ผ๊ฒน์‚ด ์ฒœ๊ตญ", "nameEn": "Hongdae Samgyeopsal Heaven", + "lat": 37.5586, "lng": 126.9258, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์–ด์šธ๋งˆ๋‹น๋กœ 55", "category": "restaurant", + "rating": 4.4, "nationalityTags": ["korean","japanese","western","chinese"], + "pricePerPerson": 18000, "cuisineType": "๊ณ ๊ธฐ๊ตฌ์ด", "cuisineTypeEn": "Korean BBQ", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์‚ผ๊ฒน์‚ด", "nameEn": "Grilled Pork Belly", "price": 17000, "spicyLevel": 0 }, + { "name": "๋ชฉ์‚ด", "nameEn": "Pork Neck BBQ", "price": 17000, "spicyLevel": 0 }, + { "name": "๋œ์žฅ์ฐŒ๊ฐœ", "nameEn": "Soybean Paste Stew", "price": 8000, "spicyLevel": 1 } + ] + }, + { + "id": "rest-005", "name": "๊ฐ•๋‚จ ์Šค์‹œ์˜ค๋งˆ์นด์„ธ ํ•˜๋‚˜", "nameEn": "Gangnam Sushi Omakase Hana", + "lat": 37.5050, "lng": 127.0235, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋…ผํ˜„๋กœ 705", "category": "restaurant", + "rating": 4.9, "nationalityTags": ["japanese","western"], + "pricePerPerson": 95000, "cuisineType": "์ผ์‹/์Šค์‹œ", "cuisineTypeEn": "Japanese Sushi", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": true, + "menuItems": [ + { "name": "์˜ค๋งˆ์นด์„ธ ์ฝ”์Šค A", "nameEn": "Omakase Set A", "price": 95000, "spicyLevel": 0 }, + { "name": "์˜ค๋งˆ์นด์„ธ ์ฝ”์Šค B", "nameEn": "Omakase Set B", "price": 150000, "spicyLevel": 0 } + ] + }, + { + "id": "rest-006", "name": "์ฐจ์ด๋‚˜ํƒ€์šด ๋งˆ๋ผํƒ•", "nameEn": "Chinatown Malatang", + "lat": 37.5672, "lng": 126.9974, "address": "์„œ์šธ ์ค‘๊ตฌ ์„์ง€๋กœ 120", "category": "restaurant", + "rating": 4.3, "nationalityTags": ["chinese","western","korean"], + "pricePerPerson": 15000, "cuisineType": "์ค‘๊ตญ ์š”๋ฆฌ", "cuisineTypeEn": "Chinese Cuisine", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": true, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "๋งˆ๋ผํƒ•", "nameEn": "Malatang Hot Pot", "price": 15000, "spicyLevel": 3 }, + { "name": "์งœ์žฅ๋ฉด", "nameEn": "Jjajangmyeon", "price": 9000, "spicyLevel": 0 }, + { "name": "์งฌ๋ฝ•", "nameEn": "Jjamppong Seafood Noodles", "price": 10000, "spicyLevel": 3 } + ] + }, + { + "id": "rest-007", "name": "์‚ฌ์ฐฐ์Œ์‹ ๋ฐœ์šฐ๊ณต์–‘", "nameEn": "Temple Food Balwoo Gongyang", + "lat": 37.5756, "lng": 126.9834, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์šฐ์ •๊ตญ๋กœ 56", "category": "restaurant", + "rating": 4.8, "nationalityTags": ["vegan","indian","western"], + "pricePerPerson": 45000, "cuisineType": "์‚ฌ์ฐฐ์Œ์‹", "cuisineTypeEn": "Buddhist Temple Food", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์ ์‹ฌ ์ฝ”์Šค", "nameEn": "Lunch Course", "price": 45000, "spicyLevel": 0, "isVegetarian": true }, + { "name": "์ €๋… ์ฝ”์Šค", "nameEn": "Dinner Course", "price": 65000, "spicyLevel": 0, "isVegetarian": true } + ] + }, + { + "id": "rest-008", "name": "์ดํƒœ์› ํ• ๋ž„ ํ„ฐํ‚ค ์ผ€๋ฐฅ", "nameEn": "Itaewon Halal Turkish Kebab", + "lat": 37.5350, "lng": 126.9932, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 184", "category": "restaurant", + "rating": 4.2, "nationalityTags": ["halal","western","indian"], + "pricePerPerson": 12000, "cuisineType": "ํ„ฐํ‚ค ์š”๋ฆฌ", "cuisineTypeEn": "Turkish / Kebab", + "isHalal": true, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์น˜ํ‚จ ์ผ€๋ฐฅ", "nameEn": "Chicken Kebab", "price": 12000, "spicyLevel": 1, "isHalal": true }, + { "name": "ํŒ”๋ผํŽ  ๋žฉ", "nameEn": "Falafel Wrap", "price": 9000, "spicyLevel": 0, "isVegetarian": true, "isHalal": true } + ] + }, + { + "id": "rest-009", "name": "๊ฐ•๋‚จ ๊ตญ๋ฐฅ์ง‘ ์–ด๋จธ๋‹ˆ", "nameEn": "Gangnam Gomtang", + "lat": 37.4985, "lng": 127.0258, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 152", "category": "restaurant", + "rating": 4.5, "nationalityTags": ["korean","western"], + "pricePerPerson": 10000, "cuisineType": "๊ตญ๋ฐฅ/์„ค๋ ํƒ•", "cuisineTypeEn": "Korean Bone Broth Soup", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์ˆœ๋Œ€๊ตญ๋ฐฅ", "nameEn": "Sundae Gukbap", "price": 10000, "spicyLevel": 1 }, + { "name": "์„ค๋ ํƒ•", "nameEn": "Seolleongtang", "price": 12000, "spicyLevel": 0 } + ] + }, + { + "id": "rest-010", "name": "๋™๋Œ€๋ฌธ ์ˆœ๋Œ€ํƒ€์šด", "nameEn": "Dongdaemun Sundae Town", + "lat": 37.5672, "lng": 127.0103, "address": "์„œ์šธ ์ค‘๊ตฌ ์ฒญ๊ณ„์ฒœ๋กœ 575", "category": "restaurant", + "rating": 4.3, "nationalityTags": ["korean"], + "pricePerPerson": 9000, "cuisineType": "๋ถ„์‹", "cuisineTypeEn": "Korean Street Food", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์ˆœ๋Œ€", "nameEn": "Sundae", "price": 5000, "spicyLevel": 0 }, + { "name": "๋–ก๋ณถ์ด", "nameEn": "Tteokbokki", "price": 5000, "spicyLevel": 3 } + ] + }, + { + "id": "rest-011", "name": "์‹ ์ดŒ ์ผ์‹๋ผ๋ฉ˜ ์‚ฌ์ฟ ๋ผ", "nameEn": "Sinchon Ramen Sakura", + "lat": 37.5582, "lng": 126.9387, "address": "์„œ์šธ ์„œ๋Œ€๋ฌธ๊ตฌ ์‹ ์ดŒ๋กœ 93", "category": "restaurant", + "rating": 4.4, "nationalityTags": ["japanese","western","korean"], + "pricePerPerson": 14000, "cuisineType": "์ผ์‹ ๋ผ๋ฉ˜", "cuisineTypeEn": "Japanese Ramen", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": true, + "menuItems": [ + { "name": "๋ˆ์ฝ”์ธ  ๋ผ๋ฉ˜", "nameEn": "Tonkotsu Ramen", "price": 14000, "spicyLevel": 0 }, + { "name": "์‡ผ์œ  ๋ผ๋ฉ˜", "nameEn": "Shoyu Ramen", "price": 13000, "spicyLevel": 0 }, + { "name": "๊ต์ž", "nameEn": "Gyoza", "price": 7000, "spicyLevel": 0 } + ] + }, + { + "id": "rest-012", "name": "์ดํƒœ์› ๋ฉ•์‹œ์นธ ํƒ€์ฝ” ๋ฐ”", "nameEn": "Itaewon Mexican Taco Bar", + "lat": 37.5362, "lng": 126.9921, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 168", "category": "restaurant", + "rating": 4.3, "nationalityTags": ["western","halal"], + "pricePerPerson": 16000, "cuisineType": "๋ฉ•์‹œ์นธ", "cuisineTypeEn": "Mexican", + "isHalal": true, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์น˜ํ‚จ ํƒ€์ฝ” 3๊ฐœ", "nameEn": "Chicken Tacos x3", "price": 16000, "spicyLevel": 2, "isHalal": true }, + { "name": "๋น„๊ฑด ํƒ€์ฝ” 3๊ฐœ", "nameEn": "Vegan Tacos x3", "price": 14000, "spicyLevel": 1, "isVegetarian": true } + ] + }, + { + "id": "rest-013", "name": "์ข…๋กœ ๋น„๋น”๋ฐฅ ๋ณธ๊ฐ€", "nameEn": "Jongno Bibimbap Original", + "lat": 37.5703, "lng": 126.9792, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ข…๋กœ 55", "category": "restaurant", + "rating": 4.6, "nationalityTags": ["all","korean","japanese","chinese","western","vegan"], + "pricePerPerson": 12000, "cuisineType": "ํ•œ์‹", "cuisineTypeEn": "Korean Traditional", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": true, "hasJapaneseMenu": true, + "menuItems": [ + { "name": "๋น„๋น”๋ฐฅ", "nameEn": "Bibimbap", "price": 12000, "spicyLevel": 1, "isVegetarian": true }, + { "name": "๋Œ์†ฅ๋น„๋น”๋ฐฅ", "nameEn": "Stone Pot Bibimbap", "price": 14000, "spicyLevel": 1 }, + { "name": "๋œ์žฅ์ฐŒ๊ฐœ", "nameEn": "Doenjang Jjigae", "price": 9000, "spicyLevel": 1, "isVegetarian": true } + ] + }, + { + "id": "rest-014", "name": "ํ™๋Œ€ ๋ฒ ํŠธ๋‚จ ์Œ€๊ตญ์ˆ˜", "nameEn": "Hongdae Vietnamese Pho", + "lat": 37.5568, "lng": 126.9219, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ํ™์ต๋กœ 14", "category": "restaurant", + "rating": 4.2, "nationalityTags": ["western","vegan","chinese"], + "pricePerPerson": 12000, "cuisineType": "๋ฒ ํŠธ๋‚จ ์š”๋ฆฌ", "cuisineTypeEn": "Vietnamese", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์†Œ๊ณ ๊ธฐ ์Œ€๊ตญ์ˆ˜", "nameEn": "Beef Pho", "price": 12000, "spicyLevel": 1 }, + { "name": "์ฑ„์†Œ ์Œ€๊ตญ์ˆ˜", "nameEn": "Vegetable Pho", "price": 11000, "spicyLevel": 0, "isVegetarian": true } + ] + }, + { + "id": "rest-015", "name": "๊ฑด๋Œ€ ์–‘๊ผฌ์น˜ ์ „๋ฌธ์ ", "nameEn": "Konkuk Lamb Skewer House", + "lat": 37.5408, "lng": 127.0699, "address": "์„œ์šธ ๊ด‘์ง„๊ตฌ ์ž์–‘๋™ 248", "category": "restaurant", + "rating": 4.4, "nationalityTags": ["chinese","korean","western"], + "pricePerPerson": 20000, "cuisineType": "์ค‘์‹/์–‘๊ผฌ์น˜", "cuisineTypeEn": "Lamb Skewers", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": false, "hasChineseMenu": true, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์–‘๊ผฌ์น˜", "nameEn": "Lamb Skewers", "price": 2000, "spicyLevel": 1 }, + { "name": "์นญ๋”ฐ์˜ค ๋งฅ์ฃผ", "nameEn": "Tsingtao Beer", "price": 5000, "spicyLevel": 0 }, + { "name": "๋งˆ๋ผํ–ฅ ์–‘๊ผฌ์น˜", "nameEn": "Spicy Mala Lamb", "price": 2500, "spicyLevel": 3 } + ] + }, + { + "id": "rest-016", "name": "์„ฑ์ˆ˜ ๋ธŒ๋Ÿฐ์น˜ ์นดํŽ˜ ์‹๋ฌผ", "nameEn": "Seongsu Brunch Sikul", + "lat": 37.5444, "lng": 127.0571, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์„ฑ์ˆ˜์ด๋กœ7๊ธธ 22", "category": "restaurant", + "rating": 4.7, "nationalityTags": ["western","vegan","japanese"], + "pricePerPerson": 22000, "cuisineType": "๋ธŒ๋Ÿฐ์น˜/์นดํŽ˜", "cuisineTypeEn": "Brunch Cafe", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์—๊ทธ ๋ฒ ๋„ค๋”•ํŠธ", "nameEn": "Eggs Benedict", "price": 22000, "spicyLevel": 0 }, + { "name": "๋น„๊ฑด ์ƒ๋Ÿฌ๋“œ๋ณผ", "nameEn": "Vegan Salad Bowl", "price": 19000, "spicyLevel": 0, "isVegetarian": true }, + { "name": "ํŒฌ์ผ€์ดํฌ ํ”Œ๋ ˆ์ดํŠธ", "nameEn": "Pancake Plate", "price": 18000, "spicyLevel": 0, "isVegetarian": true } + ] + }, + { + "id": "rest-017", "name": "์ž ์‹ค ์น˜๋งฅ ๋ช…๊ฐ€", "nameEn": "Jamsil Chicken & Beer", + "lat": 37.5128, "lng": 127.1024, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 43", "category": "restaurant", + "rating": 4.5, "nationalityTags": ["korean","western","chinese"], + "pricePerPerson": 25000, "cuisineType": "์น˜ํ‚จ/๋งฅ์ฃผ", "cuisineTypeEn": "Korean Fried Chicken", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "ํ›„๋ผ์ด๋“œ ์น˜ํ‚จ", "nameEn": "Fried Chicken", "price": 19000, "spicyLevel": 0 }, + { "name": "์–‘๋… ์น˜ํ‚จ", "nameEn": "Spicy Sauce Chicken", "price": 20000, "spicyLevel": 2 }, + { "name": "์ƒ๋งฅ์ฃผ 500ml", "nameEn": "Draft Beer 500ml", "price": 5000, "spicyLevel": 0 } + ] + }, + { + "id": "rest-018", "name": "์ธ์‚ฌ๋™ ํ•œ์ •์‹ ์ „ํ†ต", "nameEn": "Insadong Korean Table d'hote", + "lat": 37.5745, "lng": 126.9858, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ์ธ์‚ฌ๋™5๊ธธ 26", "category": "restaurant", + "rating": 4.7, "nationalityTags": ["all","korean","japanese","chinese","western"], + "pricePerPerson": 35000, "cuisineType": "ํ•œ์ •์‹", "cuisineTypeEn": "Korean Full-Course", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": true, "hasJapaneseMenu": true, + "menuItems": [ + { "name": "์ ์‹ฌ ํ•œ์ •์‹", "nameEn": "Lunch Course", "price": 35000, "spicyLevel": 1 }, + { "name": "์ €๋… ํ•œ์ •์‹", "nameEn": "Dinner Course", "price": 55000, "spicyLevel": 1 } + ] + }, + { + "id": "rest-019", "name": "์™•์‹ญ๋ฆฌ ๊ฐ์žํƒ•", "nameEn": "Wangsimni Gamjatang", + "lat": 37.5614, "lng": 127.0379, "address": "์„œ์šธ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๋กœ 130", "category": "restaurant", + "rating": 4.3, "nationalityTags": ["korean"], + "pricePerPerson": 13000, "cuisineType": "๊ฐ์žํƒ•", "cuisineTypeEn": "Pork Bone Stew", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "๊ฐ์žํƒ• (2์ธ)", "nameEn": "Gamjatang for 2", "price": 26000, "spicyLevel": 2 }, + { "name": "๋ผˆํ•ด์žฅ๊ตญ", "nameEn": "Bone Soup", "price": 12000, "spicyLevel": 1 } + ] + }, + { + "id": "rest-020", "name": "ํ™๋Œ€ ์ดํƒˆ๋ฆฌ์•ˆ ํŒŒ์Šคํƒ€", "nameEn": "Hongdae Italian Pasta", + "lat": 37.5564, "lng": 126.9235, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ํ™์ต๋กœ 21", "category": "restaurant", + "rating": 4.3, "nationalityTags": ["western","vegan"], + "pricePerPerson": 18000, "cuisineType": "์ดํƒˆ๋ฆฌ์•ˆ", "cuisineTypeEn": "Italian", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์นด๋ฅด๋ณด๋‚˜๋ผ", "nameEn": "Carbonara", "price": 18000, "spicyLevel": 0 }, + { "name": "ํ† ๋งˆํ†  ์ŠคํŒŒ๊ฒŒํ‹ฐ", "nameEn": "Tomato Spaghetti", "price": 16000, "spicyLevel": 0, "isVegetarian": true }, + { "name": "๋งˆ๋ฅด๊ฒŒ๋ฆฌํƒ€ ํ”ผ์ž", "nameEn": "Margherita Pizza", "price": 19000, "spicyLevel": 0, "isVegetarian": true } + ] + }, + { + "id": "rest-021", "name": "๊ฐ•๋‚จ ๋„คํŒ” ์ปค๋ฆฌํ•˜์šฐ์Šค", "nameEn": "Gangnam Nepal Curry House", + "lat": 37.5022, "lng": 127.0261, "address": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋กœ 222", "category": "restaurant", + "rating": 4.4, "nationalityTags": ["indian","halal","vegan"], + "pricePerPerson": 16000, "cuisineType": "๋„คํŒ”/์ธ๋„ ์š”๋ฆฌ", "cuisineTypeEn": "Nepali/Indian", + "isHalal": true, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "๋‹ฌ๋ฐง (์ฑ„์‹)", "nameEn": "Dal Bhat Veg", "price": 14000, "spicyLevel": 1, "isVegetarian": true, "isHalal": true }, + { "name": "์น˜ํ‚จ ๋ชจ๋ชจ", "nameEn": "Chicken Momo Dumplings", "price": 12000, "spicyLevel": 1, "isHalal": true }, + { "name": "์ฑ„์‹ ์ปค๋ฆฌ", "nameEn": "Vegetable Curry", "price": 14000, "spicyLevel": 2, "isVegetarian": true, "isHalal": true } + ] + }, + { + "id": "rest-022", "name": "์ƒ์•” ํ‘ธ๋“œํŠธ๋Ÿญ ๊ฑฐ๋ฆฌ", "nameEn": "Sangam Food Truck Street", + "lat": 37.5672, "lng": 126.8921, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์ƒ์•”๋™ ์›”๋“œ์ปต๊ณต์›๋กœ", "category": "restaurant", + "rating": 4.1, "nationalityTags": ["korean","western"], + "pricePerPerson": 8000, "cuisineType": "๊ธธ๊ฑฐ๋ฆฌ ์Œ์‹", "cuisineTypeEn": "Street Food", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "๋ถˆ๋‹ญ๋ณถ์Œ๋ฉด", "nameEn": "Fire Noodles", "price": 7000, "spicyLevel": 3 }, + { "name": "ํ•ซ๋„๊ทธ", "nameEn": "Corn Dog", "price": 3000, "spicyLevel": 0 }, + { "name": "์˜ค๋Ž…ํƒ•", "nameEn": "Fish Cake Soup", "price": 5000, "spicyLevel": 1 } + ] + }, + { + "id": "rest-023", "name": "๋…ธ์› ์ˆœ๋Œ€๊ตญ๋ฐฅ ์›์กฐ", "nameEn": "Nowon Original Sundae Gukbap", + "lat": 37.6540, "lng": 127.0563, "address": "์„œ์šธ ๋…ธ์›๊ตฌ ๋™์ผ๋กœ 1181", "category": "restaurant", + "rating": 4.4, "nationalityTags": ["korean"], + "pricePerPerson": 9000, "cuisineType": "๊ตญ๋ฐฅ", "cuisineTypeEn": "Korean Gukbap", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์ˆœ๋Œ€๊ตญ๋ฐฅ", "nameEn": "Sundae Gukbap", "price": 9000, "spicyLevel": 1 }, + { "name": "๋‚ด์žฅ๊ตญ๋ฐฅ", "nameEn": "Offal Gukbap", "price": 10000, "spicyLevel": 1 } + ] + }, + { + "id": "rest-024", "name": "์ดํƒœ์› ํƒœ๊ตญ ์จ๋•€", "nameEn": "Itaewon Thai Som Tam", + "lat": 37.5357, "lng": 126.9946, "address": "์„œ์šธ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 193", "category": "restaurant", + "rating": 4.3, "nationalityTags": ["western","vegan","halal"], + "pricePerPerson": 16000, "cuisineType": "ํƒœ๊ตญ ์š”๋ฆฌ", "cuisineTypeEn": "Thai Cuisine", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์จ๋•€", "nameEn": "Som Tam Papaya Salad", "price": 13000, "spicyLevel": 3, "isVegetarian": true }, + { "name": "ํŒŸํƒ€์ด", "nameEn": "Pad Thai", "price": 15000, "spicyLevel": 1 }, + { "name": "๊ทธ๋ฆฐ ์ปค๋ฆฌ", "nameEn": "Green Curry", "price": 16000, "spicyLevel": 2 } + ] + }, + { + "id": "rest-025", "name": "์ž ์‹ค ์ผ์‹ ๋ˆ๋ถ€๋ฆฌ ์•ผ๋งˆ", "nameEn": "Jamsil Donburi Yama", + "lat": 37.5116, "lng": 127.0996, "address": "์„œ์šธ ์†กํŒŒ๊ตฌ ์ž ์‹ค๋™ 184", "category": "restaurant", + "rating": 4.5, "nationalityTags": ["japanese","western","korean"], + "pricePerPerson": 13000, "cuisineType": "์ผ์‹ ๋ฎ๋ฐฅ", "cuisineTypeEn": "Japanese Donburi", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": true, + "menuItems": [ + { "name": "์นด์ธ ๋™", "nameEn": "Katsudon", "price": 13000, "spicyLevel": 0 }, + { "name": "๊ทœ๋™", "nameEn": "Gyudon Beef Bowl", "price": 12000, "spicyLevel": 0 }, + { "name": "์˜ค์•ผ์ฝ”๋™", "nameEn": "Oyakodon", "price": 13000, "spicyLevel": 0 } + ] + }, + { + "id": "rest-026", "name": "๊ฐ•๋™ ์ค‘๊ตญ์ง‘ ๋ณต๋•๋ฐฉ", "nameEn": "Gangdong Chinese Bokdeokbang", + "lat": 37.5302, "lng": 127.1235, "address": "์„œ์šธ ๊ฐ•๋™๊ตฌ ์ฒœํ˜ธ๋Œ€๋กœ 1256", "category": "restaurant", + "rating": 4.2, "nationalityTags": ["chinese","korean"], + "pricePerPerson": 10000, "cuisineType": "์ค‘์‹", "cuisineTypeEn": "Chinese", + "isHalal": false, "isVegetarianFriendly": false, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์งœ์žฅ๋ฉด", "nameEn": "Jjajangmyeon", "price": 8000, "spicyLevel": 0 }, + { "name": "์งฌ๋ฝ•", "nameEn": "Jjamppong", "price": 9000, "spicyLevel": 3 }, + { "name": "ํƒ•์ˆ˜์œก (์†Œ)", "nameEn": "Sweet & Sour Pork Small", "price": 18000, "spicyLevel": 0 } + ] + }, + { + "id": "rest-027", "name": "์ข…๋กœ ์ฒญ๊ตญ์žฅ ์ง‘", "nameEn": "Jongno Cheonggukjang", + "lat": 37.5695, "lng": 126.9810, "address": "์„œ์šธ ์ข…๋กœ๊ตฌ ๋ˆํ™”๋ฌธ๋กœ 80", "category": "restaurant", + "rating": 4.2, "nationalityTags": ["korean","vegan"], + "pricePerPerson": 10000, "cuisineType": "ํ•œ์‹ ์ฐŒ๊ฐœ", "cuisineTypeEn": "Korean Fermented Stew", + "isHalal": false, "isVegetarianFriendly": true, "hasEnglishMenu": false, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "์ฒญ๊ตญ์žฅ", "nameEn": "Fermented Soybean Stew", "price": 10000, "spicyLevel": 1, "isVegetarian": true }, + { "name": "๋œ์žฅ์ฐŒ๊ฐœ", "nameEn": "Doenjang Jjigae", "price": 9000, "spicyLevel": 1, "isVegetarian": true } + ] + }, + { + "id": "rest-028", "name": "ํ™๋Œ€ ์ค‘๋™ ํ• ๋ž„ ์‹๋‹น", "nameEn": "Hongdae Middle Eastern Halal", + "lat": 37.5558, "lng": 126.9250, "address": "์„œ์šธ ๋งˆํฌ๊ตฌ ์™€์šฐ์‚ฐ๋กœ 67", "category": "restaurant", + "rating": 4.4, "nationalityTags": ["halal","indian","western"], + "pricePerPerson": 15000, "cuisineType": "์ค‘๋™/์•„๋ž ์š”๋ฆฌ", "cuisineTypeEn": "Middle Eastern", + "isHalal": true, "isVegetarianFriendly": true, "hasEnglishMenu": true, "hasChineseMenu": false, "hasJapaneseMenu": false, + "menuItems": [ + { "name": "ํ›”๋ฌด์Šค & ํ”ผํƒ€", "nameEn": "Hummus & Pita", "price": 10000, "spicyLevel": 0, "isVegetarian": true, "isHalal": true }, + { "name": "๋žจ ์ฝ”ํ”„ํƒ€", "nameEn": "Lamb Kofta", "price": 17000, "spicyLevel": 1, "isHalal": true }, + { "name": "ํŒ”๋ผํŽ  ๋ฐ•์Šค", "nameEn": "Falafel Box", "price": 13000, "spicyLevel": 0, "isVegetarian": true, "isHalal": true } + ] + } +] diff --git a/map-app/src/hooks/useAllLocations.ts b/map-app/src/hooks/useAllLocations.ts new file mode 100644 index 000000000..67211d0ee --- /dev/null +++ b/map-app/src/hooks/useAllLocations.ts @@ -0,0 +1,30 @@ +import { useMemo } from 'react' +import type { AnyLocation } from '@/types' + +import exchangeData from '@/data/exchange.json' +import fuelData from '@/data/fuel.json' +import restaurantsData from '@/data/restaurants.json' +import cafesData from '@/data/cafes.json' +import convenienceData from '@/data/convenience.json' +import jjimjilbangData from '@/data/jjimjilbang.json' +import karaokeData from '@/data/karaoke.json' +import marketsData from '@/data/markets.json' +import attractionsData from '@/data/attractions.json' +import extrasData from '@/data/extras.json' + +export function useAllLocations(): AnyLocation[] { + return useMemo(() => { + return [ + ...exchangeData, + ...fuelData, + ...restaurantsData, + ...cafesData, + ...convenienceData, + ...jjimjilbangData, + ...karaokeData, + ...marketsData, + ...attractionsData, + ...extrasData, + ] as AnyLocation[] + }, []) +} diff --git a/map-app/src/hooks/useAllPriceRanks.ts b/map-app/src/hooks/useAllPriceRanks.ts new file mode 100644 index 000000000..fb25f7b71 --- /dev/null +++ b/map-app/src/hooks/useAllPriceRanks.ts @@ -0,0 +1,31 @@ +import { useMemo } from 'react' +import { useAllLocations } from './useAllLocations' +import { getLocationPrice } from './useFilteredMarkers' +import type { PriceCategory } from '@/types' + +const ALL_CATEGORIES: PriceCategory[] = [ + 'exchange', 'fuel', 'restaurant', 'cafe', 'convenience', + 'jjimjilbang', 'karaoke', 'market', 'attraction', 'extra', +] + +export function useAllPriceRanks(): Record { + const all = useAllLocations() + + return useMemo(() => { + const merged: Record = {} + + ALL_CATEGORIES.forEach((cat) => { + const filtered = all + .filter((loc) => loc.category === cat) + .map((loc) => ({ id: loc.id, price: getLocationPrice(loc) })) + .filter((item) => item.price > 0) + .sort((a, b) => a.price - b.price) + + filtered.slice(0, 3).forEach((item, i) => { + merged[item.id] = (i + 1) as 1 | 2 | 3 + }) + }) + + return merged + }, [all]) +} diff --git a/map-app/src/hooks/useFilteredMarkers.ts b/map-app/src/hooks/useFilteredMarkers.ts new file mode 100644 index 000000000..0113f3ef0 --- /dev/null +++ b/map-app/src/hooks/useFilteredMarkers.ts @@ -0,0 +1,107 @@ +import { useMemo } from 'react' +import { useFilterStore } from '@/store' +import { useAllLocations } from './useAllLocations' +import type { AnyLocation, PriceCategory, Restaurant } from '@/types' + +function getLocationPrice(loc: AnyLocation): number { + switch (loc.category) { + case 'exchange': return 0 // show all exchange shops + case 'fuel': { + const prices = Object.values(loc.prices).filter(Boolean) as number[] + return prices.length ? Math.min(...prices) : 0 + } + case 'restaurant': return loc.pricePerPerson + case 'cafe': return loc.americanoPrice + case 'convenience': return loc.avgItemPrice + case 'jjimjilbang': return loc.entryFee + case 'karaoke': { + const rates = loc.rates.map((r) => r.pricePerHour) + return Math.min(...rates) + } + case 'market': { + const prices = loc.popularItems.map((i) => i.price) + return Math.min(...prices) + } + case 'attraction': { + const paid = loc.tickets.filter((t) => t.price > 0) + return paid.length ? Math.min(...paid.map((t) => t.price)) : 0 + } + case 'extra': return loc.price + default: return 0 + } +} + +const ALL_CATEGORIES: PriceCategory[] = [ + 'exchange', 'fuel', 'restaurant', 'cafe', 'convenience', + 'jjimjilbang', 'karaoke', 'market', 'attraction', 'extra', +] + +function getCheapestIdsByCategory(all: AnyLocation[]): Set { + const cheapestIds = new Set() + ALL_CATEGORIES.forEach((cat) => { + const catLocs = all + .filter((loc) => loc.category === cat) + .map((loc) => ({ id: loc.id, price: getLocationPrice(loc) })) + if (catLocs.length === 0) return + // For exchange (price=0), just take first; otherwise sort by price + const sorted = catLocs.filter((l) => l.price > 0).sort((a, b) => a.price - b.price) + if (sorted.length > 0) { + cheapestIds.add(sorted[0].id) + } else { + // all free (e.g. free attractions) โ€” take first + cheapestIds.add(catLocs[0].id) + } + }) + return cheapestIds +} + +export function useFilteredMarkers(): AnyLocation[] { + const { activeCategories, nationality, priceRange, searchQuery, showCheapestOnly } = useFilterStore() + const all = useAllLocations() + + return useMemo(() => { + const cheapestIds = showCheapestOnly ? getCheapestIdsByCategory(all) : null + + const q = searchQuery.trim().toLowerCase() + + return all.filter((loc) => { + // Category filter + if (activeCategories.length > 0 && !activeCategories.includes(loc.category)) { + return false + } + + // Nationality filter (only for restaurants) + if (nationality !== 'all' && loc.category === 'restaurant') { + const rest = loc as Restaurant + if (!rest.nationalityTags.includes(nationality) && !rest.nationalityTags.includes('all')) { + return false + } + } + + // Price range filter + const price = getLocationPrice(loc) + if (price > 0 && (price < priceRange[0] || price > priceRange[1])) { + return false + } + + // Search filter + if (q) { + const name = loc.name.toLowerCase() + const nameEn = loc.nameEn.toLowerCase() + const address = loc.address.toLowerCase() + if (!name.includes(q) && !nameEn.includes(q) && !address.includes(q)) { + return false + } + } + + // Cheapest only filter + if (cheapestIds && !cheapestIds.has(loc.id)) { + return false + } + + return true + }) + }, [all, activeCategories, nationality, priceRange, searchQuery, showCheapestOnly]) +} + +export { getLocationPrice } diff --git a/map-app/src/hooks/usePriceRanks.ts b/map-app/src/hooks/usePriceRanks.ts new file mode 100644 index 000000000..93e1c158c --- /dev/null +++ b/map-app/src/hooks/usePriceRanks.ts @@ -0,0 +1,21 @@ +import { useMemo } from 'react' +import { useAllLocations } from './useAllLocations' +import { getLocationPrice } from './useFilteredMarkers' +import type { PriceRank, PriceCategory } from '@/types' + +export function usePriceRanks(category: PriceCategory): PriceRank[] { + const all = useAllLocations() + + return useMemo(() => { + const filtered = all + .filter((loc) => loc.category === category) + .map((loc) => ({ id: loc.id, price: getLocationPrice(loc) })) + .filter((item) => item.price > 0) + .sort((a, b) => a.price - b.price) + + return filtered.slice(0, 3).map((item, i) => ({ + locationId: item.id, + rank: (i + 1) as 1 | 2 | 3, + })) + }, [all, category]) +} diff --git a/map-app/src/index.css b/map-app/src/index.css new file mode 100644 index 000000000..52bb64ef3 --- /dev/null +++ b/map-app/src/index.css @@ -0,0 +1,74 @@ +/* Seed Design System */ +@import '@seed-design/css/all.css'; + +/* Leaflet CSS */ +@import 'leaflet/dist/leaflet.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + box-sizing: border-box; +} + +html, body, #root { + height: 100%; + margin: 0; + padding: 0; + font-family: 'Noto Sans KR', system-ui, sans-serif; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: var(--seed-color-bg-layer-basement); +} +::-webkit-scrollbar-thumb { + background: var(--seed-color-stroke-neutral-muted); + border-radius: 3px; +} + +/* Hide scrollbar for filter bar */ +.scrollbar-hide::-webkit-scrollbar { + display: none; +} +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* Category pin animations */ +.marker-pin { + transition: transform 0.15s ease; +} +.marker-pin:hover { + transform: scale(1.15); +} + +/* Map container */ +.leaflet-container { + width: 100%; + height: 100%; + z-index: 0; + font-family: 'Noto Sans KR', system-ui, sans-serif; +} + +/* Tooltip override */ +.leaflet-tooltip { + padding: 0 !important; + border: none !important; + background: transparent !important; + box-shadow: none !important; +} +.leaflet-tooltip::before { + display: none !important; +} + +/* Seed slider accent */ +input[type="range"].seed-range { + accent-color: var(--seed-color-bg-brand-solid); +} diff --git a/map-app/src/main.tsx b/map-app/src/main.tsx new file mode 100644 index 000000000..db032b748 --- /dev/null +++ b/map-app/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/map-app/src/store/favoritesStore.ts b/map-app/src/store/favoritesStore.ts new file mode 100644 index 000000000..95471c4d3 --- /dev/null +++ b/map-app/src/store/favoritesStore.ts @@ -0,0 +1,44 @@ +import { create } from 'zustand' + +const STORAGE_KEY = 'lowestprice_favorites' + +function loadFavorites(): string[] { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) return JSON.parse(raw) as string[] + } catch {} + return [] +} + +function saveFavorites(ids: string[]) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)) + } catch {} +} + +interface FavoritesState { + favoriteIds: string[] + isFavorite: (id: string) => boolean + toggle: (id: string) => void + clear: () => void +} + +export const useFavoritesStore = create((set, get) => ({ + favoriteIds: loadFavorites(), + + isFavorite: (id) => get().favoriteIds.includes(id), + + toggle: (id) => + set((state) => { + const next = state.favoriteIds.includes(id) + ? state.favoriteIds.filter((f) => f !== id) + : [...state.favoriteIds, id] + saveFavorites(next) + return { favoriteIds: next } + }), + + clear: () => { + saveFavorites([]) + set({ favoriteIds: [] }) + }, +})) diff --git a/map-app/src/store/filterStore.ts b/map-app/src/store/filterStore.ts new file mode 100644 index 000000000..20c08c340 --- /dev/null +++ b/map-app/src/store/filterStore.ts @@ -0,0 +1,89 @@ +import { create } from 'zustand' +import type { PriceCategory, Nationality } from '@/types' + +const STORAGE_KEY = 'lowestprice_filters' + +interface PersistedFilters { + activeCategories: PriceCategory[] + nationality: Nationality + priceRange: [number, number] +} + +function loadFilters(): PersistedFilters { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) return JSON.parse(raw) as PersistedFilters + } catch {} + return { activeCategories: [], nationality: 'all', priceRange: [0, 200000] } +} + +function saveFilters(state: PersistedFilters) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) + } catch {} +} + +interface FilterState extends PersistedFilters { + searchQuery: string + showCheapestOnly: boolean + toggleCategory: (cat: PriceCategory) => void + setAllCategories: (cats: PriceCategory[]) => void + clearCategories: () => void + setNationality: (nat: Nationality) => void + setPriceRange: (range: [number, number]) => void + setSearchQuery: (q: string) => void + toggleCheapestOnly: () => void + resetFilters: () => void +} + +const initial = loadFilters() + +export const useFilterStore = create((set) => ({ + ...initial, + searchQuery: '', + showCheapestOnly: false, + + toggleCategory: (cat) => + set((state) => { + const next = state.activeCategories.includes(cat) + ? state.activeCategories.filter((c) => c !== cat) + : [...state.activeCategories, cat] + saveFilters({ activeCategories: next, nationality: state.nationality, priceRange: state.priceRange }) + return { activeCategories: next } + }), + + setAllCategories: (cats) => + set((state) => { + saveFilters({ activeCategories: cats, nationality: state.nationality, priceRange: state.priceRange }) + return { activeCategories: cats } + }), + + clearCategories: () => + set((state) => { + saveFilters({ activeCategories: [], nationality: state.nationality, priceRange: state.priceRange }) + return { activeCategories: [] } + }), + + setNationality: (nat) => + set((state) => { + saveFilters({ activeCategories: state.activeCategories, nationality: nat, priceRange: state.priceRange }) + return { nationality: nat } + }), + + setPriceRange: (range) => + set((state) => { + saveFilters({ activeCategories: state.activeCategories, nationality: state.nationality, priceRange: range }) + return { priceRange: range } + }), + + setSearchQuery: (q) => set({ searchQuery: q }), + + toggleCheapestOnly: () => + set((state) => ({ showCheapestOnly: !state.showCheapestOnly })), + + resetFilters: () => { + const defaults: PersistedFilters = { activeCategories: [], nationality: 'all', priceRange: [0, 200000] } + saveFilters(defaults) + return set({ ...defaults, searchQuery: '', showCheapestOnly: false }) + }, +})) diff --git a/map-app/src/store/index.ts b/map-app/src/store/index.ts new file mode 100644 index 000000000..f372c74fb --- /dev/null +++ b/map-app/src/store/index.ts @@ -0,0 +1,3 @@ +export { useFilterStore } from './filterStore' +export { useUiStore } from './uiStore' +export { useFavoritesStore } from './favoritesStore' diff --git a/map-app/src/store/uiStore.ts b/map-app/src/store/uiStore.ts new file mode 100644 index 000000000..b640dc2ec --- /dev/null +++ b/map-app/src/store/uiStore.ts @@ -0,0 +1,44 @@ +import { create } from 'zustand' +import type { AnyLocation, Language } from '@/types' + +const LANG_KEY = 'lowestprice_lang' + +function loadLanguage(): Language { + try { + const v = localStorage.getItem(LANG_KEY) + if (v === 'ko' || v === 'en') return v + } catch {} + return 'ko' +} + +interface UiState { + selectedPin: AnyLocation | null + isPanelOpen: boolean + language: Language + isMobile: boolean + setSelectedPin: (pin: AnyLocation | null) => void + closePanel: () => void + toggleLanguage: () => void + setIsMobile: (v: boolean) => void +} + +export const useUiStore = create((set) => ({ + selectedPin: null, + isPanelOpen: false, + language: loadLanguage(), + isMobile: window.innerWidth < 768, + + setSelectedPin: (pin) => + set({ selectedPin: pin, isPanelOpen: pin !== null }), + + closePanel: () => set({ selectedPin: null, isPanelOpen: false }), + + toggleLanguage: () => + set((state) => { + const next = state.language === 'ko' ? 'en' : 'ko' + try { localStorage.setItem(LANG_KEY, next) } catch {} + return { language: next } + }), + + setIsMobile: (v) => set({ isMobile: v }), +})) diff --git a/map-app/src/types/common.ts b/map-app/src/types/common.ts new file mode 100644 index 000000000..90f99af89 --- /dev/null +++ b/map-app/src/types/common.ts @@ -0,0 +1,152 @@ +export type PriceCategory = + | 'exchange' + | 'fuel' + | 'restaurant' + | 'cafe' + | 'convenience' + | 'jjimjilbang' + | 'karaoke' + | 'market' + | 'attraction' + | 'extra' + +export type Nationality = + | 'all' + | 'korean' + | 'japanese' + | 'chinese' + | 'western' + | 'indian' + | 'halal' + | 'vegan' + +export type Language = 'ko' | 'en' + +export interface BaseLocation { + id: string + name: string + nameEn: string + lat: number + lng: number + address: string + addressEn?: string + category: PriceCategory + rating?: number + phone?: string + website?: string + openHours?: string +} + +export interface CategoryMeta { + id: PriceCategory + label: string + labelEn: string + emoji: string + color: string + bgColor: string + borderColor: string +} + +export const CATEGORY_META: Record = { + exchange: { + id: 'exchange', + label: 'ํ™˜์ „์†Œ', + labelEn: 'Exchange', + emoji: '๐Ÿ’ฑ', + color: '#D97706', + bgColor: 'bg-yellow-100', + borderColor: 'border-yellow-400', + }, + fuel: { + id: 'fuel', + label: '์ฃผ์œ ์†Œ', + labelEn: 'Fuel', + emoji: 'โ›ฝ', + color: '#16A34A', + bgColor: 'bg-green-100', + borderColor: 'border-green-400', + }, + restaurant: { + id: 'restaurant', + label: '์‹๋‹น', + labelEn: 'Restaurant', + emoji: '๐Ÿœ', + color: '#DC2626', + bgColor: 'bg-red-100', + borderColor: 'border-red-400', + }, + cafe: { + id: 'cafe', + label: '์นดํŽ˜', + labelEn: 'Cafe', + emoji: 'โ˜•', + color: '#92400E', + bgColor: 'bg-amber-100', + borderColor: 'border-amber-700', + }, + convenience: { + id: 'convenience', + label: 'ํŽธ์˜์ ', + labelEn: 'Convenience', + emoji: '๐Ÿช', + color: '#2563EB', + bgColor: 'bg-blue-100', + borderColor: 'border-blue-400', + }, + jjimjilbang: { + id: 'jjimjilbang', + label: '์ฐœ์งˆ๋ฐฉ', + labelEn: 'Sauna', + emoji: '๐Ÿ›', + color: '#7C3AED', + bgColor: 'bg-purple-100', + borderColor: 'border-purple-400', + }, + karaoke: { + id: 'karaoke', + label: '๋…ธ๋ž˜๋ฐฉ', + labelEn: 'Karaoke', + emoji: '๐ŸŽค', + color: '#DB2777', + bgColor: 'bg-pink-100', + borderColor: 'border-pink-400', + }, + market: { + id: 'market', + label: '์ „ํ†ต์‹œ์žฅ', + labelEn: 'Market', + emoji: '๐Ÿฎ', + color: '#EA580C', + bgColor: 'bg-orange-100', + borderColor: 'border-orange-400', + }, + attraction: { + id: 'attraction', + label: '๊ด€๊ด‘๋ช…์†Œ', + labelEn: 'Attraction', + emoji: '๐Ÿ›๏ธ', + color: '#0891B2', + bgColor: 'bg-cyan-100', + borderColor: 'border-cyan-400', + }, + extra: { + id: 'extra', + label: '๊ธฐํƒ€', + labelEn: 'More', + emoji: 'โœจ', + color: '#4F46E5', + bgColor: 'bg-indigo-100', + borderColor: 'border-indigo-400', + }, +} + +export const NATIONALITY_META: Record = { + all: { label: '์ „์ฒด', labelEn: 'All', flag: '๐ŸŒ' }, + korean: { label: 'ํ•œ๊ตญ์ธ', labelEn: 'Korean', flag: '๐Ÿ‡ฐ๐Ÿ‡ท' }, + japanese: { label: '์ผ๋ณธ์ธ', labelEn: 'Japanese', flag: '๐Ÿ‡ฏ๐Ÿ‡ต' }, + chinese: { label: '์ค‘๊ตญ์ธ', labelEn: 'Chinese', flag: '๐Ÿ‡จ๐Ÿ‡ณ' }, + western: { label: '์„œ์–‘์ธ', labelEn: 'Western', flag: '๐Ÿ‡บ๐Ÿ‡ธ' }, + indian: { label: '์ธ๋„์ธ', labelEn: 'Indian', flag: '๐Ÿ‡ฎ๐Ÿ‡ณ' }, + halal: { label: '๋ฌด์Šฌ๋ฆผ', labelEn: 'Muslim/Halal', flag: '๐Ÿ•Œ' }, + vegan: { label: '๋น„๊ฑด', labelEn: 'Vegan', flag: '๐ŸŒฑ' }, +} diff --git a/map-app/src/types/index.ts b/map-app/src/types/index.ts new file mode 100644 index 000000000..4a55416c4 --- /dev/null +++ b/map-app/src/types/index.ts @@ -0,0 +1,185 @@ +export * from './common' + +import type { BaseLocation, Nationality } from './common' + +// โ”€โ”€โ”€ Money Exchange โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ExchangeRate { + currency: 'USD' | 'JPY' | 'CNY' | 'EUR' | 'GBP' | 'THB' + buyRate: number // KRW you get per 1 foreign unit + sellRate: number // KRW you pay per 1 foreign unit +} + +export interface ExchangeLocation extends BaseLocation { + category: 'exchange' + rates: ExchangeRate[] + noCommission: boolean + minAmount?: number +} + +// โ”€โ”€โ”€ Fuel Station โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface FuelPrices { + gasoline?: number // KRW per liter (ํœ˜๋ฐœ์œ ) + diesel?: number // KRW per liter (๊ฒฝ์œ ) + lpg?: number // KRW per liter (LPG) + electric?: number // KRW per kWh (์ „๊ธฐ) + hydrogen?: number // KRW per kg (์ˆ˜์†Œ) +} + +export interface FuelStation extends BaseLocation { + category: 'fuel' + brand: string + prices: FuelPrices + selfService: boolean + carWash: boolean +} + +// โ”€โ”€โ”€ Restaurant โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface MenuItem { + name: string + nameEn: string + price: number + isVegetarian?: boolean + isHalal?: boolean + spicyLevel?: 0 | 1 | 2 | 3 +} + +export interface Restaurant extends BaseLocation { + category: 'restaurant' + nationalityTags: Nationality[] + menuItems: MenuItem[] + pricePerPerson: number + cuisineType: string + cuisineTypeEn: string + isHalal: boolean + isVegetarianFriendly: boolean + hasEnglishMenu: boolean + hasChineseMenu: boolean + hasJapaneseMenu: boolean +} + +// โ”€โ”€โ”€ Cafe โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface CafeItem { + name: string + nameEn: string + price: number + isBestValue?: boolean +} + +export interface Cafe extends BaseLocation { + category: 'cafe' + brand: string + items: CafeItem[] + americanoPrice: number + hasWifi: boolean + hasOutdoorSeating: boolean +} + +// โ”€โ”€โ”€ Convenience Store โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface ConvenienceItem { + name: string + nameEn: string + price: number + category: string +} + +export interface ConvenienceStore extends BaseLocation { + category: 'convenience' + brand: 'CU' | 'GS25' | '7-Eleven' | 'emart24' | 'Ministop' + popularItems: ConvenienceItem[] + avgItemPrice: number + open24Hours: boolean + hasAtm: boolean +} + +// โ”€โ”€โ”€ Jjimjilbang โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface Jjimjilbang extends BaseLocation { + category: 'jjimjilbang' + entryFee: number + overnightFee?: number + towelIncluded: boolean + amenities: string[] + separateGenders: boolean +} + +// โ”€โ”€โ”€ Karaoke โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface KaraokeRate { + roomSize: 'small' | 'medium' | 'large' + pricePerHour: number + maxPeople: number +} + +export interface Karaoke extends BaseLocation { + category: 'karaoke' + rates: KaraokeRate[] + hasForeignSongs: boolean + hasTambourine: boolean + discountHours?: string +} + +// โ”€โ”€โ”€ Traditional Market โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface MarketItem { + name: string + nameEn: string + price: number + unit: string +} + +export interface TraditionalMarket extends BaseLocation { + category: 'market' + marketType: string + marketTypeEn: string + popularItems: MarketItem[] + operatingDays: string + closedDay?: string +} + +// โ”€โ”€โ”€ Tourist Attraction โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export interface AttractionTicket { + type: string + typeEn: string + price: number + ageGroup?: string +} + +export interface TouristAttraction extends BaseLocation { + category: 'attraction' + attractionType: string + tickets: AttractionTicket[] + freeEntry: boolean + discountInfo?: string + discountInfoEn?: string +} + +// โ”€โ”€โ”€ Extra (Street Food, PC Bang, Accommodation) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export type ExtraType = 'streetfood' | 'pcbang' | 'accommodation' + +export interface Extra extends BaseLocation { + category: 'extra' + extraType: ExtraType + extraTypeLabel: string + extraTypeLabelEn: string + price: number + priceUnit: string + priceUnitEn: string + description: string + descriptionEn: string + emoji: string +} + +// โ”€โ”€โ”€ Union type for all pins โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +export type AnyLocation = + | ExchangeLocation + | FuelStation + | Restaurant + | Cafe + | ConvenienceStore + | Jjimjilbang + | Karaoke + | TraditionalMarket + | TouristAttraction + | Extra + +export interface PriceRank { + locationId: string + rank: 1 | 2 | 3 +} diff --git a/map-app/src/vite-env.d.ts b/map-app/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/map-app/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/map-app/tailwind.config.ts b/map-app/tailwind.config.ts new file mode 100644 index 000000000..503e14ee4 --- /dev/null +++ b/map-app/tailwind.config.ts @@ -0,0 +1,17 @@ +import type { Config } from 'tailwindcss' +import seedPlugin from '@seed-design/tailwind3-plugin' + +export default { + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: { + fontFamily: { + sans: ['Noto Sans KR', 'system-ui', 'sans-serif'], + }, + }, + }, + plugins: [seedPlugin], +} satisfies Config diff --git a/map-app/tsconfig.json b/map-app/tsconfig.json new file mode 100644 index 000000000..335e6fb21 --- /dev/null +++ b/map-app/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/map-app/tsconfig.node.json b/map-app/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/map-app/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/map-app/vite.config.ts b/map-app/vite.config.ts new file mode 100644 index 000000000..0d51f19e3 --- /dev/null +++ b/map-app/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +})