Skip to content

템플릿

새 프로젝트에 복사해 값만 채우는 파일들이다. 마크다운은 왼쪽 메뉴에서, 코드 파일은 아래에서 본다.

쓰는 법은 셋 다 같다.

  1. 대상 프로젝트의 "복사 위치"로 복사한다.

  2. {{플레이스홀더}}출시 값 입력 양식 §0의 값으로 치환한다.

  3. 남은 자리가 없는지 확인한다.

    sh
    grep -n '{{' <복사한>     # 0건이어야 한다

launch-values.md를 가장 먼저 복사한다. 나머지 템플릿이 전부 그 파일의 값을 읽어 채워진다.

마크다운 양식

템플릿언제 쓰나복사 위치관련 단계
출시 값 입력 양식맨 처음. 콘솔에서 받은 값을 여기 한 곳에 모은다. 모든 {{플레이스홀더}}의 정의처다docs/ops/launch-values.local.md (.gitignore*.local.md)02 계정 개설부터 전 단계
에셋 폴더 구조아이콘 원본과 스토어 스크린샷을 만들기 직전. 파일 이름과 폴더를 먼저 정한다docs/design/icons/ · docs/store-assets/ 폴더 규칙09 에셋
개인정보처리방침 골격법정 문서 초안을 쓸 때. 조문마다 "반드시 들어갈 것"만 있고 문장은 없다랜딩의 법정 문서 페이지 또는 언어 사전10 법정 문서 10.2
이용약관 골격위와 같다. 유료 구독이 있으면 제9~11조를 반드시 채운다위와 같다10 법정 문서 10.4
스토어 설명문두 콘솔에 붙여 넣을 문안의 정본. 콘솔에서 직접 고치지 않는다docs/ops/store-listing.md13 스토어 등록 · 13a · 13b
데이터 안전 · App Privacy 답변두 스토어가 같은 사실을 다른 폼으로 묻는다. 답을 한곳에 모아 어긋나지 않게 한다docs/ops/data-safety-answers.md13b Play Console 13b.7 · 13a 13a.2
심사 메모심사 정보를 채우기 직전. 한국어와 English를 이어 붙여 넣는다docs/ops/review-notes.md14 심사 14.1
릴리스 로그첫 빌드부터. 실패한 빌드의 원인 한 줄이 가장 쓸모 있다docs/ops/release-log.md12 빌드 · 서명 · 15 출시 후 15.4

코드 파일

템플릿언제 쓰나복사 위치관련 단계
eas.jsonEAS 빌드 프로필을 처음 만들 때. base를 나머지 프로필이 extends 한다apps/mobile/eas.json12 빌드 · 서명 12.1 · 12.8
app.config.skeleton.tsExpo 앱을 만든 직후. 정적 app.json 대신 쓴다 — 키가 없는 통합은 플러그인을 붙이지 않아야 빌드가 된다apps/mobile/app.config.ts01 프로젝트 셋업 1.4 · 07 · 08
env.example01의 env 규약을 정할 때. 접두사별로 어디에 들어가는 값인지 주석이 붙어 있다저장소 루트의 .env.example01 프로젝트 셋업 1.5 · 12 12.3

app.config.tseas.json은 서로를 본다. eas.json의 환경 변수가 빌드 시점에 app.config.ts로 들어가므로, 둘 중 하나만 고치면 로컬에서는 되는데 EAS 빌드에서 값이 비는 일이 생긴다(12.3).

eas.json

json
{
  "cli": {
    "version": ">= 24.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "base": {
      "node": "22.23.1",
      "pnpm": "12.3.4",
      "env": {
        "EXPO_NO_DOTENV": "1",
        "EXPO_APPLE_TEAM_ID": "{{APPLE_TEAM_ID}}",
        "EXPO_APPLE_TEAM_TYPE": "{{APPLE_TEAM_TYPE}}"
      }
    },
    "development": {
      "extends": "base",
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development",
      "ios": {
        "simulator": true
      }
    },
    "preview": {
      "extends": "base",
      "distribution": "internal",
      "channel": "preview"
    },
    "production": {
      "extends": "base",
      "channel": "production",
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {
      "ios": {
        "ascAppId": "{{ASC_APP_ID}}",
        "appleTeamId": "{{APPLE_TEAM_ID}}"
      },
      "android": {
        "track": "internal",
        "releaseStatus": "draft"
      }
    }
  }
}

