create-saas-starter-workspace 0.1.11 → 0.1.13

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,47 @@
1
+ ---
2
+ name: domain-connect
3
+ description: Attach a paid custom domain to the already-live web service via Vercel-managed DNS, verify SSL, and propagate the new domain everywhere it matters — Supabase Auth redirect, the webview app's target URL, and the link-preview (OG) base. Use when the Owner wants a custom domain instead of the free *.vercel.app URL, or invokes /domain-connect. Optional and paid.
4
+ ---
5
+
6
+ # /domain-connect
7
+
8
+ 운영 중인 web에 **유료 커스텀 도메인**을 붙인다. 선택 사항이다 — 기본은 무료 `*.vercel.app` 주소로 충분하고, 직접 도메인을 원할 때만 쓴다. 외부 서비스의 절차·비용·필드명은 자주 바뀌므로 기억에 의존하지 말고 그때그때 공식 문서·`--help`·실제 응답을 확인하며 적응한다.
9
+
10
+ > 전제: web이 이미 실제 주소로 떠 있어야 한다(`/web-launch` 완료). 도메인은 그 위에 얹는 작업이다. 모두 `web/` 안에서 한다(먼저 `cd web/`).
11
+
12
+ ## 베스트프랙티스 (2026): Vercel 관리형
13
+
14
+ DNS·SSL·apex(루트 도메인) 평탄화를 Vercel이 자동으로 처리하게 한다. 둘 중 하나:
15
+
16
+ - **Vercel에서 구매** (`vercel domains buy` 또는 대시보드) — 가장 단순. 네임서버가 처음부터 Vercel이다.
17
+ - **외부에서 구매 후 네임서버(NS)를 Vercel로 위임** — 기존 도메인을 쓸 때. 위임하면 그 뒤로는 Vercel이 자동 관리.
18
+
19
+ `vercel domains`·`vercel domains add` CLI라 에이전트가 대부분 수행한다. 구매·NS 변경 같은 과금·계정 행위만 Owner가 직접 확인한다.
20
+
21
+ > **Cloudflare Registrar는 비추천.** NS 위임을 막고 자사 DNS를 강제해 이 자동화와 맞지 않는다. (거기서 이미 산 도메인이라면 A/CNAME 수동 설정으로도 되지만 권장 경로가 아니다.)
22
+
23
+ ## 절차 (순서가 중요하다)
24
+
25
+ 1. **도메인 확보.** Vercel 구매 또는 외부 구매 후 NS를 Vercel로 위임한다.
26
+ 2. **프로젝트에 연결.** `vercel domains add <domain>`(또는 Settings → Domains)으로 **apex와 www 둘 다** 붙이고, 새 도메인을 기본(primary)으로 둔다.
27
+ 3. **SSL 자동 발급 확인.** Vercel이 인증서를 발급할 때까지 대기하고, `https://<domain>` 접속이 정상인지 확인한다.
28
+ 4. **전파 (빠뜨리면 조용히 깨진다).** 도메인은 web 한 곳만 바꾸는 게 아니다. 세 곳에 같이 반영한다:
29
+ - **Supabase Auth**: prod 프로젝트의 `site_url`과 redirect 허용목록(`uri_allow_list`)에 새 도메인을 추가한다(웹 로그인 리디렉션). `/web-launch` step 7의 redirect 설정 로직을 그대로 재사용한다.
30
+ - **웹뷰 앱**: `mobile/`의 `EXPO_PUBLIC_WEB_URL`을 새 도메인으로 바꾼다. 빌드 시점에 인라인되므로 **재빌드가 필요**하다(→ `eas-deploy-guide`). 앱이 옛 주소를 바라보면 화면이 비거나 로그인이 깨진다.
31
+ - **공유 미리보기(OG)**: 링크 공유 시 보이는 기준 URL(메타데이터 base)을 새 도메인으로 바꾼다.
32
+ 5. **옛 주소 유지.** `*.vercel.app`은 살려두되 새 도메인을 기본으로 한다.
33
+
34
+ ## 함정 (조용히 깨지는 것)
35
+
36
+ - **apex/www 한쪽만 등록** → 다른 쪽 접속 실패. 둘 다 붙인다.
37
+ - **redirect 미갱신**(4) → 운영 로그인이 무음으로 깨진다(`/web-launch`와 같은 사고). 가장 흔하다.
38
+ - **앱 URL은 빌드 시점 고정** → env만 바꾸고 재빌드 안 하면 앱은 여전히 옛 주소를 본다.
39
+ - **도메인을 또 바꾸면** 4의 전파를 처음부터 다시 해야 한다. 그래서 도메인은 가능한 한 일찍 정한다.
40
+
41
+ ## confirm 경계
42
+
43
+ 도메인 구매(과금)와 NS 변경은 Owner가 직접 확인한다. SSL·DNS 연결 확인과 전파(redirect·OG) 설정은 자동이다. prod Supabase 변경은 `/web-launch`와 동일하게 Owner confirm 후 적용한다.
44
+
45
+ ## 검증
46
+
47
+ 성공은 셋이 모두 참일 때다: `https://<domain>`이 LIVE이고(SSL 정상), 그 도메인이 prod Supabase Auth redirect에 반영돼 로그인이 되며, 앱·OG 기준 URL이 새 도메인을 가리킨다. 비용·콘솔 절차는 바뀌므로 Vercel·Supabase 공식 문서를 그 시점 기준으로 확인한다.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: web-launch
3
- description: Provision a private GitHub repo, Vercel project, Supabase dev/prod, and Sentry, then ship the web service's first real deploy and run a security audit. Use when the Owner is ready to take the web live for the first time — stand up its cloud and put it on a real URL — or invokes /web-launch.
3
+ description: Provision a private GitHub repo, Vercel project, Supabase dev/prod, Sentry, and PostHog, then ship the web service's first real deploy and run a security audit. Use when the Owner is ready to take the web live for the first time — stand up its cloud and put it on a real URL — or invokes /web-launch.
4
4
  ---
5
5
 
6
6
  # /web-launch
@@ -11,7 +11,7 @@ description: Provision a private GitHub repo, Vercel project, Supabase dev/prod,
11
11
 
12
12
  ## 빠른 참조 (값은 여기서 읽는다)
13
13
 
