From b11cf8acbb4f5d776917553226df80eb57b76f80 Mon Sep 17 00:00:00 2001 From: yshls Date: Tue, 27 Oct 2026 01:15:07 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(map):=20CollabCursorsOverlay=20getConta?= =?UTF-8?q?iner=20=E2=86=92=20DOM=20querySelector=EB=A1=9C=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kakao Maps에 getContainer() 메서드가 없어 TypeError 발생. role=application aria-label으로 맵 컨테이너 요소를 직접 조회. Co-Authored-By: Claude Sonnet 4.6 --- src/features/map/collaboration/CollabCursorsOverlay.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/map/collaboration/CollabCursorsOverlay.tsx b/src/features/map/collaboration/CollabCursorsOverlay.tsx index 9b3aab2d..f59a608a 100644 --- a/src/features/map/collaboration/CollabCursorsOverlay.tsx +++ b/src/features/map/collaboration/CollabCursorsOverlay.tsx @@ -59,7 +59,8 @@ export function CollabCursorsOverlay() { // Track my mouse position → update presence cursor as lat/lng useEffect(() => { if (!map) return; - const container = map.getContainer() as HTMLElement; + const container = document.querySelector('[role="application"][aria-label="한옥 위치 지도"]'); + if (!container) return; function handleMouseMove(e: MouseEvent) { const rect = container.getBoundingClientRect(); From b477036eee0c686e2b4fa00c7b98391cf8ef79b7 Mon Sep 17 00:00:00 2001 From: yshls Date: Tue, 27 Oct 2026 01:17:23 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(map):=20=EA=B3=B5=EC=9C=A0=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=83=81=ED=83=9C=20=EB=B0=B0=EC=A7=80=20+=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=AA=A8=EB=93=9C=20=ED=95=80=20=EC=A0=9C?= =?UTF-8?q?=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CollabSessionBadge: room URL 접속 시 '공유된 지도를 보고 있어요' 배지 표시 - CollabPinsOverlay: mode !== 'info' 이면 핀 공유 버튼 미렌더 (정보 모드 전용) Co-Authored-By: Claude Sonnet 4.6 --- src/features/map/MapPage.tsx | 4 +- .../map/collaboration/CollabPinsOverlay.tsx | 3 +- .../map/collaboration/CollabSessionBadge.tsx | 50 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 src/features/map/collaboration/CollabSessionBadge.tsx 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/CollabPinsOverlay.tsx b/src/features/map/collaboration/CollabPinsOverlay.tsx index b2a99cdc..7c4aac8c 100644 --- a/src/features/map/collaboration/CollabPinsOverlay.tsx +++ b/src/features/map/collaboration/CollabPinsOverlay.tsx @@ -135,6 +135,7 @@ 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(); @@ -194,7 +195,7 @@ 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'; 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 ( + + + + 공유된 지도를 보고 있어요 + + ); +} From 608ec64a88c1ceea033b0c77c794fb1734e2bf44 Mon Sep 17 00:00:00 2001 From: yshls Date: Tue, 27 Oct 2026 02:30:23 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(map):=20=EC=BB=A4=EC=84=9C=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=EB=A0=88=EC=9D=B4=20=ED=83=80=EC=9E=85=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95,=20=EB=B9=84=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EB=AA=A8=EB=93=9C=20=ED=95=80=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CollabCursorsOverlay: 함수 호이스팅으로 인한 container 널 체크 미전파(TS18047) 수정 (화살표 함수로 전환) - CollabPinsOverlay: mode !== 'info' 시 오버레이 정리 로직 추가 및 의존성 배열에 mode 반영 - CollabPinsOverlay: 핀 등록 시 실제 회원 ID(user.id) 대신 익명 세션 ID(me.presence.userId)를 사용하여 사용자 정보 유출 방지 - next.config.ts: 프로덕션 빌드 시 브라우저 소스맵 생성 및 콘솔 출력 차단 설정 추가 - AGENTS.md: 보안 절대 원칙 및 AI 행동 지침 추가 동기화 --- .agents/AGENTS.md | 16 ++++++++++++++++ AGENTS.md | 15 +++++++++++++++ next.config.ts | 2 ++ .../map/collaboration/CollabCursorsOverlay.tsx | 8 ++++---- .../map/collaboration/CollabPinsOverlay.tsx | 14 ++++++++++---- 5 files changed, 47 insertions(+), 8 deletions(-) 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/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 7c4aac8c..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'; @@ -138,7 +138,7 @@ export function CollabPinsOverlay() { 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 @@ -148,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 @@ -185,7 +191,7 @@ export function CollabPinsOverlay() { // Malformed pin — skip silently } }); - }, [map, pins]); + }, [map, pins, mode]); // Cleanup on unmount useEffect(() => { @@ -198,7 +204,7 @@ export function CollabPinsOverlay() { 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;