Skip to content

Claude/lowest price map app aj4 lr#1479

Open
dmdmkim82 wants to merge 4 commits intoNVIDIA:mainfrom
dmdmkim82:claude/lowest-price-map-app-aj4LR
Open

Claude/lowest price map app aj4 lr#1479
dmdmkim82 wants to merge 4 commits intoNVIDIA:mainfrom
dmdmkim82:claude/lowest-price-map-app-aj4LR

Conversation

@dmdmkim82
Copy link
Copy Markdown

@dmdmkim82 dmdmkim82 commented Apr 4, 2026

Summary

Related Issue

Changes

Type of Change

  • Code change for a new feature, bug fix, or refactor.
  • Code change with doc updates.
  • Doc only. Prose changes without code sample modifications.
  • Doc only. Includes code sample changes.

Testing

  • npx prek run --all-files passes (or equivalently make check).
  • npm test passes.
  • make docs builds without warnings. (for doc-only changes)

Checklist

General

Code Changes

  • Formatters applied — npx prek run --all-files auto-fixes formatting (or make format for targeted runs).
  • Tests added or updated for new or changed behavior.
  • No secrets, API keys, or credentials committed.
  • Doc pages updated for any user-facing behavior changes (new commands, changed defaults, new features, bug fixes that contradict existing docs).

Doc Changes

  • Follows the style guide. Try running the update-docs agent skill to draft changes while complying with the style guide. For example, prompt your agent with "/update-docs catch up the docs for the new changes I made in this PR."
  • New pages include SPDX license header and frontmatter, if creating a new page.
  • Cross-references and links verified.

Signed-off-by: Your Name your-email@example.com

Summary by CodeRabbit

Release Notes

  • New Features
    • Launched interactive map application for discovering lowest-priced locations across restaurants, cafes, fuel stations, attractions, and more
    • Added advanced filtering by category, price range, and cuisine type
    • Introduced nationality-specific restaurant filtering for dietary preferences
    • Added search and favorites functionality with persistent storage
    • Implemented location detail panels with comprehensive pricing and amenity information
    • Enabled multilingual support (Korean/English) and geolocation detection

claude added 4 commits April 4, 2026 06:45
React 18 + TypeScript + Vite + Leaflet.js SPA centered on Seoul, Korea
featuring 10 price categories, nationality-based food filtering, and
interactive map pins with real-time cheapest-option ranking badges.

Categories:
- 💱 사설환전소: USD/JPY/CNY/EUR exchange rates (8 locations)
- ⛽ 주유소: gasoline/diesel/LPG/electric/hydrogen prices (12 stations)
- 🍜 식당: Korean food filtered by nationality taste profile (14 restaurants)
- ☕ 카페: Americano/latte price comparison (8 cafes)
- 🏪 편의점: CU/GS25/7-Eleven/emart24 popular items (5 stores)
- 🛁 찜질방: entry fee + overnight rates (5 locations)
- 🎤 노래방: hourly rates by room size (5 venues)
- 🏮 전통시장: Gwangjang/Namdaemun/Tongin market prices (4 markets)
- 🏛️ 관광명소: ticket prices + free entry indicators (6 attractions)
- ✨ 기타: street food, PC방, guesthouses (8 locations)

Nationality filter: Korean, Japanese, Chinese, Western, Indian, Muslim/Halal, Vegan
UI: dark navy header, category-color pins, crown badge for cheapest,
    desktop sidebar + mobile bottom sheet, KO/EN language toggle,
    price range slider filter

https://claude.ai/code/session_015E1BUVprayzdEXgm6UhDXS
Data expansion (75 → 151 locations):
- exchange: 8→15, cafes: 8→18, fuel: 12→22, restaurants: 14→28
- convenience: 5→12, jjimjilbang: 5→10, karaoke: 5→10
- markets: 4→8, attractions: 6→12, extras: 8→16

New features:
- Search bar in TopBar (name/address, Escape to clear)
- My location button (geolocation flyTo via Leaflet Control)
- Map legend (collapsible bottom-left, category colors/emoji)
- EmptyState overlay when 0 markers match filters
- Selected pin highlight (scale + white border ring)
- showCheapestOnly filter (1 cheapest per category)
- Favorites system (localStorage, heart button in DetailPanel)
- Kakao Maps + Google Maps deeplinks in DetailPanel
- Filter/language/favorites persistence via localStorage
- Dynamic sidebar stats and favorites section

Bugfixes:
- closePanl → closePanel typo
- Hardcoded stat counts → dynamic countByCategory

https://claude.ai/code/session_015E1BUVprayzdEXgm6UhDXS
Complete rewrite documenting the lowest price map application:
- feature overview (search, filters, favorites, geolocation, legend)
- data table (151 locations across 10 categories)
- tech stack and project structure
- local setup instructions

https://claude.ai/code/session_015E1BUVprayzdEXgm6UhDXS
- Install @seed-design/css, @seed-design/design-token, @seed-design/tailwind3-plugin
- Add data-seed attributes to html element for token activation
- Import seed CSS tokens in index.css
- Replace hardcoded colors with semantic seed tokens throughout:
  - TopBar: dark navy → white bg with carrot brand accent (fg-brand, bg-brand-solid)
  - Sidebar: slate colors → fg-neutral-*, bg-bg-layer-*, stroke-neutral-*
  - CategoryFilterBar, NationalityDropdown: seed border/text/bg tokens
  - PriceRangeSlider: blue accent → brand-solid (carrot)
  - DetailPanel: seed surface/stroke/fg tokens; map buttons use brand/informative colors
  - App layout: bg-bg-layer-basement base background

https://claude.ai/code/session_015E1BUVprayzdEXgm6UhDXS
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

The PR replaces the entire README with Korean product documentation and introduces a complete React/TypeScript web application for browsing and filtering price information across 10 location categories. The app features interactive mapping with Leaflet, Zustand-based state management, Tailwind styling, and pre-loaded datasets of 151+ locations across Korea with category-specific details and pricing.

Changes