14
- - 리소스 이름: 루트 `workspace.json`의 `name`. repo=`<name>-web`, Vercel=`<name>`, Supabase=`<name>-dev`/`-prod`, Sentry=`<name>`.
14
+ - 리소스 이름: 루트 `workspace.json`의 `name`. repo=`<name>-web`, Vercel=`<name>`, Supabase=`<name>-dev`/`-prod`, Sentry=`<name>`, PostHog=`<name>`.
15
15
  - env 키 목록과 어느 환경에 들어가는지: `web/.env.example`.
16
16
  - dev/prod 분리 매트릭스: `web/docs/ENVIRONMENTS.md`. 무료 티어 한도·플랜별 가용성: `web/docs/LIMITS.md`.
17
17
 
@@ -23,20 +23,25 @@ description: Provision a private GitHub repo, Vercel project, Supabase dev/prod,
23
23
 
24
24
  3. **Vercel link + env 주입.** `vercel link`는 폴더명 `web`을 기본 프로젝트명으로 제안하니, 그대로 수락하지 말고 `<name>`으로 바꾼다. production 슬롯엔 prod Supabase + `SUPABASE_PROD_REF`, preview·development 슬롯엔 dev + `SUPABASE_DEV_REF`를 채운다. 끝나면 `vercel env pull`로 `.env.local`을 동기화한다.
25
25
 
26
- 4. **Sentry.** config 파일은 이미 있어 env만 채우면 된다(wizard 불필요). org·project는 `sentry-cli`로 조회·생성하고, CLI로 못 만드는 auth token은 Owner가 대시보드(Settings → Auth Tokens)에서 발급한다. **`NEXT_PUBLIC_SENTRY_DSN`은 Vercel Production 슬롯에만 넣는다**(이유는 아래 함정). 빌드용 3종(`SENTRY_ORG`·`SENTRY_PROJECT`·`SENTRY_AUTH_TOKEN`, sourcemap 업로드)은 3환경 + GitHub Secrets에 넣는다.
26
+ 4. **Sentry · PostHog.** prod-only 게이트다(키는 Vercel Production 슬롯에만 dev·preview 신호가 운영에 섞인다).
27
+ - Sentry: config 파일은 이미 있어 env만 채우면 된다(wizard 불필요). org·project는 `sentry-cli`로 조회·생성하고, CLI로 못 만드는 auth token은 Owner가 대시보드(Settings → Auth Tokens)에서 발급한다. **`NEXT_PUBLIC_SENTRY_DSN`은 Vercel Production 슬롯에만 넣는다**(이유는 아래 함정). 빌드용 3종(`SENTRY_ORG`·`SENTRY_PROJECT`·`SENTRY_AUTH_TOKEN`, sourcemap 업로드)은 3환경 + GitHub Secrets에 넣는다.
28
+ - PostHog: 프로젝트 생성 CLI가 없으므로 Owner가 대시보드(posthog.com 가입 → 프로젝트 생성)에서 Project API Key를 복사한다(🟡 Sentry auth token과 같은 수동 단계). **`NEXT_PUBLIC_POSTHOG_KEY`는 Vercel Production 슬롯에만** 넣고(비-prod에 넣으면 dev·preview 이벤트가 분석에 섞인다), `NEXT_PUBLIC_POSTHOG_HOST`(US `https://us.i.posthog.com` / EU `https://eu.i.posthog.com`)는 전 슬롯에 넣는다.
27
29
 
28
- 5. **`logs` 테이블 마이그레이션.** 감사 로그용이다. `service_role`만 insert하는 RLS 정책을 같은 마이그레이션 파일에 동봉한다. dev는 자동 적용하고, prod는 Owner confirm 후 적용한다.
30
+ 5. **`audit_log` 테이블 마이그레이션 (방식 B).** 감사 로그용이다. 마이그레이션에 `enable row level security` + 클라 정책 0개(= `anon`·`authenticated`는 0행) + `anon`·`authenticated` 권한 회수를 동봉한다. insert 서버 secret key(`admin.ts`)만 하고 RLS 우회한다(클라는 닿음). Data API는 켜둔 채(프로젝트 기본) 클라만 잠근다 — 이게 이 템플릿의 데이터 접근 자세다(인가는 서버 코드). dev는 자동 적용, prod는 Owner confirm 후 적용.
29
31
 
30
- 6. **GitHub Secrets.** CI 빌드용이다. `.env.local`에서 publishable + URL + Sentry 4종만 `gh secret set`으로 넣는다. secret key·DB ref는 CI 컴파일에 불필요하니 넣지 않는다(보안 표면 최소화).
32
+ 6. **GitHub Secrets.** CI 빌드용이다. `.env.local`에서 publishable + URL + Sentry 4 + PostHog 2종(`NEXT_PUBLIC_POSTHOG_KEY`·`NEXT_PUBLIC_POSTHOG_HOST`)을 `gh secret set`으로 넣는다. secret key·DB ref는 CI 컴파일에 불필요하니 넣지 않는다(보안 표면 최소화).
31
33
 
32
34
  7. **첫 배포 → prod redirect 갱신.** main에 push하면 CI와 Vercel 배포가 함께 트리거된다. 배포가 끝나면 **실 production 도메인을 확인해 prod Supabase의 `site_url`·`uri_allow_list`에 반영한다.** 2에서 이걸 미뤘던 이유이고, 빠뜨리면 운영 사이트 로그인이 조용히 깨진다.
33
35
 
34
- 8. **Owner 대시보드 설정.** CI를 main merge의 Required status check로 지정하고(GitHub branch protection 플랜 무관·벤더 중립), Preview에 Vercel Authentication을 켜 preview URL 크롤러를 막는다. 플랜별 가용성·한도는 본문에 박지 말고 `web/docs/LIMITS.md`를 따른다.
36
+ 8. **Owner 대시보드 설정 (배포 게이트 + preview 보호).**
37
+ - 배포 게이트: main에 직접 push하는 흐름이라 'merge 시 required check'(branch protection)만으로는 못 막는다. Vercel Git 연동에서 **'CI 통과 후에만 배포'**(Deployment Checks / Wait for Checks — Hobby 가용, `web/docs/LIMITS.md`)를 켜, 자동 점검을 통과한 커밋만 운영에 배포되고 빌드·검사 실패 시 직전 정상 배포가 그대로 유지되게 한다('깨진 게 prod를 대체하지 않음').
38
+ - preview 보호: Vercel Authentication을 켜 preview URL 크롤러를 막는다.
39
+ - 플랜별 가용성·한도는 본문에 박지 말고 `web/docs/LIMITS.md`를 따른다.
35
40
 
