create-saas-starter-workspace 0.1.10 → 0.1.11

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.
@@ -24,7 +24,7 @@ user-invocable: true
24
24
 
25
25
  ## 누가 무엇을 하나 (가역성)
26
26
 
27
- - 🟢 AI / CLI: 자격증명 파일 작업, EAS 빌드·제출, `eas.json`의 public 값.
27
+ - 🟢 AI / CLI: 자격증명 파일 작업, EAS 빌드·제출, `mobile/eas.json`의 public 값.
28
28
  - 🟡 Owner 대시보드 (AI가 정확히 안내): 첫 Apple 로그인과 2FA, 스크린샷, App Privacy·Data Safety 라벨, Play 서비스 계정 JSON.
29
29
  - 🔴 Owner 직접 (대리 불가): 카드 결제, 신원 검증, 멤버십 가입.
30
30
 
@@ -40,7 +40,7 @@ user-invocable: true
40
40
 
41
41
  EAS가 iOS 인증서·프로비저닝·APNs 키와 Android 업로드 키스토어를 자동 생성하므로 `.cer`/`.jks`를 손대지 않는다. iOS ASC API 키도 EAS 자동 생성이 기본이며, Account Holder의 1회 'Request Access' 약관 동의가 선행이다. iOS는 첫 제출이 ASC 앱 레코드를 자동 생성해 TestFlight까지 간다.
42
42
 
43
- 🔴 트랩: Android 첫 AAB는 Play Console에 **수동으로 1회 업로드**해야 한다(Play API는 첫 릴리스를 만들지 못한다). 첫 자동 제출 실패는 정상이니 선고지한다. 이후부터 `eas submit -p android`가 동작한다. Play 제출용 서비스 계정 JSON은 항상 수동(🟡)이며 EAS 서버에 저장한다(`eas.json`에 시크릿 경로를 넣지 않는다). 절차는 `store-release-guide`.
43
+ 🔴 트랩: Android 첫 AAB는 Play Console에 **수동으로 1회 업로드**해야 한다(Play API는 첫 릴리스를 만들지 못한다). 첫 자동 제출 실패는 정상이니 선고지한다. 이후부터 `eas submit -p android`가 동작한다. Play 제출용 서비스 계정 JSON은 항상 수동(🟡)이며 EAS 서버에 저장한다(`mobile/eas.json`에 시크릿 경로를 넣지 않는다). 절차는 `store-release-guide`.
44
44
 
45
45
  ### 3. 푸시 자격증명 (앱에 푸시를 켰을 때만)
46
46
 
@@ -16,7 +16,7 @@ user-invocable: true
16
16
  |------|-----|------|
17
17
  | 웹/UI — 제품 기능·화면·로직 | 웹 배포 | 웹 repo `git push` → 자동배포. 스토어 액션 0 |
18
18
  | RN 셸 순수 JS/TSX/이미지/스타일 | OTA | `npx eas-cli update` — 즉시, 재빌드·재제출 없음 |
19
- | 네이티브 모듈·`app.config.ts`·plugins·scheme·권한·SDK 업·계약 메시지 추가 | 재빌드 + 재제출 | marketing version 올림 → 빌드 → 테스트 채널 → 공개 승급 |
19
+ | 네이티브 모듈·`mobile/app.config.ts`·plugins·scheme·권한·SDK 업·계약 메시지 추가 | 재빌드 + 재제출 | marketing version 올림 → 빌드 → 테스트 채널 → 공개 승급 |
20
20
 
21
21
  `runtimeVersion`은 `appVersion`을 따른다. OTA는 같은 marketing version으로 나간 빌드에만 도달한다. 네이티브가 한 줄이라도 바뀌면 OTA로 못 내보낸다.
22
22
 
@@ -30,12 +30,12 @@ user-invocable: true
30
30
 
31
31
  ## 4. 재빌드 + 재제출 — 네이티브 변경
32
32
 
33
- 네이티브 모듈·`app.config.ts`·plugins·scheme·권한·SDK 업·계약 메시지 추가가 끼면 새 바이너리가 필요하다.
33
+ 네이티브 모듈·`mobile/app.config.ts`·plugins·scheme·권한·SDK 업·계약 메시지 추가가 끼면 새 바이너리가 필요하다.
34
34
 