app.config.skeleton.ts

ts
/**
 * app.config.ts 골격 — apps/mobile/app.config.ts로 복사해 쓴다.
 *
 * `{{플레이스홀더}}`는 templates/launch-values.md §0의 값으로 치환한다.
 * 치환이 끝나면 `grep -n '{{' app.config.ts`가 0건이어야 한다.
 *
 * 정적 app.json이 아니라 동적 설정인 이유는 두 가지다.
 *  1. 값이 env에서 온다(빌드 프로필마다 다르다).
 *  2. 계정이 아직 없는 통합은 플러그인을 **붙이지 않아야** 한다.
 *     이 플러그인들은 값이 없으면 prebuild 단계에서 throw 하므로,
 *     조건 없이 나열하면 키가 없는 사람은 앱을 빌드조차 못 한다.
 *
 * 절차 설명: guide/01-project-setup.md §1.4
 */

import { existsSync } from 'node:fs';
import { resolve } from 'node:path';

import type { ConfigContext, ExpoConfig } from 'expo/config';

// ---------------------------------------------------------------------------
// 식별자 — 한 곳에서만 쓴다. 스토어에 올라간 뒤에는 바꿀 수 없다.
// ---------------------------------------------------------------------------
const BUNDLE_ID = '{{BUNDLE_ID}}'; // iOS 번들 ID = Android 패키지명
const APP_SCHEME = '{{APP_SCHEME}}'; // 딥링크 스킴 (스킴://)
const APP_SLUG = '{{APP_SLUG}}'; // Expo slug. EAS 프로젝트와 같아야 한다
const EAS_ACCOUNT = '{{EAS_ACCOUNT}}'; // eas whoami 의 결과와 같아야 한다

// 유니버설 링크 / App Links 호스트. 랜딩이 검증 파일을 서빙하는 도메인이다.
// guide/11-landing.md 참고.
const LINK_DOMAIN = '{{DOMAIN}}';

// [선택] 위젯·확장을 쓸 때만. 관례상 'group.' + 번들 ID.
// const APP_GROUP = '{{APP_GROUP}}';

// 구글의 공개 테스트 광고 ID. 실제 ID는 env에서 오므로, 광고 계정이 없는
// 클론에서도 빌드가 된다. 스토어 빌드 전에 실제 ID가 들어갔는지 확인한다.
const TEST_ADMOB_IOS_APP_ID = 'ca-app-pub-3940256099942544~1458002511';
const TEST_ADMOB_ANDROID_APP_ID = 'ca-app-pub-3940256099942544~3347511713';

const env = process.env;

// ---------------------------------------------------------------------------
// env 읽기 헬퍼
// ---------------------------------------------------------------------------
/** 빈 문자열과 공백만 있는 값을 undefined로 만든다. */
function str(value: string | undefined): string | undefined {
  const trimmed = value?.trim();
  return trimmed ? trimmed : undefined;
}

/** 파일 경로 env. 없으면 저장소 안의 기본 경로를 본다. */
function filePath(value: string | undefined, fallback: string): string {
  return str(value) ?? fallback;
}

/** production 프로필에서만 필수인 값. */
function requiredInProduction(name: string, value: string | undefined): void {
  if (env.EAS_BUILD_PROFILE === 'production' && !value) {
    throw new Error(
      `${name} is required for the production build profile. ` +
        'Set it in the EAS project environment before building.',
    );
  }
}