Cohort / File(s) Summary
Documentation
README.md
Replaced NVIDIA NemoClaw reference stack documentation with Korean "Lowest Price Map" product README, including technology stack, features, dataset overview, and local/build instructions.
Build & Development Configuration
map-app/index.html, map-app/package.json, map-app/vite.config.ts, map-app/tsconfig.json, map-app/tsconfig.node.json, map-app/tailwind.config.ts, map-app/postcss.config.cjs
Added Vite-based React/TypeScript project setup with Tailwind CSS, PostCSS, React plugin configuration, and module resolution aliases.
Global Styles & Entrypoint
map-app/src/index.css, map-app/src/main.tsx, map-app/src/vite-env.d.ts
Configured global styling with Tailwind layers, Leaflet/Seed Design System imports, scrollbar customization, and React root initialization.
Type Definitions & Metadata
map-app/src/types/index.ts, map-app/src/types/common.ts
Defined comprehensive TypeScript interfaces for 10 location categories (exchange, fuel, restaurant, cafe, convenience, jjimjilbang, karaoke, market, attraction, extra) with discriminated union AnyLocation type and metadata constants.
State Management Stores
map-app/src/store/filterStore.ts, map-app/src/store/uiStore.ts, map-app/src/store/favoritesStore.ts, map-app/src/store/index.ts
Implemented three Zustand stores: filter state (categories, price range, nationality, search, cheapest-only toggle) with localStorage persistence, UI state (language, panel, mobile responsiveness), and favorites management with localStorage backup.
Data Processing Hooks
map-app/src/hooks/useAllLocations.ts, map-app/src/hooks/useFilteredMarkers.ts, map-app/src/hooks/useAllPriceRanks.ts, map-app/src/hooks/usePriceRanks.ts
Created memoized hooks for aggregating locations, filtering by category/nationality/price/search, computing price-based rankings (top 3 cheapest per category), and deriving display prices with category-specific logic.
Layout & Navigation Components
map-app/src/components/layout/TopBar.tsx, map-app/src/components/layout/Sidebar.tsx
Built responsive header with logo, bilingual title, search input, nationality dropdown, and language toggle; left sidebar with category filters, price range slider, cheapest-only toggle, favorites section, and statistics panel.
Filter & Common Components
map-app/src/components/filters/CategoryFilterBar.tsx, map-app/src/components/filters/NationalityDropdown.tsx, map-app/src/components/filters/PriceRangeSlider.tsx, map-app/src/components/common/CategoryIcon.tsx, map-app/src/components/common/PriceBadge.tsx
Implemented horizontally scrollable category buttons, nationality filter dropdown with flags, dual-range price slider with gradient styling, category emoji icons, and formatted price badges with optional highlight styling.
Map & Marker Components
map-app/src/components/map/MapContainer.tsx, map-app/src/components/map/MarkerLayer.tsx, map-app/src/components/map/CategoryPin.tsx, map-app/src/components/map/MyLocationButton.tsx, map-app/src/components/map/MapLegend.tsx, map-app/src/components/map/EmptyState.tsx
Created Leaflet-integrated map with CARTO tiles, custom colored category pins with optional rank badges, geolocation button with zoom animation, collapsible legend control, filtered marker rendering with price tooltips, and empty-state overlay.
Detail Panel Components
map-app/src/components/panels/DetailPanel.tsx, map-app/src/components/panels/RestaurantDetail.tsx, map-app/src/components/panels/ExchangeDetail.tsx, map-app/src/components/panels/FuelDetail.tsx, map-app/src/components/panels/GenericDetail.tsx
Implemented responsive detail panels (desktop sidebar + mobile bottom sheet) displaying category-specific content (menu items with spice levels, exchange rates by currency, fuel prices by type, attraction tickets), favorites toggle, and external map links (Kakao/Google Maps).
Static Location Data
map-app/src/data/restaurants.json, map-app/src/data/cafes.json, map-app/src/data/convenience.json, map-app/src/data/exchange.json, map-app/src/data/fuel.json, map-app/src/data/jjimjilbang.json, map-app/src/data/karaoke.json, map-app/src/data/markets.json, map-app/src/data/attractions.json, map-app/src/data/extras.json
Added 151 pre-populated location records across 10 categories with bilingual names, coordinates, ratings, category-specific pricing/attributes (menus, exchange rates, fuel types, ticket prices), and amenities.
Root Component
map-app/src/App.tsx
Assembled application layout composing TopBar, Sidebar, CategoryFilterBar, MapContainer, and responsive detail panels (desktop/mobile); registered window resize handler for mobile responsiveness detection.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as UI Components
    participant Store as Zustand Stores<br/>(Filter/UI/Favorites)
    participant Hooks as Data Hooks<br/>(useFilteredMarkers)
    participant Map as Leaflet Map<br/>(Markers/Pins)

    User->>UI: Select category filter
    UI->>Store: toggleCategory(cat)
    activate Store
    Store->>Store: Update activeCategories
    Store->>Store: Persist to localStorage
    deactivate Store
    
    Store-->>Hooks: Trigger re-compute
    activate Hooks
    Hooks->>Hooks: Filter by category
    Hooks->>Hooks: Apply nationality, price, search
    Hooks->>Hooks: Apply cheapest-only constraint
    Hooks-->>UI: Return filtered locations
    deactivate Hooks
    
    UI->>UI: Re-render marker list
    UI->>Map: Update markers with filtered data
    Map->>Map: Render colored pins by category
    Map->>Map: Apply rank badges if top 3
    Map-->>User: Display updated map
    
    User->>Map: Click marker on map
    Map->>Store: setSelectedPin(location)
    activate Store
    Store->>Store: Update selectedPin & isPanelOpen
    deactivate Store
    
    Store-->>UI: Notify detail panel
    UI->>UI: Render location details<br/>(category-specific content)
    UI-->>User: Display detail panel<br/>(sidebar or bottom sheet)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🗺️ A map unfolds with pins so bright,
Where prices dance in colors right—
Filters spin and markers gleam,
The lowest deals fulfill the dream! 🎯✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Claude/lowest price map app aj4 lr' is vague and uninformative. It includes a branch-like prefix and an unclear suffix that do not convey the main purpose or scope of the substantial changes (a complete app implementation with 151 locations, multiple features, and design system integration). Use a clear, descriptive title that summarizes the primary change, e.g., 'feat: add Lowest Price Map interactive web app with location filters and geolocation' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch claude/lowest-price-map-app-aj4LR

Comment @coderabbitai help to get the list of available commands and usage tips.

@dmdmkim82
Copy link
Copy Markdown
Author

ㄴㄴ

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (13)
map-app/src/components/map/EmptyState.tsx (1)

16-38: Use semantic design tokens instead of hardcoded colors.

Lines 17, 23, 26, and 35 hardcode visual colors, which makes this overlay drift from the tokenized theme system and complicates dark/light consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/map/EmptyState.tsx` around lines 16 - 38, Replace
hardcoded color strings in the EmptyState component with the app's semantic
design tokens: swap the container background '#fff', boxShadow color, header
color '#1e293b', paragraph color '#64748b', and button background '#fbbf24' /
text color '#000' to use theme tokens or CSS variables (e.g.,
theme.colors.surface, theme.shadows.overlay, theme.colors.textPrimary,
theme.colors.textSecondary, theme.colors.accent, theme.colors.onAccent). Update
JSX in the EmptyState component (references: the top-level div, the h2 element
using language, the p element using language, and the button with
onClick={resetFilters}) to read colors from the shared theme/token source
instead of literals so light/dark modes stay consistent.
map-app/src/components/map/CategoryPin.tsx (1)

12-19: Consider extracting rank badge logic for readability.

The nested ternary works but can be harder to scan. A small lookup object or helper would improve clarity.

Optional refactor using a lookup
+const RANK_BADGES: Record<1 | 2 | 3, string> = {
+  1: '<div class="absolute -top-2 -right-2 w-5 h-5 bg-yellow-400 rounded-full flex items-center justify-center text-xs font-bold text-yellow-900 shadow-md z-10">👑</div>',
+  2: '<div class="absolute -top-2 -right-2 w-5 h-5 bg-slate-300 rounded-full flex items-center justify-center text-xs font-bold text-slate-700 shadow-md z-10">2</div>',
+  3: '<div class="absolute -top-2 -right-2 w-5 h-5 bg-amber-600 rounded-full flex items-center justify-center text-xs font-bold text-white shadow-md z-10">3</div>',
+}
+
 export function createCategoryIcon(
   category: PriceCategory,
   rank?: 1 | 2 | 3,
 ): L.DivIcon {
   const meta = CATEGORY_META[category]
-
-  const rankBadge =
-    rank === 1
-      ? '<div class="absolute -top-2 -right-2 w-5 h-5 bg-yellow-400 rounded-full flex items-center justify-center text-xs font-bold text-yellow-900 shadow-md z-10">👑</div>'
-      : rank === 2
-      ? '<div class="absolute -top-2 -right-2 w-5 h-5 bg-slate-300 rounded-full flex items-center justify-center text-xs font-bold text-slate-700 shadow-md z-10">2</div>'
-      : rank === 3
-      ? '<div class="absolute -top-2 -right-2 w-5 h-5 bg-amber-600 rounded-full flex items-center justify-center text-xs font-bold text-white shadow-md z-10">3</div>'
-      : ''
+  const rankBadge = rank ? RANK_BADGES[rank] : ''
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/map/CategoryPin.tsx` around lines 12 - 19, The nested
ternary that builds rankBadge in CategoryPin.tsx is hard to read; extract the
logic into a small helper or lookup map (e.g., a const rankBadgeMap or a
function getRankBadge(rank)) that maps 1,2,3 to their respective HTML strings
and returns '' for others, then replace the ternary with a simple lookup like
rankBadgeMap[rank] || '' or call getRankBadge(rank); ensure you preserve the
exact HTML classes and content and update any imports/exports if you place the
helper outside the component.
map-app/src/components/filters/CategoryFilterBar.tsx (1)

6-9: Consider extracting CATEGORIES to a shared constant.

