Conversation
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
📝 WalkthroughWalkthroughThe 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
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Comment |
|
ㄴㄴ |
There was a problem hiding this comment.
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 extractingCATEGORIESto a shared constant.This array is duplicated in
MapLegend.tsx,Sidebar.tsx, and here. Extract to@/typesalongsideCATEGORY_METAto 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 ofuseUiStore.
useUiStoreis imported twice—once on line 2 and again on line 4 with the aliasuseUi. 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
setSelectedPinalongsidelanguageon 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 insiderenderDetail().
selectedPinis 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 === 0is only true whenminAmountis defined and equals 0, the!== undefinedcheck 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, andsizeLabelEnare 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
MenuItemgains 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: SameMath.min()on potentially empty array concern applies here.As noted in
MarkerLayer.tsx, ifloc.rates(karaoke) orloc.popularItems(market) are empty,Math.min(...[])returnsInfinity. 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.
200000is hardcoded here and also separately asMAXinmap-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
⛔ Files ignored due to path filters (1)
map-app/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (50)
README.mdmap-app/index.htmlmap-app/package.jsonmap-app/postcss.config.cjsmap-app/src/App.tsxmap-app/src/components/common/CategoryIcon.tsxmap-app/src/components/common/PriceBadge.tsxmap-app/src/components/filters/CategoryFilterBar.tsxmap-app/src/components/filters/NationalityDropdown.tsxmap-app/src/components/filters/PriceRangeSlider.tsxmap-app/src/components/layout/Sidebar.tsxmap-app/src/components/layout/TopBar.tsxmap-app/src/components/map/CategoryPin.tsxmap-app/src/components/map/EmptyState.tsxmap-app/src/components/map/MapContainer.tsxmap-app/src/components/map/MapLegend.tsxmap-app/src/components/map/MarkerLayer.tsxmap-app/src/components/map/MyLocationButton.tsxmap-app/src/components/panels/DetailPanel.tsxmap-app/src/components/panels/ExchangeDetail.tsxmap-app/src/components/panels/FuelDetail.tsxmap-app/src/components/panels/GenericDetail.tsxmap-app/src/components/panels/RestaurantDetail.tsxmap-app/src/data/attractions.jsonmap-app/src/data/cafes.jsonmap-app/src/data/convenience.jsonmap-app/src/data/exchange.jsonmap-app/src/data/extras.jsonmap-app/src/data/fuel.jsonmap-app/src/data/jjimjilbang.jsonmap-app/src/data/karaoke.jsonmap-app/src/data/markets.jsonmap-app/src/data/restaurants.jsonmap-app/src/hooks/useAllLocations.tsmap-app/src/hooks/useAllPriceRanks.tsmap-app/src/hooks/useFilteredMarkers.tsmap-app/src/hooks/usePriceRanks.tsmap-app/src/index.cssmap-app/src/main.tsxmap-app/src/store/favoritesStore.tsmap-app/src/store/filterStore.tsmap-app/src/store/index.tsmap-app/src/store/uiStore.tsmap-app/src/types/common.tsmap-app/src/types/index.tsmap-app/src/vite-env.d.tsmap-app/tailwind.config.tsmap-app/tsconfig.jsonmap-app/tsconfig.node.jsonmap-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" |
There was a problem hiding this comment.
🧩 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.jsonRepository: 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.mjsRepository: 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) |
There was a problem hiding this comment.
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.
| <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)} /> | ||
| )} |
There was a problem hiding this comment.
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.
| <button | ||
| onClick={resetFilters} | ||
| style={{ |
There was a problem hiding this comment.
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.
| <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>.
| 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]) |
There was a problem hiding this comment.
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.
| 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] } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "filterStore.ts" -o -name "PriceRangeSlider.tsx" | head -20Repository: NVIDIA/NemoClaw
Length of output: 147
🏁 Script executed:
cat -n map-app/src/store/filterStore.tsRepository: NVIDIA/NemoClaw
Length of output: 3477
🏁 Script executed:
cat -n map-app/src/components/filters/PriceRangeSlider.tsxRepository: 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.
| export interface Karaoke extends BaseLocation { | ||
| category: 'karaoke' | ||
| rates: KaraokeRate[] |
There was a problem hiding this comment.
🧩 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 -150Repository: 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 -80Repository: 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' | |||
There was a problem hiding this comment.
🧩 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.
| # 💰 최저가 지도 (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 | ||
| [](https://react.dev) | ||
| [](https://www.typescriptlang.org) | ||
| [](https://vitejs.dev) | ||
| [](https://leafletjs.com) | ||
| [](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개일 때 오버레이 안내 + 필터 초기화 버튼 | ||
|
|
There was a problem hiding this comment.
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)
[
🪛 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.
| # 💰 최저가 지도 (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 | ||
| [](https://react.dev) | ||
| [](https://www.typescriptlang.org) | ||
| [](https://vitejs.dev) | ||
| [](https://leafletjs.com) | ||
| [](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) |
There was a problem hiding this comment.
🧩 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 mdRepository: 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)
[
🪛 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)
Summary
Related Issue
Changes
Type of Change
Testing
npx prek run --all-filespasses (or equivalentlymake check).npm testpasses.make docsbuilds without warnings. (for doc-only changes)Checklist
General
Code Changes
npx prek run --all-filesauto-fixes formatting (ormake formatfor targeted runs).Doc Changes
update-docsagent skill to draft changes while complying with the style guide. For example, prompt your agent with "/update-docscatch up the docs for the new changes I made in this PR."Signed-off-by: Your Name your-email@example.com
Summary by CodeRabbit
Release Notes