From 9ffd6035b083154f72e196a4a8db85f6ecc51176 Mon Sep 17 00:00:00 2001
From: twoimo <32544727+twoimo@users.noreply.github.com>
Date: Fri, 23 Oct 2026 16:39:28 +0900
Subject: [PATCH] docs: streamline bilingual repository READMEs
---
README.ko.md | 163 ++++++++++++++++++------------------
README.md | 231 +++++++++++++++++----------------------------------
2 files changed, 156 insertions(+), 238 deletions(-)
diff --git a/README.ko.md b/README.ko.md
index 75080bac0b..28d4c400e6 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -1,119 +1,120 @@
+

+
쯔동여지도 · Tzudong Map
+
쯔양 영상에 등장한 맛집을 지도에서 찾고, 방문 경험을 나누는 서비스입니다.
-
-
-
Tzudong Map
-
쯔양 영상 속 맛집을 지도 중심으로 탐색하는 제품입니다.
-
- 라이브 앱
- ·
- 최신 릴리즈
- ·
- English
- ·
+ 지도 둘러보기 ·
+ English ·
+ 문서 안내 ·
변경 기록
- ·
- MIT
-
-
-
-
+
+
+
----
+[기능](#기능) · [미리보기](#미리보기) · [로컬 개발](#로컬-개발) · [저장소 구성](#저장소-구성) · [문서 안내](#문서-안내)
-Tzudong Map은 먹방 영상의 장소 근거를 사용자용 지도, 운영자용 검수 콘솔, 콘텐츠 제작용 스토리보드 워크스페이스로 바꿉니다.
+## 기능
-[핵심 기능](#핵심-기능) · [스택](#스택) · [빠른 시작](#빠른-시작) · [제품 투어](#제품-투어)
+- **맛집 탐색:** 국내·해외 지도에서 맛집을 검색·필터링하고, 상세 패널에서 영상 출처를 확인합니다.
+- **방문 경험 공유:** 리뷰, 도장, 랭킹, 좋아요, 프로필을 통해 방문 기록과 커뮤니티 활동을 이어갑니다.
+- **데이터 관리:** 보호된 관리자 콘솔에서 검수, 출처 확인, 승인과 변경 이력을 관리합니다.
+- **콘텐츠 제작:** 스토리보드·썸네일 워크스페이스에서 기획, 생성, 수정을 진행합니다.
+- **영상 근거 처리:** 백엔드 워커가 크롤링, 평가, 미디어 처리와 공개용 데이터 생성을 담당합니다.
-## 핵심 기능
-
-| 제품 영역 | 역할 |
-| --- | --- |
-| **지도 탐색** | 검색, 필터, 음식 마커 클러스터, 현재 위치 흐름, 맛집 상세 바텀시트. |
-| **커뮤니티 루프** | 리뷰, 도장, 랭킹, 좋아요, 프로필 화면으로 반복 사용 경험 구성. |
-| **관리자 운영** | 보호된 검수, source readback, 승인/삭제/복원, 감사 가능한 mutation 흐름. |
-| **스토리보드 워크스페이스** | 채팅 기반 기획, 10컷 생성, 컷 메타데이터, 이미지 재생성, provider readiness UX. |
-| **근거 중심 파이프라인** | 크롤링, Rule/LLM-as-a-Judge 평가, fail-closed 검증, Supabase 적재 payload 생성. |
+## 미리보기
-## 스택
+저장소에 보존된 화면 시연입니다.
-- 프런트엔드: Next.js 16 (App Router), React 19, Tailwind CSS, Lucide 아이콘.
-- 웹 런타임: Node 24.x. 일상 설치/유닛은 Bun을 쓸 수 있고, 릴리스 패키지 권위는 npm 11.6.2, `package.json`, `package-lock.json`입니다.
-- TypeScript: 네이티브 CLI `@typescript/native` `7.0.2`, 안정 API/호환 브리지는 `6.0.2` (`npm run typecheck:parity`).
-- 직렬 승격: `develop -> data -> main`. 이 트리는 hosted apply, 법령 준수, 라이브 URL 상태를 주장하지 않습니다.
-- 변경 기록: [CHANGELOG.ko.md](CHANGELOG.ko.md) / [CHANGELOG.md](CHANGELOG.md).
-- 문서 색인: [docs/README.md](docs/README.md). 제품 설계: [docs/product/DESIGN.md](docs/product/DESIGN.md).
-
-## 빠른 시작
-
-Node 24.x와 npm 11.6.2를 사용합니다. 웹 패키지는 `apps/web`에 있습니다.
+
+
+
-```bash
-git clone https://github.com/twoimo/tzudong.git
-cd tzudong/apps/web
-npm ci
-```
+
+모바일 화면과 스토리보드 워크스페이스
-저장소가 관리하는 [로컬 Supabase 스택](backend/supabase/README.md)을 준비하고 마이그레이션한 뒤, `apps/web`에서 실행합니다.
+
+
+ 홈 지도
 |
+ 리뷰 피드
 |
+
+
+ 도장
 |
+ 랭킹·프로필
 |
+
+
-```bash
-npm run dev -- --port 8080
-```
+
-[http://127.0.0.1:8080](http://127.0.0.1:8080)을 엽니다. 개발 실행기는 생성된 로컬 스택, 허용된 브라우저 origin, 마이그레이션 ledger를 검증한 뒤 webpack으로 Next.js를 시작합니다. Bun 명령과 명시적 hosted 디버깅은 [웹 개발 안내](apps/web/README.md)를 참고하세요.
+
-## 제품 투어
+## 로컬 개발
-### Desktop
+[apps/web/package.json](apps/web/package.json)에 선언된 **Node.js 24.x**와 **npm 11.6.2**를 사용합니다. 로컬 데이터베이스에는 Python 3와 Docker Compose도 필요합니다. 고정된 도구 버전과 준비 사항은 [로컬 스택 운영 안내](docs/operations/nightly-regression.md#local-compose-bootstrap)를 따릅니다.
-**지도 탐색과 맛집 상세**
+1. 저장소를 복제하고 웹 의존성을 설치합니다.
-
-
-
+ ```sh
+ git clone https://github.com/twoimo/tzudong.git
+ cd tzudong/apps/web
+ npm ci
+ ```
-**관리자 스토리보드 워크스페이스**
+2. 저장소 루트에서 [준비 절차](docs/operations/nightly-regression.md#local-compose-bootstrap)에 따라 저장소가 관리하는 [로컬 Supabase 스택](backend/supabase/README.md)을 생성하고 마이그레이션합니다.
+3. `apps/web`로 돌아와 개발 서버를 실행합니다.
-
+ ```sh
+ npm run dev
+ ```
-### Mobile
+[http://127.0.0.1:3000](http://127.0.0.1:3000)을 엽니다. 실행기는 생성된 로컬 환경, 서비스 준비 상태, 브라우저 origin, 마이그레이션 대장을 확인한 뒤 Next.js를 시작합니다. Hosted 디버깅과 지도 공급자 설정은 [웹 개발 안내](apps/web/README.md)를 참고하세요.
-
-
- 홈 지도 마커 탐색 → 맛집 상세 전체 보기
 |
- 리뷰 피드 리뷰 스크롤 → 맛집 상세 열기
 |
-
-
- 도장 페이지 도장 맛집 탐색 → 상세 읽기
 |
- 랭킹 프로필 1위 프로필 → 탭 전환
 |
-
-
+일상 개발에는 Bun도 사용할 수 있습니다. 릴리스 패키지의 기준은 npm manifest와 lockfile입니다. `apps/web`에서 사용하는 주요 검사 명령은 다음과 같습니다.
-## 로컬 아키텍처
+```sh
+npm run lint
+npm run test:unit
+npm run typecheck:parity
+```
-대화형 워크플로: [storyboard-local-mlx.html](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.html)
+변경한 동작에 필요한 검사는 [검증 안내](docs/agents/verification.md)에 따라 선택합니다.
-워크플로 소스: [storyboard-local-mlx.workflow.json](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.workflow.json)
+## 저장소 구성
-대화형 라이프사이클: [storyboard-local-mlx.lifecycle.html](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.lifecycle.html)
+| 경로 | 역할 |
+| --- | --- |
+| [apps/web/](apps/web/) | Next.js 공개 앱, 맛집 지도, 커뮤니티, 보호된 관리자 콘솔. |
+| [backend/](backend/) | 파이프라인 제어, 크롤러, 평가, 미디어 워커, Supabase 마이그레이션. |
+| [docs/](docs/) | 제품 설계, 아키텍처, 운영과 릴리스 안내. |
-라이프사이클 소스: [storyboard-local-mlx.lifecycle.json](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.lifecycle.json)
+웹 앱과 백엔드 워커는 Supabase를 데이터 저장 경계로 사용합니다. 오래 걸리는 크롤링·미디어 처리는 백엔드 워커가 맡습니다. 상세 흐름은 [백엔드 아키텍처](backend/ARCHITECTURE.md)와 [데이터 계약](backend/DATA_CONTRACTS.md)에 있습니다.
-대화형 지도 발견: [map-discovery.html](docs/architecture/map-discovery/map-discovery.html)
+| 웹 스택 | 도구·서비스 |
+| --- | --- |
+| Next.js 16 · React 19 · TypeScript | Tailwind CSS · Radix UI · Lucide |
+| Node.js 24.x · npm 11.6.2 | 일상 개발용 Bun |
+| Supabase | PostgreSQL · Auth · Storage |
-지도 발견 소스: [map-discovery.workflow.json](docs/architecture/map-discovery/map-discovery.workflow.json)
+정확한 의존성은 [package.json](apps/web/package.json)과 [package-lock.json](apps/web/package-lock.json)을 기준으로 확인합니다.
-라이프사이클 첫 화면은 세 가지 경계를 모델합니다. 메모리: 통합 RAM (M=128 GiB), 이미지 동시성 (c=1), 상주 (sum r_i <= M_headroom) (디스크 존재는 상주가 아닙니다. PNG 원본과 WebP 파생본은 모델 예산 밖입니다). 큐 대기: (W=t_claim-t_enqueue) if a live heartbeat exists, else (W=inf) with no cloud failover (stale leaseToken -> 409 worker_lease_lost). FSM 불변식: revision_conflict / project_busy / exclusive claim / lease_lost no durable write / restore = 불변 스냅샷에서 새 리비전(모델 호출 없음) / fail-closed / serial scenes / no stale overwrite. Viewer UI는 meta.locale이 생략되어 영어로 떨어집니다.
+## 문서 안내
-외부 인공지능은 기본 비활성화입니다. 텍스트와 이미지 공급자는 서로 독립입니다(`local-mlx`, `chatgpt-manual`, `grok-manual`; `manual`, `openai-api`, `xai-api`는 명시적으로 켠 경우에만). 큐에 넣은 것만으로 성공이 아닙니다. 워커가 작업을 원자적으로 claim하고, 루프백 `mlx-serve`를 호출하고, PNG 원본과 WebP 파생본을 저장한 뒤 관리자 UI가 결과를 다시 읽어야 합니다. 가드는 원자적 claim, lease/heartbeat 만료, revision/version 충돌입니다. 복원은 이제 불변 DB 스냅샷을 사용합니다. 버전 미리보기와 확인된 프로젝트 전체/단일 장면 복원이 과거 텍스트, 장면 순서, 에셋 참조, 출처, 원본 이미지 바이트를 보존합니다. 복원은 모델 호출이나 작업 큐잉 없이 새 리비전을 쓰고, 현재 공급자 설정과 동의는 그대로 유지합니다. 적용된 프로젝트의 이력은 마이그레이션이 캡처한 상태에서 시작하며 지어낸 이전 버전을 만들지 않습니다. 소유자/관리자 확인, 현재 리비전, 사용 중 프로젝트 거부, 멱등 요청 ID가 이 작업을 보호합니다. 다이어그램은 의도된 파이프라인을 설명합니다. 호스티드 apply와 실제 운영 세션은 별도 증거입니다.
+| 시작점 | 내용 |
+| --- | --- |
+| [문서 색인](docs/README.md) | 공통 안내와 문서별 위치. |
+| [웹 개발](apps/web/README.md) | 로컬·Hosted 개발, 환경 설정, 웹 명령. |
+| [백엔드 운영](backend/README.md) | 파이프라인 진입점, 환경변수 계약, 실행 요약. |
+| [제품 설계](docs/product/DESIGN.md) · [아키텍처 도식](docs/architecture/) | 제품 화면과 시스템 구조. |
+| [검증](docs/agents/verification.md) · [Nightly 운영](docs/operations/nightly-regression.md) | 변경 범위별 검사와 독립 회귀 검사. |
+| [릴리스 안내](docs/agents/release.md) | 보호된 승격, 롤백, 배포 후 확인. |
+| [성능 근거](apps/web/performance/) · [변경 기록](CHANGELOG.ko.md) | 측정 시점·범위·한계와 변경 이력. |
-## 개인정보
+변경은 보호된 `develop → data → main` 순서로 승격합니다. 운영 배포에는 릴리스 안내에 따른 커밋 승인과 배포 후 확인이 필요합니다. CI, Nightly 회귀 검사, 운영 배포 상태는 각각 기록합니다.
-소스 보호는 fail-closed입니다. 회원 생성은 게시된 방침 확인에 묶이고, 검증된 보호자 경로가 있기 전까지 만 14세 미만 가입은 막히며, 광고 동의는 목적/채널/야간을 분리하고, 공용 필터와 메모리 전용 기기 위치, 삭제·보존·사고의 Preview → Confirm → Apply → Readback → Audit을 유지합니다.
+## 보안과 라이선스
-이 보호 장치는 법령 준수나 운영 증명이 아닙니다. 외부 증거 게이트는 2026-09-22에 보류되었으며 제품 보호 장치는 계속 fail-closed로 유지됩니다. 현재 승격, 롤백, 배포 readback 요건은 [docs/agents/release.md](docs/agents/release.md)를 따릅니다. 위 라이브 앱/릴리즈 링크는 상태 참조일 뿐입니다.
+보안 문제는 [SECURITY.md](SECURITY.md)에 따라 제보해 주세요. 소스 라이선스는 [MIT](LICENSE)입니다.
diff --git a/README.md b/README.md
index 47bd12e7da..d936b36e5a 100644
--- a/README.md
+++ b/README.md
@@ -1,203 +1,120 @@
----
-
-Tzudong Map turns mukbang video evidence into a usable restaurant map: users discover places on mobile, operators verify data in an admin console, and the storyboard workspace creates visual cuts for content production.
-
-[Highlights](#highlights) · [Stack](#stack) · [Quick start](#quick-start) · [Product tour](#product-tour)
-
-## Highlights
-
-| Product surface | What it does |
-| --- | --- |
-| **Map discovery** | Search, filters, clustered food markers, current-location flows, and restaurant detail bottom sheets. |
-| **Community loops** | Reviews, stamp passport, ranking, likes, and profile surfaces for repeat engagement. |
-| **Admin operations** | Guarded moderation, source readback, approve/delete/restore flows, and audit-friendly mutations. |
-| **Storyboard workspace** | Chat-driven storyboard planning, 10-cut generation, cut metadata, image refresh, and provider readiness UX. |
-| **Evidence pipeline** | Crawling, Rule/LLM-as-a-Judge evaluation, fail-closed validation, and Supabase-ready payloads. |
+[Features](#features) · [Preview](#preview) · [Local development](#local-development) · [Repository](#repository) · [Documentation](#documentation)
-## Stack
+## Features
-- Frontend: Next.js 16 (App Router), React 19, Tailwind CSS, and Lucide icons.
-- Web runtime: Node 24.x. Day-to-day install/unit flows may use Bun; npm 11.6.2, `package.json`, and `package-lock.json` are the release package authority.
-- TypeScript: native CLI `@typescript/native` `7.0.2`; stable API/compatibility bridge `6.0.2` via `npm run typecheck:parity`.
-- Serialized content patches: `develop -> data -> main`. Hosted apply, legal compliance, and live URL state are not claimed by this tree.
-- Change history: [CHANGELOG.md](CHANGELOG.md) / [CHANGELOG.ko.md](CHANGELOG.ko.md).
-- Docs index: [docs/README.md](docs/README.md). Product design: [docs/product/DESIGN.md](docs/product/DESIGN.md).
+- **Discover restaurants:** browse domestic and overseas maps, search and filter places, and open restaurant details with video sources.
+- **Keep track of visits:** reviews, stamps, rankings, likes, and profiles connect discovery with community activity.
+- **Manage the data:** the guarded admin console supports review, moderation, source checks, and audited changes.
+- **Prepare visual content:** storyboard and thumbnail workspaces support planning, generation, and revision.
+- **Process video evidence:** backend workers handle crawling, evaluation, media processing, and publication payloads.
-## Quick start
-
-Use Node 24.x and npm 11.6.2. The web package lives in `apps/web`:
-
-```bash
-git clone https://github.com/twoimo/tzudong.git
-cd tzudong/apps/web
-npm ci
-```
-
-Prepare and migrate the repository-owned [local Supabase stack](backend/supabase/README.md), then run from `apps/web`:
-
-```bash
-npm run dev -- --port 8080
-```
+## Preview
-Open [http://127.0.0.1:8080](http://127.0.0.1:8080). The development runner validates the generated local stack, allowed browser origin, and migration ledger before starting Next.js with webpack. See the [web development guide](apps/web/README.md) for Bun commands and explicit hosted debugging.
-
-## Product tour
-
-### Desktop
-
-**Map discovery and restaurant detail**
+Recorded interface demos from the repository:
-
+
-**Admin storyboard workspace**
-
-
-
-### Mobile
+
+Mobile screens and storyboard workspace
- Home map Browse markers → expand restaurant detail
 |
- Review feed Scroll reviews → open restaurant detail
 |
+ Home map
 |
+ Review feed
 |
- Stamp passport Browse stamped places → read detail
 |
- Ranking profile Open top profile → switch tabs
 |
+ Stamp passport
 |
+ Ranking and profile
 |
-## Architecture
-
-Public map, admin, and the evidence pipeline meet in Supabase. Long work stays off the web request.
-
-```mermaid
-flowchart LR
- web["apps/web"] --> db[("Supabase")]
- ops["CI / cron"] --> control["pipeline-api"]
- control --> worker["pipeline-worker"]
- worker --> crawl["crawler / evaluation"]
- crawl --> db
- crawl --> ext["External providers"]
- worker --> db
-```
-
-Open a diagram to pan, zoom, and follow a path. Sources sit next to each HTML file.
-
-| Diagram | Open |
-| --- | --- |
-| System map | [tzudong-system-architecture.html](docs/architecture/tzudong-system-architecture.html) |
-| Map discovery | [map-discovery.html](docs/architecture/map-discovery/map-discovery.html) |
-| Evidence pipeline | [tzudong-data-pipeline.html](docs/architecture/data-pipeline/tzudong-data-pipeline.html) |
-| Storyboard workflow | [storyboard-local-mlx.html](docs/architecture/storyboard-local-mlx/storyboard-local-mlx.html) |
-
-Promotion is `develop -> data -> main`. A production build runs only when `TZUDONG_APPROVED_PRODUCTION_SHA` matches that commit. Detail lives in [docs/README.md](docs/README.md).
+
-### Measured query and normalization optimizations (2026-09-21)
+
-`apps/web/lib/restaurant-review-lookup.ts` re-built normalized names and addresses on every restaurant lookup, so one related-review count sweep (400 restaurants × 1,200 candidates = 480,000 candidate visits) re-ran about 1.94M string normalizations. Each record's prepared names and addresses are now cached in a WeakMap keyed by identity plus a field signature that invalidates when the record changes. The same sweep now performs 22,400 normalizations on a cold cache and 0 on a warm cache, and all 400 lookups return byte-identical id arrays. Retained evidence: [benchmark.json](apps/web/performance/review-lookup-memoization-20260921/benchmark.json) with the [repro script](apps/web/performance/review-lookup-memoization-20260921/benchmark-review-lookup.mjs). Median sweep time on this machine was 291.9 ms before and 144.4 ms cold / 132.6 ms warm after; the ratio (about 2x faster with 86.4x fewer normalizations) is the claim, not the absolute time.
+## Local development
-Chunked `restaurants` and `reviews` lookups in `apps/web/hooks/use-restaurants.tsx` now run at most four chunks in parallel and still fill results by chunk index, so ordering is unchanged while round trips become `ceil(ceil(N / 80) / 4)` instead of `ceil(N / 80)` ([analytic table](apps/web/performance/review-lookup-memoization-20260921/chunked-roundtrips.json)).
+Use **Node.js 24.x** and **npm 11.6.2**, as declared in [apps/web/package.json](apps/web/package.json). The local database setup also needs Python 3 and Docker with Compose; follow the pinned prerequisites in the [local stack runbook](docs/operations/nightly-regression.md#local-compose-bootstrap).
-Public `/feed` skips the approved-canonical `restaurants` lookup for a page when every reviewed restaurant on that page is already `approved`, which drops one database round trip per feed page in the common case. The lookup still runs whenever a review points at a non-approved or missing restaurant, so canonical substitution behavior is unchanged.
+1. Clone the repository and install the web dependencies:
-The same related-review sweep also re-ran name-compatibility checks for every candidate because the name gate ran first. `apps/web/lib/restaurant-review-lookup.ts` now applies the address gate first, computes name compatibility lazily only for candidates that share an address, skips candidates already present in the direct-id set, and intersects address sets without spreading them into arrays. The baseline is a frozen copy of the implementation from immediately before this change (`63c0dd1a`), not a re-written approximation. The 400 × 1,200 sweep returns byte-identical id arrays while name-gate evaluations drop from 480,000 to 12,852 (37.35x) and cold median sweep time drops from 83.28 ms to 37.89 ms (2.20x, p95 2.17x). Retained evidence: [benchmark.json](apps/web/performance/review-lookup-address-gate-20260921/benchmark.json) with the [repro script](apps/web/performance/review-lookup-address-gate-20260921/benchmark-review-lookup-address-gate.mjs). The report states its own absolute, relative, and noise budgets; the result is accepted only when dispersion stays within 15% MAD and the improvement exceeds the combined relative noise. The ratio is the claim, not the absolute time.
+ ```sh
+ git clone https://github.com/twoimo/tzudong.git
+ cd tzudong/apps/web
+ npm ci
+ ```
-`apps/web/hooks/useUnvisitedRestaurants.tsx` rebuilt the merged restaurant list and evaluated every restaurant twice on each render of the home recommendation popup. The merge and the visit classification are now memoized on their inputs, and the unvisited list plus the visited count come from a single pass. Over 40 re-renders with unchanged inputs on a 1,200-restaurant list, `mergeRestaurants` calls drop 40 → 1, visit checks 96,000 → 1,200 (80x), and median wall time per render set drops from 279.7 ms to 5.1 ms (54.69x, p95 56.93x), with an identical unvisited list and count. Retained evidence: [benchmark.json](apps/web/performance/unvisited-restaurants-20260921/benchmark.json) with the [repro script](apps/web/performance/unvisited-restaurants-20260921/benchmark-unvisited-restaurants.mjs).
+2. From the repository root, prepare and migrate the repository-managed [local Supabase stack](backend/supabase/README.md) using the [bootstrap instructions](docs/operations/nightly-regression.md#local-compose-bootstrap).
+3. Return to `apps/web` and start the development server:
-### Home map mode filtering (2026-09-22)
+ ```sh
+ npm run dev
+ ```
-`getRestaurantListByMode` in `apps/web/components/home/home-map-container.tsx` ran on every map move and swipe and rebuilt work that only depended on the restaurant row: a lowercased address string from three fields, then a scan of 35 overseas keywords with `String.includes`, plus a second keyword list when a country filter was selected. Classification now lives in `apps/web/lib/home-map-mode-filter.ts`, which caches each row's address text and both verdicts (coordinate bounds, overseas keyword) on the row object behind a field signature that invalidates on in-place edits, indexes keywords by first character instead of scanning the full list at every character, and reuses the keyword-index and country-verdict caches when the same country keyword list comes back. On a frozen copy of the implementation from immediately before the change, the 4,000-row fixture drops from a 2.449 ms median to 0.220 ms for domestic (11.11x, p95 11.38x), from 2.174 ms to 0.168 ms for overseas (12.94x, p95 12.91x), and from 2.639 ms to 0.253 ms with a country filter (10.43x, p95 10.03x), keeping identical id order and object identity with 0 mismatches; the slowest single call stays at 0.295 ms p95 against the 16 ms frame budget. Retained evidence: [benchmark.json](apps/web/performance/home-map-mode-filter-20260922/benchmark.json) with the [repro script](apps/web/performance/home-map-mode-filter-20260922/benchmark-home-map-mode-filter.mjs). The ratio is the claim, not the absolute time.
+Open [http://127.0.0.1:3000](http://127.0.0.1:3000). The runner checks the generated local environment, service readiness, browser origin, and migration ledger before starting Next.js. Hosted debugging and map-provider configuration have separate setup instructions in the [web development guide](apps/web/README.md).
-### Responsive my-page surfaces (2026-09-22)
+Bun is supported for everyday development. npm's manifest and lockfile remain the release package authority. Common checks from `apps/web` are:
-`/mypage/profile` kept its desktop cards inside `md:h-full md:min-h-0 md:grid-rows-2`, so two viewport rows split the scroller height while the password form needed 348px. At 1024x900 the 비밀번호 변경 card measured `clientHeight 203` against `scrollHeight 348` under `overflow: hidden`, which clipped the form inside the card. The page surface now keeps only `lg:grid-cols-2 lg:auto-rows-auto lg:content-stretch lg:items-stretch` (the follow-up below moved the matrix breakpoint from `md` to `lg`): rows follow card content, the password card measures 306px with `clientHeight` equal to `scrollHeight`, and the grid box equals its scroll height (958 vs 956). `data-mypage-profile-viewport-fit` now reads `content` instead of `true`.
-
-The shared list grid (`myPageResponsiveListClass`) went two-up at `md`, where the 256px sidebar leaves about 465px of content, so each card was about 227px wide. Bookmark headers then overflowed their own `overflow-hidden` card at every desktop width (768: clientWidth 225 vs scrollWidth 308, 1024: 356 vs 478, 1280: 318 vs 478, 1440: 371 vs 478) and the address column collapsed to 17px next to the 160px thumbnail. Review cards clipped at 768 (225 vs 248). The grid now goes two-up at `lg` and three-up at `2xl`, the bookmark and review header text columns take `min-w-0` with a `shrink-0` action, and the bookmark thumbnail is 128px wide with a matching `sizes` hint.
-
-### Responsive my-page follow-up (2026-09-22)
-
-The profile dashboard matrix started at `md`, where the 256px sidebar leaves 465px of content, so its two tracks measured 207.5px each and the tier headline ellipsized (`clientWidth 130` against `scrollWidth 155`). The matrix, its `col-start`/`row-start`/`h-full`/`min-h-0`/`overflow-hidden` placement, and the `content-stretch` surface now start at `lg`: at 1024 the tracks measure 333.5px and nothing ellipsizes, while 768-1023 stack into a single 431-697px card column with every card sized to its content (the tier card is 167px instead of a stretched 306px). At `2xl` the surface adds a third track, so 등급/비밀번호/최근 활동/계정 삭제/마케팅 수신 fill three 394px (1536) or 522px (1920) tracks, with the consent card spanning both rows in the third track. The profile page height drops from 958px to 538px at 1536 and above.
-
-Bookmark cards pair a 128px thumbnail with text, so the shared list grid stayed too tight for them: at `lg` a two-up card is 358px, which left a 136px title column and a 180px address column against a 246px address, and three-up at `2xl` (405px) still cut the address to 227px. `myPageResponsiveMediaListClass` now waits for `xl` and never adds a third track, so bookmarks render one 713px track through 1024 and 486/566/614/806px two-up from 1280 with no ellipsis at any checked width. Review cards keep the shared `lg`/`2xl` grid because they stay under their clamp everywhere.
-
-The submission lists put `md:col-span-2 xl:col-span-3` on their load-more row while the grid only declares columns from `lg`; at 768 `grid-column: span 2` created two implicit tracks (231.25px and 228.75px) and halved every submission card. The row now matches the declared tracks (`lg:col-span-2 2xl:col-span-3`): 472px single track at 768, 358px two-up at 1024, 405px three-up at 1536.
-
-Retained evidence: [audit.json](apps/web/performance/mypage-responsive-20260922/audit.json) and [audit-round2.json](apps/web/performance/mypage-responsive-20260922/audit-round2.json), measured through the Chrome DevTools Protocol on the local dev server with a signed-in session at 390/640/768/1024/1280/1440 plus 1536/1600 for the list grid and 390/768/1024/1280/1536/1920 for the follow-up sweep. After the change no container with `overflow: hidden|clip` clips its own content on any of the six `/mypage` routes at any checked width, `documentElement.scrollWidth` equals `clientWidth` everywhere, and list cards measure 358/486/566px at 1024/1280/1440 and 405/427px three-up at 1536/1600. The source contracts are frozen in `tests-unit/mypage-mobile-cleanup-contract.test.ts` and `tests-unit/web-quality-performance-source.test.ts`; `npm run test:unit` (2314 tests, 318 files), `npm run lint`, and `npm run typecheck:parity` pass. The follow-up sweep covered all six routes at 390/768/1024/1280/1536/1920 (36 combinations) with 0 clipped containers, no document overflow, and no ellipsized text beyond a 3px title overflow at 768 on one long name. Local rendering does not establish hosted or production rendering.
-
-### Responsive my-page desktop fill and target sizes (2026-09-22)
-
-The full sweep across all seven `/mypage` routes at 320/390/640/768/1024/1280/1536/1920 (56 route-width combinations) found no horizontal overflow, no content cut by `overflow: hidden`, and no clipping beyond the app shell that hands scrolling to its inner panel. It did find trailing empty panel inside a stretched card and two undersized targets.
-
-From `lg` up the profile matrix stretches the tier card to the password card's height, so the card measured 306px against 167px of content and ended with 139px of empty panel at 1024/1280/1440/1536/1920. The progress block now carries `lg:flex lg:flex-1 lg:flex-col lg:justify-center`, growing to 198px and centering the progress bar between the title and the metrics: dead space is 0 at every desktop width and the card height is unchanged at 306px.
-
-The shared inline link (`myPageInlineLinkClass`, used by 영상 보기, 영상 #n and 관련 영상 보기) measured 68x20, below the 24px floor in WCAG 2.2 SC 2.5.8. It now carries `-my-1 min-h-8 py-1`, giving a 32px hit box while the negative margin holds the metadata row at 33px. The sidebar 닉네임 수정 button, the smallest desktop control at 35x28, moved to `h-8 px-2.5` for 39x32. The mobile hero keeps its own 28px 수정 badge because that class string is frozen by the cleanup contract, and 28px clears the floor.
-
-Retained evidence: [audit-round3.json](apps/web/performance/mypage-responsive-20260922/audit-round3.json). The 2-up bookmark media list from `xl` and the 3-up text list from `2xl` were re-measured at 1024/1280/1536/1920 and left as they were, with every address and title still uncut. The new contracts are frozen in `tests-unit/mypage-mobile-cleanup-contract.test.ts`. Local rendering does not establish hosted or production rendering.
-
-`/mypage/profile` was re-measured after the desktop-fill change at 320/390/414/640/768/1024/1280/1536/1920 with a signed-in session: 0 overflowing elements and 0 clipped containers at every width, `documentElement.scrollWidth` equal to `clientWidth` everywhere, a 0px sidebar below 768 and 256px above it, and the dashboard matrix at one track below 768, two tracks from 1024, and three from 1536 where the consent card spans both rows in the third track (covered ratio 0.879-0.906). At 1536 the page height is 538px against 958px at 1280. Retained evidence: [audit-verification-20260922.json](apps/web/performance/mypage-responsive-20260922/audit-verification-20260922.json). The one intentional difference below 768px is that the 등급 대시보드 tier-progress card is replaced by the grouped 내 활동 / 제보하기 quick-action list, so the 39% tier progress is desktop-only.
-
-### Regional and district clustering reuse (2026-09-22)
-
-One map refresh calls `getRegionalClusters` once and `getSeoulDistrictClusters` twice (`minClusterSize` 1 and 3) on the same list, and `apps/web/lib/clustering.ts` rebuilt every lookup table per call: the 18-name region table, the 18-entry short-name object plus its `Object.entries`, the 25-name district table, and a per-restaurant derived pass. District classification — the Seoul gate, 25 `String.includes` probes and the nearest-centre fallback — also ran once per call, so a single refresh classified every restaurant twice.
-
-The tables are module-level constants built once at import, and each restaurant's derived values (id, address, coordinates, category label, and lazily the region or district) live in a `WeakMap` keyed by list identity with a field-level check that invalidates on in-place edits to either the list (length, order) or a row. `getRegionalClusters` builds that pass and caches it; `getSeoulDistrictClusters` reuses it when it is already there and otherwise computes in place without writing the cache, so a single-shot call is never charged for building entries it will not reuse. District classification runs once and is projected for both `minClusterSize` values.
-
-The two grouping paths are separate functions. A single function accepting either the cached entries or the raw list measured anywhere from 0.5x to 1.1x of the previous code depending on the run, because the mixed argument shape and the doubled loop body left the isolated path in a lower JIT tier; splitting it into one monomorphic function per path removed that swing.
-
-Against a frozen copy of the implementation from immediately before the change, the real 3-call bundle is 10.10x (300 rows), 13.82x (1,000), 16.22x (2,000) and 17.70x (4,000) faster on the warm path and 1.50x-2.31x faster on the cold path, with the per-call district pass after `getRegionalClusters` at 12.54x-24.11x warm and 1.36x-2.36x cold. The bundle's p95 is 0.038/0.087/0.142/0.247 ms warm and 0.36/0.68/3.10/2.20 ms cold against the 16 ms frame budget. Cluster order, member order, category order, centre coordinates and id arrays are identical with 0 mismatches at all four sizes on both paths, and the harness's self-comparison controls read 0.95-1.14.
-
-The isolated single-shot cold call (`getSeoulDistrictClusters` with no preceding `getRegionalClusters` on that list) is reported as a diagnostic rather than gated: no caller invokes it that way, since `NaverMapView` always clusters regions first on the same list, and at 0.05-0.57 ms per call it swings between 0.54x and 1.11x with the JIT tier rather than with the code. The report states its own budgets — a 16 ms absolute frame budget, warm >= 2x and cold >= 1.3x on the bundle, a per-target no-regression rule, and 15% MAD on the warm samples whose medians back the headline ratios — and accepted three consecutive runs. Retained evidence: [benchmark.json](apps/web/performance/clustering-region-district-20260922/benchmark.json) with the [repro script](apps/web/performance/clustering-region-district-20260922/benchmark-clustering.mjs); the pre-change semantics are frozen in `tests-unit/clustering-region-district.test.ts` (21 tests). The ratio is the claim, not the absolute time, and local measurement does not establish hosted or production behaviour.
-
-### Expanded-cluster marker lookup (2026-09-22)
-
-While any cluster was expanded, `NaverMapView` copied the current id map and then inserted every restaurant ever seen in that session into both render maps. The snapshot was append-only. Marker drawing only looks up the current row, an expanded id, or the selected id, in that order.
+```sh
+npm run lint
+npm run test:unit
+npm run typecheck:parity
+```
-The copy is gone. The snapshot now keeps those ids and drops the rest, and a lookup reads the existing maps plus that snapshot. On a 4,000-row list the render maps no longer hold a second copy: 9,600 entries become the 4,800 already in the lookup. After 4,000 stale rows the retained set is 4,813 instead of 17,600. After 12,000 stale rows it is 4,813 instead of 33,600. Probes for current, expanded, and selected ids matched the previous lookup with 0 mismatches; a stale id that is none of those is no longer returned. Wall-clock medians stayed inside the noise budget (combined MAD / median 0.26–0.47 against a 0.15 relative noise budget, absolute samples under the 16 ms frame budget), so this change does not claim a latency speedup. Retained evidence: [benchmark.json](apps/web/performance/expanded-cluster-snapshot-20260922/benchmark.json). Local measurement does not establish hosted or production behaviour.
+Choose checks for the affected behavior using the [verification guide](docs/agents/verification.md).
-### Visible marker review bubbles (2026-09-22)
+## Repository
-`selectVisibleMarkerReviewBubbleTargets` built a related-id set for every candidate, then sorted the whole list to keep 3 or 5 bubbles. A restaurant id already makes that set non-empty, so the second pass never removed a row. Selection now keeps a bounded window of `limit` rows with the same stable rank order and builds related ids only for those rows. A missing list or options returns `[]` and logs `invalid-input` without the payload.
+| Path | Responsibility |
+| --- | --- |
+| [apps/web/](apps/web/) | Next.js public app, restaurant maps, community features, and guarded admin console. |
+| [backend/](backend/) | Pipeline control, crawlers, evaluation, media workers, and Supabase migrations. |
+| [docs/](docs/) | Product design, architecture, operations, and release guidance. |
-Against a frozen copy of the previous full sort, 200 / 1,000 / 4,000 candidates at limit 5 return identical targets (0 mismatches). Related-id sets per call drop from 205 / 1,005 / 4,005 to 5. Rank objects drop from the full candidate count to at most 5. Median wall-clock ratios were 1.85x / 2.24x / 3.57x, but combined MAD / median was 0.22–0.46 against a 0.15 relative noise budget, so the time ratio is not claimed. Samples stay under the 16 ms frame budget. Retained evidence: [benchmark.json](apps/web/performance/visible-marker-review-bubbles-20260922/benchmark.json). Local measurement does not establish hosted or production behaviour.
+The web app and backend workers share Supabase as their persistence boundary. Long-running crawling and media work belongs to backend workers. See the [backend architecture](backend/ARCHITECTURE.md) and [data contracts](backend/DATA_CONTRACTS.md) for the detailed flow.
-### Deployment and release path (verified 2026-09-21)
+| Web stack | Tooling and services |
+| --- | --- |
+| Next.js 16 · React 19 · TypeScript | Tailwind CSS · Radix UI · Lucide |
+| Node.js 24.x · npm 11.6.2 | Bun for everyday development |
+| Supabase | PostgreSQL · Auth · Storage |
-`apps/web/scripts/vercel-ignore-build.mjs` is the build gate: preview builds run only for `develop`, every other branch preview is skipped, and a `main` production build additionally requires `TZUDONG_APPROVED_PRODUCTION_SHA` to equal the commit being deployed. Branch-specific preview URLs therefore do not appear for feature branches by design; a CLI deployment of the branch head (`vercel deploy` from `apps/web`, project `tzudong`) is what produces a reviewable preview.
+Exact dependencies are in [package.json](apps/web/package.json) and [package-lock.json](apps/web/package-lock.json).
-On the Hobby team, commits must be authored by the team owner. Commits authored with a placeholder address are refused before the build starts with `readyState: BLOCKED`, `alwaysRefuseToBuild: true`, and `seatBlock.blockCode: TEAM_ACCESS_REQUIRED` (Vercel: "the commit author doesn't have permission to create deployments for this project"). The repository now commits with the owner's GitHub-linked address, and the same CLI deployment that was refused before this change reaches `BUILDING`/`READY` afterwards.
+## Documentation
-`develop`, `data`, and `main` are protected: the `Release` and `Promotion Path` checks are required, force pushes are disabled, administrators are enforced, and conversation resolution is required, so promotion is serialized through pull requests in the order `develop -> data -> main`. Preview deployments carry Vercel Authentication, so an unauthenticated request to a preview URL answers `302` to the Vercel login flow rather than the application.
+| Start here | Contents |
+| --- | --- |
+| [Documentation index](docs/README.md) | Cross-project guides and where documentation belongs. |
+| [Web development](apps/web/README.md) | Local and hosted development, configuration, and web commands. |
+| [Backend operations](backend/README.md) | Pipeline entry points, environment contracts, and run summaries. |
+| [Product design](docs/product/DESIGN.md) · [Architecture diagrams](docs/architecture/) | Product surfaces and system diagrams. |
+| [Verification](docs/agents/verification.md) · [Nightly operations](docs/operations/nightly-regression.md) | Scoped checks and the independent regression lane. |
+| [Release guide](docs/agents/release.md) | Protected promotion, rollback, and deployment readback. |
+| [Performance evidence](apps/web/performance/) · [Changelog](CHANGELOG.md) | Dated measurements, their scope and limits, and change history. |
-## Privacy
+Changes follow the protected `develop → data → main` path. A production release needs the approved commit and deployment readback described in the release guide. CI, Nightly regression, and production deployment are tracked separately.
-Source safeguards stay fail-closed: challenge-bound account creation, no under-14 registration until a verified guardian path exists, purpose/channel marketing consent with a separate night grant, shared redaction, memory-only device location, and Preview → Confirm → Apply → Readback → Audit for deletion/retention/incidents.
+## Security and license
-These safeguards are not legal compliance or production proof. The external evidence gates were parked on 2026-09-22; product safeguards remain fail-closed. Current promotion, rollback, and deployment readback requirements are in [docs/agents/release.md](docs/agents/release.md). The live app and release links above are status references only.
+Report security issues using [SECURITY.md](SECURITY.md). Source license: [MIT](LICENSE).