Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# API 서버 주소 (클라이언트에 공개되는 값만 사용)
EXPO_PUBLIC_API_URL=

# 팀 검토 전용 빌드에서만 true. 정식 서비스에서는 설정하지 않습니다.
EXPO_PUBLIC_TEAM_REVIEW=false
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ expo-env.d.ts
web-build/
ios/
android/

# Vercel local project linkage
.vercel/
.vercel
5 changes: 5 additions & 0 deletions .vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.git
.env*
docs
*.log
22 changes: 22 additions & 0 deletions docs/team-review-deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 팀 검토용 Vercel 임시 배포

검토 전용 Vercel 프로젝트 `sudam-team-review`를 사용합니다. GitHub 저장소 `SUDAM-Developer/FrontEnd`의 `develop` 브랜치에 머지·푸시하면 자동 배포되며, 빌드 성공 후 https://sudam-team-review.vercel.app 에 반영됩니다.

배포 설정 PR은 현재 검토 사이트에 포함된 PR #10(보강 조율), #11(수입·알림·하단 탭) 변경사항에 의존합니다. 해당 PR을 먼저 리뷰·머지한 후 배포 설정 PR을 머지합니다. PR 생성만으로 develop이나 현재 운영 주소가 변경되지는 않습니다.

- `npm run build`: 일반 웹 빌드. 검토 플래그가 없으면 기존 production 제한 유지
- `npm run build:review -- --clear`: 검토 빌드. 공개 가능한 예시 데이터와 미리보기 경로 허용
- `EXPO_PUBLIC_TEAM_REVIEW=true`: 검토 전용 빌드에서만 설정. 공개되는 값이며 보안/인증 수단이 아님
- `/` 및 이전 `/review` → `/terms`: 앱의 원래 진입 흐름인 약관 → 프로필 → 홈·하단 탭 사용. 별도 화면 목록 페이지는 제공하지 않음
- 보강 조율, 약관·프로필, 회차 기록·완료, 수입 상태, 배너 모두 확인 가능
- 배너는 기본 검토 링크에서는 시안만 표시하고, `controls=1`일 때 동작 확인 도구 표시
- 브라우저 새로고침/직접 경로 접근을 위한 Vercel SPA rewrite 구성
- 검색 제외 요청을 위한 X-Robots-Tag: noindex, nofollow. 접근 제어를 제공하는 기능은 아님
- 실제 API·메시지 발송·결제·입금 확인은 연결하지 않음
- `.env`와 Git 메타데이터 등은 업로드에서 제외. 정식 도메인은 연결하지 않음

Vercel에서는 Build Command `npm run build`, Output Directory `dist`, 검토 프로젝트에만 Build 환경 변수 `EXPO_PUBLIC_TEAM_REVIEW=true`를 사용합니다. 로컬에선 `build:review` 스크립트로 같은 빌드를 생성합니다. 검토용 URL의 접근 보호 설정은 Vercel 프로젝트에서 별도로 관리합니다.

검토 모드에서는 메모리에 검토용 프로필을 초기 설정해 수입 탭 직접 접속·새로고침에도 가입 화면으로 돌아가지 않습니다. 일반 빌드에서는 초기 프로필이 비어 있는 기존 동작을 유지합니다.

