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 />}
<ShareCollabButton />
</div>
</MapChips>
Expand Down
11 changes: 6 additions & 5 deletions src/features/map/collaboration/CollabCursorsOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ 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<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 @@ -72,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);
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