Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) 로직을 기본으로 포함하여 코드를 제안한다.
15 changes: 15 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) 로직을 기본으로 포함하여 코드를 제안한다.


<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know
Expand Down
2 changes: 2 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
4 changes: 3 additions & 1 deletion src/features/map/MapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -573,7 +574,8 @@ export default function MapPage() {
</FloatingHomeButton>
)}
<CategoryChips align="start" />
<div style={{ marginLeft: 'auto' }}>
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
{collabRoom && <CollabSessionBadge />}
Comment thread
yshls marked this conversation as resolved.
<ShareCollabButton />
</div>
</MapChips>
Expand Down
8 changes: 4 additions & 4 deletions src/features/map/collaboration/CollabCursorsOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function CollabCursorsOverlay() {
const container = document.querySelector<HTMLElement>('[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;
Expand All @@ -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);
Expand Down
17 changes: 12 additions & 5 deletions src/features/map/collaboration/CollabPinsOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -135,9 +135,10 @@ const ActionRow = styled.div`

export function CollabPinsOverlay() {
const map = useMapStore((s) => s.map);
const mode = useMapStore((s) => s.mode);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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
Expand All @@ -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
Expand Down Expand Up @@ -184,7 +191,7 @@ export function CollabPinsOverlay() {
// Malformed pin — skip silently
}
});
}, [map, pins]);
}, [map, pins, mode]);

// Cleanup on unmount
useEffect(() => {
Expand All @@ -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;
Expand Down
50 changes: 50 additions & 0 deletions src/features/map/collaboration/CollabSessionBadge.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Badge aria-label="공유 지도 세션 중">
<HugeiconsIcon icon={UserMultiple02Icon} size={14} strokeWidth={2} />
<Dot />
공유된 지도를 보고 있어요
</Badge>
);
}
Loading