36
41
  ## 함정 (조용히 깨지는 것)
37
42
 
38
43
  - **prod redirect 미갱신**(7) → 운영 로그인이 무음으로 깨진다. 가장 흔한 사고다.
39
- - **Sentry DSN을 비-prod 슬롯에 주입** → preview 에러가 production으로 찍혀 무료 한도를 깎는다. 클라이언트 SDK는 번들에 `APP_ENV` 신호가 없어 DSN 존재만으로 송출을 켜기 때문이다(서버·엣지는 `APP_ENV`로 가드돼 안전하다). 그래서 4의 DSN은 Production 슬롯 전용이다.
44
+ - **Sentry DSN을 비-prod 슬롯에 주입** → preview 에러가 production으로 찍혀 무료 한도를 깎는다. 클라이언트 SDK는 번들에 `APP_ENV` 신호가 없어 DSN 존재만으로 송출을 켜기 때문이다(서버·엣지는 `APP_ENV`로 가드돼 안전하다). 그래서 4의 DSN은 Production 슬롯 전용이다. **PostHog `NEXT_PUBLIC_POSTHOG_KEY`도 같은 이유로 Production 슬롯 전용**이다(비-prod에 넣으면 dev·preview 이벤트가 분석에 섞인다).
40
45
  - **`vercel link` 기본 프로젝트명**이 폴더명 `web`(3) → `<name>`으로 바꾸지 않으면 리소스 이름이 어긋난다.
41
46
  - **secret scanning push protection**은 private+free에서 미지원이다(GitHub Advanced Security 유료) — 진단에서 이 항목만 건너뛴다.
42
47
 
@@ -25,9 +25,9 @@
25
25
 
26
26
  ## 이 템플릿의 성격
27
27
 
28
- 빈 골격이다. `web/features/`와 `web/supabase/migrations/`는 비어 있고, 로그인·글·피드 같은 제품 기능은 사용자 주제에 맞춰 대화로 만든다. 인프라(데이터 접근 seam, RLS 기본 활성화, 환경 분리, 로깅·테스트, 디자인 시스템, 코드 컨벤션)는 갖춰져 있다.
28
+ 빈 골격이다. `web/features/`와 `web/supabase/migrations/`는 비어 있고, 로그인·글·피드 같은 제품 기능은 사용자 주제에 맞춰 대화로 만든다. 인프라(서버측 인가 데이터 접근 seam, 환경 분리, 로깅·테스트, 디자인 시스템, 코드 컨벤션)는 갖춰져 있다.
29
29
 
30
- 기능 제작은 `web/AGENTS.md`의 규약을 따른다(FSD-light, `features/<f>/repository.ts` 데이터 접근 seam, RLS 동봉, schema 3층). 앱 작업은 `mobile/AGENTS.md`와 자동 로드되는 가이드 스킬(`native-app-guide`·`bridge-guide`·`eas-deploy-guide`·`store-release-guide`)을 따른다.
30
+ 기능 제작은 `web/AGENTS.md`의 규약을 따른다(FSD-light, `features/<f>/repository.ts` 서버측 데이터 접근 seam, 클라 차단 동봉(RLS·grant), schema 3층). 앱 작업은 `mobile/AGENTS.md`와 자동 로드되는 가이드 스킬(`native-app-guide`·`bridge-guide`·`eas-deploy-guide`·`store-release-guide`)을 따른다.
31
31
 
32
32
  ## 운영 원칙
33
33
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  | 명령 | 무엇을 하나 | 당신이 하는 일 |
20
20
  | --- | --- | --- |
21
- | `/web-launch` | GitHub·Vercel·Supabase·Sentry를 만들고 `web/`을 실제 주소로 첫 배포 | 비가역 작업만 확인 |
21
+ | `/web-launch` | GitHub·Vercel·Supabase·Sentry·PostHog를 만들고 `web/`을 실제 주소로 첫 배포 | 비가역 작업만 확인 |
22
22
  | `/probe <아이디어>` | 만들 서비스를 인터뷰로 구체화 | 질문에 답하기 |
23
23
  | `/sketch` | 화면 디자인 시안 보여주기 | 마음에 드는 안 고르기 |
24
24
  | "OO 기능 만들어줘" | 로그인·글쓰기·피드·승인 등 기능을 대화로 제작 | 원하는 바 말하기 |
@@ -181,8 +181,9 @@ type HandoffSession = { accessToken: string; refreshToken: string };
181
181
  * OPTION A — Supabase table + RLS, single-use delete (recommended; survives
182
182
  * multi-instance deploys, truly single-use because the read deletes the row).
183
183
  *
184
- * Migration to add to your web (ship RLS in the SAME file — this template's web
185
- * forbids tables without RLS):
184
+ * Migration to add to your web (ship the client-lockout in the SAME file — this
185
+ * template's web denies client table access under 방식 B: enable RLS with zero
186
+ * client policies + revoke anon/authenticated grants; the secret key bypasses it):
186
187
  *
187
188
  * create table public.app_handoff_nonces (
188
189
  * code text primary key,
@@ -30,6 +30,14 @@ SENTRY_AUTH_TOKEN=
30
30
  SENTRY_ORG=
31
31
  SENTRY_PROJECT=
32
32
 
33
+ # ─── PostHog (사용자 이벤트 분석) ───────────────────────────────
34
+ # Sentry DSN과 같은 prod-only 게이트: 키는 Vercel "Production" 슬롯에만 넣는다.
35
+ # 브라우저 번들엔 APP_ENV 신호가 없어, 키 존재 여부로 송출을 켠다(키 없으면 no-op).
36
+ # 비-prod 슬롯에 키를 넣으면 dev·preview 이벤트가 분석에 섞인다.
37
+ # 키 = PostHog 대시보드 Project Settings의 Project API Key. 호스트 = US: https://us.i.posthog.com / EU: https://eu.i.posthog.com.
38
+ NEXT_PUBLIC_POSTHOG_KEY=
39
+ NEXT_PUBLIC_POSTHOG_HOST=
40
+
33
41
  # ─── 배포 환경 ─────────────────────────────────────────────────
34
42
  # 코드 분기는 벤더 중립 APP_ENV로 한다(lib/app-env.ts).
35
43
  # Vercel은 아래를 자동 주입하므로 설정 불필요(VERCEL_ENV가 APP_ENV로 매핑됨).
@@ -59,3 +59,7 @@ jobs:
59
59
  SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
60
60
  SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
61
61
  SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
62
+ # PostHog는 prod-only 게이트라 키가 없으면 init이 no-op이다. CI 빌드는 검증용이므로
63
+ # 비어 있어도 통과한다(실제 송출은 Vercel Production 빌드에서만).
64
+ NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
65
+ NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}
@@ -14,7 +14,7 @@
14
14
  2. `/probe <아이디어>` — 적응형 인터뷰로 요구사항 도출