35
- - marketing version 올림은 **필수**다. `app.config.ts`의 `version`을 올린다. 안 올리면 ITMS-90186으로 거절된다(`autoIncrement`는 buildNumber/versionCode만 올린다).
35
+ - marketing version 올림은 **필수**다. `mobile/app.config.ts`의 `version`을 올린다. 안 올리면 ITMS-90186으로 거절된다(`autoIncrement`는 buildNumber/versionCode만 올린다).
36
36
  - 빌드 + 자동 제출 → iOS는 TestFlight, Android은 내부 트랙까지 간다.
37
37
  - 같은 production 빌드를 실기기에서 테스트 채널로 확인하고, 그대로 공개로 승급한다.
38
- - 구체 명령·`eas.json` 프로파일·자격증명·무료 티어는 `eas-deploy-guide`.
38
+ - 구체 명령·`mobile/eas.json` 프로파일·자격증명·무료 티어는 `eas-deploy-guide`.
39
39
 
40
40
  ## 5. 공개 승급 (Owner confirm)
41
41
 
@@ -75,7 +75,7 @@ user-invocable: false
75
75
 
76
76
  ## 어댑터 (웹 측)
77
77
 
78
- 계약(`contract.ts`·`reader.ts`)은 이미 `web/lib/bridge/`에 설치돼 있다. 옵트인은 세션 핸드오프 레이어뿐이다 — 네이티브 소셜 로그인을 켤 때만 `docs/web-adapter/`의 POST 라우트(`route-app-bridge.ts`)·nonce·CSP를 웹 repo에 추가한다. 웹이 더하는 의존성은 `zod` 하나뿐이고, 네이티브·webview 라이브러리는 웹에 들어가지 않는다(웹은 `window.ReactNativeWebView` 전역으로만 앱에 닿는다). gronxb 등 라이브러리는 기각한다 — 네이티브-API-소유·monorepo 전제가 web=SSOT와 충돌한다.
78
+ 계약(`contract.ts`·`reader.ts`)은 이미 `web/lib/bridge/`에 설치돼 있다. 옵트인은 세션 핸드오프 레이어뿐이다 — 네이티브 소셜 로그인을 켤 때만 `mobile/docs/web-adapter/`의 POST 라우트(`route-app-bridge.ts`)·nonce·CSP를 웹 repo에 추가한다. 웹이 더하는 의존성은 `zod` 하나뿐이고, 네이티브·webview 라이브러리는 웹에 들어가지 않는다(웹은 `window.ReactNativeWebView` 전역으로만 앱에 닿는다). gronxb 등 라이브러리는 기각한다 — 네이티브-API-소유·monorepo 전제가 web=SSOT와 충돌한다.
79
79
 
80
80
  ## 함정 (조용히 깨지는 것)
81
81
 
@@ -41,23 +41,23 @@ npx eas-cli build --profile production --platform all --auto-submit --no-wait
41
41
  |------|------|
42
42
  | 웹/UI(제품 기능·화면·로직) | 웹 재배포 — 앱은 그대로, 다음 로드에 새 웹을 띄운다 |
43
43
  | 네이티브 셸의 순수 JS/TSX/이미지/스타일 | OTA: `npx eas-cli update` (즉시) |
44
- | 네이티브 모듈·`app.config.ts`·plugins·scheme·권한·SDK·계약 메시지 추가 | 재빌드 + 재제출 |
44
+ | 네이티브 모듈·`mobile/app.config.ts`·plugins·scheme·권한·SDK·계약 메시지 추가 | 재빌드 + 재제출 |
45
45
 
46
46
  - `runtimeVersion.policy: "appVersion"`이라 같은 marketing version으로 나간 빌드끼리만 OTA가 도달한다. 네이티브가 한 줄이라도 바뀌면 OTA로 못 보낸다.