const appMode = str(env.EXPO_PUBLIC_APP_MODE) === 'demo' ? 'demo' : 'live';
const easProjectId = str(env.EXPO_PUBLIC_EAS_PROJECT_ID);
const appleTeamId = str(env.EXPO_PUBLIC_APPLE_TEAM_ID);
const iosAppId = str(env.EXPO_PUBLIC_IOS_APP_ID);
const linkDomain = str(env.EXPO_PUBLIC_UNIVERSAL_LINK_DOMAIN) ?? LINK_DOMAIN;

// Google 로그인. iOS URL 스킴(역방향 클라이언트 ID)이 있을 때만 플러그인을 붙인다.
const googleIosUrlScheme = str(env.EXPO_PUBLIC_GOOGLE_IOS_URL_SCHEME);

// Firebase(Crashlytics) 설정 파일 **경로**. 값이 아니라 파일이다.
// 두 파일이 실제로 있을 때만 플러그인을 붙인다 — 하나라도 없으면
// @react-native-firebase 플러그인이 prebuild에서 throw 한다.
// EAS에서는 `--type file` 환경 변수라 빌더가 절대 경로로 풀어 준다.
const googleServicesIos = filePath(
  env.GOOGLE_SERVICES_INFO_PLIST,
  './firebase/GoogleService-Info.plist',
);
const googleServicesAndroid = filePath(env.GOOGLE_SERVICES_JSON, './firebase/google-services.json');
const firebaseEnabled =
  existsSync(resolve(__dirname, googleServicesIos)) &&
  existsSync(resolve(__dirname, googleServicesAndroid));
// ⚠️ react-native.config.js 의 같은 조건과 **표현을 똑같이** 유지한다.
// 오토링크는 플러그인이 아니라 package.json 을 보고 네이티브 모듈을 링크한다.

// ---------------------------------------------------------------------------
// 프로덕션 게이트 — 백엔드 없는 스토어 빌드를 막는다
// ---------------------------------------------------------------------------
requiredInProduction('EXPO_PUBLIC_SUPABASE_URL', str(env.EXPO_PUBLIC_SUPABASE_URL));
// 스토어 리뷰 링크·강제 업데이트 링크가 이 숫자 ID로 만들어진다.
// 가짜 값을 넣으면 사용자가 404 페이지로 간다.
requiredInProduction('EXPO_PUBLIC_IOS_APP_ID', iosAppId);

// iOS 권한 문구. 심사는 목적이 적히지 않은 권한 요청을 반려한다.
const PHOTOS_PERMISSION = '사진을 고르기 위해 사진 보관함에 접근합니다.';
const CAMERA_PERMISSION = '사진을 바로 찍기 위해 카메라를 사용합니다.';
const TRACKING_PERMISSION = '맞춤 광고에 사용됩니다. 허용하지 않아도 모든 기능을 쓸 수 있어요.';