화면 목록 방식은 사용자 요청에 따라 제거했습니다. 이미 구현된 앱 화면을 직접 배포하며, 현재 미구현인 홈·일정·정산 리포트 등의 준비 중 표시는 유지합니다. 화면별 확인용 직접 경로는 남아 있지만 기본 서비스 진입에는 노출하지 않습니다.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"ios": "expo start --ios",
"typecheck": "tsc --noEmit",
"export:ios": "expo export --platform ios",
"export:android": "expo export --platform android"
"export:android": "expo export --platform android",
"build:review": "node scripts/build-review.mjs"
},
"dependencies": {
"@expo/metro-runtime": "~58.0.15",
Expand Down
16 changes: 16 additions & 0 deletions scripts/build-review.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const cli = fileURLToPath(new URL('../node_modules/expo/bin/cli', import.meta.url));
const result = spawnSync(
process.execPath,
[cli, 'export', '--platform', 'web', ...process.argv.slice(2)],
{
stdio: 'inherit',
env: { ...process.env, EXPO_PUBLIC_TEAM_REVIEW: 'true' },
},
);
if (result.error) {
process.stderr.write(`${result.error.message}\n`);
process.exit(1);
}
process.exit(result.status ?? 1);
3 changes: 2 additions & 1 deletion src/app/(tabs)/income.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { CAN_PREVIEW } from '@/config/review';
import { useState } from 'react';
import { Tabs, useLocalSearchParams } from 'expo-router';
import { IncomeScreen } from '@/features/income/IncomeScreen';
import { incomePreviewState } from '@/features/income/previewData';
export default function Screen() {
const { preview } = useLocalSearchParams<{ preview?: string }>();
const [retried, setRetried] = useState(false);
const state = __DEV__
const state = CAN_PREVIEW
? incomePreviewState(retried ? 'normal' : preview)
: { status: 'unavailable' as const };
return (
Expand Down
3 changes: 2 additions & 1 deletion src/app/dev/components.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CAN_PREVIEW } from '@/config/review';
import { useState } from 'react';
import { Redirect } from 'expo-router';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
Expand All @@ -22,7 +23,7 @@ export default function ComponentsPreview() {
const [chip, setChip] = useState(false);
const [search, setSearch] = useState('은행을 검색하세요');

if (!__DEV__) return <Redirect href="/" />;
if (!CAN_PREVIEW) return <Redirect href="/" />;

return (
<ScrollView style={s.page} contentContainerStyle={s.content}>
Expand Down
5 changes: 3 additions & 2 deletions src/app/dev/income.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { CAN_PREVIEW } from '@/config/review';
import { useEffect } from 'react';
import { Redirect, useLocalSearchParams } from 'expo-router';
import { useOnboarding } from '@/features/onboarding/OnboardingProvider';
export default function IncomePreview() {
const { state } = useLocalSearchParams<{ state?: string }>();
const { profile, setProfile } = useOnboarding();
useEffect(() => {
if (__DEV__ && !profile.name) setProfile({ name: '미리보기', subjects: [] });
if (CAN_PREVIEW && !profile.name) setProfile({ name: '미리보기', subjects: [] });
}, [profile.name, setProfile]);
if (!__DEV__) return <Redirect href="/" />;
if (!CAN_PREVIEW) return <Redirect href="/" />;
if (!profile.name) return null;
return <Redirect href={{ pathname: '/income', params: { preview: state ?? 'normal' } }} />;
}
3 changes: 2 additions & 1 deletion src/app/dev/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { CAN_PREVIEW } from '@/config/review';
import { useRef } from 'react';
import { Redirect, useLocalSearchParams } from 'expo-router';
import ProfileScreen from '@/features/profile/ProfileScreen';
import type { SaveProfile } from '@/features/profile/profile';
export default function ProfilePreview() {
const { scenario } = useLocalSearchParams<{ scenario?: string }>();
const attempts = useRef(0);
if (!__DEV__) return <Redirect href="/profile" />;
if (!CAN_PREVIEW) return <Redirect href="/profile" />;
const save: SaveProfile = async (_, signal) => {
const attempt = ++attempts.current;
await new Promise<void>((resolve, reject) => {
Expand Down
10 changes: 7 additions & 3 deletions src/app/dev/push-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CAN_PREVIEW, IS_TEAM_REVIEW } from '@/config/review';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Redirect } from 'expo-router';
import { Redirect, useLocalSearchParams } from 'expo-router';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { ActionButton } from '@/components/ActionButton';
Expand All @@ -8,6 +9,7 @@ import { semantic as c } from '@/styles/colors';
import { typography as t } from '@/styles/typography';
export default function BannerPreview() {
const { show, dismiss } = usePushBanner();
const { controls } = useLocalSearchParams<{ controls?: string }>();
const ids = useRef<string[]>([]);
const [opened, setOpened] = useState(false);
const sample = useCallback(
Expand All @@ -23,13 +25,15 @@ export default function BannerPreview() {
[show],
);
useEffect(() => {
if (__DEV__) sample();
if (CAN_PREVIEW) sample();
const ownIds = ids.current;
return () => {
ownIds.forEach((id) => dismiss(id));
};
}, [sample, dismiss]);
if (!__DEV__) return <Redirect href="/home" />;
if (!CAN_PREVIEW) return <Redirect href="/home" />;
if (IS_TEAM_REVIEW && controls !== '1') return <SafeAreaView style={s.safe} />;

function clear() {
ids.current.forEach((id) => dismiss(id));
ids.current.length = 0;
Expand Down
3 changes: 2 additions & 1 deletion src/app/dev/terms.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CAN_PREVIEW } from '@/config/review';
import { useRef } from 'react';
import { Redirect, useLocalSearchParams } from 'expo-router';
import TermsScreen from '@/features/terms/TermsScreen';
Expand All @@ -6,7 +7,7 @@ import type { SaveTerms } from '@/features/terms/terms';
export default function TermsPreview() {
const { scenario } = useLocalSearchParams<{ scenario?: string }>();
const attempts = useRef(0);
if (!__DEV__) return <Redirect href="/terms" />;
if (!CAN_PREVIEW) return <Redirect href="/terms" />;
const save: SaveTerms = async (_, signal) => {
const attempt = ++attempts.current;
await new Promise<void>((resolve, reject) => {
Expand Down
6 changes: 6 additions & 0 deletions src/app/review.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Redirect } from 'expo-router';

// 이전 공유 주소도 앱의 기본 진입 화면으로 연결합니다.
export default function ReviewRedirect() {
return <Redirect href="/terms" />;
}
3 changes: 3 additions & 0 deletions src/config/review.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** 빌드 시에만 켜는 검토 전용 모드. 일반 배포에서는 예시 데이터·개발 경로를 열지 않습니다. */
export const IS_TEAM_REVIEW = process.env.EXPO_PUBLIC_TEAM_REVIEW === 'true';
export const CAN_PREVIEW = __DEV__ || IS_TEAM_REVIEW;
3 changes: 2 additions & 1 deletion src/features/makeup/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CAN_PREVIEW } from '@/config/review';
import { useState } from 'react';
import { Redirect, useLocalSearchParams } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand All @@ -15,7 +16,7 @@ export function MakeupPreview() {
}>();
const [visible, setVisible] = useState(true);
const [actions] = useState(() => createPreviewActions(params.scenario));
if (!__DEV__) return <Redirect href="/home" />;
if (!CAN_PREVIEW) return <Redirect href="/home" />;
const count = Math.min(3, Math.max(1, Number(params.count) || 1));
return (
<SafeAreaView style={s.preview}>
Expand Down
6 changes: 5 additions & 1 deletion src/features/onboarding/OnboardingProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IS_TEAM_REVIEW } from '@/config/review';
import { createContext, useContext, useState } from 'react';
import type { Dispatch, ReactNode, SetStateAction } from 'react';
type Profile = { name: string; subjects: string[] };
Expand All @@ -18,7 +19,10 @@ export function OnboardingProvider({ children }: { children: ReactNode }) {
const [agreed, setAgreed] = useState([false, false, false]);
const [name, setName] = useState('');
const [selected, setSelected] = useState<string[]>([]);
const [profile, setProfile] = useState<Profile>({ name: '', subjects: [] });
const [profile, setProfile] = useState<Profile>({
name: IS_TEAM_REVIEW ? '검토용' : '',
subjects: [],
});
const [editing, setEditing] = useState(false);
return (
<Context.Provider
Expand Down
3 changes: 2 additions & 1 deletion src/features/session-record/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { CAN_PREVIEW } from '@/config/review';
import { useRef, useState } from 'react';
import { Redirect, useLocalSearchParams, useRouter } from 'expo-router';
import { Modal, Text, View } from 'react-native';
Expand Down Expand Up @@ -55,7 +56,7 @@ export function SessionPreview({
const [makeupActions] = useState(() => createPreviewActions(params.scenario));
const recordAttempts = useRef(0);
const cancelAttempts = useRef(0);
if (!__DEV__) return <Redirect href="/home" />;
if (!CAN_PREVIEW) return <Redirect href="/home" />;
const variant = params.variant ?? 'first';
const empty = variant === 'empty';
const lesson: LessonContext = {
Expand Down
23 changes: 23 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"framework": null,
"installCommand": "npm ci",
"buildCommand": "npm run build",
"outputDirectory": "dist",
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Robots-Tag",
"value": "noindex, nofollow"
}
]
}
]
}
Loading