47
- - 재빌드·재제출 땐 marketing version(`app.config.ts`의 `version`) 올림이 필수다 — 안 올리면 ITMS-90186으로 거절된다(`autoIncrement`는 buildNumber/versionCode만 올린다).
47
+ - 재빌드·재제출 땐 marketing version(`mobile/app.config.ts`의 `version`) 올림이 필수다 — 안 올리면 ITMS-90186으로 거절된다(`autoIncrement`는 buildNumber/versionCode만 올린다).
48
48
  - `eas submit`은 공개 심사까지 가지 않는다 — iOS는 TestFlight, Android은 internal 트랙까지만. 공개 출시(ASC "Submit for Review", Play production 승급)는 Owner가 콘솔에서 수동 + 명시 confirm으로 한다(`app-launch`·`store-release-guide`). Android 첫 AAB는 Play Console에 수동으로 1회 업로드해야 한다(첫 자동 제출 실패는 정상).
49
49
 
50
50
  ## 함정 (조용히 깨지는 것)
51
51
 
52
52
  - **실기기 = prod 웹 https.** 실기기는 Owner 컴퓨터의 `localhost`에 닿지 못하고, http LAN은 secure-context가 아니라 카메라·마이크·Web Crypto·클립보드·Secure 쿠키가 실기기에서 경고 없이 깨진다(Android cleartext·iOS ATS). 그래서 빌드의 `EXPO_PUBLIC_WEB_URL`은 항상 prod https다.
53
- - **URL 가드는 protocol·hostname만 본다**(`src/config/env.ts`). https거나 localhost면 통과하므로 `localhost` URL은 가드를 통과하고도 실기기에선 깨진다 — "실기기=prod https"는 코드가 강제하는 불변식이 아니라 Owner·AI가 지키는 규칙이다. localhost·`10.0.2.2`와 ATS/cleartext 예외는 `development` 프로파일에만 추가한다(`production`엔 절대 넣지 않는다).
53
+ - **URL 가드는 protocol·hostname만 본다**(`mobile/src/config/env.ts`). https거나 localhost면 통과하므로 `localhost` URL은 가드를 통과하고도 실기기에선 깨진다 — "실기기=prod https"는 코드가 강제하는 불변식이 아니라 Owner·AI가 지키는 규칙이다. localhost·`10.0.2.2`와 ATS/cleartext 예외는 `development` 프로파일에만 추가한다(`production`엔 절대 넣지 않는다).
54
54
  - **Expo Go로는 안 뜬다**(네이티브 모듈). 비개발자 환경엔 시뮬레이터도 없다고 가정한다 — 포장된 길은 production 빌드를 실폰에 설치하는 것이다.
55
55
  - **무료 티어 빌드 수는 제한적이다.** 초기 네이티브 셋업이 iOS 빌드를 여러 개 소모할 수 있으니 ad-hoc preview로 낭비하지 않는다(정확한 한도는 위 Expo 요금 페이지).
56
56
  - **SDK 버전을 추측하지 않는다.** 작성 전 `mobile/package.json`·v56 문서를 읽는다. 행동에 영향을 주는 v56 브레이킹: 이벤트 구독 정리는 `subscription.remove()`(`removeEventListener` 제거), `@expo/vector-icons` deprecated → `@react-native-vector-icons/*`, edge-to-edge 강제 기본, `expo-status-bar`의 `backgroundColor`·`translucent` props 제거.
57
57
 
58
58
  ## 자격증명 · 플랫폼 (짧게)
59
59
 
60
- - 자격증명(iOS 인증서·프로비저닝·APNs, Android 키스토어)은 EAS-managed로 자동 생성된다. `eas.json`엔 public 값만, 시크릿은 EAS 서버에 둔다. `.env`는 커밋하지 않는다(`.env.example`만, 플레이스홀더만).
60
+ - 자격증명(iOS 인증서·프로비저닝·APNs, Android 키스토어)은 EAS-managed로 자동 생성된다. `mobile/eas.json`엔 public 값만, 시크릿은 EAS 서버에 둔다. `.env`는 커밋하지 않는다(`mobile/.env.example`만, 플레이스홀더만).
61
61
  - iOS 시뮬은 Mac/Xcode 전용이다. Windows·Linux는 본인 iPhone + EAS 클라우드 빌드로 한다(Mac 불필요). Android 에뮬은 전 OS에서 되되 Google Play Services 포함 이미지여야 한다.