export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  name: '{{APP_NAME_EN}}', // EAS·대시보드·스토어가 이 값을 키로 쓴다
  slug: APP_SLUG,
  owner: EAS_ACCOUNT,
  scheme: APP_SCHEME,
  version: '1.0.0',
  orientation: 'portrait',
  userInterfaceStyle: 'automatic',
  icon: './assets/icon.png',

  // [선택] 스토어 표시 이름과 iOS 권한 문구의 현지화.
  // locales: { ko: './locales/ko.json', en: './locales/en.json' },

  ios: {
    bundleIdentifier: BUNDLE_ID,
    supportsTablet: false,
    usesAppleSignIn: true,
    ...(appleTeamId ? { appleTeamId } : {}),
    ...(iosAppId ? { appStoreUrl: `https://apps.apple.com/app/id${iosAppId}` } : {}),
    ...(firebaseEnabled ? { googleServicesFile: googleServicesIos } : {}),
    associatedDomains: [`applinks:${linkDomain}`, `webcredentials:${linkDomain}`],
    infoPlist: {
      ITSAppUsesNonExemptEncryption: false,
      NSPhotoLibraryUsageDescription: PHOTOS_PERMISSION,
      NSCameraUsageDescription: CAMERA_PERMISSION,
    },
  },

  android: {
    package: BUNDLE_ID,
    playStoreUrl: `https://play.google.com/store/apps/details?id=${BUNDLE_ID}`,
    ...(firebaseEnabled ? { googleServicesFile: googleServicesAndroid } : {}),
    adaptiveIcon: {
      backgroundColor: '#FFFFFF',
      foregroundImage: './assets/adaptive-icon.png',
      monochromeImage: './assets/adaptive-icon-monochrome.png',
    },
    // --- [선택] 푸시를 쓴다면 ---------------------------------------------
    // Android 13부터 `POST_NOTIFICATIONS`가 런타임 권한이다. 선언하지 않으면
    // 알림이 오류 없이 그냥 뜨지 않는다. 선언한 뒤 화면에서 직접 요청한다.
    // ⚠️ `SCHEDULE_EXACT_ALARM`은 Play의 **제한 권한**이다. 정확한 시각이
    //    필요한 예약 알림이 실제로 있을 때만 넣고, 없으면 지운다 —
    //    선언해 두면 Play Console에서 사용 사유를 소명해야 한다.
    // permissions: [
    //   'android.permission.POST_NOTIFICATIONS',
    //   'android.permission.SCHEDULE_EXACT_ALARM',
    // ],
    // guide/08-push.md 참고.

    // ⚠️ `pathPrefix`를 빼면 이 호스트의 **모든** 경로를 앱이 가로챈다 —
    // 랜딩 첫 화면도, 약관·개인정보처리방침 링크도 앱으로 열린다.
    // 앱이 실제로 처리하는 경로만 적는다. 경로가 여럿이면 필터를 여러 개 둔다.
    intentFilters: [
      {
        action: 'VIEW',
        autoVerify: true,
        data: [{ scheme: 'https', host: linkDomain, pathPrefix: '/join' }],
        category: ['BROWSABLE', 'DEFAULT'],
      },
    ],
  },

  // OTA 업데이트는 네이티브 지문에 묶인다. 네이티브가 바뀌면 지문이 바뀌고,
  // 호환되지 않는 JS 번들이 기존 빌드로 나가지 않는다.
  runtimeVersion: { policy: 'fingerprint' },
  updates: {
    ...(easProjectId ? { url: `https://u.expo.dev/${easProjectId}` } : {}),
    fallbackToCacheTimeout: 0,
  },

  extra: {
    ...(easProjectId ? { eas: { projectId: easProjectId } } : {}),
    // 앱 코드는 env 대신 이 값을 읽어도 된다(빌드 시점에 박힌다).
    appMode,
  },

  plugins: [
    'expo-router',
    'expo-localization',
    'expo-secure-store',
    'expo-web-browser',
    'expo-apple-authentication',
    [
      'expo-splash-screen',
      {
        image: './assets/splash-icon.png',
        imageWidth: 300,
        resizeMode: 'contain',
        backgroundColor: '#FFFFFF',
      },
    ],
    [
      'expo-build-properties',
      {
        // [선택] Crashlytics — Firebase를 쓸 때만 정적 프레임워크로 바꾼다.
        // 설정 파일이 없는 클론은 기본 Podfile 그대로 둔다.
        //
        // `@react-native-firebase`는 firebase-ios-sdk를 Swift Package Manager로
        // 받는 것이 기본인데, 그 경로는 정적 링크에서 `pod install`을 중단시킨다.
        // 그래서 SPM을 끄고(아래 `@react-native-firebase/app` 플러그인의
        // `disableSPM`) CocoaPods + 정적 프레임워크로 간다 — SPM이 꺼졌을 때
        // firebase-ios-sdk가 요구하는 조합이다.
        //
        // `useFrameworks`는 Podfile의 **모든** pod의 링크 방식을 바꾼다.
        // `forceStaticLinking`은 지정한 pod만 그 프레임워크 빌드 안에서 정적
        // 라이브러리로 남긴다 — 모듈러 헤더에 걸려 넘어지는 pod의 탈출구다.
        // 다른 pod에서 링크 오류가 나면 그 이름을 이 배열에 추가한다.
        // 자세한 사정: guide/07-crash-analytics.md
        ios: firebaseEnabled
          ? { useFrameworks: 'static' as const, forceStaticLinking: ['RNFBApp', 'RNFBCrashlytics'] }
          : {},
        android: {
          compileSdkVersion: 36,
          targetSdkVersion: 36,
          minSdkVersion: 24,
          // 릴리스 빌드만 축소·난독화한다. 디버그 빌드는 그대로 둔다.
          enableMinifyInReleaseBuilds: true,
          enableShrinkResourcesInReleaseBuilds: true,
        },
      },
    ],
    [
      'expo-image-picker',
      { photosPermission: PHOTOS_PERMISSION, cameraPermission: CAMERA_PERMISSION },
    ],
    ['expo-tracking-transparency', { userTrackingPermission: TRACKING_PERMISSION }],
    [
      'react-native-google-mobile-ads',
      {
        iosAppId: str(env.EXPO_PUBLIC_ADMOB_IOS_APP_ID) ?? TEST_ADMOB_IOS_APP_ID,
        androidAppId: str(env.EXPO_PUBLIC_ADMOB_ANDROID_APP_ID) ?? TEST_ADMOB_ANDROID_APP_ID,
        delayAppMeasurementInit: true,
        userTrackingUsageDescription: TRACKING_PERMISSION,
      },
    ],

    // --- 조건부: 값이 있을 때만 붙는다 -------------------------------------
    ...(googleIosUrlScheme
      ? [
          ['@react-native-google-signin/google-signin', { iosUrlScheme: googleIosUrlScheme }] as [
            string,
            Record<string, unknown>,
          ],
        ]
      : []),
    ...(firebaseEnabled
      ? ([
          ['@react-native-firebase/app', { ios: { disableSPM: true } }],
          '@react-native-firebase/crashlytics',
        ] as (string | [string, Record<string, unknown>])[])
      : []),

    // --- [선택] 푸시를 쓴다면 ---------------------------------------------
    // `expo install expo-notifications` 뒤에 주석을 푼다. 아이콘·색을 비워 두면
    // Android 상태바에 흰 사각형이 뜬다. 위 android.permissions도 같이 푼다.
    // guide/08-push.md 참고.
    // ['expo-notifications', {
    //   icon: './assets/notification-icon.png',  // Android 상태바 아이콘(단색 PNG)
    //   color: '#000000',
    // }],

    // --- [선택] 위젯을 쓴다면 ---------------------------------------------
    // iOS 위젯과 Android 위젯은 서로 다른 라이브러리이고,
    // iOS 쪽은 App Group({{APP_GROUP}})을 Apple 포털에도 등록해야 한다.
    // ['expo-widgets', { groupIdentifier: APP_GROUP, widgets: [] }],
    // ['react-native-android-widget', { widgets: [] }],
  ],
});

