@designfever/web-review-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ # df-web-review-kit 초기 정리
2
+
3
+ ## 결정된 방향
4
+
5
+ - Repository: `Designfever/df-web-review-kit`
6
+ - NPM package: `@designfever/web-review-kit`
7
+ - 목적: QA 전용 도구가 아니라 웹 페이지 위에 얹는 검수용 toolkit.
8
+ - 1차 대상: Lexus, KIA, Toyota 같은 웹 프로젝트.
9
+ - 설치 방식: 각 프로젝트에 npm package로 설치하고, 프로젝트별 설정만 넣는다.
10
+ - 단축키: 기본은 `Shift + Q`로 QA mode 진입.
11
+ - Chrome extension은 1차 대상이 아니다. 소스 접근 없는 사이트까지 다뤄야 할 때 adapter/wrapper로 나중에 검토한다.
12
+
13
+ ## 큰 구조
14
+
15
+ 패키지는 core와 module, adapter로 나눈다.
16
+
17
+ - Core
18
+ - overlay root 생성
19
+ - keyboard shortcut 관리
20
+ - panel/modal/layer 공통 UI
21
+ - 현재 URL/viewport/scroll 상태 수집
22
+ - module lifecycle 관리
23
+ - QA module
24
+ - 현재 페이지 기준 QA list 표시
25
+ - text-only QA 등록
26
+ - 화면 영역 드래그 캡처 QA 등록
27
+ - DOM anchor 계산
28
+ - Grid module
29
+ - 기존 grid helper를 나중에 module로 이관
30
+ - 1차 MVP에는 인터페이스만 열어둔다
31
+ - Figma module
32
+ - Figma overlay helper를 나중에 module로 이관
33
+ - 1차 MVP에는 인터페이스만 열어둔다
34
+ - Adapter
35
+ - 저장소/외부 서비스 연동만 담당
36
+ - core와 QA module은 df-sheet, GitHub, localStorage 같은 저장소를 직접 알지 않는다
37
+
38
+ ## Adapter로 뺀다는 뜻
39
+
40
+ Core는 "무엇을 수집하고 어떤 UI를 보여줄지"만 담당한다.
41
+ Adapter는 "수집한 QA item을 어디에 저장하고 어떻게 읽어올지"만 담당한다.
42
+
43
+ 예시:
44
+
45
+ - Lexus: `dfSheetAdapter`
46
+ - 초기 개발/검증: `localAdapter`
47
+ - 미래 확장: `githubIssueAdapter`, `jsonFileAdapter` 등
48
+
49
+ 이렇게 분리하면 df-sheet 연동 방식이 바뀌어도 overlay core를 건드리지 않아도 된다.
50
+
51
+ ## 1차 MVP
52
+
53
+ 우선 df-sheet 연동 없이 local adapter로 기능을 검증한다.
54
+
55
+ 1. npm package scaffold
56
+ 2. target project에서 초기화 가능한 API
57
+ 3. `Shift + Q`로 QA mode toggle
58
+ 4. 현재 URL 기준 QA list 표시
59
+ 5. text-only QA 생성
60
+ 6. drag capture QA 생성
61
+ 7. DOM anchor + relative coordinate 저장
62
+ 8. local adapter 저장/조회/삭제
63
+
64
+ 1차에서 Figma overlay와 grid helper를 완성하려고 하지 않는다.
65
+ 다만 나중에 붙일 수 있게 module 구조는 처음부터 열어둔다.
66
+
67
+ ## Local adapter 저장 방식
68
+
69
+ 이름은 local adapter지만 실제 저장은 둘로 나눈다.
70
+
71
+ - Metadata: `localStorage`
72
+ - Screenshot/blob: `IndexedDB`
73
+
74
+ 이유:
75
+
76
+ - `localStorage`에 screenshot data URL을 계속 넣으면 용량이 빨리 찬다.
77
+ - IndexedDB는 이미지 blob 저장에 더 적합하다.
78
+
79
+ ## QA item 기본 데이터
80
+
81
+ ```ts
82
+ type ReviewItem = {
83
+ id: string;
84
+ projectId: string;
85
+ pageUrl: string;
86
+ normalizedPath: string;
87
+ kind: "text" | "capture";
88
+ title?: string;
89
+ comment: string;
90
+ status: "open" | "resolved";
91
+ viewport: {
92
+ width: number;
93
+ height: number;
94
+ };
95
+ scroll?: {
96
+ x: number;
97
+ y: number;
98
+ };
99
+ anchor?: DomAnchor;
100
+ selection?: RelativeSelection;
101
+ screenshotAssetId?: string;
102
+ externalIssueId?: string;
103
+ createdAt: string;
104
+ updatedAt: string;
105
+ };
106
+ ```
107
+
108
+ ## DOM anchor 전략
109
+
110
+ 단순 viewport 좌표만 저장하면 반응형 화면에서 위치가 틀어진다.
111
+ 따라서 선택 영역과 가장 가까운 DOM element를 anchor로 잡고, 그 element 기준 상대 좌표를 저장한다.
112
+
113
+ Anchor 우선순위:
114
+
115
+ 1. `data-qa-id` 또는 package 설정으로 지정한 attribute
116
+ 2. `id`가 안정적인 element
117
+ 3. 의미 있는 class/name/role 기반 selector
118
+ 4. 텍스트 fingerprint
119
+ 5. DOM path fallback
120
+
121
+ Relative coordinate 예시:
122
+
123
+ ```ts
124
+ type RelativeSelection = {
125
+ x: number; // (selection.left - anchor.left) / anchor.width
126
+ y: number; // (selection.top - anchor.top) / anchor.height
127
+ width: number; // selection.width / anchor.width
128
+ height: number; // selection.height / anchor.height
129
+ };
130
+ ```
131
+
132
+ 나중에 다시 표시할 때는 현재 anchor rect를 찾고 위 비율로 위치를 복원한다.
133
+
134
+ ## QA 생성 흐름
135
+
136
+ Text-only:
137
+
138
+ 1. 사용자가 QA mode에서 text 등록 선택
139
+ 2. 현재 URL, viewport width/height 저장
140
+ 3. 사용자가 comment 입력
141
+ 4. adapter에 저장
142
+
143
+ Capture:
144
+
145
+ 1. 사용자가 capture 선택
146
+ 2. 깨진 영역을 drag로 선택
147
+ 3. 선택 영역 screenshot 생성
148
+ 4. 선택 영역과 가장 가까운 DOM anchor 계산
149
+ 5. anchor 기준 relative coordinate 저장
150
+ 6. comment 입력
151
+ 7. adapter에 저장
152
+
153
+ Screenshot 구현은 처음에는 `html2canvas` 계열로 시작할 수 있다.
154
+ 단, cross-origin image/video/canvas는 깨질 수 있으니 정확도가 중요해지면 browser extension 또는 native capture 방식도 검토한다.
155
+
156
+ ## df-sheet 연동 방향
157
+
158
+ df-sheet adapter는 QA item을 df-sheet issue로 매핑한다.
159
+
160
+ - 생성: QA item 생성 시 df-sheet issue 생성
161
+ - 조회: 현재 URL에 해당하는 unresolved issue만 QA list에 표시
162
+ - 해결: df-sheet issue status가 `done`이면 QA list에서 제외
163
+ - 첨부: capture screenshot은 issue attachment로 등록
164
+ - Metadata: page URL, viewport, anchor, selection 정보를 issue에 저장
165
+
166
+ df-sheet에 dedicated metadata field가 없으면 1차는 description이나 hidden JSON block으로 저장할 수 있다.
167
+ 다만 장기적으로는 issue metadata field를 추가하는 편이 낫다.
168
+
169
+ ## 예상 초기화 API
170
+
171
+ ```ts
172
+ import { createWebReviewKit, localAdapter } from "@designfever/web-review-kit";
173
+
174
+ createWebReviewKit({
175
+ projectId: "lexus-renewal",
176
+ adapter: localAdapter({
177
+ storageKey: "lexus-review-items",
178
+ }),
179
+ hotkeys: {
180
+ qa: "Shift+Q",
181
+ },
182
+ anchors: {
183
+ attribute: "data-qa-id",
184
+ },
185
+ modules: {
186
+ qa: true,
187
+ grid: false,
188
+ figma: false,
189
+ },
190
+ });
191
+ ```
192
+
193
+ ## 나중에 붙일 모듈
194
+
195
+ Grid helper:
196
+
197
+ - breakpoint/grid overlay
198
+ - column/gutter 표시
199
+ - 프로젝트별 grid preset
200
+
201
+ Figma helper:
202
+
203
+ - Figma screenshot 또는 frame overlay
204
+ - opacity/blend mode 조절
205
+ - viewport별 기준 이미지 전환
206
+
207
+ 둘 다 core overlay와 단축키/패널 시스템은 공유하되, QA 저장소와는 강하게 묶지 않는다.
208
+
209
+ ## 열어둔 질문
210
+
211
+ - package 배포를 public npm으로 할지 private npm으로 할지
212
+ - target project가 Next/Vite/vanilla를 모두 포함하는지
213
+ - React dependency를 peer로 둘지, framework-agnostic DOM package로 갈지
214
+ - df-sheet 인증은 cookie 기반인지 token 기반인지
215
+ - df-sheet에 QA metadata 전용 field를 추가할지
216
+ - target site에 `data-qa-id`를 어느 정도 심을지
217
+ - screenshot 정확도를 어디까지 요구할지
218
+
219
+ ## 다음 세션에서 할 일
220
+
221
+ 1. package scaffold 선택: Vite library, tsup, or rollup
222
+ 2. core API 타입 먼저 작성
223
+ 3. local adapter 인터페이스 작성
224
+ 4. QA overlay 최소 UI 구현
225
+ 5. Lexus/KIA/Toyota 중 하나에 playground로 붙여 검증
226
+
@@ -0,0 +1,222 @@
1
+ # Installation
2
+
3
+ Host project에 `df-web-review-kit`를 설치하고 `/review` route에서 `mountReviewShell()`을 호출한다.
4
+
5
+ ## Package install
6
+
7
+ NPM package로 사용할 때:
8
+
9
+ ```bash
10
+ pnpm add @designfever/web-review-kit react react-dom
11
+ ```
12
+
13
+ Supabase remote/presence를 쓰면 host project에 Supabase client도 설치한다.
14
+
15
+ ```bash
16
+ pnpm add @supabase/supabase-js
17
+ ```
18
+
19
+ Lexus repo 안에서 검증할 때는 file dependency를 사용한다.
20
+
21
+ ```json
22
+ "@designfever/web-review-kit": "file:packages/df-web-review-kit"
23
+ ```
24
+
25
+ ## Vite route
26
+
27
+ Vite project에서는 `page/review/index.html`과 `page/review/index.tsx` 같은 review entry를 만든다.
28
+
29
+ Minimal `index.html`:
30
+
31
+ ```html
32
+ <div id="root"></div>
33
+ <script type="module" src="./index.tsx"></script>
34
+ ```
35
+
36
+ Minimal `index.tsx`:
37
+
38
+ ```tsx
39
+ import {
40
+ createReviewPagesFromGlob,
41
+ mountReviewShell,
42
+ } from '@designfever/web-review-kit/react-shell';
43
+ import {
44
+ REVIEW_WORKFLOW_STATUS_OPTIONS,
45
+ localAdapter,
46
+ } from '@designfever/web-review-kit';
47
+
48
+ const REVIEW_PROJECT_ID = 'my-project';
49
+ const local = localAdapter({
50
+ storageKey: `${REVIEW_PROJECT_ID}-review-items`,
51
+ });
52
+
53
+ const pages = createReviewPagesFromGlob(import.meta.glob('/**/index.tsx'), {
54
+ exclude: (href) => href === '/review/',
55
+ });
56
+
57
+ mountReviewShell({
58
+ projectId: REVIEW_PROJECT_ID,
59
+ pages,
60
+ adapters: [
61
+ {
62
+ label: 'local',
63
+ get: (id) => local.get(id),
64
+ list: (query) => local.list(query),
65
+ create: (item) => local.create(item),
66
+ statusOptions: REVIEW_WORKFLOW_STATUS_OPTIONS,
67
+ updateStatus: ({ id, status }) => local.update(id, { status }),
68
+ syncSubmission: ({ id, patch }) => local.update(id, patch),
69
+ remove: (id) => local.remove(id),
70
+ },
71
+ ],
72
+ reviewPathPrefix: '/review',
73
+ });
74
+ ```
75
+
76
+ ## Supabase remote example
77
+
78
+ Supabase를 붙일 때는 host project에서 client를 만들고 package adapter에 주입한다.
79
+
80
+ ```tsx
81
+ import {
82
+ createFallbackPresenceAdapter,
83
+ createLocalPresenceAdapter,
84
+ createSupabasePresenceAdapter,
85
+ type ReviewShellAdapter,
86
+ type SupabasePresenceClient,
87
+ } from '@designfever/web-review-kit/react-shell';
88
+ import {
89
+ REVIEW_WORKFLOW_STATUS_OPTIONS,
90
+ localAdapter,
91
+ supabaseAdapter,
92
+ type SupabaseReviewClient,
93
+ } from '@designfever/web-review-kit';
94
+ import { createClient } from '@supabase/supabase-js';
95
+
96
+ const REVIEW_PROJECT_ID = 'lexus-official-v2026';
97
+ const REVIEW_PATH_PREFIX = '/review';
98
+
99
+ const local = localAdapter({
100
+ storageKey: `${REVIEW_PROJECT_ID}-review-items`,
101
+ });
102
+
103
+ const supabaseClient = import.meta.env.VITE_REVIEW_SUPABASE_ANON_KEY
104
+ ? createClient(
105
+ import.meta.env.VITE_REVIEW_SUPABASE_URL,
106
+ import.meta.env.VITE_REVIEW_SUPABASE_ANON_KEY
107
+ )
108
+ : null;
109
+
110
+ const remote = supabaseClient
111
+ ? supabaseAdapter({
112
+ client: supabaseClient as unknown as SupabaseReviewClient,
113
+ table: import.meta.env.VITE_REVIEW_SUPABASE_TABLE || 'review_items',
114
+ projectId: REVIEW_PROJECT_ID,
115
+ source: 'supabase',
116
+ reviewPathPrefix: REVIEW_PATH_PREFIX,
117
+ })
118
+ : null;
119
+
120
+ const adapters = [
121
+ {
122
+ label: 'local',
123
+ get: (id) => local.get(id),
124
+ list: (query) => local.list(query),
125
+ create: (item) => local.create(item),
126
+ statusOptions: REVIEW_WORKFLOW_STATUS_OPTIONS,
127
+ updateStatus: ({ id, status }) => local.update(id, { status }),
128
+ syncSubmission: ({ id, patch }) => local.update(id, patch),
129
+ remove: (id) => local.remove(id),
130
+ },
131
+ ...(remote
132
+ ? [
133
+ {
134
+ label: 'supabase',
135
+ get: (id) => remote.get(id),
136
+ list: (query) => remote.list(query),
137
+ create: (item) => remote.create(item),
138
+ statusOptions: REVIEW_WORKFLOW_STATUS_OPTIONS,
139
+ updateStatus: ({ id, status }) => remote.update(id, { status }),
140
+ remove: (id) => remote.remove(id),
141
+ } satisfies ReviewShellAdapter,
142
+ ]
143
+ : []),
144
+ ] satisfies ReviewShellAdapter[];
145
+
146
+ const localPresence = createLocalPresenceAdapter({
147
+ channelName: `${REVIEW_PROJECT_ID}:review-presence`,
148
+ });
149
+
150
+ const presence = supabaseClient
151
+ ? createFallbackPresenceAdapter(
152
+ createSupabasePresenceAdapter({
153
+ client: supabaseClient as unknown as SupabasePresenceClient,
154
+ channelPrefix: 'review-presence',
155
+ private: import.meta.env.VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE === 'true',
156
+ }),
157
+ localPresence
158
+ )
159
+ : localPresence;
160
+ ```
161
+
162
+ 그 다음 `mountReviewShell({ adapters, presence, ... })`에 넘긴다.
163
+
164
+ ## Environment
165
+
166
+ ```env
167
+ VITE_REVIEW_SUPABASE_URL=https://your-project.supabase.co
168
+ VITE_REVIEW_SUPABASE_ANON_KEY=
169
+ VITE_REVIEW_SUPABASE_TABLE=review_items
170
+ VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE=false
171
+ ```
172
+
173
+ Rules:
174
+
175
+ - browser env에는 Supabase `anon` key만 넣는다.
176
+ - `service_role` key는 절대 browser env에 넣지 않는다.
177
+ - package는 Supabase dependency를 직접 만들지 않는다. host project가 `createClient()`를 호출한다.
178
+
179
+ ## Viewport preset
180
+
181
+ Project별 design width가 다르면 `presets`를 넘긴다.
182
+
183
+ ```tsx
184
+ mountReviewShell({
185
+ projectId: REVIEW_PROJECT_ID,
186
+ pages,
187
+ adapters,
188
+ presets: [
189
+ { label: 'Mobile', kind: 'mobile', width: 540, height: 1080, designWidth: 540 },
190
+ { label: 'Tablet', kind: 'tablet', width: 768, height: 1024, designWidth: 768 },
191
+ { label: 'Desktop', kind: 'desktop', width: 1440, height: 900, designWidth: 1440 },
192
+ { label: 'Wide', kind: 'wide', width: 1980, height: 1080, designWidth: 1980 },
193
+ ],
194
+ });
195
+ ```
196
+
197
+ ## Development commands
198
+
199
+ Lexus repo 기준:
200
+
201
+ ```bash
202
+ pnpm dev:review
203
+ pnpm review-kit:typecheck
204
+ pnpm typecheck:review
205
+ pnpm review-kit:build
206
+ pnpm build:review
207
+ ```
208
+
209
+ Package source를 수정하면 `pnpm review-kit:build`로 `dist`를 갱신한다.
210
+
211
+ ## Publish checklist
212
+
213
+ 0.1 package 배포 전 확인:
214
+
215
+ - `packages/df-web-review-kit/package.json`의 `files`에 `dist`, `src`, `docs`, `README.md` 포함
216
+ - `pnpm review-kit:typecheck`
217
+ - `pnpm review-kit:build`
218
+ - local source에서 note/dom/area 생성 확인
219
+ - local item을 remote로 등록하면 local draft 삭제 확인
220
+ - remote source에서 status update/delete 확인
221
+ - `/review?source=supabase&target=...&item=...` restore 확인
222
+ - Supabase `reviewNumber`가 삭제 후 재사용되지 않는지 확인
@@ -0,0 +1,79 @@
1
+ # Package split checkpoint
2
+
3
+ Date: 2026-06-20
4
+ Branch: `uforgot/feat/review-kit-stabilize-ui`
5
+
6
+ ## Goal
7
+
8
+ Prepare `packages/df-web-review-kit` to behave like an independent package before moving Figma overlay or editing proposal work into it.
9
+
10
+ This checkpoint is not the actual repo split and does not publish the package.
11
+
12
+ ## Public entrypoints
13
+
14
+ Only these imports are public:
15
+
16
+ ```ts
17
+ import { createWebReviewKit, localAdapter } from '@designfever/web-review-kit';
18
+ import { mountReviewShell } from '@designfever/web-review-kit/react-shell';
19
+ ```
20
+
21
+ The package export map exposes:
22
+
23
+ - `.` → core API, adapters, shared types.
24
+ - `./react-shell` → review shell UI, presence adapters, page glob helper.
25
+ - `./package.json` → package metadata for tooling.
26
+
27
+ `src/*` paths are not public API.
28
+
29
+ ## Publish/file policy
30
+
31
+ `package.json#files` intentionally includes only:
32
+
33
+ - `dist`
34
+ - `docs`
35
+ - `README.md`
36
+
37
+ `src` is kept in the repo for development but excluded from the package file list so consumers rely on the exported API and generated declarations.
38
+
39
+ ## Dependency policy
40
+
41
+ - `react` and `react-dom` stay peer dependencies.
42
+ - `lucide-react` stays bundled into the built `react-shell` output for now. It is a dev/build dependency, not a host peer dependency.
43
+ - Figma token/API/fetch logic must not enter package core in this branch.
44
+
45
+ ## Host consumption policy
46
+
47
+ The Lexus host imports the package through the same public package entrypoints it would use after a repo split:
48
+
49
+ - `@designfever/web-review-kit`
50
+ - `@designfever/web-review-kit/react-shell`
51
+
52
+ Vite no longer aliases those package imports to `packages/df-web-review-kit/src` in dev. The host resolves the installed file dependency under `node_modules`, so `pnpm review-kit:build` must be run after package source changes to refresh `dist` and sync the installed package.
53
+
54
+ ## Review/test page policy
55
+
56
+ The Lexus `/review` page remains the integration smoke page. It is a consumer of the package, not part of the package surface.
57
+
58
+ If this package is moved to a separate repo later, keep a small playground/example app outside the package publish files so review-shell behavior can still be manually verified.
59
+
60
+ ## Verification gate
61
+
62
+ Before moving this item to review, run:
63
+
64
+ ```bash
65
+ pnpm review-kit:typecheck
66
+ pnpm review-kit:build
67
+ pnpm exec tsc --noEmit
68
+ pnpm exec vite build --mode seo
69
+ pnpm --dir packages/df-web-review-kit pack --pack-destination /tmp/df-web-review-kit-pack
70
+ ```
71
+
72
+ Manual smoke:
73
+
74
+ - Open `/review`.
75
+ - Confirm empty QA state.
76
+ - Inject or create a local QA item.
77
+ - Confirm card and prompt modal still render.
78
+ - Confirm settings and sitemap modals still open.
79
+ - Confirm browser console has no errors/warnings.
@@ -0,0 +1,138 @@
1
+ # review-kit presence handoff
2
+
3
+ ## 목적
4
+
5
+ QA 작업 중 "누가 어떤 review page를 보고 있는지"를 보여주기 위한 presence 기능 handoff 문서다.
6
+
7
+ Presence는 저장소가 아니다. `local`, `df-sheet`, `supabase` 같은 review item adapter는 영속 데이터 CRUD를 맡고, presence adapter는 현재 접속 세션 상태만 공유한다.
8
+
9
+ ## 현재 구현
10
+
11
+ 추가된 파일:
12
+
13
+ - `src/react-shell/presence.ts`
14
+ - `src/react-shell/types.ts`
15
+ - `src/react-shell.tsx`
16
+ - Lexus mount: `page/review/index.tsx`
17
+
18
+ 현재 `page/review/index.tsx`는 local 개발용 presence adapter를 붙인다.
19
+
20
+ ```ts
21
+ import { createLocalPresenceAdapter } from '@designfever/web-review-kit/react-shell';
22
+
23
+ const REVIEW_PRESENCE_ADAPTER = createLocalPresenceAdapter({
24
+ channelName: `${REVIEW_PROJECT_ID}:review-presence`,
25
+ });
26
+
27
+ mountReviewShell({
28
+ projectId: REVIEW_PROJECT_ID,
29
+ pages,
30
+ adapters: REVIEW_ADAPTERS,
31
+ presence: REVIEW_PRESENCE_ADAPTER,
32
+ });
33
+ ```
34
+
35
+ 현재 Lexus pilot은 `VITE_REVIEW_SUPABASE_ANON_KEY`가 있으면 Supabase Presence를 쓰고, 없으면 `createLocalPresenceAdapter()`로 fallback한다.
36
+
37
+ `createLocalPresenceAdapter()`는 `BroadcastChannel` 기반이다. 같은 origin의 여러 review 탭에서만 동작한다. 실제 팀 공유용 구현은 Supabase adapter를 사용한다.
38
+
39
+ ```env
40
+ VITE_REVIEW_SUPABASE_URL=https://vhqnvfkamnpgyqclohso.supabase.co
41
+ VITE_REVIEW_SUPABASE_ANON_KEY=
42
+ VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE=false
43
+ ```
44
+
45
+ ## Public contract
46
+
47
+ ```ts
48
+ type ReviewPresenceAdapter = {
49
+ label: string;
50
+ connect: (
51
+ context: ReviewPresenceContext
52
+ ) => Promise<ReviewPresenceSession> | ReviewPresenceSession;
53
+ };
54
+
55
+ type ReviewPresenceSession = {
56
+ update: (state: Partial<ReviewPresenceState>) => void | Promise<void>;
57
+ subscribe: (
58
+ callback: (users: ReviewPresenceUser[]) => void
59
+ ) => () => void;
60
+ disconnect: () => void | Promise<void>;
61
+ };
62
+ ```
63
+
64
+ `ReviewPresenceState`는 현재 이 shape을 사용한다.
65
+
66
+ ```ts
67
+ type ReviewPresenceState = {
68
+ projectId: string;
69
+ sessionId: string;
70
+ userId: string;
71
+ displayName: string;
72
+ color: string;
73
+ routeKey: string;
74
+ target: string;
75
+ source: 'local' | 'df-sheet';
76
+ viewport: {
77
+ label: string;
78
+ width: number;
79
+ height: number;
80
+ kind: 'mobile' | 'tablet' | 'desktop' | 'wide';
81
+ };
82
+ mode: 'idle' | 'note' | 'element' | 'area';
83
+ selectedItemId?: string | null;
84
+ selectedReviewNumber?: number | null;
85
+ status: 'idle' | 'reviewing' | 'editing';
86
+ updatedAt: string;
87
+ };
88
+ ```
89
+
90
+ ## Shell behavior
91
+
92
+ - Settings의 `User ID`가 비어 있으면 presence는 연결하지 않는다.
93
+ - `User ID`가 있으면 우측 QA list header 아래에 `online N`과 user chip을 보여준다.
94
+ - user chip에는 `displayName`, `target`, `viewport.label`을 표시한다.
95
+ - 현재 탭은 `is-self` class로 표시된다.
96
+ - 업데이트는 느린 상태만 보낸다:
97
+ - route/target
98
+ - source
99
+ - viewport
100
+ - review mode
101
+ - selected item/review number
102
+ - status
103
+ - scroll, mousemove, cursor 위치는 보내지 않는다.
104
+
105
+ ## 왜 storage adapter와 분리했나
106
+
107
+ storage adapter:
108
+
109
+ - `get`
110
+ - `list`
111
+ - `create`
112
+ - `updateStatus`
113
+ - `remove`
114
+ - remote promote/move
115
+
116
+ presence adapter:
117
+
118
+ - `connect`
119
+ - `update`
120
+ - `subscribe`
121
+ - `disconnect`
122
+
123
+ 수명주기가 다르다. storage는 item의 영속 상태이고, presence는 websocket session 상태다. 같은 adapter array에 넣으면 API가 어색해지고, remote item source를 바꾸는 일과 협업 session을 바꾸는 일이 섞인다.
124
+
125
+ ## 다음 작업
126
+
127
+ 1. `supabasePresenceAdapter` 추가
128
+ 2. `@supabase/supabase-js`를 peer/dev dependency로 둘지 별도 package adapter로 분리할지 결정
129
+ 3. Supabase project/env 연결
130
+ 4. private channel + Realtime Authorization 적용
131
+ 5. 두 브라우저 또는 두 기기에서 같은 project presence 확인
132
+
133
+ ## 주의점
134
+
135
+ - Presence payload는 작게 유지한다.
136
+ - high-frequency state는 Presence에 넣지 않는다.
137
+ - 현재 `ReviewSource`가 `'local' | 'df-sheet'`로 고정되어 있어서 source generalization 작업과 Supabase storage adapter 작업이 만나면 타입을 먼저 풀어야 한다.
138
+ - `User ID`는 현재 localStorage 기반이다. 실제 remote presence에서는 auth user id나 project member id로 바꾸는 편이 맞다.