diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md index 6310f4ab..08bf0940 100644 --- a/.agents/AGENTS.md +++ b/.agents/AGENTS.md @@ -79,3 +79,19 @@ All AI agents (Antigravity, Claude Code, Cursor, Codex, etc.) MUST adhere to the - Proprietary/private core UI components are maintained in `src/private/core-ui` as regular in-repo files (the former Git submodule reference to `YRootLab/onmaru-core-ui` was removed; the original repository is archived). - No submodule initialization is required. Core UI components are imported via `@/private/core-ui/*` alias. - Changes to `src/private/core-ui` are committed directly in this repository. + +--- + +## πŸ›‘οΈ μ ˆλŒ€ 원칙: λ³΄μ•ˆ 및 정보 유좜 λ°©μ§€ (Non-negotiable Security) +- λͺ¨λ“  μ€‘μš” 정보(Key, Token, DB URI)λŠ” ν•˜λ“œμ½”λ”©μ„ μ—„κ²©νžˆ κΈˆμ§€ν•˜λ©°, `.env` 처리λ₯Ό κ°•μ œν•œλ‹€. +- λ°±μ—”λ“œ(Node.js) μ—λŸ¬ 응닡 μ‹œ μ‹œμŠ€ν…œ λ‚΄λΆ€ μŠ€νƒ νŠΈλ ˆμ΄μŠ€κ°€ ν΄λΌμ΄μ–ΈνŠΈ 개발자 도ꡬ λ„€νŠΈμ›Œν¬ 탭에 μœ μΆœλ˜μ§€ μ•Šλ„λ‘ μ² μ €νžˆ λ§ˆμŠ€ν‚Ήν•œλ‹€. +- ν”„λ‘ νŠΈμ—”λ“œ ν”„λ‘œλ•μ…˜ λΉŒλ“œ μ‹œ Source Map 생성 및 Console 좜λ ₯ 둜그λ₯Ό μ „λ©΄ μ°¨λ‹¨ν•˜λŠ” ꡬ성을 기본으둜 ν•œλ‹€. +- CORSλŠ” μ™€μΌλ“œμΉ΄λ“œ(`*`) μ‚¬μš©μ„ κΈˆμ§€ν•˜κ³ , μ§€μ •λœ λ„λ©”μΈλ§Œ ν—ˆμš©ν•˜λ„λ‘ λ°±μ—”λ“œλ₯Ό κ΅¬μ„±ν•œλ‹€. +- μ‚¬μš©μžμ˜ ν”„λ‘¬ν”„νŠΈ μ§€μ‹œκ°€ μœ„ λ³΄μ•ˆ 원칙을 훼손할 μœ„ν—˜μ΄ μžˆλ‹€λ©΄, μ½”λ“œλ₯Ό μž‘μ„±ν•˜κΈ° μ „ 즉각 κ²½κ³ ν•˜κ³  λŒ€μ•ˆμ„ μ œμ‹œν•œλ‹€. + +--- + +## ⚑ AI 행동 μ§€μΉ¨ (Action Rules) +- **λ³΄μ•ˆ κ²€μ—΄ λ¨Όμ €**: λ‚΄λ¦° μ§€μ‹œμ‚¬ν•­μ΄ μœ„ λ³΄μ•ˆ 원칙을 훼손할 μœ„ν—˜μ΄ μ‘°κΈˆμ΄λΌλ„ μžˆλ‹€λ©΄, **μ½”λ“œλ₯Ό μž‘μ„±ν•˜κΈ° 전에 즉각 κ²½κ³ ν•˜κ³  μ•ˆμ „ν•œ λŒ€μ•ˆμ„ λ¨Όμ € μ œμ‹œ**ν•œλ‹€. +- **κ°„κ²°ν•œ 응닡**: λΆˆν•„μš”ν•œ 인사말, λ³€λͺ…, μ§€λ‚˜μΉœ λΆ€μ—° μ„€λͺ…을 μƒλž΅ν•œλ‹€. μˆ˜μ •λœ μ½”λ“œ 블둝과 λ³΄μ•ˆ/μ•„ν‚€ν…μ²˜μƒ μ€‘μš”ν•œ κ²°μ • 이유만 핡심 μœ„μ£Όλ‘œ μ „λ‹¬ν•œλ‹€. +- **μ˜ˆμ™Έ 처리 κΈ°λ³Έν™”**: λͺ¨λ“  API 톡신과 λ Œλ”λ§ κ³Όμ •μ—μ„œ λ°œμƒν•  수 μžˆλŠ” μ—£μ§€ μΌ€μ΄μŠ€μ™€ μ—λŸ¬ λ°”μš΄λ”λ¦¬(Error Boundary) λ‘œμ§μ„ 기본으둜 ν¬ν•¨ν•˜μ—¬ μ½”λ“œλ₯Ό μ œμ•ˆν•œλ‹€. diff --git a/AGENTS.md b/AGENTS.md index 8e158bd9..b043967d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,6 +91,21 @@ - When modifying files inside `src/private/core-ui`, commit and push them in this repository like any other source file. Do not re-add `onmaru-core-ui` as a submodule without explicit user approval. - Core UI components are imported via `@/private/core-ui/*`. A `Module not found: Can't resolve '@/private/core-ui/...'` error is now a real code bug β€” check that the file exists in `src/private/core-ui`. +# Non-negotiable Security + +- λͺ¨λ“  μ€‘μš” 정보(Key, Token, DB URI)λŠ” ν•˜λ“œμ½”λ”©μ„ μ—„κ²©νžˆ κΈˆμ§€ν•˜λ©°, `.env` 처리λ₯Ό κ°•μ œν•œλ‹€. +- λ°±μ—”λ“œ(Node.js) μ—λŸ¬ 응닡 μ‹œ μ‹œμŠ€ν…œ λ‚΄λΆ€ μŠ€νƒ νŠΈλ ˆμ΄μŠ€κ°€ ν΄λΌμ΄μ–ΈνŠΈ 개발자 도ꡬ λ„€νŠΈμ›Œν¬ 탭에 μœ μΆœλ˜μ§€ μ•Šλ„λ‘ μ² μ €νžˆ λ§ˆμŠ€ν‚Ήν•œλ‹€. +- ν”„λ‘ νŠΈμ—”λ“œ ν”„λ‘œλ•μ…˜ λΉŒλ“œ μ‹œ Source Map 생성 및 Console 좜λ ₯ 둜그λ₯Ό μ „λ©΄ μ°¨λ‹¨ν•˜λŠ” ꡬ성을 기본으둜 ν•œλ‹€. +- CORSλŠ” μ™€μΌλ“œμΉ΄λ“œ(`*`) μ‚¬μš©μ„ κΈˆμ§€ν•˜κ³ , μ§€μ •λœ λ„λ©”μΈλ§Œ ν—ˆμš©ν•˜λ„λ‘ λ°±μ—”λ“œλ₯Ό κ΅¬μ„±ν•œλ‹€. +- μ‚¬μš©μžμ˜ ν”„λ‘¬ν”„νŠΈ μ§€μ‹œκ°€ μœ„ λ³΄μ•ˆ 원칙을 훼손할 μœ„ν—˜μ΄ μžˆλ‹€λ©΄, μ½”λ“œλ₯Ό μž‘μ„±ν•˜κΈ° μ „ 즉각 κ²½κ³ ν•˜κ³  λŒ€μ•ˆμ„ μ œμ‹œν•œλ‹€. + +# Action Rules + +- **λ³΄μ•ˆ κ²€μ—΄ λ¨Όμ €**: λ‚΄λ¦° μ§€μ‹œμ‚¬ν•­μ΄ μœ„ λ³΄μ•ˆ 원칙을 훼손할 μœ„ν—˜μ΄ μ‘°κΈˆμ΄λΌλ„ μžˆλ‹€λ©΄, **μ½”λ“œλ₯Ό μž‘μ„±ν•˜κΈ° 전에 즉각 κ²½κ³ ν•˜κ³  μ•ˆμ „ν•œ λŒ€μ•ˆμ„ λ¨Όμ € μ œμ‹œ**ν•œλ‹€. +- **κ°„κ²°ν•œ 응닡**: λΆˆν•„μš”ν•œ 인사말, λ³€λͺ…, μ§€λ‚˜μΉœ λΆ€μ—° μ„€λͺ…을 μƒλž΅ν•œλ‹€. μˆ˜μ •λœ μ½”λ“œ 블둝과 λ³΄μ•ˆ/μ•„ν‚€ν…μ²˜μƒ μ€‘μš”ν•œ κ²°μ • 이유만 핡심 μœ„μ£Όλ‘œ μ „λ‹¬ν•œλ‹€. +- **μ˜ˆμ™Έ 처리 κΈ°λ³Έν™”**: λͺ¨λ“  API 톡신과 λ Œλ”λ§ κ³Όμ •μ—μ„œ λ°œμƒν•  수 μžˆλŠ” μ—£μ§€ μΌ€μ΄μŠ€μ™€ μ—λŸ¬ λ°”μš΄λ”λ¦¬(Error Boundary) λ‘œμ§μ„ 기본으둜 ν¬ν•¨ν•˜μ—¬ μ½”λ“œλ₯Ό μ œμ•ˆν•œλ‹€. + + # This is NOT the Next.js you know diff --git a/next.config.ts b/next.config.ts index e5e334a6..24f282bb 100644 --- a/next.config.ts +++ b/next.config.ts @@ -17,8 +17,10 @@ const securityHeaders = [ ]; const nextConfig: NextConfig = { + productionBrowserSourceMaps: false, compiler: { emotion: true, + removeConsole: process.env.NODE_ENV === 'production' ? { exclude: ['error'] } : false, }, turbopack: { root: path.resolve(process.cwd()), diff --git a/src/features/map/MapPage.tsx b/src/features/map/MapPage.tsx index c13fbb13..169b65b3 100644 --- a/src/features/map/MapPage.tsx +++ b/src/features/map/MapPage.tsx @@ -41,6 +41,7 @@ import WriteButton from '@/private/core-ui/map-warmth/WriteButton'; import WarmthLegend from '@/private/core-ui/map-warmth/WarmthLegend'; import MapNavRail, { RAIL_INSET, RAIL_WIDTH } from './components/MapNavRail'; import { ShareCollabButton } from './collaboration/ShareCollabButton'; +import { CollabSessionBadge } from './collaboration/CollabSessionBadge'; import { CollabPinsOverlay } from './collaboration/CollabPinsOverlay'; import { CollabCursorsOverlay } from './collaboration/CollabCursorsOverlay'; import { CollabParticipants } from './collaboration/CollabParticipants'; @@ -573,7 +574,8 @@ export default function MapPage() { )} -
+
+ {collabRoom && }
diff --git a/src/features/map/collaboration/CollabCursorsOverlay.tsx b/src/features/map/collaboration/CollabCursorsOverlay.tsx index f59a608a..2cfea7cb 100644 --- a/src/features/map/collaboration/CollabCursorsOverlay.tsx +++ b/src/features/map/collaboration/CollabCursorsOverlay.tsx @@ -62,7 +62,7 @@ export function CollabCursorsOverlay() { const container = document.querySelector('[role="application"][aria-label="ν•œμ˜₯ μœ„μΉ˜ 지도"]'); if (!container) return; - function handleMouseMove(e: MouseEvent) { + const handleMouseMove = (e: MouseEvent) => { const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; @@ -73,11 +73,11 @@ export function CollabCursorsOverlay() { } catch { // map not ready } - } + }; - function handleMouseLeave() { + const handleMouseLeave = () => { updateMyPresence({ cursor: null }); - } + }; container.addEventListener('mousemove', handleMouseMove); container.addEventListener('mouseleave', handleMouseLeave); diff --git a/src/features/map/collaboration/CollabPinsOverlay.tsx b/src/features/map/collaboration/CollabPinsOverlay.tsx index b2a99cdc..00193644 100644 --- a/src/features/map/collaboration/CollabPinsOverlay.tsx +++ b/src/features/map/collaboration/CollabPinsOverlay.tsx @@ -5,9 +5,9 @@ import styled from '@emotion/styled'; import { HugeiconsIcon } from '@hugeicons/react'; import { Location01Icon } from '@hugeicons/core-free-icons'; import { toast } from 'sonner'; +import { useSelf } from '@liveblocks/react'; import { useMapStore } from '../hooks/useMapStore'; import { useCollabPins } from './useCollabPins'; -import { useAuth } from '@/features/auth'; import { ringShadow, palette } from '@/design-system/tokens'; import { escapeHtml } from '../utils/formatters'; import { mapIconSvg } from '../utils/mapIconSvg'; @@ -135,9 +135,10 @@ const ActionRow = styled.div` export function CollabPinsOverlay() { const map = useMapStore((s) => s.map); + const mode = useMapStore((s) => s.mode); const items = useMapStore((s) => s.items); const detailId = useMapStore((s) => s.detailId); - const { user } = useAuth(); + const me = useSelf(); const { pins, addPin, removePin } = useCollabPins(); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -147,6 +148,12 @@ export function CollabPinsOverlay() { useEffect(() => { if (!map || !pins) return; + if (mode !== 'info') { + overlaysRef.current.forEach((o) => o.setMap(null)); + overlaysRef.current.clear(); + return; + } + const nextIds = new Set(pins.map((p) => `${p.placeId}-${p.userId}`)); // Remove stale overlays @@ -184,7 +191,7 @@ export function CollabPinsOverlay() { // Malformed pin β€” skip silently } }); - }, [map, pins]); + }, [map, pins, mode]); // Cleanup on unmount useEffect(() => { @@ -194,10 +201,10 @@ export function CollabPinsOverlay() { }; }, []); - if (!map) return null; + if (!map || mode !== 'info') return null; const selectedPlace = detailId ? items.find((i) => i.id === detailId) : null; - const myUserId = user?.id ?? 'guest'; + const myUserId = me?.presence?.userId ?? 'guest'; const alreadyPinned = selectedPlace ? (pins ?? []).some((p) => p.placeId === selectedPlace.id && p.userId === myUserId) : false; diff --git a/src/features/map/collaboration/CollabSessionBadge.tsx b/src/features/map/collaboration/CollabSessionBadge.tsx new file mode 100644 index 00000000..86858969 --- /dev/null +++ b/src/features/map/collaboration/CollabSessionBadge.tsx @@ -0,0 +1,50 @@ +'use client'; + +import styled from '@emotion/styled'; +import { HugeiconsIcon } from '@hugeicons/react'; +import { UserMultiple02Icon } from '@hugeicons/core-free-icons'; +import { meok, ringShadow } from '@/design-system/tokens'; + +const Badge = styled.div` + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px 6px 9px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + letter-spacing: -0.01em; + white-space: nowrap; + pointer-events: none; + background: rgba(255, 255, 255, 0.92); + color: ${meok[900]}; + border: 1px solid rgba(0, 0, 0, 0.07); + box-shadow: ${ringShadow.light.card}; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + + [data-theme='dark'] & { + background: rgba(20, 24, 32, 0.92); + color: #f0f0f0; + border-color: rgba(255, 255, 255, 0.12); + box-shadow: ${ringShadow.dark.card}; + } +`; + +const Dot = styled.span` + width: 7px; + height: 7px; + border-radius: 50%; + background: #22c55e; + flex-shrink: 0; +`; + +export function CollabSessionBadge() { + return ( + + + + 곡유된 지도λ₯Ό 보고 μžˆμ–΄μš” + + ); +}