// ---------------------------------------------------------------------------
// [선택] 네이티브 설정을 코드로 고쳐야 할 때
//
// ios/ 와 android/ 는 커밋하지 않는 생성물이다. 직접 고치면 다음
// `expo prebuild --clean`에서 사라지고, EAS 빌더에는 애초에 반영되지 않는다.
// 그래서 모든 네이티브 수정은 config plugin으로 쓴다.
//
//   import { withProjectBuildGradle, withGradleProperties } from 'expo/config-plugins';
//
//   function withGradleFixes(config: ExpoConfig): ExpoConfig {
//     return withProjectBuildGradle(config, (mod) => {
//       // 앵커 문자열을 못 찾으면 조용히 넘어가지 말고 throw 한다.
//       // 그래야 템플릿이 바뀌었을 때 prebuild가 그 자리에서 알려 준다.
//       return mod;
//     });
//   }
//
// 실제로 필요했던 사례(광고 SDK의 Kotlin 메타데이터, WorkManager 중복 클래스)는
// guide/01-project-setup.md의 함정 절에 있다. R8 축소·난독화 쪽은
// guide/12-build-signing.md.
// ---------------------------------------------------------------------------

env.example

sh
# 프로젝트 env 양식 — 저장소 루트의 `.env.example`로 복사한다.
#
# 값은 전부 비어 있다. 채운 파일은 커밋하지 않는다.
#   .env              서버·CLI·Edge Function 용 (루트)
#   apps/mobile/.env.local   앱 용
# .gitignore 에 `.env` 와 `.env*.local` 을 넣는다.
#
# 접두사 규칙 (guide/01-project-setup.md §1.5)
#   EXPO_PUBLIC_*  → 앱 번들에 그대로 박힌다. 시크릿 금지.
#   NEXT_PUBLIC_*  → 랜딩 브라우저 번들에 박힌다. 시크릿 금지.
#   접두사 없음     → 서버·CLI 전용. 여기에만 시크릿을 둔다.
#
# 각 값을 어느 콘솔에서 받아 오는지는 templates/launch-values.md 에 있다.
# 값이 채워진 줄은 프로젝트 값이 아니라 안전한 기본값이다.

