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: 2 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"plugins": ["expo-router"],
"web": {
"bundler": "metro",
"output": "single"
"output": "single",
"favicon": "./assets/images/favicon.png"
},
"ios": {
"supportsTablet": true
Expand Down
Binary file added assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions assets/images/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions docs/profile-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# T-03 프로필 입력

새 T-03 SVG 기준으로 프로필 화면을 분리했습니다.

- 공통 TextInput, Chip, ActionButton, Loading 및 디자인 토큰 재사용
- 이름: 앞뒤 공백 제거 기준 2~10자, 최대 입력 길이 10자. 빈 값·한 글자·공백만 입력 시 완료 비활성화
- 과목: 선택 사항, 복수 선택 및 해제 가능. 390px 시안에서는 한 줄 배치, 작은 화면에서는 줄바꿈
- 완료: 로컬 상태를 반영하고 기존 흐름대로 `/home` 이동
- 설정의 수정: 기존 값 표시, 저장 후 설정으로 복귀
- 저장 중: 공통 로딩 및 버튼 점 표시, 입력·과목 선택·중복 제출 차단
- 저장 실패: 입력 유지 및 재시도 안내. 완료 버튼으로 재시도
- API 미확정이므로 실제 서버 저장은 연결하지 않았습니다. `onSave`로 추후 확정된 API를 연결합니다.

## 개발 미리보기

- `/dev/profile`: 빈 입력 및 검증
- `/dev/profile?scenario=loading`: 김나연·영어 선택. 완료 시 3초간 저장 중 표시 후 홈 이동
- `/dev/profile?scenario=error`: 첫 저장 실패 후 완료 버튼 재시도 시 성공

시나리오의 지연·오류는 개발 경로에만 있으며 production은 실제 프로필 경로로 전환됩니다.
Binary file added docs/screenshots/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/terms.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/terms-consent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# T-02 약관 동의 구현

- `/terms`: 실제 가입 흐름의 약관 화면. API 미확정 상태로 서버 요청 없이 메모리의 동의 상태를 반영하고 `/profile`로 이동합니다.
- 동의 전 다음 버튼은 비활성화됩니다. 개별 동의와 전체 동의를 양방향으로 동기화합니다.
- 공통 `ActionButton`, `Loading`, 색상·간격·타이포그래피 토큰을 재사용합니다.
- 약관 원문은 제공되지 않아 보기 버튼에서 미등록 안내를 표시합니다. 실제 법적 동의 및 저장 구현이 완료된 상태는 아닙니다.
- `TermsScreen.onSave`는 확정된 API를 연결하기 위한 비동기 경계입니다. 입력은 화면 내부 모델이며 백엔드 요청 형식이 아닙니다. 약관 버전·동의 시각 등 요청 규격은 명세 확정 후 연결합니다.
- 저장 실패 시 선택 상태를 유지하고 오류 안내 및 다시 시도 버튼을 표시합니다. 중복 요청을 막고 화면 종료 시 AbortSignal로 취소를 전달합니다.

## 개발 중 미리보기

개발 서버에서만 접근할 수 있으며 production에서는 `/terms`로 이동합니다.

- `/dev/terms`: 정상 선택 및 저장 성공 (650ms 후 프로필 이동)
- `/dev/terms?scenario=loading`: 전체 선택 후 시작. 다음 버튼을 누르면 3초간 공통 로딩 표시 후 성공
- `/dev/terms?scenario=error`: 전체 선택 후 시작. 첫 제출은 실패, 다시 시도는 성공

대기와 실패는 미리보기 전용이며 실제 가입 화면에는 인위적인 네트워크 지연을 추가하지 않았습니다.

## 확인할 사항

- 정상/저장 중/오류 SVG 기준 구현. OS 상태바는 실제 시스템 표시를 사용하고 정적 시각·배터리를 그리지 않습니다.
- 미선택 상태 CTA는 필수 동의 차단을 위해 공통 비활성 스타일을 사용합니다.
- 약관 원문과 동의 저장 API는 후속 연결이 필요합니다.
38 changes: 37 additions & 1 deletion public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions src/app/dev/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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" />;
const save: SaveProfile = async (_, signal) => {
const attempt = ++attempts.current;
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(
() => {
signal.removeEventListener('abort', abort);
resolve();
},
scenario === 'loading' ? 3000 : 650,
);
function abort() {
clearTimeout(timer);
reject(new Error('Aborted'));
}
signal.addEventListener('abort', abort, { once: true });
if (signal.aborted) abort();
});
if (scenario === 'error' && attempt === 1) throw new Error('Preview save failure');
};
return (
<ProfileScreen
onSave={save}
initialProfile={
scenario === 'loading' || scenario === 'error'
? { name: '김나연', subjects: ['영어'] }
: undefined
}
/>
);
}
32 changes: 32 additions & 0 deletions src/app/dev/terms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useRef } from 'react';
import { Redirect, useLocalSearchParams } from 'expo-router';
import TermsScreen from '@/features/terms/TermsScreen';
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" />;
const save: SaveTerms = async (_, signal) => {
const attempt = ++attempts.current;
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(
() => {
signal.removeEventListener('abort', abort);
resolve();
},
scenario === 'loading' ? 3000 : 650,
);
function abort() {
clearTimeout(timer);
reject(new Error('Aborted'));
}
signal.addEventListener('abort', abort, { once: true });
if (signal.aborted) abort();
});
if (scenario === 'error' && attempt === 1) throw new Error('Preview save failure');
};
return (
<TermsScreen onSave={save} initialChecked={scenario === 'loading' || scenario === 'error'} />
);
}
7 changes: 4 additions & 3 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import OnboardingApp from '@/features/onboarding/OnboardingApp';
export default function Screen() {
return <OnboardingApp screen="welcome" />;
import { Redirect } from 'expo-router';

export default function Index() {
return <Redirect href="/terms" />;
}
4 changes: 2 additions & 2 deletions src/app/profile.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import OnboardingApp from '@/features/onboarding/OnboardingApp';
import ProfileScreen from '@/features/profile/ProfileScreen';
import { Redirect } from 'expo-router';
import { useOnboarding } from '@/features/onboarding/OnboardingProvider';
export default function Screen() {
const { agreed, editing, profile } = useOnboarding();
if (!agreed.every(Boolean) && !(editing && profile.name)) return <Redirect href="/terms" />;
return <OnboardingApp screen="profile" />;
return <ProfileScreen />;
}
2 changes: 1 addition & 1 deletion src/app/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { useOnboarding } from '@/features/onboarding/OnboardingProvider';
export default function Screen() {
const { profile } = useOnboarding();
if (!profile.name) return <Redirect href="/" />;
return <OnboardingApp screen="settings" />;
return <OnboardingApp />;
}
6 changes: 2 additions & 4 deletions src/app/terms.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
import OnboardingApp from '@/features/onboarding/OnboardingApp';
export default function Screen() {
return <OnboardingApp screen="terms" />;
}
import TermsScreen from '@/features/terms/TermsScreen';
export default TermsScreen;
Loading
Loading