62
62
 
63
63
  ## 검증
@@ -6,7 +6,7 @@ user-invocable: false
6
6
 
7
7
  # 셸 레시피 — 앱 껍데기가 메우는 네이티브 공백
8
8
 
9
- 배포된 prod 웹 https를 로드하는 얇은 WebView 셸이다. 모든 화면·기능·로그인은 웹이 가지고, 셸은 WebView가 막거나 못 하는 네이티브 공백만 메운다. 셸이 진짜 네이티브로 제공하는 건 다음이다. 오프라인 화면(유일하게 분명한 네이티브 전용 화면), 첫/하드 로드와 크래시 재마운트에 한정한 로딩·에러 오버레이, Android 하드웨어 뒤로가기, 외부 링크의 시스템 브라우저 라우팅, safe-area inset 주입, 카메라/마이크/파일 업로드 권한 배선(능력 자체는 웹·라이브러리의 것). 구현은 `src/webview/`에 모이고, 링크 경계는 `src/config/env.ts`의 `ALLOWED_ORIGINS`를 SSOT로 쓴다.
9
+ 배포된 prod 웹 https를 로드하는 얇은 WebView 셸이다. 모든 화면·기능·로그인은 웹이 가지고, 셸은 WebView가 막거나 못 하는 네이티브 공백만 메운다. 셸이 진짜 네이티브로 제공하는 건 다음이다. 오프라인 화면(유일하게 분명한 네이티브 전용 화면), 첫/하드 로드와 크래시 재마운트에 한정한 로딩·에러 오버레이, Android 하드웨어 뒤로가기, 외부 링크의 시스템 브라우저 라우팅, safe-area inset 주입, 카메라/마이크/파일 업로드 권한 배선(능력 자체는 웹·라이브러리의 것). 구현은 `mobile/src/webview/`에 모이고, 링크 경계는 `mobile/src/config/env.ts`의 `ALLOWED_ORIGINS`를 SSOT로 쓴다.
10
10
 
