@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.
- package/README.md +241 -0
- package/dist/chunk-U5K2YGGL.js +3486 -0
- package/dist/chunk-U5K2YGGL.js.map +1 -0
- package/dist/index.cjs +3524 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/react-shell.cjs +8841 -0
- package/dist/react-shell.cjs.map +1 -0
- package/dist/react-shell.d.cts +156 -0
- package/dist/react-shell.d.ts +156 -0
- package/dist/react-shell.js +5844 -0
- package/dist/react-shell.js.map +1 -0
- package/dist/types-D_mNjOHx.d.cts +183 -0
- package/dist/types-D_mNjOHx.d.ts +183 -0
- package/docs/README.md +37 -0
- package/docs/adapter-handoff.md +146 -0
- package/docs/concept.md +102 -0
- package/docs/df-sheet-adapter.md +336 -0
- package/docs/df-sheet-next.md +222 -0
- package/docs/initial-plan.md +226 -0
- package/docs/installation.md +222 -0
- package/docs/package-split-checkpoint.md +79 -0
- package/docs/presence-handoff.md +138 -0
- package/docs/review-feedback-2026-06-20.md +267 -0
- package/docs/smoke-baseline-2026-06-20.md +41 -0
- package/docs/stabilize-ui-work-guide.md +243 -0
- package/docs/supabase-presence.md +198 -0
- package/docs/supabase-review-items.md +365 -0
- package/docs/supabase.md +205 -0
- package/package.json +61 -0
|
@@ -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로 바꾸는 편이 맞다.
|