@designfever/web-review-kit 0.1.0 → 0.2.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 +59 -184
- package/dist/{chunk-U5K2YGGL.js → chunk-EJDROXJM.js} +2583 -3085
- package/dist/chunk-EJDROXJM.js.map +1 -0
- package/dist/index.cjs +2615 -2900
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -6
- package/dist/index.d.ts +10 -6
- package/dist/index.js +220 -7
- package/dist/index.js.map +1 -1
- package/dist/react-shell.cjs +8364 -6651
- package/dist/react-shell.cjs.map +1 -1
- package/dist/react-shell.d.cts +7 -3
- package/dist/react-shell.d.ts +7 -3
- package/dist/react-shell.js +5163 -3425
- package/dist/react-shell.js.map +1 -1
- package/dist/{types-D_mNjOHx.d.cts → types-NiCp9JJQ.d.cts} +6 -14
- package/dist/{types-D_mNjOHx.d.ts → types-NiCp9JJQ.d.ts} +6 -14
- package/docs/README.md +21 -30
- package/docs/adaptor.sample.ts +182 -0
- package/docs/architecture.md +125 -0
- package/docs/db-setup.md +253 -0
- package/docs/figma-overlay.md +52 -0
- package/docs/grid-overlay.md +38 -0
- package/docs/installation.md +75 -40
- package/package.json +8 -3
- package/dist/chunk-U5K2YGGL.js.map +0 -1
- package/docs/adapter-handoff.md +0 -146
- package/docs/concept.md +0 -102
- package/docs/df-sheet-adapter.md +0 -336
- package/docs/df-sheet-next.md +0 -222
- package/docs/initial-plan.md +0 -226
- package/docs/package-split-checkpoint.md +0 -79
- package/docs/presence-handoff.md +0 -138
- package/docs/review-feedback-2026-06-20.md +0 -267
- package/docs/smoke-baseline-2026-06-20.md +0 -41
- package/docs/stabilize-ui-work-guide.md +0 -243
- package/docs/supabase-presence.md +0 -198
- package/docs/supabase-review-items.md +0 -365
- package/docs/supabase.md +0 -205
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
# Supabase review item adapter
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
`df-web-review-kit`의 remote QA 저장소를 Supabase table로 검증하기 위한 문서다.
|
|
6
|
-
|
|
7
|
-
Lexus pilot:
|
|
8
|
-
|
|
9
|
-
```txt
|
|
10
|
-
project: bb-qa
|
|
11
|
-
url: https://vhqnvfkamnpgyqclohso.supabase.co
|
|
12
|
-
table: review_items
|
|
13
|
-
source: supabase
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Environment
|
|
17
|
-
|
|
18
|
-
```env
|
|
19
|
-
VITE_REVIEW_SUPABASE_URL=https://vhqnvfkamnpgyqclohso.supabase.co
|
|
20
|
-
VITE_REVIEW_SUPABASE_ANON_KEY=
|
|
21
|
-
VITE_REVIEW_SUPABASE_TABLE=review_items
|
|
22
|
-
VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE=false
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`anon` key만 browser env에 넣는다. `service_role` key는 browser env에 넣지 않는다.
|
|
26
|
-
|
|
27
|
-
## Schema
|
|
28
|
-
|
|
29
|
-
Supabase SQL editor에서 실행한다.
|
|
30
|
-
|
|
31
|
-
```sql
|
|
32
|
-
create table if not exists public.review_items (
|
|
33
|
-
id text primary key,
|
|
34
|
-
project_id text not null,
|
|
35
|
-
route_key text not null,
|
|
36
|
-
source text not null default 'supabase',
|
|
37
|
-
review_number integer,
|
|
38
|
-
status text not null default 'todo',
|
|
39
|
-
item jsonb not null,
|
|
40
|
-
created_at timestamptz not null default now(),
|
|
41
|
-
updated_at timestamptz not null default now()
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
create unique index if not exists review_items_project_review_number_idx
|
|
45
|
-
on public.review_items (project_id, source, review_number)
|
|
46
|
-
where review_number is not null;
|
|
47
|
-
|
|
48
|
-
create index if not exists review_items_project_route_updated_idx
|
|
49
|
-
on public.review_items (project_id, source, route_key, updated_at desc);
|
|
50
|
-
|
|
51
|
-
create index if not exists review_items_project_status_idx
|
|
52
|
-
on public.review_items (project_id, source, status);
|
|
53
|
-
|
|
54
|
-
create table if not exists public.review_project_counters (
|
|
55
|
-
project_id text not null,
|
|
56
|
-
source text not null default 'supabase',
|
|
57
|
-
next_review_number integer not null default 1,
|
|
58
|
-
updated_at timestamptz not null default now(),
|
|
59
|
-
primary key (project_id, source),
|
|
60
|
-
constraint review_project_counters_next_review_number_check
|
|
61
|
-
check (next_review_number > 0)
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
create or replace function public.create_review_item(
|
|
65
|
-
p_id text,
|
|
66
|
-
p_project_id text,
|
|
67
|
-
p_route_key text,
|
|
68
|
-
p_source text,
|
|
69
|
-
p_status text,
|
|
70
|
-
p_item jsonb
|
|
71
|
-
)
|
|
72
|
-
returns public.review_items
|
|
73
|
-
language plpgsql
|
|
74
|
-
security invoker
|
|
75
|
-
set search_path = public
|
|
76
|
-
as $$
|
|
77
|
-
declare
|
|
78
|
-
v_review_number integer;
|
|
79
|
-
v_now timestamptz := now();
|
|
80
|
-
v_row public.review_items;
|
|
81
|
-
begin
|
|
82
|
-
insert into public.review_project_counters (
|
|
83
|
-
project_id,
|
|
84
|
-
source,
|
|
85
|
-
next_review_number,
|
|
86
|
-
updated_at
|
|
87
|
-
)
|
|
88
|
-
select
|
|
89
|
-
p_project_id,
|
|
90
|
-
p_source,
|
|
91
|
-
coalesce(max(review_number), 0) + 2,
|
|
92
|
-
v_now
|
|
93
|
-
from public.review_items
|
|
94
|
-
where project_id = p_project_id
|
|
95
|
-
and source = p_source
|
|
96
|
-
on conflict (project_id, source) do update
|
|
97
|
-
set next_review_number = greatest(
|
|
98
|
-
public.review_project_counters.next_review_number + 1,
|
|
99
|
-
excluded.next_review_number
|
|
100
|
-
),
|
|
101
|
-
updated_at = excluded.updated_at
|
|
102
|
-
returning next_review_number - 1 into v_review_number;
|
|
103
|
-
|
|
104
|
-
insert into public.review_items (
|
|
105
|
-
id,
|
|
106
|
-
project_id,
|
|
107
|
-
route_key,
|
|
108
|
-
source,
|
|
109
|
-
review_number,
|
|
110
|
-
status,
|
|
111
|
-
item,
|
|
112
|
-
created_at,
|
|
113
|
-
updated_at
|
|
114
|
-
)
|
|
115
|
-
values (
|
|
116
|
-
p_id,
|
|
117
|
-
p_project_id,
|
|
118
|
-
p_route_key,
|
|
119
|
-
p_source,
|
|
120
|
-
v_review_number,
|
|
121
|
-
p_status,
|
|
122
|
-
p_item || jsonb_build_object(
|
|
123
|
-
'id', p_id,
|
|
124
|
-
'reviewNumber', v_review_number,
|
|
125
|
-
'projectId', p_project_id,
|
|
126
|
-
'routeKey', p_route_key,
|
|
127
|
-
'normalizedPath', coalesce(nullif(p_item->>'normalizedPath', ''), p_route_key),
|
|
128
|
-
'status', p_status,
|
|
129
|
-
'externalIssueId', p_id,
|
|
130
|
-
'submittedAt', coalesce(p_item->>'submittedAt', v_now::text),
|
|
131
|
-
'submitStatus', coalesce(p_item->>'submitStatus', 'submitted'),
|
|
132
|
-
'createdAt', v_now::text,
|
|
133
|
-
'updatedAt', v_now::text
|
|
134
|
-
),
|
|
135
|
-
v_now,
|
|
136
|
-
v_now
|
|
137
|
-
)
|
|
138
|
-
returning * into v_row;
|
|
139
|
-
|
|
140
|
-
return v_row;
|
|
141
|
-
end;
|
|
142
|
-
$$;
|
|
143
|
-
|
|
144
|
-
grant execute on function public.create_review_item(
|
|
145
|
-
text,
|
|
146
|
-
text,
|
|
147
|
-
text,
|
|
148
|
-
text,
|
|
149
|
-
text,
|
|
150
|
-
jsonb
|
|
151
|
-
) to anon;
|
|
152
|
-
|
|
153
|
-
grant select, insert, update, delete on public.review_items to anon;
|
|
154
|
-
grant select, insert, update on public.review_project_counters to anon;
|
|
155
|
-
|
|
156
|
-
alter table public.review_items enable row level security;
|
|
157
|
-
alter table public.review_project_counters enable row level security;
|
|
158
|
-
|
|
159
|
-
drop policy if exists review_items_lexus_read on public.review_items;
|
|
160
|
-
create policy review_items_lexus_read
|
|
161
|
-
on public.review_items
|
|
162
|
-
for select
|
|
163
|
-
to anon
|
|
164
|
-
using (project_id = 'lexus-official-v2026');
|
|
165
|
-
|
|
166
|
-
drop policy if exists review_items_lexus_insert on public.review_items;
|
|
167
|
-
create policy review_items_lexus_insert
|
|
168
|
-
on public.review_items
|
|
169
|
-
for insert
|
|
170
|
-
to anon
|
|
171
|
-
with check (project_id = 'lexus-official-v2026');
|
|
172
|
-
|
|
173
|
-
drop policy if exists review_items_lexus_update on public.review_items;
|
|
174
|
-
create policy review_items_lexus_update
|
|
175
|
-
on public.review_items
|
|
176
|
-
for update
|
|
177
|
-
to anon
|
|
178
|
-
using (project_id = 'lexus-official-v2026')
|
|
179
|
-
with check (project_id = 'lexus-official-v2026');
|
|
180
|
-
|
|
181
|
-
drop policy if exists review_items_lexus_delete on public.review_items;
|
|
182
|
-
create policy review_items_lexus_delete
|
|
183
|
-
on public.review_items
|
|
184
|
-
for delete
|
|
185
|
-
to anon
|
|
186
|
-
using (project_id = 'lexus-official-v2026');
|
|
187
|
-
|
|
188
|
-
drop policy if exists review_project_counters_lexus_read on public.review_project_counters;
|
|
189
|
-
create policy review_project_counters_lexus_read
|
|
190
|
-
on public.review_project_counters
|
|
191
|
-
for select
|
|
192
|
-
to anon
|
|
193
|
-
using (project_id = 'lexus-official-v2026');
|
|
194
|
-
|
|
195
|
-
drop policy if exists review_project_counters_lexus_insert on public.review_project_counters;
|
|
196
|
-
create policy review_project_counters_lexus_insert
|
|
197
|
-
on public.review_project_counters
|
|
198
|
-
for insert
|
|
199
|
-
to anon
|
|
200
|
-
with check (project_id = 'lexus-official-v2026');
|
|
201
|
-
|
|
202
|
-
drop policy if exists review_project_counters_lexus_update on public.review_project_counters;
|
|
203
|
-
create policy review_project_counters_lexus_update
|
|
204
|
-
on public.review_project_counters
|
|
205
|
-
for update
|
|
206
|
-
to anon
|
|
207
|
-
using (project_id = 'lexus-official-v2026')
|
|
208
|
-
with check (project_id = 'lexus-official-v2026');
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## Migration from max+1 RPC
|
|
212
|
-
|
|
213
|
-
이미 `review_items`와 이전 `create_review_item` RPC를 만든 DB에서는 아래 블록만 실행한다.
|
|
214
|
-
|
|
215
|
-
```sql
|
|
216
|
-
create table if not exists public.review_project_counters (
|
|
217
|
-
project_id text not null,
|
|
218
|
-
source text not null default 'supabase',
|
|
219
|
-
next_review_number integer not null default 1,
|
|
220
|
-
updated_at timestamptz not null default now(),
|
|
221
|
-
primary key (project_id, source),
|
|
222
|
-
constraint review_project_counters_next_review_number_check
|
|
223
|
-
check (next_review_number > 0)
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
create or replace function public.create_review_item(
|
|
227
|
-
p_id text,
|
|
228
|
-
p_project_id text,
|
|
229
|
-
p_route_key text,
|
|
230
|
-
p_source text,
|
|
231
|
-
p_status text,
|
|
232
|
-
p_item jsonb
|
|
233
|
-
)
|
|
234
|
-
returns public.review_items
|
|
235
|
-
language plpgsql
|
|
236
|
-
security invoker
|
|
237
|
-
set search_path = public
|
|
238
|
-
as $$
|
|
239
|
-
declare
|
|
240
|
-
v_review_number integer;
|
|
241
|
-
v_now timestamptz := now();
|
|
242
|
-
v_row public.review_items;
|
|
243
|
-
begin
|
|
244
|
-
insert into public.review_project_counters (
|
|
245
|
-
project_id,
|
|
246
|
-
source,
|
|
247
|
-
next_review_number,
|
|
248
|
-
updated_at
|
|
249
|
-
)
|
|
250
|
-
select
|
|
251
|
-
p_project_id,
|
|
252
|
-
p_source,
|
|
253
|
-
coalesce(max(review_number), 0) + 2,
|
|
254
|
-
v_now
|
|
255
|
-
from public.review_items
|
|
256
|
-
where project_id = p_project_id
|
|
257
|
-
and source = p_source
|
|
258
|
-
on conflict (project_id, source) do update
|
|
259
|
-
set next_review_number = greatest(
|
|
260
|
-
public.review_project_counters.next_review_number + 1,
|
|
261
|
-
excluded.next_review_number
|
|
262
|
-
),
|
|
263
|
-
updated_at = excluded.updated_at
|
|
264
|
-
returning next_review_number - 1 into v_review_number;
|
|
265
|
-
|
|
266
|
-
insert into public.review_items (
|
|
267
|
-
id,
|
|
268
|
-
project_id,
|
|
269
|
-
route_key,
|
|
270
|
-
source,
|
|
271
|
-
review_number,
|
|
272
|
-
status,
|
|
273
|
-
item,
|
|
274
|
-
created_at,
|
|
275
|
-
updated_at
|
|
276
|
-
)
|
|
277
|
-
values (
|
|
278
|
-
p_id,
|
|
279
|
-
p_project_id,
|
|
280
|
-
p_route_key,
|
|
281
|
-
p_source,
|
|
282
|
-
v_review_number,
|
|
283
|
-
p_status,
|
|
284
|
-
p_item || jsonb_build_object(
|
|
285
|
-
'id', p_id,
|
|
286
|
-
'reviewNumber', v_review_number,
|
|
287
|
-
'projectId', p_project_id,
|
|
288
|
-
'routeKey', p_route_key,
|
|
289
|
-
'normalizedPath', coalesce(nullif(p_item->>'normalizedPath', ''), p_route_key),
|
|
290
|
-
'status', p_status,
|
|
291
|
-
'externalIssueId', p_id,
|
|
292
|
-
'submittedAt', coalesce(p_item->>'submittedAt', v_now::text),
|
|
293
|
-
'submitStatus', coalesce(p_item->>'submitStatus', 'submitted'),
|
|
294
|
-
'createdAt', v_now::text,
|
|
295
|
-
'updatedAt', v_now::text
|
|
296
|
-
),
|
|
297
|
-
v_now,
|
|
298
|
-
v_now
|
|
299
|
-
)
|
|
300
|
-
returning * into v_row;
|
|
301
|
-
|
|
302
|
-
return v_row;
|
|
303
|
-
end;
|
|
304
|
-
$$;
|
|
305
|
-
|
|
306
|
-
grant execute on function public.create_review_item(
|
|
307
|
-
text,
|
|
308
|
-
text,
|
|
309
|
-
text,
|
|
310
|
-
text,
|
|
311
|
-
text,
|
|
312
|
-
jsonb
|
|
313
|
-
) to anon;
|
|
314
|
-
|
|
315
|
-
grant select, insert, update on public.review_project_counters to anon;
|
|
316
|
-
|
|
317
|
-
alter table public.review_project_counters enable row level security;
|
|
318
|
-
|
|
319
|
-
drop policy if exists review_project_counters_lexus_read on public.review_project_counters;
|
|
320
|
-
create policy review_project_counters_lexus_read
|
|
321
|
-
on public.review_project_counters
|
|
322
|
-
for select
|
|
323
|
-
to anon
|
|
324
|
-
using (project_id = 'lexus-official-v2026');
|
|
325
|
-
|
|
326
|
-
drop policy if exists review_project_counters_lexus_insert on public.review_project_counters;
|
|
327
|
-
create policy review_project_counters_lexus_insert
|
|
328
|
-
on public.review_project_counters
|
|
329
|
-
for insert
|
|
330
|
-
to anon
|
|
331
|
-
with check (project_id = 'lexus-official-v2026');
|
|
332
|
-
|
|
333
|
-
drop policy if exists review_project_counters_lexus_update on public.review_project_counters;
|
|
334
|
-
create policy review_project_counters_lexus_update
|
|
335
|
-
on public.review_project_counters
|
|
336
|
-
for update
|
|
337
|
-
to anon
|
|
338
|
-
using (project_id = 'lexus-official-v2026')
|
|
339
|
-
with check (project_id = 'lexus-official-v2026');
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
## Current adapter behavior
|
|
343
|
-
|
|
344
|
-
- `list`: `project_id`, `source`, optional `route_key`, optional `status`로 조회한다.
|
|
345
|
-
- `create`: local draft id/number를 버리고 새 `id`를 만든 뒤 `create_review_item` RPC로 canonical `review_number`를 발급하고 insert한다.
|
|
346
|
-
- `update`: status 포함 item patch를 JSONB와 query 컬럼에 반영한다.
|
|
347
|
-
- `remove`: row를 삭제한다.
|
|
348
|
-
|
|
349
|
-
## Security note
|
|
350
|
-
|
|
351
|
-
위 RLS는 dev 검증용이다. 누구든 anon key와 project id를 알면 QA row를 만들고 수정할 수 있다.
|
|
352
|
-
|
|
353
|
-
package 배포 전에는 다음 중 하나로 좁히는 것을 권장한다.
|
|
354
|
-
|
|
355
|
-
- authenticated user + project member table 기반 RLS
|
|
356
|
-
- Supabase Edge Function 또는 project backend proxy
|
|
357
|
-
- private deployment 내부에서만 노출되는 service endpoint
|
|
358
|
-
|
|
359
|
-
## Numbering note
|
|
360
|
-
|
|
361
|
-
local item의 `#id`는 개인 draft 번호라 여러 사용자가 동시에 작업하면 겹칠 수 있다.
|
|
362
|
-
|
|
363
|
-
remote source에 등록될 때는 Supabase adapter가 local 번호를 버리고 새 canonical `review_number`를 받는다. 기본 create 경로는 `create_review_item` RPC를 사용한다. RPC는 `review_project_counters` row를 `insert ... on conflict ... do update returning`으로 증가시켜 같은 project/source 번호 발급만 짧게 직렬화한다. 삭제된 번호도 재사용하지 않는다.
|
|
364
|
-
|
|
365
|
-
`unsafeClientReviewNumberFallback: true`를 adapter option에 넣으면 예전 client-side `max(review_number) + 1` fallback을 사용할 수 있지만, 동시 등록에서 충돌 가능성이 있으므로 dev 임시 용도 외에는 쓰지 않는다.
|
package/docs/supabase.md
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# Supabase setup
|
|
2
|
-
|
|
3
|
-
Supabase는 `df-web-review-kit`의 remote QA 저장소와 team presence backend로 쓸 수 있다.
|
|
4
|
-
|
|
5
|
-
Lexus pilot:
|
|
6
|
-
|
|
7
|
-
```txt
|
|
8
|
-
Supabase project: bb-qa
|
|
9
|
-
url: https://vhqnvfkamnpgyqclohso.supabase.co
|
|
10
|
-
review project id: lexus-official-v2026
|
|
11
|
-
source: supabase
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## 역할
|
|
15
|
-
|
|
16
|
-
Supabase review item adapter:
|
|
17
|
-
|
|
18
|
-
- local draft를 remote QA row로 등록
|
|
19
|
-
- canonical `reviewNumber` 발급
|
|
20
|
-
- remote item list/get/update/delete
|
|
21
|
-
- deep link restore용 item JSON 저장
|
|
22
|
-
|
|
23
|
-
Supabase presence adapter:
|
|
24
|
-
|
|
25
|
-
- 같은 review project 안의 접속자 상태 공유
|
|
26
|
-
- shell UI에서는 현재 page에 있는 user id만 표시
|
|
27
|
-
- sitemap에서는 page별 presence 표시로 확장 가능
|
|
28
|
-
|
|
29
|
-
## Required env
|
|
30
|
-
|
|
31
|
-
```env
|
|
32
|
-
VITE_REVIEW_SUPABASE_URL=https://your-project.supabase.co
|
|
33
|
-
VITE_REVIEW_SUPABASE_ANON_KEY=
|
|
34
|
-
VITE_REVIEW_SUPABASE_TABLE=review_items
|
|
35
|
-
VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE=false
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`anon` key만 browser env에 넣는다. `service_role` key는 browser에 넣지 않는다.
|
|
39
|
-
|
|
40
|
-
## Database setup
|
|
41
|
-
|
|
42
|
-
새 Supabase project에서는 [supabase-review-items.md](supabase-review-items.md)의 `Schema` SQL을 실행한다.
|
|
43
|
-
|
|
44
|
-
이미 이전 `max(review_number) + 1` RPC를 실행한 DB에서는 [supabase-review-items.md](supabase-review-items.md)의 `Migration from max+1 RPC` 블록만 실행한다.
|
|
45
|
-
|
|
46
|
-
현재 table:
|
|
47
|
-
|
|
48
|
-
- `review_items`: QA row와 restore payload 저장
|
|
49
|
-
- `review_project_counters`: `(project_id, source)`별 다음 번호 저장
|
|
50
|
-
|
|
51
|
-
현재 RPC:
|
|
52
|
-
|
|
53
|
-
- `create_review_item`: counter row를 증가시키고 item을 insert
|
|
54
|
-
|
|
55
|
-
## Review number policy
|
|
56
|
-
|
|
57
|
-
local `#id`는 개인 draft 번호다. 여러 사람이 local에서 동시에 만들면 번호가 겹칠 수 있다.
|
|
58
|
-
|
|
59
|
-
remote 등록 시 Supabase adapter는 local id와 local `reviewNumber`를 버린다. `create_review_item` RPC가 새 UUID와 canonical `reviewNumber`를 부여한다.
|
|
60
|
-
|
|
61
|
-
번호 발급은 `review_project_counters`를 사용한다.
|
|
62
|
-
|
|
63
|
-
- 동시 등록 중복 방지
|
|
64
|
-
- 삭제 후 번호 재사용 방지
|
|
65
|
-
- project/source별 번호 분리
|
|
66
|
-
|
|
67
|
-
검증된 smoke:
|
|
68
|
-
|
|
69
|
-
```txt
|
|
70
|
-
concurrent create: #7, #8
|
|
71
|
-
delete #7/#8
|
|
72
|
-
next create: #9
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## RLS policy
|
|
76
|
-
|
|
77
|
-
현재 Lexus pilot RLS는 dev 검증용이다.
|
|
78
|
-
|
|
79
|
-
```txt
|
|
80
|
-
project_id = 'lexus-official-v2026'
|
|
81
|
-
role = anon
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
이 방식은 anon key와 project id를 알면 row create/update/delete가 가능하다. production/package 운영에서는 다음 중 하나로 좁힌다.
|
|
85
|
-
|
|
86
|
-
- Supabase Auth + project member table 기반 RLS
|
|
87
|
-
- Supabase Edge Function
|
|
88
|
-
- project backend proxy
|
|
89
|
-
- private deployment 내부 service endpoint
|
|
90
|
-
|
|
91
|
-
여러 project를 같은 `bb-qa` DB에 넣는 것은 가능하다. `review_projects`와 `review_items.project_id`로 분리한다. 예:
|
|
92
|
-
|
|
93
|
-
```sql
|
|
94
|
-
insert into public.review_projects (id, label)
|
|
95
|
-
values
|
|
96
|
-
('lexus-official-v2026', 'Lexus Official v2026'),
|
|
97
|
-
('hdc-lab', 'HDC Lab')
|
|
98
|
-
on conflict (id) do update
|
|
99
|
-
set label = excluded.label;
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Project를 추가하면 해당 `project_id`를 허용하는 RLS policy도 같이 추가해야 한다.
|
|
103
|
-
|
|
104
|
-
## Host code wiring
|
|
105
|
-
|
|
106
|
-
```tsx
|
|
107
|
-
import {
|
|
108
|
-
createFallbackPresenceAdapter,
|
|
109
|
-
createLocalPresenceAdapter,
|
|
110
|
-
createSupabasePresenceAdapter,
|
|
111
|
-
type SupabasePresenceClient,
|
|
112
|
-
} from '@designfever/web-review-kit/react-shell';
|
|
113
|
-
import {
|
|
114
|
-
supabaseAdapter,
|
|
115
|
-
type SupabaseReviewClient,
|
|
116
|
-
} from '@designfever/web-review-kit';
|
|
117
|
-
import { createClient } from '@supabase/supabase-js';
|
|
118
|
-
|
|
119
|
-
const REVIEW_PROJECT_ID = 'lexus-official-v2026';
|
|
120
|
-
const REVIEW_PATH_PREFIX = '/review';
|
|
121
|
-
|
|
122
|
-
const supabaseClient = import.meta.env.VITE_REVIEW_SUPABASE_ANON_KEY
|
|
123
|
-
? createClient(
|
|
124
|
-
import.meta.env.VITE_REVIEW_SUPABASE_URL,
|
|
125
|
-
import.meta.env.VITE_REVIEW_SUPABASE_ANON_KEY
|
|
126
|
-
)
|
|
127
|
-
: null;
|
|
128
|
-
|
|
129
|
-
const reviewAdapter = supabaseClient
|
|
130
|
-
? supabaseAdapter({
|
|
131
|
-
client: supabaseClient as unknown as SupabaseReviewClient,
|
|
132
|
-
table: import.meta.env.VITE_REVIEW_SUPABASE_TABLE || 'review_items',
|
|
133
|
-
projectId: REVIEW_PROJECT_ID,
|
|
134
|
-
source: 'supabase',
|
|
135
|
-
reviewPathPrefix: REVIEW_PATH_PREFIX,
|
|
136
|
-
})
|
|
137
|
-
: null;
|
|
138
|
-
|
|
139
|
-
const localPresence = createLocalPresenceAdapter({
|
|
140
|
-
channelName: `${REVIEW_PROJECT_ID}:review-presence`,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
const presence = supabaseClient
|
|
144
|
-
? createFallbackPresenceAdapter(
|
|
145
|
-
createSupabasePresenceAdapter({
|
|
146
|
-
client: supabaseClient as unknown as SupabasePresenceClient,
|
|
147
|
-
channelPrefix: 'review-presence',
|
|
148
|
-
private: import.meta.env.VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE === 'true',
|
|
149
|
-
}),
|
|
150
|
-
localPresence
|
|
151
|
-
)
|
|
152
|
-
: localPresence;
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Presence channel
|
|
156
|
-
|
|
157
|
-
Topic:
|
|
158
|
-
|
|
159
|
-
```txt
|
|
160
|
-
review-presence-<projectId>
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Adapter는 topic에 안전하지 않은 문자를 `-`로 normalize한다.
|
|
164
|
-
|
|
165
|
-
`private=false`면 별도 Realtime RLS 없이 빠르게 검증할 수 있다. `private=true`로 바꾸려면 Supabase Realtime authorization policy가 필요하다.
|
|
166
|
-
|
|
167
|
-
## Validation
|
|
168
|
-
|
|
169
|
-
After SQL:
|
|
170
|
-
|
|
171
|
-
1. `/review?source=supabase` 접속
|
|
172
|
-
2. local item 생성
|
|
173
|
-
3. remote 등록
|
|
174
|
-
4. local draft 삭제 확인
|
|
175
|
-
5. remote source에서 item 표시 확인
|
|
176
|
-
6. status dropdown 변경 확인
|
|
177
|
-
7. delete 확인
|
|
178
|
-
8. 새 remote item을 만들어 번호가 재사용되지 않는지 확인
|
|
179
|
-
9. review page를 두 탭에서 열고 같은 page presence 표시 확인
|
|
180
|
-
|
|
181
|
-
SQL smoke:
|
|
182
|
-
|
|
183
|
-
- concurrent create 2개가 서로 다른 `review_number`를 받아야 한다.
|
|
184
|
-
- 두 row 삭제 후 다음 create가 더 큰 번호를 받아야 한다.
|
|
185
|
-
|
|
186
|
-
## Troubleshooting
|
|
187
|
-
|
|
188
|
-
`create_review_item rpc is required`
|
|
189
|
-
|
|
190
|
-
- Supabase client mock이나 wrapper에 `rpc`가 없는 경우다.
|
|
191
|
-
- 실제 `@supabase/supabase-js` client를 넘긴다.
|
|
192
|
-
|
|
193
|
-
`permission denied for table review_project_counters`
|
|
194
|
-
|
|
195
|
-
- counter table grant 또는 RLS policy가 빠진 상태다.
|
|
196
|
-
- `Migration from max+1 RPC` 블록을 다시 확인한다.
|
|
197
|
-
|
|
198
|
-
remote item은 보이지만 presence가 안 보임
|
|
199
|
-
|
|
200
|
-
- `VITE_REVIEW_SUPABASE_ANON_KEY`가 비어 있으면 local presence fallback만 동작한다.
|
|
201
|
-
- `VITE_REVIEW_SUPABASE_PRESENCE_PRIVATE=true`인데 Realtime authorization policy가 없으면 실패할 수 있다.
|
|
202
|
-
|
|
203
|
-
remote 번호가 local 번호와 다름
|
|
204
|
-
|
|
205
|
-
- 정상이다. local 번호는 draft용이고 Supabase 번호가 canonical 번호다.
|