# ---------------------------------------------------------------------------
# apps/mobile (Expo) — 클라이언트, 정의상 공개 값
# ---------------------------------------------------------------------------
# demo | live. demo 는 '요청'일 뿐이고 개발 빌드와 development/preview 채널에서만
# 실제로 켜진다. production 은 백엔드 URL이 없으면 설정 단계에서 throw 한다.
EXPO_PUBLIC_APP_MODE=demo
EXPO_PUBLIC_SUPABASE_URL=                      # Project Settings › API › Project URL
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=          # publishable key (구버전이면 anon key)
# 커스텀 SMTP를 붙이기 전에는 false로 둔다. 기본 SMTP는 한도가 낮아 심사 중에 막힌다.
EXPO_PUBLIC_EMAIL_OTP_ENABLED=false
# 카카오는 앱에 클라이언트 키가 없다. 서버에 프로바이더가 켜졌는지만 알려 주는 플래그다.
EXPO_PUBLIC_KAKAO_LOGIN_ENABLED=false

# RevenueCat 공개 SDK 키 (appl_ / goog_ 로 시작)
EXPO_PUBLIC_RC_IOS_KEY=
EXPO_PUBLIC_RC_ANDROID_KEY=

# AdMob — 비우면 구글 공개 테스트 ID로 떨어진다. 스토어 빌드 전에 실제 값으로 채운다.
EXPO_PUBLIC_ADMOB_IOS_APP_ID=                  # ca-app-pub-…~… (앱 ID)
EXPO_PUBLIC_ADMOB_ANDROID_APP_ID=
EXPO_PUBLIC_ADMOB_BANNER_IOS=                  # ca-app-pub-…/… (광고 단위 ID)
EXPO_PUBLIC_ADMOB_BANNER_ANDROID=

# Google 로그인
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=              # 유형: 웹 애플리케이션. Android도 이 값을 본다
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=              # 유형: iOS
EXPO_PUBLIC_GOOGLE_IOS_URL_SCHEME=             # iOS 클라이언트의 역방향 클라이언트 ID

# 링크·문의
EXPO_PUBLIC_UNIVERSAL_LINK_DOMAIN=             # {{DOMAIN}} — 스킴도 경로도 없는 호스트만
EXPO_PUBLIC_APP_URL=                           # {{WEB_ORIGIN}} — 약관·지원 링크의 기준
EXPO_PUBLIC_SUPPORT_EMAIL=                     # {{SUPPORT_EMAIL}}

# Apple / EAS
EXPO_PUBLIC_APPLE_TEAM_ID=                     # Membership › Team ID → {{APPLE_TEAM_ID}}
# eas.json 의 build.base.env 에 박는 두 이름. 비대화형 빌드가 팀을 묻지 않게 한다.
EXPO_APPLE_TEAM_ID=                            # 위와 같은 값 → {{APPLE_TEAM_ID}}
EXPO_APPLE_TEAM_TYPE=                          # INDIVIDUAL | COMPANY_OR_ORGANIZATION → {{APPLE_TEAM_TYPE}}
EXPO_PUBLIC_IOS_APP_ID=                        # ASC 앱 생성 후 숫자 ID → {{ASC_APP_ID}}
EXPO_PUBLIC_EAS_PROJECT_ID=                    # `eas init` 결과 → {{EAS_PROJECT_ID}}

# 심사용 고정 코드 로그인의 앱 쪽 두 값. 아무것도 인증하지 않는다 — 판정은 서버 시크릿이
# 한다(아래 Edge Function 시크릿 절의 REVIEWER_*). guide/04-auth.md §4.7.
EXPO_PUBLIC_REVIEWER_EMAIL=                    # [선택 — 심사 기간에만. 승인 뒤 unset] 같은 주소 → {{REVIEWER_EMAIL}}
EXPO_PUBLIC_REVIEWER_OTP_HINT=                 # [선택 — 심사 기간에만. 승인 뒤 unset] 배너에 찍는 코드. REVIEWER_OTP와 같은 값

# Firebase 설정 파일의 **경로**다(값이 아니다). 로컬에서는 비워 두고 파일만
# apps/mobile/firebase/ 에 넣어도 된다. EAS에서는 `--type file` 환경 변수로 넣는다.
GOOGLE_SERVICES_INFO_PLIST=
GOOGLE_SERVICES_JSON=

# ---------------------------------------------------------------------------
# 백엔드 CLI — 마이그레이션·설정 푸시에 쓴다 (루트 .env)
# ---------------------------------------------------------------------------
SUPABASE_PROJECT_REF=                          # Project Settings › General → {{SUPABASE_REF}}
SUPABASE_SECRET_KEY=                           # secret key — 서버 전용. 앱에 넣지 않는다
SUPABASE_DB_PASSWORD=                          # 프로젝트 생성 시 정한 DB 비밀번호

# ---------------------------------------------------------------------------
# 인증 메일 SMTP — `supabase config push` 가 셸에서 읽는다
# ---------------------------------------------------------------------------
SUPABASE_AUTH_SMTP_HOST=                       # {{SMTP_HOST}}
# 포트는 env가 아니다. config.toml `[auth.email.smtp] port = 465` 고정. guide/04-auth.md 참고.
SUPABASE_AUTH_SMTP_USER=                       # {{SMTP_USER}}
SUPABASE_AUTH_SMTP_PASS=                       # 2단계 인증을 쓰면 '메일 전용 비밀번호'
SUPABASE_AUTH_SMTP_ADMIN_EMAIL=                # 발신자로 표시할 주소

# ---------------------------------------------------------------------------
# 소셜 로그인 프로바이더 — 같은 방식으로 셸에서 읽힌다
# ---------------------------------------------------------------------------
# Apple: 네이티브 로그인만 쓰면 CLIENT_ID 는 번들 ID 하나, SECRET 은 비워 둔다.
# 웹(Services ID) 플로우를 켤 때만 Services ID 를 콤마로 덧붙이고 .p8 로 JWT 를 만든다.
SUPABASE_AUTH_APPLE_CLIENT_ID=                 # {{BUNDLE_ID}}
SUPABASE_AUTH_APPLE_SECRET=
APPLE_SIGNIN_KEY_ID=                           # 위 JWT 를 만든 키의 Key ID

# Google: 웹 클라이언트의 ID/시크릿. 네이티브 ID 는 위 EXPO_PUBLIC_* 에 있다.
SUPABASE_AUTH_GOOGLE_CLIENT_ID=
SUPABASE_AUTH_GOOGLE_SECRET=
GOOGLE_ANDROID_CLIENT_ID=                      # [참고] 콘솔 등록만 필요. 앱 env 에는 안 들어간다

# 카카오: REST API 키와 Client Secret(상태를 '사용함'으로 켠 뒤)
SUPABASE_AUTH_KAKAO_CLIENT_ID=
SUPABASE_AUTH_KAKAO_SECRET=

# ---------------------------------------------------------------------------
# Edge Function 시크릿 — 파일이 아니라 `supabase secrets set` 으로 넣는다
# ---------------------------------------------------------------------------
RC_SECRET_API_KEY=                             # RevenueCat Secret API key v1 (sk_ 로 시작)
RC_WEBHOOK_AUTH=                               # 임의의 긴 문자열. 웹훅 Authorization 헤더와 같은 값
EXPO_ACCESS_TOKEN=                             # [선택] 서버에서 푸시를 보낼 때

# 심사용 고정 코드 로그인 — guide/04-auth.md §4.7. 네 값이 두 짝이다.
REVIEWER_EMAIL=                                # [선택 — 심사 기간에만. 승인 뒤 unset] 허용 목록. 주소 하나만
REVIEWER_OTP=                                  # [선택 — 심사 기간에만. 승인 뒤 unset] 진짜 6자리 코드. 상수 시간 비교

# ---------------------------------------------------------------------------
# 스토어 제출 (eas submit) — 파일은 저장소 밖에 둔다
# ---------------------------------------------------------------------------
ASC_APP_SPECIFIC_PASSWORD=                     # [선택] API Key 를 쓰면 비워 둔다
ASC_API_KEY_ID=                                # [선택] App Store Connect API
ASC_API_ISSUER_ID=                             # [선택]
ASC_API_KEY_P8_PATH=                           # [선택] 내려받은 .p8 경로
PLAY_SERVICE_ACCOUNT_JSON_PATH=                # [선택] Play 서비스 계정 키 경로
PLAY_APP_SIGNING_SHA256=                       # [나중] 앱 서명 키 인증서 SHA-256 (AA:BB:CC:…)

# ---------------------------------------------------------------------------
# 참고 값 — env 로 읽지는 않지만 한곳에 두면 편하다
# ---------------------------------------------------------------------------
FIREBASE_PROJECT_ID=                           # {{FIREBASE_PROJECT_ID}}
ADMOB_PUBLISHER_ID=                            # {{ADMOB_PUBLISHER_ID}} — 랜딩의 /app-ads.txt 에 들어간다

# ---------------------------------------------------------------------------
# apps/landing (Next.js, 공개 웹)
# ---------------------------------------------------------------------------
NEXT_PUBLIC_APP_URL=                           # {{WEB_ORIGIN}} — canonical·OG·사이트맵의 기준
NEXT_PUBLIC_SUPPORT_EMAIL=                     # {{SUPPORT_EMAIL}}
NEXT_PUBLIC_IOS_APP_ID=                        # {{ASC_APP_ID}} — 비어 있으면 App Store 배지를 숨긴다
NEXT_PUBLIC_ANDROID_PACKAGE=                   # {{BUNDLE_ID}}
NEXT_PUBLIC_PRICE_MONTHLY=                     # {{PRICE_MONTHLY}}
NEXT_PUBLIC_PRICE_YEARLY=                      # {{PRICE_YEARLY}}

# ---------------------------------------------------------------------------
# apps/admin (선택, 로컬 전용 운영 콘솔) — 배포본에 넣지 않는다
# ---------------------------------------------------------------------------
SUPABASE_URL=                                  # {{SUPABASE_REF}}에서 파생된다: https://{{SUPABASE_REF}}.supabase.co
ADMIN_PASSWORD=
ADMIN_SESSION_SECRET=                          # openssl rand -hex 32

# 사업자 정보(약관·개인정보처리방침에 들어가는 값)는 env 가 아니다.
# templates/launch-values.md §12 에 적고 문서 생성에만 쓴다.