15
15
  3. `/sketch` — shadcn과 프로젝트 디자인 컨벤션 기반 시안
16
16
  4. TDD 개발 — Vitest 테스트 작성, 통과, 커밋을 반복
17
- 5. `git push` CI 통과 시 Vercel 자동 배포
17
+ 5. `git push` 로컬 전수 점검(typecheck·lint·test·build) → push → CI 통과 시 Vercel 자동 배포
18
18
 
19
19
  `.vercel/`이 없으면 `/web-launch`를 먼저 실행한다. 의존성·CLI 로그인은 설치 스크립트와 `pnpm run doctor`가 챙긴다.
20
20
 
@@ -39,18 +39,25 @@ Owner가 직접(CLI 미지원, AI는 안내만): 카드 결제, 도메인 DNS
39
39
  ## 핵심 코드 규칙
40
40
 
41
41
  - TDD 우선: 새 기능은 Vitest 테스트를 작성하고 통과시킨 뒤 커밋한다. 테스트는 `tests/`에 누적한다.
42
- - 로그(서버): `lib/logger.ts`를 쓴다. 에러는 Sentry로, 감사 로그는 Supabase `logs` 테이블로 보낸다.
43
- - 로그(클라이언트): 에러는 `Sentry.captureException()`(browser SDK), 영속 이벤트는 server action을 거쳐 `lib/logger.ts`, 일시 디버그는 `console.log`. `lib/logger.ts`는 server-only라 클라이언트에서 직접 import하지 않는다.
42
+ - push 전 점검: `git push`는 main 직접 push CI → 운영 배포를 트리거한다. 그러므로 push 전에 `pnpm typecheck && pnpm lint && pnpm test && pnpm build`를 모두 통과시킨다(깨진 코드를 운영에 올리지 않도록 로컬에서 먼저 막는다).
43
+ - 관측은 목적별로 갈라 보낸다( 곳에 몬다):
44
+ - 오류(예외·크래시) → Sentry. 서버는 `lib/logger.ts`의 `error`, 클라이언트는 `Sentry.captureException()`(browser SDK).
45
+ - 감사 기록(누가·무엇·언제 — 가입·삭제·권한변경·관리자 처리 등 상태를 바꾸는 의미 있는 동작) → Supabase `audit_log` 표. 서버에서 `lib/logger.ts`의 `audit`로 남긴다(secret key, server-only). 로그 스트림이 아니라 비즈니스 데이터라 DB에 둔다. 의미 있는·저볼륨만 남기고, 고볼륨·디버그는 보내지 않는다.
46
+ - 사용자 행동 분석 → PostHog. 클라이언트에서 `lib/analytics.ts`의 `captureEvent`로 핵심 이벤트만 남긴다(autocapture off, 과도 추적 금지). 키가 prod에만 주입되므로 운영에서만 송출된다.
47
+ - 임시 디버그 → `console.log`(Vercel 런타임 로그, 1시간 휘발). 남길 가치가 있으면 위 셋으로 승격한다.
48
+ - `lib/logger.ts`는 server-only라 클라이언트에서 직접 import하지 않는다. `lib/analytics.ts`는 client 전용이라 서버에서 import하지 않는다.
44
49
  - env: 클라이언트·일반 코드는 `import { env } from "@/lib/env"`(NEXT_PUBLIC_ 만). 서버 전용 키(SUPABASE_SECRET_KEY 등)는 `import { env } from "@/lib/env.server"`(server-only 가드). `process.env.X` 직접 접근은 빌드 시점 설정 파일(`next.config.*`, `sentry.{server,edge}.config.ts`, `instrumentation.ts`, `instrumentation-client.ts`, `vercel.json`)에서만 허용한다.
45
- - 보안 경계 (RLS 기본 + 서버측 처리):
46
- - 노출되는 모든 테이블은 RLS를 켠다. Supabase 테이블은 RLS 정책을 같은 마이그레이션 파일에 동봉한다(동봉하지 않으면 거부).
47
- - 쓰기와 민감한 읽기는 서버측에서 한다(Server Component·Action의 쿠키 기반 user-scoped 클라이언트). queries·actions는 `repository.ts`에만 의존한다.
48
- - realtime 구독과 storage signed-upload는 client-direct 경로를 유지한다.
49
- - `service_role`은 server-only 관리 경로 전용이다. 여기서 '서버측'은 user-scoped 클라이언트를 뜻하며 `service_role`과 다르다.
50
- - secret key: `lib/supabase/admin.ts`에서만 쓴다(server-only 가드). `NEXT_PUBLIC_*` 접두사를 붙이지 않는다. auth 흐름에서 `admin.ts`를 import하지 않는다(RLS·세션 우회 위험).
50
+ - 보안 경계 (서버측 인가 방식 B):
51
+ - 테이블 데이터 읽기·쓰기는 전부 서버에서 secret key로 한다. feature의 `repository.ts`가 유일한 데이터 접근 지점이고(server-only), `queries.ts`·`actions.ts`는 `repository.ts`에만 의존한다.
52
+ - secret key는 RLS를 우회하므로 "누가 무엇을 할 수 있는지"(소유권·권한)는 반드시 서버 코드가 확인한다. 모든 읽기·쓰기가 거치는 단일 인가 지점에서 요청자 == 자원 소유자/권한을 검사한다.
53
+ - 세션은 서버에서 `server.ts`(user-scoped)로 재검증한다: `getClaims()`(또는 `getUser()`)를 쓰고 `getSession()`은 신뢰하지 않는다. 클라 로그인은 `client.ts`(browser)로 한다. 이 두 클라이언트는 인증(세션) 전용이고 테이블 데이터를 직접 읽지 않는다.
54
+ - 클라 차단(fail-closed 백스톱): 테이블은 같은 마이그레이션에서 `enable row level security` + 클라 정책 0개(= `anon`·`authenticated`는 0행, secret key는 우회) + `anon`·`authenticated` 테이블 권한 회수를 동봉한다(동봉하지 않으면 거부). RLS는 인가 수단이 아니라 클라 차단 백스톱이다 — 인가는 서버 코드가 진다.
55
+ - 파일 업로드는 서버가 발급한 signed-upload 토큰으로 한다(서버가 인가, 바이트는 클라가 직접 전송). 비공개 파일 읽기는 서버가 권한 확인 짧은 signed URL을 발급한다.
56
+ - 새 기능에는 공격자 시점 회귀 테스트(IDOR: 다른 사용자로 남의 자원에 접근하면 거부되는지)를 함께 둔다.
57
+ - secret key: `lib/supabase/admin.ts`에서만 만든다(server-only 가드. import는 `features/*/repository.ts`·`lib/`만 — ESLint 강제). `NEXT_PUBLIC_*` 접두사를 붙이지 않는다. auth 흐름에선 `admin.ts`를 import하지 않는다(세션 우회 위험).
51
58
  - Supabase Auth: `@supabase/ssr`을 쓴다. `@supabase/auth-helpers-nextjs`(deprecated)는 쓰지 않는다.
52
59
  - 환경 분기: 서버 코드는 `import { env } from "@/lib/env.server"` 후 `env.APP_ENV`를 비교한다(값: `production` | `preview` | `development`). `APP_ENV`는 벤더 중립 신호다. Vercel은 `VERCEL_ENV`를 여기 매핑하고, 그 외 호스트는 `APP_ENV`를 직접 설정한다(`lib/app-env.ts`).
53
- - Vercel Cron route: prod에서만 돈다. 핸들러 시작에서 `env.APP_ENV !== "production"`이면 즉시 no-op으로 반환해, preview가 dev DB·외부 API를 낭비하지 않게 한다. 성공은 `audit`, 실패는 `error`로 `lib/logger.ts`에 남긴다(Sentry·logs 송출은 자동). 테스트 함정: `env`는 모듈 로드 시 한 번만 parse돼서 `vi.stubEnv`가 듣지 않는다 — cron 테스트는 `vi.mock("@/lib/env.server")`로 모듈을 통째 교체한다. 헤더 검증·idempotent·`CRON_SECRET`·`vercel.json`은 Vercel 문서를 따른다.
60
+ - Vercel Cron route: prod에서만 돈다. 핸들러 시작에서 `env.APP_ENV !== "production"`이면 즉시 no-op으로 반환해, preview가 dev DB·외부 API를 낭비하지 않게 한다. 성공은 `audit`, 실패는 `error`로 `lib/logger.ts`에 남긴다(Sentry·audit_log 송출은 자동). 테스트 함정: `env`는 모듈 로드 시 한 번만 parse돼서 `vi.stubEnv`가 듣지 않는다 — cron 테스트는 `vi.mock("@/lib/env.server")`로 모듈을 통째 교체한다. 헤더 검증·idempotent·`CRON_SECRET`·`vercel.json`은 Vercel 문서를 따른다.
54
61
  - 의존성 정책: caret range, `pnpm-lock.yaml` 커밋, CI `--frozen-lockfile`. `.npmrc`의 `minimum-release-age=14d`로 갓 나온 npm 패키지는 cooldown 후에만 설치한다. 의존성 변경은 lockfile과 같은 커밋에 포함한다.
55
62
 
56
63
  ## 폴더 구조 (FSD-light)
@@ -67,7 +74,7 @@ features/<feature>/ # 도메인 모듈 (자기완결)
67
74
  ├── queries.ts # 데이터 쿼리 — repository.ts에만 의존
68
75
  ├── schema.ts # zod (form·경계 검증) — DB schema와 정합 유지
69
76
  └── index.ts # public API — 외부 import는 여기만
70
- supabase/migrations/ # DB schema SSOT (.sql + RLS)
77
+ supabase/migrations/ # DB schema SSOT (.sql + 클라 차단: RLS enable·grant 회수)
71
78
  components/ui/ # shadcn primitives (글로벌 재사용)
72
79
  lib/ # cross-cutting 인프라
73
80
  ├── supabase/{server,client,admin}.ts
@@ -11,6 +11,7 @@ dev와 prod를 서로 다른 Supabase 프로젝트로 완전히 격리한다. Ow
11
11
  | Supabase Auth | dev project, redirect URL `localhost:3000`·`*.vercel.app` | prod project, prod 도메인 redirect | 사용자 계정도 별개 |
12
12
  | Sentry | 1 프로젝트, `environment=development` 태그 | 같은 프로젝트, `environment=production` 태그 | env 태그 + 송출 가드 |
13
13
  | Sentry 송출 | off (`APP_ENV !== "production"`) | on | 5k/월 한도 보호. preview에서도 송출하지 않음 |
14
+ | PostHog | off (키 미주입) | on (Production 슬롯 키) | `NEXT_PUBLIC_POSTHOG_KEY`를 Production 슬롯에만 주입 → dev·preview 이벤트 미혼입 |
14
15
  | Vercel Cron | preview·dev에서 실행 안 함(`APP_ENV === "production"` 가드) | 정상 실행 | 코드 분기 |
15
16
  | env vars 소스 | `vercel env pull --environment=development` → `.env.local` | Vercel "Production" 슬롯(자동 주입) | gitignored |
16
17
  | GitHub Actions CI | PR·main 모두 같은 GH Secrets로 컴파일·테스트(앱 런타임 실행은 없음) | 동일 | CI는 dev/prod를 구분하지 않는다. 런타임 분기는 Vercel이 담당 |
@@ -87,7 +88,7 @@ Sentry.init({
87
88
  ## 마이그레이션 워크플로 (AI 주도)
88
89
 
89
90
  1. `npx supabase migration new <name>`로 파일 생성
90
- 2. SQL 작성 (RLS 정책 동봉 필수)
91
+ 2. SQL 작성 (클라 차단 동봉 필수: `enable row level security` + 클라 정책 0개 + `anon`·`authenticated` 권한 회수. 인가는 서버 코드가 진다 — 방식 B)
91
92
  3. `npx supabase db push --project-ref $SUPABASE_DEV_REF`로 dev 적용 (자동)
92
93
  4. dev에서 검증한 뒤 Owner에게 보고: "dev 적용·검증 완료. 변경 요약: ___. prod에도 적용할까요?"
93
94
  5. Owner confirm
@@ -97,7 +98,7 @@ Sentry.init({
97
98
 
98
99
  ## 비용·한도
99
100
 
100
- 무료 티어 숫자는 [`LIMITS.md`](LIMITS.md)가 SSOT다. 핵심만: dev/prod 분리에 Supabase 무료 슬롯 2개를 모두 쓰고, Sentry는 prod만 송출해 한도를 보호한다. Vercel 로그 보존은 1시간이라, 그 너머는 Sentry와 Supabase `logs` 테이블에서 조회한다.
101
+ 무료 티어 숫자는 [`LIMITS.md`](LIMITS.md)가 SSOT다. 핵심만: dev/prod 분리에 Supabase 무료 슬롯 2개를 모두 쓰고, Sentry는 prod만 송출해 한도를 보호한다. Vercel 로그 보존은 1시간이라, 그 너머는 Sentry와 Supabase `audit_log` 테이블에서 조회한다.
101
102
 
102
103
  ## 다루지 않는 것
103
104
 
@@ -32,6 +32,14 @@
32
32
  - organization·user: 1
33
33
  - 출처: <https://sentry.io/pricing/>
34
34
 
35
+ ## PostHog Free (Product Analytics)
36
+
37
+ - 이벤트: ~1,000,000 / 월 (무료 한도, 초과분은 사용량 과금 또는 송출 중단)
38
+ - 보존: 무료는 짧은 보존창 (정확한 일수는 공식 docs 확인)
39
+ - 출처: <https://posthog.com/pricing>
40
+
41
+ > 과도 추적은 무료 한도를 빠르게 소진한다. autocapture는 끄고(`instrumentation-client.ts`) 핵심 이벤트만 `lib/analytics.ts`로 남긴다.
42
+
35
43
  ## GitHub Free
36
44
 
37
45
  - private repo: 무제한
@@ -51,4 +59,5 @@
51
59
  | Supabase | 프로젝트 비활성화 → Pro $25/mo 또는 데이터 정리 |
52
60
  | Vercel | 함수·빌드 정지 → 다음 사이클 대기 또는 Pro $20/mo |
53
61
  | Sentry | 새 이벤트 drop → 사이클 대기 또는 Team $26/mo |
62
+ | PostHog | 무료 한도 초과분 drop 또는 사용량 과금 → 이벤트 줄이기 또는 유료 플랜 |
54
63
  | GitHub Actions | 차단 → 다음 사이클 대기 또는 분 구입 |
@@ -13,7 +13,28 @@ const eslintConfig = defineConfig([
13
13
  },
14
14
  rules: {
15
15
  "@typescript-eslint/no-deprecated": "warn",
16
- // feature 캡슐화: 다른 feature는 public index(@/features/<name>)로만 import — 내부 deep import 금지.
16
+ // feature 캡슐화 + 방식 B 데이터 접근 경계.
17
+ "no-restricted-imports": ["error", {
18
+ patterns: [
19
+ {
20
+ // 다른 feature는 public index(@/features/<name>)로만 import — 내부 deep import 금지.
21
+ group: ["@/features/*/*"],
22
+ message: "feature 내부 deep import 금지 — public API(@/features/<name>)로만 import.",
23
+ },
24
+ {
25
+ // 방식 B: secret key(admin)는 데이터 접근 seam에서만. 아래 repository.ts·lib/ override가 재허용.
26
+ group: ["@/lib/supabase/admin"],
27
+ message: "secret key(admin)는 데이터 접근 seam 전용 — features/<f>/repository.ts 또는 lib/에서만 import한다(인가는 서버 코드).",
28
+ },
29
+ ],
30
+ }],
31
+ },
32
+ },
33
+ {
34
+ // 데이터 접근 seam: repository.ts는 유일한 데이터 접근 지점이라 admin(secret key)을 import할 수 있다.
35
+ // 위 블록의 admin 제한을 여기서만 푼다(feature deep import 금지는 유지).
36
+ files: ["features/**/repository.ts"],
37
+ rules: {
17
38
  "no-restricted-imports": ["error", {
18
39
  patterns: [{
19
40
  group: ["@/features/*/*"],
@@ -1,5 +1,6 @@
1
1
  // Browser SDK 초기화. Next가 client bundle에 자동 포함.
2
2
  import * as Sentry from "@sentry/nextjs";
3
+ import posthog from "posthog-js";
3
4
 
4
5
  // 브라우저 번들엔 배포 환경 신호(APP_ENV/VERCEL_ENV)가 없다 — Next는 NEXT_PUBLIC_* 만 인라인한다.
5
6
  // 그래서 송출 게이트를 DSN 존재로 대신한다. 이 코드의 정확성은 "web-launch가 DSN을 Vercel Production
@@ -14,5 +15,20 @@ Sentry.init({
14
15
  tracesSampleRate: 0,
15
16
  });
16
17
 
18
+ // PostHog(사용자 이벤트 분석). Sentry DSN과 같은 prod-only 게이트: 키가 있을 때만 init한다.
19
+ // web-launch가 NEXT_PUBLIC_POSTHOG_KEY를 Vercel Production 슬롯에만 넣으므로, dev·preview에선
20
+ // 키가 없어 송출이 꺼진다. 핵심 이벤트는 lib/analytics.ts로 명시 캡처한다(과도 추적 안 함).
21
+ const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
22
+ if (posthogKey) {
23
+ posthog.init(posthogKey, {
24
+ api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com",
25
+ defaults: "2026-01-30",
26
+ autocapture: false, // 클릭·폼 자동 캡처 끔. 핵심 이벤트만 명시 캡처.
27
+ capture_pageview: true, // 페이지 이동은 남긴다(어느 화면에서 이탈하는지).
28
+ capture_pageleave: true, // 페이지 이탈도 남긴다.
29
+ person_profiles: "identified_only", // 익명 사용자 프로필 미생성(개인정보 최소화).
30
+ });
31
+ }
32
+
17
33
  // Next 16: 내비게이션 계측을 위해 Sentry가 요구하는 hook.
18
34
  export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
@@ -0,0 +1,32 @@
1
+ // 사용자 이벤트 분석(PostHog) — 핵심 이벤트만 명시적으로 남기는 단일 진입점.
2
+ //
3
+ // 클라이언트 전용이다(posthog-js는 브라우저 SDK). 서버 코드에서 import하지 않는다.
4
+ // 송출 게이트는 instrumentation-client.ts와 동일하다: NEXT_PUBLIC_POSTHOG_KEY가 있을 때만
5
+ // 동작하고, 없으면(dev·preview) 조용히 no-op이다. web-launch가 키를 Vercel Production 슬롯에만
6
+ // 넣으므로 운영에서만 쌓인다.
7
+ //
8
+ // "과하게 추적하지 않는다": autocapture는 꺼져 있고(instrumentation-client.ts), 여기서 부르는
9
+ // 핵심 이벤트만 쌓인다. 이벤트 이름은 snake_case 동사_명사로 통일한다
10
+ // (예: signup_completed · post_created · post_read).
11
+ import posthog from "posthog-js";
12
+
13
+ const enabled = !!process.env.NEXT_PUBLIC_POSTHOG_KEY;
14
+
15
+ // 핵심 이벤트 1건을 남긴다. 민감한 개인정보는 properties에 담지 않는다.
16
+ export function captureEvent(event: string, properties?: Record<string, unknown>) {
17
+ if (!enabled) return;
18
+ posthog.capture(event, properties);
19
+ }
20
+
21
+ // 로그인 사용자를 식별한다(identified_only 모드에서 프로필 생성·이후 이벤트 연결).
22
+ // id만 넘기고 민감정보는 넣지 않는다.
23
+ export function identifyUser(distinctId: string, properties?: Record<string, unknown>) {
24
+ if (!enabled) return;
25
+ posthog.identify(distinctId, properties);
26
+ }
27
+
28
+ // 로그아웃 시 분석 신원을 초기화한다(다음 사용자와 섞이지 않게).
29
+ export function resetAnalytics() {
30
+ if (!enabled) return;
31
+ posthog.reset();
32
+ }
@@ -12,10 +12,15 @@ const ClientSchema = z.object({
12
12
  NEXT_PUBLIC_SUPABASE_URL: z.url(),
13
13
  NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: z.string().min(1),
14
14
  NEXT_PUBLIC_SENTRY_DSN: emptyAsUndefined(z.url().optional()),
15
+ // PostHog: prod-only 게이트(키 존재 시에만 송출). 미설정이면 조용히 비활성.
16
+ NEXT_PUBLIC_POSTHOG_KEY: emptyAsUndefined(z.string().optional()),
17
+ NEXT_PUBLIC_POSTHOG_HOST: emptyAsUndefined(z.url().optional()),
15
18
  });
16
19
 
17
20
  export const env = ClientSchema.parse({
18
21
  NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
19
22
  NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
20
23
  NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
24
+ NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
25
+ NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
21
26
  });
@@ -13,13 +13,14 @@ async function write(level: Level, message: string, context?: Record<string, unk
13
13
  Sentry.captureException(new Error(message), { extra: context });
14
14
  }
15
15
 
16
- // RLS 정책이 service_role만 insert 허용하므로 admin 클라이언트.
16
+ // audit_log은 RLS 켜고 클라 정책이 없어(deny-all) 클라이언트가 닿지 못한다.
17
+ // secret key(admin)는 RLS를 우회하므로 감사 기록은 서버에서만 insert한다.
17
18
  if (env.SUPABASE_SECRET_KEY) {
18
19
  try {
19
20
  const admin = createSupabaseAdmin();
20
- await admin.from("logs").insert({ level, message, context });
21
+ await admin.from("audit_log").insert({ level, message, context });
21
22
  } catch {
22
- /* logs 테이블 미존재·연결 실패는 무시 */
23
+ /* audit_log 테이블 미존재·연결 실패는 무시 */
23
24
  }
24
25
  }
25
26
  }
@@ -21,6 +21,7 @@
21
21
  "clsx": "^2.1.1",
22
22
  "lucide-react": "^1.14.0",
23
23
  "next": "^16.2.6",
24
+ "posthog-js": "^1.381.0",
24
25
  "react": "^19.2.4",
25
26
  "react-dom": "^19.2.4",
26
27
  "server-only": "^0.0.1",
@@ -35,6 +35,9 @@ importers:
35
35
  next:
36
36
  specifier: ^16.2.6
37
37
  version: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
38
+ posthog-js:
39
+ specifier: ^1.381.0
40
+ version: 1.381.0
38
41
  react:
39
42
  specifier: ^19.2.4
40
43
  version: 19.2.4
@@ -953,6 +956,12 @@ packages:
953
956
  '@oxc-project/types@0.128.0':
954
957
  resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==}
955
958
 
959
+ '@posthog/core@1.30.9':
960
+ resolution: {integrity: sha512-Cn004VJ7ZWQRaVQG7efh+pMtRMIVZznktngXe5I3E8wClRdMwCZaSa6jo/X04Oc04z8PeMp4GEcWVkdEhmAEXw==}
961
+
962
+ '@posthog/types@1.381.0':
963
+ resolution: {integrity: sha512-AW68BovKFCNbPdq3VjOzfQeSQRYMvQVv+46LDywWFXO/oOTXFKwjY92FaJQSTXWgTNgDpqigCw3yUFDinK3hZA==}
964
+
956
965
  '@prisma/instrumentation@7.6.0':
957
966
  resolution: {integrity: sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==}
958
967
  peerDependencies:
@@ -1636,6 +1645,9 @@ packages:
1636
1645
  '@types/tedious@4.0.14':
1637
1646
  resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==}
1638
1647
 
1648
+ '@types/trusted-types@2.0.7':
1649
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
1650
+
1639
1651
  '@types/validate-npm-package-name@4.0.2':
1640
1652
  resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==}
1641
1653
 
@@ -2211,6 +2223,9 @@ packages:
2211
2223
  resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
2212
2224
  engines: {node: '>=18'}
2213
2225
 
2226
+ core-js@3.49.0:
2227
+ resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
2228
+
2214
2229
  cors@2.8.6:
2215
2230
  resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
2216
2231
  engines: {node: '>= 0.10'}
@@ -2347,6 +2362,9 @@ packages:
2347
2362
  dom-accessibility-api@0.6.3:
2348
2363
  resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
2349
2364
 
2365
+ dompurify@3.4.8:
2366
+ resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==}
2367
+
2350
2368
  dotenv@16.6.1:
2351
2369
  resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
2352
2370
  engines: {node: '>=12'}
@@ -2665,6 +2683,9 @@ packages:
2665
2683
  resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
2666
2684
  engines: {node: ^12.20 || >= 14.13}
2667
2685
 
2686
+ fflate@0.4.8:
2687
+ resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
2688
+
2668
2689
  figures@6.1.0:
2669
2690
  resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
2670
2691
  engines: {node: '>=18'}
@@ -3681,10 +3702,16 @@ packages:
3681
3702
  resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
3682
3703
  engines: {node: '>=0.10.0'}
3683
3704
 
3705
+ posthog-js@1.381.0:
3706
+ resolution: {integrity: sha512-botkF0PUSd19qUTB7lJxKRQAc+9b9v3XcAZnqG/4LMDXJPvMxPeSCj6OVx8e9GCZN38kOg6yUcXMXDTcBqKdlw==}
3707
+
3684
3708
  powershell-utils@0.1.0:
3685
3709
  resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
3686
3710
  engines: {node: '>=20'}
3687
3711
 
3712
+ preact@10.29.2:
3713
+ resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==}
3714
+
3688
3715
  prelude-ls@1.2.1:
3689
3716
  resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
3690
3717
  engines: {node: '>= 0.8.0'}
@@ -3723,6 +3750,9 @@ packages:
3723
3750
  resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
3724
3751
  engines: {node: '>=0.6'}
3725
3752
 
3753
+ query-selector-shadow-dom@1.0.1:
3754
+ resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==}
3755
+
3726
3756
  queue-microtask@1.2.3:
3727
3757
  resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
3728
3758
 
@@ -4389,6 +4419,9 @@ packages:
4389
4419
  resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
4390
4420
  engines: {node: '>= 8'}
4391
4421
 
4422
+ web-vitals@5.3.0:
4423
+ resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==}
4424
+
4392
4425
  webidl-conversions@3.0.1:
4393
4426
  resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
4394
4427
 
@@ -5403,6 +5436,12 @@ snapshots:
5403
5436
 
5404
5437
  '@oxc-project/types@0.128.0': {}
5405
5438
 
5439
+ '@posthog/core@1.30.9':
5440
+ dependencies:
5441
+ '@posthog/types': 1.381.0
5442
+
5443
+ '@posthog/types@1.381.0': {}
5444
+
5406
5445
  '@prisma/instrumentation@7.6.0(@opentelemetry/api@1.9.1)':
5407
5446
  dependencies:
5408
5447
  '@opentelemetry/api': 1.9.1
@@ -5999,6 +6038,9 @@ snapshots:
5999
6038
  dependencies:
6000
6039
  '@types/node': 20.19.40
6001
6040
 
6041
+ '@types/trusted-types@2.0.7':
6042
+ optional: true
6043
+
6002
6044
  '@types/validate-npm-package-name@4.0.2': {}
6003
6045
 
6004
6046
  '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
@@ -6594,6 +6636,8 @@ snapshots:
6594
6636
 
6595
6637
  cookie@1.1.1: {}
6596
6638
 
6639
+ core-js@3.49.0: {}
6640
+
6597
6641
  cors@2.8.6:
6598
6642
  dependencies:
6599
6643
  object-assign: 4.1.1
@@ -6707,6 +6751,10 @@ snapshots:
6707
6751
 
6708
6752
  dom-accessibility-api@0.6.3: {}
6709
6753
 
6754
+ dompurify@3.4.8:
6755
+ optionalDependencies:
6756
+ '@types/trusted-types': 2.0.7
6757
+
6710
6758
  dotenv@16.6.1: {}
6711
6759
 
6712
6760
  dotenv@17.4.2: {}
@@ -7202,6 +7250,8 @@ snapshots:
7202
7250
  node-domexception: 1.0.0
7203
7251
  web-streams-polyfill: 3.3.3
7204
7252
 
7253
+ fflate@0.4.8: {}
7254
+
7205
7255
  figures@6.1.0:
7206
7256
  dependencies:
7207
7257
  is-unicode-supported: 2.1.0
@@ -8165,8 +8215,21 @@ snapshots:
8165
8215
  dependencies:
8166
8216
  xtend: 4.0.2
8167
8217
 
8218
+ posthog-js@1.381.0:
8219
+ dependencies:
8220
+ '@posthog/core': 1.30.9
8221
+ '@posthog/types': 1.381.0
8222
+ core-js: 3.49.0
8223
+ dompurify: 3.4.8
8224
+ fflate: 0.4.8
8225
+ preact: 10.29.2
8226
+ query-selector-shadow-dom: 1.0.1
8227
+ web-vitals: 5.3.0
8228
+
8168
8229
  powershell-utils@0.1.0: {}
8169
8230
 
8231
+ preact@10.29.2: {}
8232
+
8170
8233
  prelude-ls@1.2.1: {}
8171
8234
 
8172
8235
  pretty-format@27.5.1:
@@ -8205,6 +8268,8 @@ snapshots:
8205
8268
  dependencies:
8206
8269
  side-channel: 1.1.0
8207
8270
 
8271
+ query-selector-shadow-dom@1.0.1: {}
8272
+
8208
8273
  queue-microtask@1.2.3: {}
8209
8274
 
8210
8275
  range-parser@1.2.1: {}
@@ -8982,6 +9047,8 @@ snapshots:
8982
9047
 
8983
9048
  web-streams-polyfill@3.3.3: {}
8984
9049
 
9050
+ web-vitals@5.3.0: {}
9051
+
8985
9052
  webidl-conversions@3.0.1: {}
8986
9053
 
8987
9054
  webidl-conversions@8.0.1: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-saas-starter-workspace",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "강의용 SaaS 워크스페이스(web + mobile + 에이전트 도구)를 한 번에 만드는 생성기. 생성 → 의존성 설치 → doctor 점검까지 한 흐름.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",