11
11
  셸은 4.2(최소기능, [App Review Guidelines](https://developer.apple.com/app-store/review/guidelines/))를 통과하도록 설계된 게 아니라 출발점이다. 4.2를 넘으려면 Owner가 진짜 네이티브 차별점을 더해야 한다. 흔한 다음 단계는 네이티브 푸시인데 v1엔 없다(contract.ts에 forward-compat `PUSH_TOKEN_UPDATED` 상수와 `push.v1` capability 자리만 있고, `expo-notifications`·토큰 취득·핸들러는 전무하다). 켜려면 AI가 expo-notifications를 추가하고, 권한·토큰을 등록하고, `PUSH_TOKEN_UPDATED`로 웹에 전달하고, cold-start는 딥링크 섹션의 가드를 배선한다. 4.2·스토어 심사·개발자 계정 가격은 `store-release-guide`(SSOT).
12
12
 
@@ -14,9 +14,9 @@ user-invocable: false
14
14
 
15
15
  - SDK·RN·React·라이브러리 버전과 브레이킹 변경: `mobile/package.json` + v56 문서(<https://docs.expo.dev/versions/v56.0.0/>). 학습 데이터와 다르니 메모리를 믿지 않는다.
16
16
  - 설치된 네이티브 API의 실제 시그니처: `node_modules/react-native-webview/lib/*.d.ts` 등 타입 정의. docs에만 있고 코드엔 없는 이름이 흔하다(아래 함정).
17
- - 링크 경계·허용 origin: `src/config/env.ts`의 `ALLOWED_ORIGINS`(SSOT). URL 가드 동작도 여기.
17
+ - 링크 경계·허용 origin: `mobile/src/config/env.ts`의 `ALLOWED_ORIGINS`(SSOT). URL 가드 동작도 여기.
18
18
  - `EXPO_PUBLIC_WEB_URL`의 정확한 값: `mobile/.env.example`(플레이스홀더만 커밋된다).
19
- - 셸 구현 본체: `src/webview/`(`Host.tsx`·`linkBoundary.ts`·`linkBoundary.test.ts`).
19
+ - 셸 구현 본체: `mobile/src/webview/`(`Host.tsx`·`linkBoundary.ts`·`linkBoundary.test.ts`).
20
20
 
21
21
  ## 먼저: 추측하지 말고 확인
22
22
 
@@ -24,7 +24,7 @@ user-invocable: false
24
24
 
25
25
  - 컴포넌트는 `React.JSX.Element`를 반환한다. strict TS를 쓰고 `any`를 회피한다.
26
26
  - `react-native-url-polyfill/auto`가 `index.ts`에서 먼저 로드돼야 `new URL().origin`이 Hermes에서 동작한다(링크 allow-list의 전제).
27
- - `EXPO_PUBLIC_WEB_URL`은 빌드 시점에 인라인된다(바꾸려면 재빌드). 인라인 출처는 클라우드 빌드=`eas.json` `build.<profile>.env`, 로컬 `expo start`=Owner가 만든 `.env`다. `.env`는 커밋하지 않고(`.gitignore`), 커밋되는 `.env.example`은 플레이스홀더만 담는다(정확한 값은 `mobile/.env.example`을 직접 본다). `env.ts` 가드는 protocol/hostname만 본다. 실기기인지 시뮬레이터인지 못 구분하므로 localhost URL도 가드는 통과하나 실기기에선 실패한다. "실기기=prod https"는 코드가 강제하는 불변식이 아니라 Owner/AI 규칙이다.
27
+ - `EXPO_PUBLIC_WEB_URL`은 빌드 시점에 인라인된다(바꾸려면 재빌드). 인라인 출처는 클라우드 빌드=`mobile/eas.json` `build.<profile>.env`, 로컬 `expo start`=Owner가 만든 `.env`다. `.env`는 커밋하지 않고(`.gitignore`), 커밋되는 `mobile/.env.example`은 플레이스홀더만 담는다(정확한 값은 `mobile/.env.example`을 직접 본다). `env.ts` 가드는 protocol/hostname만 본다. 실기기인지 시뮬레이터인지 못 구분하므로 localhost URL도 가드는 통과하나 실기기에선 실패한다. "실기기=prod https"는 코드가 강제하는 불변식이 아니라 Owner/AI 규칙이다.
28
28
 
29
29
  ## safe-area (노치 여백)
30
30
 
@@ -45,7 +45,7 @@ edge-to-edge가 이 SDK에선 강제 기본이다(Android의 최신 targetSdk가
45
45
 
46
46
  ## 링크 경계 (외부 링크)
47
47
 
48
- 부수효과 없는 순수 분류자 `classifyLink(url, allowedOrigins)`(`src/webview/linkBoundary.ts`)가 단일 SSOT다. 셸의 모든 링크 판단이 이 한 곳을 공유한다: 내비게이션(`onShouldStartLoadWithRequest` + `onOpenWindow`)과 브릿지 OPEN_EXTERNAL. 보안 경계(origin 허용목록·스킴 게이트)이므로 네이티브 import 없이 Node(vitest)로 단위 테스트한다(`linkBoundary.test.ts`).
48
+ 부수효과 없는 순수 분류자 `classifyLink(url, allowedOrigins)`(`mobile/src/webview/linkBoundary.ts`)가 단일 SSOT다. 셸의 모든 링크 판단이 이 한 곳을 공유한다: 내비게이션(`onShouldStartLoadWithRequest` + `onOpenWindow`)과 브릿지 OPEN_EXTERNAL. 보안 경계(origin 허용목록·스킴 게이트)이므로 네이티브 import 없이 Node(vitest)로 단위 테스트한다(`linkBoundary.test.ts`).
49
49
 
50
50
  세 가지 판정:
51
51
  - 파싱 실패 → `"block"`(웹뷰에도 OS에도 안 넘김).
@@ -90,7 +90,7 @@ WebView 프로세스가 죽으면 `key`를 증가시켜 통째 재마운트한
90
90
 
91
91
  - 로딩: `startInLoadingState`/`renderLoading`을 쓰지 않는다(그 이름은 docs에만 있고 코드엔 없다 — 추측하지 말고 확인하라는 이유다). 직접 관리하는 `loading` state를 첫 `onLoadEnd`에서 해제한다(`loading` i18n).
92
92
  - 에러: `onError`(네트워크/전송) + `onHttpError`(메인 프레임 에러 상태)만으로 에러 화면을 띄운다(`error.*` i18n + 재시도 버튼). 재시도 = `key` 증가 통째 재마운트(크래시 복구와 동일).
93
- - 사용자 문자열은 전부 `src/i18n.ts` `t()`를 경유한다.
93
+ - 사용자 문자열은 전부 `mobile/src/i18n.ts` `t()`를 경유한다.
94
94
 
95
95
  ## 카메라 · 마이크 · 파일 업로드 (능력은 웹·라이브러리, 셸은 권한만)
96
96
 
@@ -98,7 +98,7 @@ WebView 프로세스가 죽으면 `key`를 증가시켜 통째 재마운트한
98
98
 
99
99
  - iOS usage string 3종: `NSCameraUsageDescription`·`NSPhotoLibraryUsageDescription`·`NSMicrophoneUsageDescription`(`NSPhotoLibraryAddUsageDescription`은 저장 구현 시만) + WebView `mediaCapturePermissionGrantType="grant"`. iOS는 usage string 누락 시 Apple이 런타임에 앱을 종료시킨다.
100
100
  - Android: `CAMERA` 권한과 `<queries>` IMAGE_CAPTURE 인텐트 가시성(app.config.ts `withImageCaptureQuery` 플러그인). Android 11+ 패키지 가시성 때문에 둘은 별개 요구사항이다. 권한=카메라 사용, queries=카메라 앱 조회(`resolveActivity`). queries가 누락되면 갤러리·파일 선택은 되나 `<input type=file capture>`의 '카메라 촬영' 옵션이 안 뜬다(크래시는 아니다).
101
- - 키보드 focus: `app.config.ts` `android.softwareKeyboardLayoutMode: "resize"` · iOS는 WebView prop `keyboardDisplayRequiresUserAction={false}`(Host.tsx).
101
+ - 키보드 focus: `mobile/app.config.ts` `android.softwareKeyboardLayoutMode: "resize"` · iOS는 WebView prop `keyboardDisplayRequiresUserAction={false}`(Host.tsx).
102
102
 
103
103
  ## 아이콘 (벡터 아이콘)
104
104
 
@@ -106,7 +106,7 @@ WebView 프로세스가 죽으면 `key`를 증가시켜 통째 재마운트한
106
106
 
107
107
  ## 로그인 · 소셜(FAST-FOLLOW)
108
108
 
109
- 로그인은 웹이 담당한다. 기본 상태에서 앱은 로그인에 아무것도 하지 않는다. 웹 로그인 화면이 웹뷰 안에 그대로 뜨고 쿠키·세션도 웹의 `@supabase/ssr`가 처리한다. 네이티브 로그인 모달(`src/auth/LoginScreen.tsx`)은 웹이 `REQUEST_SESSION_INSTALL`을 보낼 때만 뜨는 옵트인 스캐폴드이며, 유일하게 동작하는 버튼('이메일로 계속')은 모달을 닫아 웹 로그인으로 되돌릴 뿐이다(인증·재로드·네비게이션 없음). 소셜 원클릭(Google/Apple)은 disabled 'coming soon' 스텁이다(fast-follow).
109
+ 로그인은 웹이 담당한다. 기본 상태에서 앱은 로그인에 아무것도 하지 않는다. 웹 로그인 화면이 웹뷰 안에 그대로 뜨고 쿠키·세션도 웹의 `@supabase/ssr`가 처리한다. 네이티브 로그인 모달(`mobile/src/auth/LoginScreen.tsx`)은 웹이 `REQUEST_SESSION_INSTALL`을 보낼 때만 뜨는 옵트인 스캐폴드이며, 유일하게 동작하는 버튼('이메일로 계속')은 모달을 닫아 웹 로그인으로 되돌릴 뿐이다(인증·재로드·네비게이션 없음). 소셜 원클릭(Google/Apple)은 disabled 'coming soon' 스텁이다(fast-follow).
110
110
 
111
111
  소셜을 켤 땐 웹뷰 안 OAuth로 하지 말고 네이티브로 구현한다: 네이티브 SDK로 idToken을 받아 세션 핸드오프(`requestSessionHandoff`, 상세는 bridge-guide). 이유는 UX가 아니라 동작 자체다. Google은 임베디드 웹뷰 OAuth를 차단하고([`disallowed_useragent`, 2021/2023 시행](https://developers.googleblog.com/upcoming-security-changes-to-googles-oauth-20-authorization-endpoint-in-embedded-webviews/)), [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252)도 임베디드 user-agent를 금지한다. 그래서 시스템 브라우저(ASWebAuthenticationSession/Custom Tabs)나 네이티브 자격증명 선택기로 빠져나가야 한다. Apple 4.8은 조건부다: 구글 등 서드파티 소셜을 제공할 때만 네이티브 Sign in with Apple도 함께 필요하다(이메일/비번만이면 미적용).
112
112
 
@@ -5,7 +5,7 @@ description: Make working pages in the dev server so Owner can pick from design
5
5
 
6
6
  # /sketch
7
7
 
8
- `app/sketch/<feature>/`에 Owner의 의도에 맞춰 동작하는 페이지를 스케치한다.
8
+ `web/app/sketch/<feature>/`에 Owner의 의도에 맞춰 동작하는 페이지를 스케치한다.
9
9
  Owner는 dev 서버 브라우저로 결과물을 보고 피드백·확정할 수 있어야 한다.
10
10
 
11
11
  디자인 시안은 서로 다른 안을 나란히 펼쳐, Owner가 어느 룩으로 갈지 고르게 한다.
@@ -16,10 +16,10 @@ Owner는 dev 서버 브라우저로 결과물을 보고 피드백·확정할 수
16
16
 
17
17
  ## 정리 (확정 후)
18
18
 
19
- `app/sketch/`는 *현재 검토 중인 시안*만 사는 임시 공간이다. 확정되면 채택안만 이 경로에 남기고 나머지 변형은 비워, 다음 스케치가 죽은 안 위에서 시작하지 않게 한다.
19
+ `web/app/sketch/`는 *현재 검토 중인 시안*만 사는 임시 공간이다. 확정되면 채택안만 이 경로에 남기고 나머지 변형은 비워, 다음 스케치가 죽은 안 위에서 시작하지 않게 한다.
20
20
 
21
21
  비운 안은 버리지 말고 디자인 레퍼런스로 옮겨 둔다 — 탈락한 룩·상태 케이스는 다음 디자인 작업에서 다시 꺼내 보는 자산이다. 즉 정리는 *삭제*가 아니라 *라이브 경로에서 빼서 보관*이다. 어디에·어떻게 보관할지 모호하면 Owner에게 묻는다.
22
22
 
23
23
  ## 검증
24
24
 
25
- `app/sketch/<feature>/`에 채택안 하나만 남아 dev 서버에서 돌고, 탈락 안들은 사라진 게 아니라 레퍼런스로 옮겨져 있다.
25
+ `web/app/sketch/<feature>/`에 채택안 하나만 남아 dev 서버에서 돌고, 탈락 안들은 사라진 게 아니라 레퍼런스로 옮겨져 있다.
@@ -122,7 +122,7 @@ Google은 플랫폼마다 OAuth 클라이언트가 필요하다. 코드에는 **
122
122
 
123
123
  한국 개발자는 Apple Services ID 등록/갱신 시 server-to-server 엔드포인트가 필수가 됐다 ([Apple 공지](https://developer.apple.com/news/?id=j9zukcr6)). 이건 웹 OAuth 방식에만 해당하므로, 기본인 네이티브 idToken 방식을 쓰면 Services ID 자체가 없어 이 요구를 회피한다. 그래서 한국 Owner에게는 네이티브 idToken 방식을 강력 권장한다.
124
124
 
125
- 핸드오프 함수(`requestSessionHandoff`)는 구현·테스트됐으나 호출자가 없다. 켜려면 입력인 네이티브 idToken 획득도 미배선이고, 출력을 받는 웹 라우트(POST `/auth/app-bridge` + nonce consume)는 이 앱이 아니라 `docs/web-adapter`에서 웹 repo에 설치해야 한다. `secureSession` get/set도 v1 호출자 없음. 기본 로그인은 웹의 것이다. 실제 켜기·콘솔 셋업은 fast-follow이며 이 문서가 단계별 절차를 담는다.
125
+ 핸드오프 함수(`requestSessionHandoff`)는 구현·테스트됐으나 호출자가 없다. 켜려면 입력인 네이티브 idToken 획득도 미배선이고, 출력을 받는 웹 라우트(POST `/auth/app-bridge` + nonce consume)는 이 앱이 아니라 `mobile/docs/web-adapter`에서 웹 repo에 설치해야 한다. `secureSession` get/set도 v1 호출자 없음. 기본 로그인은 웹의 것이다. 실제 켜기·콘솔 셋업은 fast-follow이며 이 문서가 단계별 절차를 담는다.
126
126
 
127
127
  ---
128
128
 
@@ -138,18 +138,18 @@ EAS가 서명을 관리한다. Owner가 fastlane을 직접 구성할 일은 없
138
138
 
139
139
  - iOS: 배포 인증서, 프로비저닝 프로파일, APNs 키(푸시용)
140
140
  - Android: 업로드 키스토어
141
- - iOS 제출용 ASC API 키: EAS 자동 생성 (Account Holder가 1회 'Request Access' 약관 동의 선행) — 권장. (`eas.json`엔 대안인 `appleId` 방식 placeholder도 있으나 둘 중 하나만.)
141
+ - iOS 제출용 ASC API 키: EAS 자동 생성 (Account Holder가 1회 'Request Access' 약관 동의 선행) — 권장. (`mobile/eas.json`엔 대안인 `appleId` 방식 placeholder도 있으나 둘 중 하나만.)
142
142
 
143
143
  ### 수동인 것 (🟡)
144
144
 
145
- - **Play 서비스 계정 JSON** (Android 제출 인증): 한 번만 만든다. `eas.json`엔 경로를 넣지 않는다(시크릿 커밋 위험). 첫 `eas submit -p android`가 JSON 경로를 물으면 그때 주고, EAS가 서버에 저장한다(`$ENV` 보간 미지원). 만드는 순서 ([Expo 절차](https://github.com/expo/fyi/blob/main/creating-google-service-account.md)):
145
+ - **Play 서비스 계정 JSON** (Android 제출 인증): 한 번만 만든다. `mobile/eas.json`엔 경로를 넣지 않는다(시크릿 커밋 위험). 첫 `eas submit -p android`가 JSON 경로를 물으면 그때 주고, EAS가 서버에 저장한다(`$ENV` 보간 미지원). 만드는 순서 ([Expo 절차](https://github.com/expo/fyi/blob/main/creating-google-service-account.md)):
146
146
  1. Cloud Console → **Google Play Android Developer API**(`androidpublisher.googleapis.com`) **Enable**
147
147
  2. IAM & Admin → **서비스 계정 생성** (역할 지정은 skip)
148
148
  3. 그 계정 → 키 → **JSON 키 생성·다운로드** (시크릿, 커밋 금지)
149
149
  4. Play Console → **사용자 및 권한** → 그 SA 이메일 **초대** + 앱/계정 권한 부여 (권한 반영 지연 가능)
150
150
  - **Android FCM v1 서비스 계정**(푸시용): Firebase Console에서 수동 생성 → EAS 업로드.
151
151
 
152
- > `eas.json`엔 public 값만 커밋한다. 시크릿은 EAS 서버에 둔다.
152
+ > `mobile/eas.json`엔 public 값만 커밋한다. 시크릿은 EAS 서버에 둔다.
153
153
  > `google-services.json`(public·커밋) ↔ FCM SA JSON(시크릿) ↔ Play 제출 SA JSON. 셋은 별개다. 혼동하지 말라.
154
154
 
155
155
  ### EAS 무료 티어 (선고지 — 스냅샷)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-saas-starter-workspace",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "강의용 SaaS 워크스페이스(web + mobile + 에이전트 도구)를 한 번에 만드는 생성기. 생성 → 의존성 설치 → doctor 점검까지 한 흐름.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",