This array is duplicated in MapLegend.tsx, Sidebar.tsx, and here. Extract to @/types alongside CATEGORY_META to maintain a single source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/filters/CategoryFilterBar.tsx` around lines 6 - 9,
CATEGORIES is duplicated across CategoryFilterBar (const CATEGORIES:
PriceCategory[]), MapLegend, and Sidebar; extract it into a shared module (e.g.,
add CATEGORIES to the existing `@/types` where CATEGORY_META lives) and import it
in CategoryFilterBar, MapLegend, and Sidebar to maintain a single source of
truth; update references to use the imported CATEGORIES and remove the local
const from each component.
map-app/src/components/layout/Sidebar.tsx (1)

2-4: Remove duplicate import of useUiStore.

useUiStore is imported twice—once on line 2 and again on line 4 with the alias useUi. Both are from the same store. Consolidate to a single import.

Proposed fix
-import { useUiStore, useFilterStore, useFavoritesStore } from '@/store'
+import { useUiStore, useFilterStore, useFavoritesStore } from '@/store'
 import { useAllLocations } from '@/hooks/useAllLocations'
-import { useUiStore as useUi } from '@/store'
 import { CATEGORY_META } from '@/types'

Then on line 24, replace:

-  const { setSelectedPin } = useUi()
+  const { setSelectedPin } = useUiStore()

Or destructure setSelectedPin alongside language on line 21:

-  const { language } = useUiStore()
+  const { language, setSelectedPin } = useUiStore()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/layout/Sidebar.tsx` around lines 2 - 4, The file
imports useUiStore twice (once as useUiStore and again aliased as useUi); remove
the duplicate import (keep a single import of useUiStore), then replace any
usages of useUi with useUiStore or adjust the destructuring where language and
setSelectedPin are obtained (e.g., destructure setSelectedPin alongside language
from useUiStore) so all references use the single import.
map-app/src/components/layout/TopBar.tsx (1)

20-26: Clarify or document the search toggle behavior.

Current behavior: when search is open with a query, clicking the button clears the query but keeps the search open. When search is open without a query, clicking closes it. This two-step close behavior might confuse users who expect a single click to close the search panel.

Consider closing the search when clearing, or adding a clear comment explaining the intentional UX:

Alternative: close search when clearing query
 function handleSearchToggle() {
   if (searchOpen && searchQuery) {
     setSearchQuery('')
+    setSearchOpen(false)
-  } else {
-    setSearchOpen((v) => !v)
+  } else if (searchOpen) {
+    setSearchOpen(false)
+  } else {
+    setSearchOpen(true)
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/layout/TopBar.tsx` around lines 20 - 26,
handleSearchToggle currently clears the query but leaves the panel open (if
searchOpen && searchQuery), which is confusing; update handleSearchToggle to
also close the panel when clearing by calling setSearchQuery('') and then
setSearchOpen(false) (or setSearchOpen(v => !v) only when there is no query),
referencing the function handleSearchToggle and state vars searchOpen,
searchQuery, setSearchQuery, setSearchOpen; if you prefer to keep current
behavior instead, add a clear comment above handleSearchToggle explaining the
intentional two-step UX so the behavior is explicit to future readers.
README.md (1)

79-84: Add language specifiers to fenced code blocks.

Static analysis flagged missing language specifiers on lines 79 and 107. Specify the language for syntax highlighting and linter compliance.

Proposed fix
-```
+```text
 Frontend   React 18 + TypeScript 5 + Vite 5
-```
+```text
 map-app/src/
 ├── components/

Also applies to: 107-119

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 79 - 84, The README contains fenced code blocks
without language specifiers (for example the block starting with "Frontend  
React 18 + TypeScript 5 + Vite 5" and the directory tree block starting with
"map-app/src/"); update those fenced code blocks to include an appropriate
language tag (e.g., text) so static analysis and syntax highlighting pass, and
scan the rest of README for any other triple-backtick blocks (including the one
referenced around lines 107-119) and add language specifiers consistently.
map-app/src/components/panels/DetailPanel.tsx (1)

30-38: Redundant null check inside renderDetail().

selectedPin is already guarded at line 25 with an early return, so the check at line 31 is unnecessary. However, keeping it is harmless and provides defense if the function is ever refactored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/panels/DetailPanel.tsx` around lines 30 - 38, The
inner null-check in renderDetail is redundant because selectedPin is already
early-returned before renderDetail is called; remove the inner guard "if
(!selectedPin) return null" from the renderDetail function and let the switch on
selectedPin.category handle rendering (functions/components referenced:
renderDetail, selectedPin, ExchangeDetail, FuelDetail, RestaurantDetail,
GenericDetail) so behavior remains the same but the code is cleaner.
map-app/src/components/panels/ExchangeDetail.tsx (1)

22-26: Simplify redundant condition check.

Since location.minAmount === 0 is only true when minAmount is defined and equals 0, the !== undefined check is redundant.

♻️ Suggested simplification
-        {location.minAmount !== undefined && location.minAmount === 0 && (
+        {location.minAmount === 0 && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/panels/ExchangeDetail.tsx` around lines 22 - 26, The
JSX condition in the ExchangeDetail component redundantly checks both
location.minAmount !== undefined and location.minAmount === 0; simplify by
removing the unnecessary undefined check and only test location.minAmount === 0
where the span is rendered. Update the conditional expression around the span
that references location.minAmount so it reads a single equality check
(location.minAmount === 0) to keep the intent clear and concise.
map-app/src/components/panels/GenericDetail.tsx (1)

42-48: Consider extracting inline constants to module scope.

brandColors, sizeLabel, and sizeLabelEn are defined inside their respective switch cases. Extracting them to module scope would slightly improve readability and avoid recreating these objects on each render.

Also applies to: 105-106

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/panels/GenericDetail.tsx` around lines 42 - 48, The
constants brandColors, sizeLabel, and sizeLabelEn are being recreated inside the
component/switch scope causing unnecessary reallocation on each render; extract
them to module scope above the GenericDetail component (declare top-level consts
named brandColors, sizeLabel, and sizeLabelEn) and update the component to
reference these top-level constants (keep the same object keys and values so
usages in the switch/case and JSX remain unchanged). Ensure imports/types remain
valid and remove the inline definitions at their current locations so the
component uses the module-scoped identifiers.
map-app/src/components/panels/RestaurantDetail.tsx (2)

2-3: Consider consolidating imports from the same module.

-import { useUiStore } from '@/store'
-import { useFilterStore } from '@/store'
+import { useUiStore, useFilterStore } from '@/store'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/panels/RestaurantDetail.tsx` around lines 2 - 3, Two
separate imports from the same module in RestaurantDetail.tsx (useUiStore and
useFilterStore) should be consolidated into a single import from '@/store';
update the import statements so both hooks are imported together (e.g., import {
useUiStore, useFilterStore } from '@/store') to reduce redundancy and keep
imports tidy.

86-105: Using array index as key is acceptable here but consider unique identifiers if available.

The menu items appear to be static per restaurant, so using the index as a key is fine. However, if MenuItem gains a unique identifier in the future, prefer using that to avoid potential issues with list reordering or filtering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/panels/RestaurantDetail.tsx` around lines 86 - 105,
The list rendering in RestaurantDetail uses the array index as the React key
(key={i}) which can cause problems if items are reordered or filtered; update
the map to use a stable unique identifier from each menu item (e.g., item.id or
another unique property) instead of the index, falling back to index only if no
unique id exists—change the key on the displayItems.map elements so it
references the item's unique id (use the displayItems and item symbols shown in
the component).
map-app/src/hooks/useFilteredMarkers.ts (1)

17-23: Same Math.min() on potentially empty array concern applies here.

As noted in MarkerLayer.tsx, if loc.rates (karaoke) or loc.popularItems (market) are empty, Math.min(...[]) returns Infinity. This would affect price range filtering and cheapest calculations.

Consider adding guards:

♻️ Defensive empty array handling
     case 'karaoke': {
       const rates = loc.rates.map((r) => r.pricePerHour)
-      return Math.min(...rates)
+      return rates.length ? Math.min(...rates) : 0
     }
     case 'market': {
       const prices = loc.popularItems.map((i) => i.price)
-      return Math.min(...prices)
+      return prices.length ? Math.min(...prices) : 0
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/hooks/useFilteredMarkers.ts` around lines 17 - 23, The karaoke
and market branches in useFilteredMarkers.ts call Math.min on arrays that may be
empty (loc.rates, loc.popularItems); change each to guard the array first and
return a null sentinel when empty (e.g., in the 'karaoke' branch: if
loc.rates.length === 0 return null else return Math.min(...rates); similarly for
'market' with loc.popularItems) and then ensure downstream comparisons that
consume these values (filters/cheapest calculations) skip or handle nulls
appropriately.
map-app/src/store/filterStore.ts (1)

17-17: Share the default price bounds with the slider.

200000 is hardcoded here and also separately as MAX in map-app/src/components/filters/PriceRangeSlider.tsx:5-7. If one side changes without the other, rehydration/reset can produce an out-of-range slider state. A shared exported constant or default tuple would keep both paths aligned.

Also applies to: 85-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/store/filterStore.ts` at line 17, The default max price (200000)
is hardcoded in the filter store return (priceRange) and duplicated as MAX in
PriceRangeSlider.tsx; extract a shared exported constant or default tuple (e.g.,
DEFAULT_PRICE_RANGE or PRICE_RANGE_BOUNDS) from one module (or a new constants
module) and import it into both map-app/src/store/filterStore.ts (replace the
literal in the returned priceRange) and
map-app/src/components/filters/PriceRangeSlider.tsx (replace MAX/tuple usage) so
both use the same source of truth and avoid out-of-range rehydration/reset
issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@map-app/package.json`:
- Line 10: The lint script "lint" in package.json calls eslint for TypeScript
but the required devDependencies are missing; update map-app's package.json
devDependencies to include at minimum "eslint", "@typescript-eslint/parser", and
"@typescript-eslint/eslint-plugin" (and any other project-specific ESLint
plugins/configs you use such as "eslint-plugin-react" or a shared config), then
run your package manager to install them (e.g., npm/yarn/pnpm install
--save-dev) so the "lint" script can run successfully.

In `@map-app/src/App.tsx`:
- Line 13: The isMobile breakpoint in the handle function (which sets isMobile
via setIsMobile(window.innerWidth < 768)) does not match the CSS hiding class on
the mobile CategoryFilterBar container (it uses lg:hidden), causing a mismatch
between 768px and 1023px; fix by either changing the JS breakpoint to 1024
(e.g., update handle to use window.innerWidth < 1024 so setIsMobile aligns with
lg:hidden) or change the container class on CategoryFilterBar from lg:hidden to
md:hidden to match the existing 768px threshold—update references to isMobile,
setIsMobile/handle, and the CategoryFilterBar container class accordingly and
add a brief comment documenting the chosen breakpoint.

In `@map-app/src/components/filters/NationalityDropdown.tsx`:
- Around line 19-68: The dropdown lacks ARIA roles and keyboard handling—update
the toggle button and menu rendering to add accessibility: give the toggle
button aria-haspopup="menu", aria-expanded={open}, and aria-controls pointing to
the menu id; ensure the menu container has role="menu" and each option button
has role="menuitem" and tabIndex={-1} (or 0 when focused). Add keyboard
handlers: on the toggle handle Enter/Space to open, and when open capture Escape
to call setOpen(false) and move focus back to the toggle; when opening
programmatically focus the first menu item (use refs for the list items). Use
existing symbols setOpen, open, the toggle button, the menu div that maps
NATIONALITIES and the per-item buttons (accessing NATIONALITY_META) to locate
where to add aria attributes, onKeyDown handlers, refs, and an effect to listen
for Escape/focus management.

In `@map-app/src/components/map/EmptyState.tsx`:
- Around line 31-33: The button in the EmptyState component is missing an
explicit type which can cause accidental form submissions; update the JSX
<button> element that calls resetFilters (the button with
onClick={resetFilters}) to include type="button" so it won't act as a submit
button when this component is rendered inside a <form>.

In `@map-app/src/components/map/MapLegend.tsx`:
- Around line 70-89: The LegendPanel is mounted into its own React root once in
the useEffect (creating root via createRoot and calling root.render(<LegendPanel
/>)), so it never updates when the app language changes; modify the effect for
the control created in useEffect to include language in its dependency array,
and when language changes re-render the separate root (call root.render with the
new props) or unmount/remount the control so the UI updates. Also change
LegendPanel to accept a language prop (instead of reading useUiStore()
internally) and pass that prop into root.render (e.g., root.render(<LegendPanel
language={language} />)) so the isolated root receives updates.

In `@map-app/src/components/map/MarkerLayer.tsx`:
- Around line 17-22: The tooltip display in MarkerLayer.tsx's getDisplayPrice()
currently picks gasoline>electric>hydrogen while ranking in
useFilteredMarkers.ts's getLocationPrice() uses Math.min across available
prices, causing mismatched badges; update getDisplayPrice() to determine the
same cheapest fuel type/value as getLocationPrice() (inspect loc.prices fields
used there), then return the formatted string for that fuel type (e.g., "휘발유
₩.../L", "전기 ₩.../kWh", "수소 ₩.../kg"), so the displayed price and the
ranking/badge are derived from the same minimum-price calculation.
- Around line 27-28: The tooltip label logic in MarkerLayer for cases 'karaoke'
and 'market' uses Math.min(...loc.rates.map(...)) and
Math.min(...loc.popularItems.map(...)) which will produce Infinity for empty
arrays; update the cases to defensively check loc.rates.length and
loc.popularItems.length (and filter out invalid entries) and fall back to a
sensible string (e.g., "무료" or "-" or "요금 정보 없음") when empty, mirroring the
defensive pattern used in the 'attraction' branch (check for freeEntry or
paid.length === 0); locate the switch handling in MarkerLayer.tsx and modify the
'karaoke' and 'market' branches to compute minPrice only when the arrays are
non-empty, otherwise return the fallback label.

In `@map-app/src/components/map/MyLocationButton.tsx`:
- Around line 10-12: The map control in MyLocationButton.tsx sets btn.innerHTML
to an emoji and btn.title to a hardcoded Korean string; replace the hardcoded
title with a localized label (via the app's i18n function or a passed-in prop)
and add an explicit accessible name (e.g., btn.setAttribute('aria-label',
localizedLabel)) while keeping a visible icon; also ensure the control exposes
the label for screen readers (avoid relying solely on title by adding visually
hidden text or aria-label) so the btn element, innerHTML and title usage in
MyLocationButton are updated to use the localizedLabel.

In `@map-app/src/components/panels/DetailPanel.tsx`:
- Around line 150-151: The top-of-function comment "Mobile bottom sheet with
drag support" is inaccurate because MobileBottomSheet currently has no
drag/touch handlers; either implement drag-to-dismiss or change the comment. To
fix: if adding drag, implement touch/mouse event handlers inside
MobileBottomSheet (e.g., onTouchStart/onMouseDown, onTouchMove/onMouseMove,
onTouchEnd/onMouseUp) that track initialY/currentY and translate/animate the
sheet and call the existing close/dismiss function when threshold passed;
otherwise simply update the comment to "Mobile bottom sheet" (or similar) to
reflect current behavior. Reference the MobileBottomSheet function and any
existing close/dismiss props/state when wiring the drag logic.

In `@map-app/src/components/panels/FuelDetail.tsx`:
- Around line 49-50: The price rendering uses price.toLocaleString() which
relies on the browser default locale; update FuelDetail.tsx to format numbers
with the app's selected locale instead. In the FuelDetail component, obtain the
current locale (e.g., from i18n via useTranslation().i18n.language or from
react-intl's useIntl().locale) and replace price.toLocaleString() with a
locale-aware formatter such as new Intl.NumberFormat(appLocale).format(price)
(or pass appLocale into toLocaleString) so the numeric price matches the
selected language while keeping the existing meta.unit display.

In `@map-app/src/data/attractions.json`:
- Around line 71-78: The filtering/ranking logic in useFilteredMarkers.ts
incorrectly ignores attractions with freeEntry: true because it only considers
tickets with t.price > 0; update the price-extraction and lowest-price
computation in useFilteredMarkers (and any helper like getLowestTicketPrice or
computePriceFromTickets) to treat freeEntry === true as price 0 and to include
tickets with price === 0 when determining lowest price; ensure any condition
that currently filters out t.price <= 0 is changed to allow zero-priced tickets
or explicitly map freeEntry to a 0-price fallback so these attractions appear
correctly as ₩0 in lowest-price flows.

In `@map-app/src/data/extras.json`:
- Around line 296-303: The entry with extraType "accommodation" for the
laundromat (identify by extraTypeLabel "코인 빨래방" and extraTypeLabelEn "Coin
Laundromat") is misclassified; change the extraType value to the correct
category (e.g., "laundromat" or the project’s canonical key for laundry
services) so subtype filtering and comparisons work correctly, and keep the
existing labels, price, and descriptions unchanged.

In `@map-app/src/data/jjimjilbang.json`:
- Line 31: Remove the "overnightFee" property when its value is null so the
optional number is omitted (do not set it to null); locate the JSON objects that
include "overnightFee": null (three occurrences in the jjimjilbang dataset) and
delete those key/value pairs so the shape remains an absent optional number
rather than null; ensure any JSON validators/consumers still accept the objects
after removal.

In `@map-app/src/data/restaurants.json`:
- Around line 272-274: The restaurant entry for rest-024 has conflicting halal
metadata: "halal" appears in nationalityTags while isHalal is false; make these
consistent by either removing "halal" from the nationalityTags array or setting
isHalal to true. Locate the rest-024 JSON object in restaurants.json and update
the nationalityTags array or the isHalal property so both reflect the same halal
status (use the "nationalityTags" and "isHalal" fields for the change).

In `@map-app/src/store/favoritesStore.ts`:
- Around line 5-10: The loadFavorites function should validate the parsed JSON
before returning it: after calling JSON.parse(raw) for STORAGE_KEY, check that
the result is an array and that every element is a string (e.g.,
Array.isArray(result) && result.every(item => typeof item === 'string')); if the
check fails, fall back to returning [] so downstream uses like
favoriteIds.includes(...) won't throw. Keep the existing try/catch but add this
defensive shape-check and return [] on invalid shape or parse errors.

In `@map-app/src/store/filterStore.ts`:
- Around line 12-17: Validate and sanitize the parsed localStorage payload in
loadFilters(): after JSON.parse(raw) (STORAGE_KEY) confirm the result matches
PersistedFilters by checking that activeCategories is an Array of strings,
nationality is one of the allowed union values, and priceRange is an array/tuple
of two finite numbers (first <= second); if any check fails, return the default
filters. Move the default price bounds ([0, 200000]) into a shared constant
(e.g., DEFAULT_PRICE_RANGE) used by loadFilters() and resetFilters(), and ensure
downstream code (like PriceRangeSlider) receives the validated tuple of two
numbers to avoid NaN in calculations.

In `@map-app/src/types/index.ts`:
- Around line 111-113: The issue is that Karaoke.rates and Market.popularItems
can be empty arrays so Math.min(...loc.rates.map(...)) and
Math.min(...loc.popularItems.map(...)) yield Infinity; fix by hardening the
consumer: in useFilteredMarkers (the places calling Math.min on loc.rates and
loc.popularItems) check that loc.rates.length and loc.popularItems.length are >
0 before calling Math.min and provide a sensible fallback (e.g., treat as
undefined/skip that marker or use a sentinel value) so Infinity is never used in
filtering/sorting; alternatively, if emptiness is truly invalid, change the
model types Karaoke.rates and Market.popularItems from
KaraokeRate[]/MarketItem[] to non-empty tuple types (e.g., readonly
[KaraokeRate, ...KaraokeRate[]]) and update any construction sites accordingly.

In `@map-app/vite.config.ts`:
- Line 3: The Vite config currently uses the undefined __dirname (in imports and
alias setup); replace __dirname usage with an ESM-safe derived dirname: import
fileURLToPath from 'url' and compute the directory from import.meta.url, then
use that derived variable in the existing path.resolve()/alias calls (keep
references to path.resolve, the alias setup, and the vite.config.ts export such
as the alias entries) so the config runs under "type: module" ESM.

In `@README.md`:
- Around line 1-57: The README uses emojis in headings and list items (e.g. the
title line "# 💰 최저가 지도 (Lowest Price Map)" and section headers like "### 🗺️ 지도
& 핀", "### 🔍 검색 & 필터", and list items with heart/location emojis) which
violates the "No emoji in technical prose" guideline; remove all emojis from
headings, badges, and bullet points and replace them with plain text equivalents
(e.g. "# 최저가 지도 (Lowest Price Map)", "## 지도 & 핀", "## 검색 & 필터", and "즐겨찾기 / 내
위치"), update any inline emoji characters in feature bullets (💰, 🗺️, 🔍, ❤️,
📍, etc.) to text labels, and ensure README.md still conveys the same
information and structure without emojis.

---

Nitpick comments:
In `@map-app/src/components/filters/CategoryFilterBar.tsx`:
- Around line 6-9: CATEGORIES is duplicated across CategoryFilterBar (const
CATEGORIES: PriceCategory[]), MapLegend, and Sidebar; extract it into a shared
module (e.g., add CATEGORIES to the existing `@/types` where CATEGORY_META lives)
and import it in CategoryFilterBar, MapLegend, and Sidebar to maintain a single
source of truth; update references to use the imported CATEGORIES and remove the
local const from each component.

In `@map-app/src/components/layout/Sidebar.tsx`:
- Around line 2-4: The file imports useUiStore twice (once as useUiStore and
again aliased as useUi); remove the duplicate import (keep a single import of
useUiStore), then replace any usages of useUi with useUiStore or adjust the
destructuring where language and setSelectedPin are obtained (e.g., destructure
setSelectedPin alongside language from useUiStore) so all references use the
single import.

In `@map-app/src/components/layout/TopBar.tsx`:
- Around line 20-26: handleSearchToggle currently clears the query but leaves
the panel open (if searchOpen && searchQuery), which is confusing; update
handleSearchToggle to also close the panel when clearing by calling
setSearchQuery('') and then setSearchOpen(false) (or setSearchOpen(v => !v) only
when there is no query), referencing the function handleSearchToggle and state
vars searchOpen, searchQuery, setSearchQuery, setSearchOpen; if you prefer to
keep current behavior instead, add a clear comment above handleSearchToggle
explaining the intentional two-step UX so the behavior is explicit to future
readers.

In `@map-app/src/components/map/CategoryPin.tsx`:
- Around line 12-19: The nested ternary that builds rankBadge in CategoryPin.tsx
is hard to read; extract the logic into a small helper or lookup map (e.g., a
const rankBadgeMap or a function getRankBadge(rank)) that maps 1,2,3 to their
respective HTML strings and returns '' for others, then replace the ternary with
a simple lookup like rankBadgeMap[rank] || '' or call getRankBadge(rank); ensure
you preserve the exact HTML classes and content and update any imports/exports
if you place the helper outside the component.

In `@map-app/src/components/map/EmptyState.tsx`:
- Around line 16-38: Replace hardcoded color strings in the EmptyState component
with the app's semantic design tokens: swap the container background '#fff',
boxShadow color, header color '#1e293b', paragraph color '#64748b', and button
background '#fbbf24' / text color '#000' to use theme tokens or CSS variables
(e.g., theme.colors.surface, theme.shadows.overlay, theme.colors.textPrimary,
theme.colors.textSecondary, theme.colors.accent, theme.colors.onAccent). Update
JSX in the EmptyState component (references: the top-level div, the h2 element
using language, the p element using language, and the button with
onClick={resetFilters}) to read colors from the shared theme/token source
instead of literals so light/dark modes stay consistent.

In `@map-app/src/components/panels/DetailPanel.tsx`:
- Around line 30-38: The inner null-check in renderDetail is redundant because
selectedPin is already early-returned before renderDetail is called; remove the
inner guard "if (!selectedPin) return null" from the renderDetail function and
let the switch on selectedPin.category handle rendering (functions/components
referenced: renderDetail, selectedPin, ExchangeDetail, FuelDetail,
RestaurantDetail, GenericDetail) so behavior remains the same but the code is
cleaner.

In `@map-app/src/components/panels/ExchangeDetail.tsx`:
- Around line 22-26: The JSX condition in the ExchangeDetail component
redundantly checks both location.minAmount !== undefined and location.minAmount
=== 0; simplify by removing the unnecessary undefined check and only test
location.minAmount === 0 where the span is rendered. Update the conditional
expression around the span that references location.minAmount so it reads a
single equality check (location.minAmount === 0) to keep the intent clear and
concise.

In `@map-app/src/components/panels/GenericDetail.tsx`:
- Around line 42-48: The constants brandColors, sizeLabel, and sizeLabelEn are
being recreated inside the component/switch scope causing unnecessary
reallocation on each render; extract them to module scope above the
GenericDetail component (declare top-level consts named brandColors, sizeLabel,
and sizeLabelEn) and update the component to reference these top-level constants
(keep the same object keys and values so usages in the switch/case and JSX
remain unchanged). Ensure imports/types remain valid and remove the inline
definitions at their current locations so the component uses the module-scoped
identifiers.

In `@map-app/src/components/panels/RestaurantDetail.tsx`:
- Around line 2-3: Two separate imports from the same module in
RestaurantDetail.tsx (useUiStore and useFilterStore) should be consolidated into
a single import from '@/store'; update the import statements so both hooks are
imported together (e.g., import { useUiStore, useFilterStore } from '@/store')
to reduce redundancy and keep imports tidy.
- Around line 86-105: The list rendering in RestaurantDetail uses the array
index as the React key (key={i}) which can cause problems if items are reordered
or filtered; update the map to use a stable unique identifier from each menu
item (e.g., item.id or another unique property) instead of the index, falling
back to index only if no unique id exists—change the key on the displayItems.map
elements so it references the item's unique id (use the displayItems and item
symbols shown in the component).

In `@map-app/src/hooks/useFilteredMarkers.ts`:
- Around line 17-23: The karaoke and market branches in useFilteredMarkers.ts
call Math.min on arrays that may be empty (loc.rates, loc.popularItems); change
each to guard the array first and return a null sentinel when empty (e.g., in
the 'karaoke' branch: if loc.rates.length === 0 return null else return
Math.min(...rates); similarly for 'market' with loc.popularItems) and then
ensure downstream comparisons that consume these values (filters/cheapest
calculations) skip or handle nulls appropriately.

In `@map-app/src/store/filterStore.ts`:
- Line 17: The default max price (200000) is hardcoded in the filter store
return (priceRange) and duplicated as MAX in PriceRangeSlider.tsx; extract a
shared exported constant or default tuple (e.g., DEFAULT_PRICE_RANGE or
PRICE_RANGE_BOUNDS) from one module (or a new constants module) and import it
into both map-app/src/store/filterStore.ts (replace the literal in the returned
priceRange) and map-app/src/components/filters/PriceRangeSlider.tsx (replace
MAX/tuple usage) so both use the same source of truth and avoid out-of-range
rehydration/reset issues.

In `@README.md`:
- Around line 79-84: The README contains fenced code blocks without language
specifiers (for example the block starting with "Frontend   React 18 +
TypeScript 5 + Vite 5" and the directory tree block starting with
"map-app/src/"); update those fenced code blocks to include an appropriate
language tag (e.g., text) so static analysis and syntax highlighting pass, and
scan the rest of README for any other triple-backtick blocks (including the one
referenced around lines 107-119) and add language specifiers consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59e006a3-7b53-4b59-8225-74eaae213ebb

📥 Commits

Reviewing files that changed from the base of the PR and between c99e3e8 and 58341d4.

⛔ Files ignored due to path filters (1)
  • map-app/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (50)
  • README.md
  • map-app/index.html
  • map-app/package.json
  • map-app/postcss.config.cjs
  • map-app/src/App.tsx
  • map-app/src/components/common/CategoryIcon.tsx
  • map-app/src/components/common/PriceBadge.tsx
  • map-app/src/components/filters/CategoryFilterBar.tsx
  • map-app/src/components/filters/NationalityDropdown.tsx
  • map-app/src/components/filters/PriceRangeSlider.tsx
  • map-app/src/components/layout/Sidebar.tsx
  • map-app/src/components/layout/TopBar.tsx
  • map-app/src/components/map/CategoryPin.tsx
  • map-app/src/components/map/EmptyState.tsx
  • map-app/src/components/map/MapContainer.tsx
  • map-app/src/components/map/MapLegend.tsx
  • map-app/src/components/map/MarkerLayer.tsx
  • map-app/src/components/map/MyLocationButton.tsx
  • map-app/src/components/panels/DetailPanel.tsx
  • map-app/src/components/panels/ExchangeDetail.tsx
  • map-app/src/components/panels/FuelDetail.tsx
  • map-app/src/components/panels/GenericDetail.tsx
  • map-app/src/components/panels/RestaurantDetail.tsx
  • map-app/src/data/attractions.json
  • map-app/src/data/cafes.json
  • map-app/src/data/convenience.json
  • map-app/src/data/exchange.json
  • map-app/src/data/extras.json
  • map-app/src/data/fuel.json
  • map-app/src/data/jjimjilbang.json
  • map-app/src/data/karaoke.json
  • map-app/src/data/markets.json
  • map-app/src/data/restaurants.json
  • map-app/src/hooks/useAllLocations.ts
  • map-app/src/hooks/useAllPriceRanks.ts
  • map-app/src/hooks/useFilteredMarkers.ts
  • map-app/src/hooks/usePriceRanks.ts
  • map-app/src/index.css
  • map-app/src/main.tsx
  • map-app/src/store/favoritesStore.ts
  • map-app/src/store/filterStore.ts
  • map-app/src/store/index.ts
  • map-app/src/store/uiStore.ts
  • map-app/src/types/common.ts
  • map-app/src/types/index.ts
  • map-app/src/vite-env.d.ts
  • map-app/tailwind.config.ts
  • map-app/tsconfig.json
  • map-app/tsconfig.node.json
  • map-app/vite.config.ts

"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's an eslint config file that might indicate expected packages
fd -t f -e json -e js -e cjs -e mjs 'eslint' map-app/
# Check if eslint is perhaps installed at the monorepo root
cat package.json 2>/dev/null | jq '.devDependencies | keys | map(select(startswith("eslint")))' 2>/dev/null || echo "No root package.json or no eslint deps"

Repository: NVIDIA/NemoClaw

Length of output: 73


🏁 Script executed:

# Check the actual map-app/package.json file
cat map-app/package.json

Repository: NVIDIA/NemoClaw

Length of output: 918


🏁 Script executed:

# Check root package.json devDependencies more comprehensively
head -100 package.json | grep -A 50 '"devDependencies"'

Repository: NVIDIA/NemoClaw

Length of output: 371


🏁 Script executed:

# Check for monorepo configuration files
ls -la | grep -E '(lerna|pnpm-workspace|\.npmrc)'
cat pnpm-workspace.yaml 2>/dev/null || cat lerna.json 2>/dev/null || echo "No standard monorepo config found at root level"

Repository: NVIDIA/NemoClaw

Length of output: 106


🏁 Script executed:

# Check for ESLint configuration files
find . -maxdepth 2 -name '.eslintrc*' -o -name 'eslint.config.*'

Repository: NVIDIA/NemoClaw

Length of output: 107


🏁 Script executed:

# Check if `@typescript-eslint` packages exist anywhere in root or dependencies
grep -r "@typescript-eslint" package.json 2>/dev/null || echo "No `@typescript-eslint` packages found in root package.json"

Repository: NVIDIA/NemoClaw

Length of output: 116


🏁 Script executed:

# Check if root package.json has a workspaces field
grep -A 5 '"workspaces"' package.json 2>/dev/null || echo "No workspaces field found"

Repository: NVIDIA/NemoClaw

Length of output: 84


🏁 Script executed:

# Check the root eslint.config.mjs to understand the eslint setup
cat eslint.config.mjs

Repository: NVIDIA/NemoClaw

Length of output: 3004


Missing ESLint and TypeScript ESLint dependencies for the lint script.

The lint script invokes eslint . --ext ts,tsx, but eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin, and other linting dependencies are not listed in map-app/package.json devDependencies. The script will fail at runtime without these packages.

Proposed fix
   "devDependencies": {
     "@types/leaflet": "^1.9.12",
     "@types/react": "^18.3.3",
     "@types/react-dom": "^18.3.0",
+    "@typescript-eslint/eslint-plugin": "^7.0.0",
+    "@typescript-eslint/parser": "^7.0.0",
     "@vitejs/plugin-react": "^4.3.1",
     "autoprefixer": "^10.4.19",
+    "eslint": "^8.57.0",
+    "eslint-plugin-react-hooks": "^4.6.2",
+    "eslint-plugin-react-refresh": "^0.4.7",
     "postcss": "^8.4.40",
     "tailwindcss": "^3.4.7",
     "typescript": "^5.5.3",
     "vite": "^5.3.5"
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/package.json` at line 10, The lint script "lint" in package.json
calls eslint for TypeScript but the required devDependencies are missing; update
map-app's package.json devDependencies to include at minimum "eslint",
"@typescript-eslint/parser", and "@typescript-eslint/eslint-plugin" (and any
other project-specific ESLint plugins/configs you use such as
"eslint-plugin-react" or a shared config), then run your package manager to
install them (e.g., npm/yarn/pnpm install --save-dev) so the "lint" script can
run successfully.

const { setIsMobile } = useUiStore()

useEffect(() => {
const handle = () => setIsMobile(window.innerWidth < 768)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Breakpoint mismatch between JS and CSS.

The isMobile state is set at < 768px (Line 13), but the mobile CategoryFilterBar container uses lg:hidden which hides at ≥ 1024px (Line 31). This creates an intermediate range (768–1023px) where isMobile is false but the mobile filter bar is still visible.

If this is intentional for tablet layouts, consider documenting it. Otherwise, align the breakpoints:

Option A: Use `md:hidden` to match 768px threshold
-          <div className="lg:hidden bg-bg-layer-default border-b border-stroke-neutral-subtle shadow-sm z-20 flex-shrink-0">
+          <div className="md:hidden bg-bg-layer-default border-b border-stroke-neutral-subtle shadow-sm z-20 flex-shrink-0">
Option B: Change JS breakpoint to 1024px
-    const handle = () => setIsMobile(window.innerWidth < 768)
+    const handle = () => setIsMobile(window.innerWidth < 1024)

Also applies to: 31-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/App.tsx` at line 13, The isMobile breakpoint in the handle
function (which sets isMobile via setIsMobile(window.innerWidth < 768)) does not
match the CSS hiding class on the mobile CategoryFilterBar container (it uses
lg:hidden), causing a mismatch between 768px and 1023px; fix by either changing
the JS breakpoint to 1024 (e.g., update handle to use window.innerWidth < 1024
so setIsMobile aligns with lg:hidden) or change the container class on
CategoryFilterBar from lg:hidden to md:hidden to match the existing 768px
threshold—update references to isMobile, setIsMobile/handle, and the
CategoryFilterBar container class accordingly and add a brief comment
documenting the chosen breakpoint.

Comment on lines +19 to +68
<button
onClick={() => setOpen((v) => !v)}
className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-bg-layer-default border border-stroke-neutral-subtle hover:border-stroke-neutral-muted
text-sm font-medium text-fg-neutral-muted transition-all shadow-sm whitespace-nowrap"
>
<span>{current.flag}</span>
<span>{language === 'ko' ? current.label : current.labelEn}</span>
<svg
className={`w-4 h-4 text-fg-neutral-subtle transition-transform ${open ? 'rotate-180' : ''}`}
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>

{open && (
<div className="absolute top-full right-0 mt-1.5 w-48 bg-bg-layer-floating rounded-xl shadow-xl border border-stroke-neutral-subtle z-50 overflow-hidden">
<div className="py-1">
{NATIONALITIES.map((nat) => {
const meta = NATIONALITY_META[nat]
const isSelected = nationality === nat
return (
<button
key={nat}
onClick={() => { setNationality(nat); setOpen(false) }}
className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-bg-layer-fill transition-colors
${isSelected ? 'bg-bg-brand-weak text-fg-brand font-semibold' : 'text-fg-neutral-muted'}`}
>
<span className="text-lg">{meta.flag}</span>
<div className="flex flex-col items-start">
<span className="font-medium">{language === 'ko' ? meta.label : meta.labelEn}</span>
{language === 'ko' && (
<span className="text-xs text-fg-neutral-subtle">{meta.labelEn}</span>
)}
</div>
{isSelected && (
<svg className="ml-auto w-4 h-4 text-fg-brand" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</button>
)
})}
</div>
</div>
)}

{open && (
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add ARIA/keyboard semantics for dropdown accessibility.

The dropdown works visually, but screen-reader state and keyboard escape handling are missing.

♿ Suggested accessibility patch
 export function NationalityDropdown() {
   const [open, setOpen] = useState(false)
+  const menuId = 'nationality-menu'
   const { nationality, setNationality } = useFilterStore()
   const { language } = useUiStore()
   const current = NATIONALITY_META[nationality]
 
   return (
-    <div className="relative">
+    <div
+      className="relative"
+      onKeyDown={(e) => {
+        if (e.key === 'Escape') setOpen(false)
+      }}
+    >
       <button
         onClick={() => setOpen((v) => !v)}
+        aria-haspopup="menu"
+        aria-expanded={open}
+        aria-controls={menuId}
         className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-bg-layer-default border border-stroke-neutral-subtle hover:border-stroke-neutral-muted
           text-sm font-medium text-fg-neutral-muted transition-all shadow-sm whitespace-nowrap"
       >
@@
       {open && (
-        <div className="absolute top-full right-0 mt-1.5 w-48 bg-bg-layer-floating rounded-xl shadow-xl border border-stroke-neutral-subtle z-50 overflow-hidden">
+        <div
+          id={menuId}
+          role="menu"
+          className="absolute top-full right-0 mt-1.5 w-48 bg-bg-layer-floating rounded-xl shadow-xl border border-stroke-neutral-subtle z-50 overflow-hidden"
+        >
           <div className="py-1">
             {NATIONALITIES.map((nat) => {
               const meta = NATIONALITY_META[nat]
               const isSelected = nationality === nat
               return (
                 <button
                   key={nat}
+                  role="menuitemradio"
+                  aria-checked={isSelected}
                   onClick={() => { setNationality(nat); setOpen(false) }}
                   className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-bg-layer-fill transition-colors
                     ${isSelected ? 'bg-bg-brand-weak text-fg-brand font-semibold' : 'text-fg-neutral-muted'}`}
                 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/filters/NationalityDropdown.tsx` around lines 19 - 68,
The dropdown lacks ARIA roles and keyboard handling—update the toggle button and
menu rendering to add accessibility: give the toggle button
aria-haspopup="menu", aria-expanded={open}, and aria-controls pointing to the
menu id; ensure the menu container has role="menu" and each option button has
role="menuitem" and tabIndex={-1} (or 0 when focused). Add keyboard handlers: on
the toggle handle Enter/Space to open, and when open capture Escape to call
setOpen(false) and move focus back to the toggle; when opening programmatically
focus the first menu item (use refs for the list items). Use existing symbols
setOpen, open, the toggle button, the menu div that maps NATIONALITIES and the
per-item buttons (accessing NATIONALITY_META) to locate where to add aria
attributes, onKeyDown handlers, refs, and an effect to listen for Escape/focus
management.

Comment on lines +31 to +33
<button
onClick={resetFilters}
style={{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Set explicit button type to prevent accidental form submission.

Please add type="button" on Line 31 so this component remains safe if rendered inside a <form>.

Proposed fix
         <button
+          type="button"
           onClick={resetFilters}
           style={{
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={resetFilters}
style={{
<button
type="button"
onClick={resetFilters}
style={{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/map/EmptyState.tsx` around lines 31 - 33, The button
in the EmptyState component is missing an explicit type which can cause
accidental form submissions; update the JSX <button> element that calls
resetFilters (the button with onClick={resetFilters}) to include type="button"
so it won't act as a submit button when this component is rendered inside a
<form>.

Comment on lines +70 to +89
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(<LegendPanel />)
return container
},
}))()

control.addTo(map)
return () => {
root?.unmount()
control.remove()
}
}, [map])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Legend won't update when language changes.

LegendPanel is rendered into a separate React root created once during mount. When useUiStore().language changes in the main app, this separate root won't re-render because the useEffect has [map] as its only dependency.

Consider adding language to the dependency array and re-rendering the root when it changes:

Proposed fix
 export function MapLegend() {
   const map = useMap()
+  const { language } = useUiStore()

   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(<LegendPanel />)
+        root.render(<LegendPanel language={language} />)
         return container
       },
     }))()

     control.addTo(map)
     return () => {
       root?.unmount()
       control.remove()
     }
-  }, [map])
+  }, [map, language])

   return null
 }

Then update LegendPanel to accept language as a prop instead of calling useUiStore() internally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/components/map/MapLegend.tsx` around lines 70 - 89, The
LegendPanel is mounted into its own React root once in the useEffect (creating
root via createRoot and calling root.render(<LegendPanel />)), so it never
updates when the app language changes; modify the effect for the control created
in useEffect to include language in its dependency array, and when language
changes re-render the separate root (call root.render with the new props) or
unmount/remount the control so the UI updates. Also change LegendPanel to accept
a language prop (instead of reading useUiStore() internally) and pass that prop
into root.render (e.g., root.render(<LegendPanel language={language} />)) so the
isolated root receives updates.

Comment on lines +12 to +17
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] }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "filterStore.ts" -o -name "PriceRangeSlider.tsx" | head -20

Repository: NVIDIA/NemoClaw

Length of output: 147


🏁 Script executed:

cat -n map-app/src/store/filterStore.ts

Repository: NVIDIA/NemoClaw

Length of output: 3477


🏁 Script executed:

cat -n map-app/src/components/filters/PriceRangeSlider.tsx

Repository: NVIDIA/NemoClaw

Length of output: 3810


Add runtime validation to prevent invalid persisted state from breaking the store.

Line 15 uses a type assertion (as PersistedFilters) but does not validate the shape at runtime. If localStorage.getItem(STORAGE_KEY) returns a corrupted or stale payload—such as "null", "{}", or an invalid shape—JSON.parse() succeeds but produces mismatched data that gets spread directly into the store initialization at line 42. This malformed priceRange then flows into PriceRangeSlider.tsx, where calculations like (priceRange[0]/MAX)*100 (lines 46, 69) will produce NaN or fail silently.

Sanitize the parsed value before returning from loadFilters(): validate that activeCategories is an array, nationality matches the union, and priceRange is a tuple of two valid numbers. Fall back to defaults when any member is invalid.

Additionally, extract the default price bounds ([0, 200000]) into a shared constant to reduce duplication between loadFilters() (line 17) and resetFilters() (line 85).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/store/filterStore.ts` around lines 12 - 17, Validate and sanitize
the parsed localStorage payload in loadFilters(): after JSON.parse(raw)
(STORAGE_KEY) confirm the result matches PersistedFilters by checking that
activeCategories is an Array of strings, nationality is one of the allowed union
values, and priceRange is an array/tuple of two finite numbers (first <=
second); if any check fails, return the default filters. Move the default price
bounds ([0, 200000]) into a shared constant (e.g., DEFAULT_PRICE_RANGE) used by
loadFilters() and resetFilters(), and ensure downstream code (like
PriceRangeSlider) receives the validated tuple of two numbers to avoid NaN in
calculations.

Comment on lines +111 to +113
export interface Karaoke extends BaseLocation {
category: 'karaoke'
rates: KaraokeRate[]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the files exist and examine the types file
fd -type f -name "index.ts" | grep types

# Then check the hook file
fd -type f -name "useFilteredMarkers.ts"

Repository: NVIDIA/NemoClaw

Length of output: 400


🏁 Script executed:

# Read the types file to see the Karaoke and TraditionalMarket interfaces
cat -n map-app/src/types/index.ts | head -150

Repository: NVIDIA/NemoClaw

Length of output: 4901


🏁 Script executed:

# Read the hook file to see how Math.min is used
cat -n map-app/src/hooks/useFilteredMarkers.ts | head -80

Repository: NVIDIA/NemoClaw

Length of output: 3412


Encode the non-empty collection invariant or harden the consumer.

map-app/src/hooks/useFilteredMarkers.ts:17-20 (karaoke) and 21-24 (market) call Math.min(...loc.rates.map(...)) and Math.min(...loc.popularItems.map(...)) without an empty check. With KaraokeRate[] and MarketItem[] as standard arrays, empty data still type-checks and turns into Infinity at runtime, skewing price filtering/ranking. (Note: fuel and attraction cases defensively check .length before calling Math.min.) If empty arrays are not valid, model them as non-empty tuples; otherwise guard the hook before calling Math.min.

🧩 Suggested contract change
 export interface Karaoke extends BaseLocation {
   category: 'karaoke'
-  rates: KaraokeRate[]
+  rates: [KaraokeRate, ...KaraokeRate[]]
   hasForeignSongs: boolean
   hasTambourine: boolean
   discountHours?: string
 }
@@
 export interface TraditionalMarket extends BaseLocation {
   category: 'market'
   marketType: string
   marketTypeEn: string
-  popularItems: MarketItem[]
+  popularItems: [MarketItem, ...MarketItem[]]
   operatingDays: string
   closedDay?: string
 }

Also applies to: 127-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/src/types/index.ts` around lines 111 - 113, The issue is that
Karaoke.rates and Market.popularItems can be empty arrays so
Math.min(...loc.rates.map(...)) and Math.min(...loc.popularItems.map(...)) yield
Infinity; fix by hardening the consumer: in useFilteredMarkers (the places
calling Math.min on loc.rates and loc.popularItems) check that loc.rates.length
and loc.popularItems.length are > 0 before calling Math.min and provide a
sensible fallback (e.g., treat as undefined/skip that marker or use a sentinel
value) so Infinity is never used in filtering/sorting; alternatively, if
emptiness is truly invalid, change the model types Karaoke.rates and
Market.popularItems from KaraokeRate[]/MarketItem[] to non-empty tuple types
(e.g., readonly [KaraokeRate, ...KaraokeRate[]]) and update any construction
sites accordingly.

@@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json files with type: module =="
fd -H '^package\.json$' | while read -r f; do
  if rg -n '"type"\s*:\s*"module"' "$f" >/dev/null; then
    echo "$f"
    rg -n '"type"\s*:\s*"module"' "$f"
  fi
done

echo
echo "== vite.config.ts usage of path resolution =="
rg -n --iglob '**/vite.config.ts' '__dirname|import\.meta\.url|fileURLToPath|new URL'

Repository: NVIDIA/NemoClaw

Length of output: 346


Remove __dirname from Vite config—undefined in ESM contexts.

Lines 3 and 9 use __dirname with path.resolve(). Since package.json declares "type": "module", __dirname is undefined at runtime and will break the build.

Use the ESM-safe pattern instead:

ESM-compatible alias setup
 import { defineConfig } from 'vite'
 import react from '@vitejs/plugin-react'
-import path from 'path'
+import { fileURLToPath, URL } from 'node:url'
@@
   resolve: {
     alias: {
-      '@': path.resolve(__dirname, './src'),
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
     },
   },
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@map-app/vite.config.ts` at line 3, The Vite config currently uses the
undefined __dirname (in imports and alias setup); replace __dirname usage with
an ESM-safe derived dirname: import fileURLToPath from 'url' and compute the
directory from import.meta.url, then use that derived variable in the existing
path.resolve()/alias calls (keep references to path.resolve, the alias setup,
and the vite.config.ts export such as the alias entries) so the config runs
under "type: module" ESM.

Comment on lines +1 to 57
# 💰 최저가 지도 (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.

<!-- start-quickstart-guide -->

### 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개일 때 오버레이 안내 + 필터 초기화 버튼

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Emoji usage in technical prose violates coding guidelines.

The README contains extensive emoji usage (💰, 🗺️, 🔍, ❤️, 📍, etc.) throughout technical documentation. As per coding guidelines, Markdown files should have "No emoji in technical prose."

🧰 Tools
🪛 LanguageTool

[style] ~5-~5: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1846 characters long)
Context: ...-61dafb?logo=react)](https://react.dev) TypeScript Vite [![Leaflet](https://img.shields.io/badge/L...

(EN_EXCESSIVE_EXCLAMATION)

🪛 markdownlint-cli2 (0.22.0)

[warning] 23-23: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 30-30: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 38-38: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 43-43: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 47-47: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 52-52: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 55-55: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 1 - 57, The README uses emojis in headings and list
items (e.g. the title line "# 💰 최저가 지도 (Lowest Price Map)" and section headers
like "### 🗺️ 지도 & 핀", "### 🔍 검색 & 필터", and list items with heart/location
emojis) which violates the "No emoji in technical prose" guideline; remove all
emojis from headings, badges, and bullet points and replace them with plain text
equivalents (e.g. "# 최저가 지도 (Lowest Price Map)", "## 지도 & 핀", "## 검색 & 필터", and
"즐겨찾기 / 내 위치"), update any inline emoji characters in feature bullets (💰, 🗺️,
🔍, ❤️, 📍, etc.) to text labels, and ensure README.md still conveys the same
information and structure without emojis.

Comment on lines +1 to +131
# 💰 최저가 지도 (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.

<!-- start-quickstart-guide -->

### 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
```

<!-- end-quickstart-guide -->

---

## 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 <name> 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 <policy-file>` 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 <name> 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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if map-app already has its own README or if there's documentation structure
fd -t f 'README' map-app/
# Check if there are other docs that reference the original README content
rg -l 'NemoClaw|OpenClaw' --type md

Repository: NVIDIA/NemoClaw

Length of output: 849


Move this README to map-app/README.md and restore the root README with NemoClaw documentation.

This change replaces the entire root README with unrelated map application documentation. The map-app directory currently has no README; place this content there instead of at the repository root.

Additionally, this README violates the no-emoji-in-technical-prose guideline. Remove or significantly reduce emoji usage throughout (💰, 🗺️, 🔍, ❤️, 📍, 📱, 🌐, 🌑, etc.).

🧰 Tools
🪛 LanguageTool

[style] ~5-~5: Using many exclamation marks might seem excessive (in this case: 3 exclamation marks for a text that’s 1846 characters long)
Context: ...-61dafb?logo=react)](https://react.dev) TypeScript Vite [![Leaflet](https://img.shields.io/badge/L...

(EN_EXCESSIVE_EXCLAMATION)

🪛 markdownlint-cli2 (0.22.0)

[warning] 23-23: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 30-30: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 38-38: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 43-43: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 47-47: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 52-52: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 55-55: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 79-79: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 107-107: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants