-
Notifications
You must be signed in to change notification settings - Fork 0
feat(phase-2): 스토어 심사 경로·EAS 스캐폴드 #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { Redirect, useRouter } from 'expo-router'; | ||
| import * as WebBrowser from 'expo-web-browser'; | ||
| import { Alert, Linking, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; | ||
|
|
||
| import { deleteOwnAccount } from '../lib/account'; | ||
| import { useAuth } from '../lib/auth'; | ||
|
|
||
| const privacyPolicyUrl = () => { | ||
| const base = process.env.EXPO_PUBLIC_WEB_URL?.replace(/\/$/, ''); | ||
| return base ? `${base}/privacy` : null; | ||
| }; | ||
|
|
||
| export default function SettingsScreen() { | ||
| const { session, loading, signOut } = useAuth(); | ||
| const router = useRouter(); | ||
|
|
||
| if (loading) { | ||
| return ( | ||
| <View style={[styles.container, styles.center]}> | ||
| <Text style={styles.muted}>불러오는 중…</Text> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| if (!session) { | ||
| return <Redirect href='/login' />; | ||
| } | ||
|
|
||
| const email = session.user.email ?? '(이메일 없음)'; | ||
| const policyUrl = privacyPolicyUrl(); | ||
|
|
||
| const openPrivacy = async () => { | ||
| if (!policyUrl) { | ||
| Alert.alert('설정 필요', 'EXPO_PUBLIC_WEB_URL 에 배포된 웹 URL을 넣어 주세요.'); | ||
| return; | ||
| } | ||
| await WebBrowser.openBrowserAsync(policyUrl); | ||
| }; | ||
|
|
||
| const handleDelete = () => { | ||
| Alert.alert('계정 삭제', '관심종목·타임라인 기록이 모두 삭제되며 되돌릴 수 없습니다. 계속할까요?', [ | ||
| { text: '취소', style: 'cancel' }, | ||
| { | ||
| text: '삭제', | ||
| style: 'destructive', | ||
| onPress: () => { | ||
| void (async () => { | ||
| try { | ||
| await deleteOwnAccount(); | ||
| router.replace('/login'); | ||
| } catch (err) { | ||
| Alert.alert('삭제 실패', err instanceof Error ? err.message : '다시 시도해 주세요.'); | ||
| } | ||
| })(); | ||
| }, | ||
| }, | ||
| ]); | ||
| }; | ||
|
|
||
| return ( | ||
| <ScrollView contentContainerStyle={styles.container}> | ||
| <Text style={styles.label}>로그인</Text> | ||
| <Text style={styles.email}>{email}</Text> | ||
|
|
||
| <View style={styles.section}> | ||
| <Text style={styles.sectionTitle}>세션</Text> | ||
| <Pressable | ||
| onPress={async () => { | ||
| try { | ||
| await signOut(); | ||
| router.replace('/login'); | ||
| } catch (err) { | ||
| Alert.alert('로그아웃 실패', err instanceof Error ? err.message : '다시 시도해 주세요.'); | ||
| } | ||
| }} | ||
| accessibilityRole='button' | ||
| style={styles.button} | ||
| > | ||
| <Text style={styles.buttonText}>로그아웃</Text> | ||
| </Pressable> | ||
| </View> | ||
|
|
||
| <View style={styles.section}> | ||
| <Text style={styles.sectionTitle}>법적 고지</Text> | ||
| <Pressable onPress={() => void openPrivacy()} accessibilityRole='button' style={styles.linkButton}> | ||
| <Text style={styles.linkText}>개인정보 처리방침</Text> | ||
| </Pressable> | ||
| {policyUrl ? ( | ||
| <Pressable onPress={() => void Linking.openURL(policyUrl)} accessibilityRole='link' style={styles.linkButton}> | ||
| <Text style={styles.linkTextMuted}>{policyUrl}</Text> | ||
| </Pressable> | ||
| ) : ( | ||
| <Text style={styles.muted}>스토어 제출 전 EXPO_PUBLIC_WEB_URL 을 설정하세요.</Text> | ||
| )} | ||
| </View> | ||
|
|
||
| <View style={[styles.section, styles.dangerSection]}> | ||
| <Text style={styles.dangerTitle}>계정 삭제</Text> | ||
| <Text style={styles.dangerBody}>관심종목·타임라인 기록이 모두 삭제되며 되돌릴 수 없습니다.</Text> | ||
| <Pressable onPress={handleDelete} accessibilityRole='button' style={styles.dangerButton}> | ||
| <Text style={styles.dangerButtonText}>계정 삭제</Text> | ||
| </Pressable> | ||
| </View> | ||
| </ScrollView> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { padding: 20, gap: 12, backgroundColor: '#fff', flexGrow: 1 }, | ||
| center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, | ||
| label: { fontSize: 12, color: '#666', textTransform: 'uppercase', letterSpacing: 1 }, | ||
| email: { fontSize: 16, fontWeight: '600', color: '#111' }, | ||
| section: { marginTop: 12, gap: 8, borderWidth: 1, borderColor: '#ddd', borderRadius: 8, padding: 14 }, | ||
| sectionTitle: { fontSize: 15, fontWeight: '600', color: '#111' }, | ||
| button: { | ||
| alignSelf: 'flex-start', | ||
| borderWidth: 1, | ||
| borderColor: '#ccc', | ||
| borderRadius: 8, | ||
| paddingHorizontal: 12, | ||
| paddingVertical: 8, | ||
| }, | ||
| buttonText: { fontSize: 14, color: '#333' }, | ||
| linkButton: { alignSelf: 'flex-start' }, | ||
| linkText: { fontSize: 14, color: '#2563eb', textDecorationLine: 'underline' }, | ||
| linkTextMuted: { fontSize: 12, color: '#666' }, | ||
| muted: { fontSize: 13, color: '#666', lineHeight: 18 }, | ||
| dangerSection: { borderColor: '#fecaca', backgroundColor: '#fef2f2' }, | ||
| dangerTitle: { fontSize: 15, fontWeight: '600', color: '#991b1b' }, | ||
| dangerBody: { fontSize: 13, color: '#991b1b', lineHeight: 18 }, | ||
| dangerButton: { | ||
| alignSelf: 'flex-start', | ||
| backgroundColor: '#b91c1c', | ||
| borderRadius: 8, | ||
| paddingHorizontal: 12, | ||
| paddingVertical: 8, | ||
| marginTop: 4, | ||
| }, | ||
| dangerButtonText: { fontSize: 14, fontWeight: '600', color: '#fff' }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "cli": { | ||
| "version": ">= 16.0.0", | ||
| "appVersionSource": "remote" | ||
| }, | ||
| "build": { | ||
| "development": { | ||
| "developmentClient": true, | ||
| "distribution": "internal" | ||
| }, | ||
| "preview": { | ||
| "distribution": "internal", | ||
| "android": { | ||
| "buildType": "apk" | ||
| } | ||
| }, | ||
| "production": { | ||
| "autoIncrement": true | ||
| } | ||
| }, | ||
| "submit": { | ||
| "production": {} | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { supabase } from './supabase'; | ||
|
|
||
| export const deleteOwnAccount = async (): Promise<void> => { | ||
| const { error } = await supabase.rpc('delete_own_account'); | ||
| if (error) throw error; | ||
|
|
||
| const { error: signOutError } = await supabase.auth.signOut(); | ||
| if (signOutError) throw signOutError; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,7 +40,9 @@ | |
| "typecheck": "tsc --noEmit", | ||
| "lint": "biome lint .", | ||
| "test": "jest", | ||
| "test:coverage": "jest --coverage" | ||
| "test:coverage": "jest --coverage", | ||
| "eas:build:ios": "eas build --platform ios --profile production", | ||
| "eas:build:android": "eas build --platform android --profile production" | ||
|
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
# 확인: 저장소가 EAS CLI를 로컬 의존성으로 선언하는지 검사합니다.
if ! rg -n '"eas-cli"\s*:' --glob 'package.json' --glob '!node_modules/**' .; then
echo "로컬 eas-cli 의존성이 없습니다."
exit 1
fi
# 확인: 빌드 스크립트와 문서의 실행 경로를 함께 출력합니다.
rg -n '"eas:build:(ios|android)"|pnpm dlx eas-cli' \
apps/mobile/package.json docs/phase-2-store.mdRepository: scs0209/ticker-journal Length of output: 182 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- applicable repository knowledge files ---'
find /tmp/coderabbit-repo-knowledge/scs0209-ticker-journal-479580f4 -type f -name '*.md' -maxdepth 3 -print
printf '%s\n' '--- package manifests and workspace/package-manager files ---'
git ls-files '*/package.json' 'package.json' 'pnpm-workspace.yaml' '.npmrc' 'package-lock.json' 'pnpm-lock.yaml' | sed -n '1,120p'
printf '%s\n' '--- mobile package manifest ---'
cat -n apps/mobile/package.json
printf '%s\n' '--- workspace/package-manager declarations ---'
for f in package.json pnpm-workspace.yaml .npmrc; do
if [ -f "$f" ]; then
echo "--- $f"
cat -n "$f"
fi
done
printf '%s\n' '--- EAS documentation references ---'
if [ -f docs/phase-2-store.md ]; then
rg -n -C 3 'eas|EAS' docs/phase-2-store.md
fiRepository: scs0209/ticker-journal Length of output: 5616 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- EAS CLI in lockfile ---'
rg -n -C 2 '(^|[/@ ])eas-cli([:`@/`]|$)|eas-cli@' pnpm-lock.yaml || true
printf '%s\n' '--- repository-owned EAS wrappers or declarations ---'
rg -n --hidden \
--glob '!node_modules/**' \
--glob '!pnpm-lock.yaml' \
--glob '!*.md' \
'(^|[^A-Za-z0-9_-])eas-cli([^A-Za-z0-9_-]|$)|(^|[^A-Za-z0-9_-])eas build([^A-Za-z0-9_-]|$)' \
. || trueRepository: scs0209/ticker-journal Length of output: 449 EAS CLI 실행 경로를 고정하세요.
🤖 Prompt for AI Agents |
||
| }, | ||
| "private": true | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
|
|
||
| test('개인정보 처리방침 페이지가 열린다', async ({ page }) => { | ||
| await page.goto('/privacy'); | ||
| await expect(page.getByRole('heading', { name: '개인정보 처리방침' })).toBeVisible(); | ||
| await expect(page.getByText(/계정 삭제/)).toBeVisible(); | ||
| }); | ||
|
|
||
| test('설정은 비로그인 시 로그인으로 보낸다', async ({ page }) => { | ||
| await page.goto('/settings'); | ||
| await expect(page).toHaveURL(/\/login/); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { APP_NAME } from '@ticker-journal/shared'; | ||
| import Link from 'next/link'; | ||
|
|
||
| export default function PrivacyPage() { | ||
| return ( | ||
| <main className='min-h-screen bg-zinc-50 text-zinc-900'> | ||
| <div className='mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-16'> | ||
| <div className='flex flex-col gap-2'> | ||
| <p className='text-xs uppercase tracking-[0.14em] text-zinc-500'>{APP_NAME}</p> | ||
| <h1 className='text-3xl font-semibold tracking-tight'>개인정보 처리방침</h1> | ||
| <p className='text-sm text-zinc-500'>최종 갱신: 2026-08-31</p> | ||
| </div> | ||
|
|
||
| <section className='flex flex-col gap-3 text-base leading-7 text-zinc-700'> | ||
| <h2 className='text-lg font-semibold text-zinc-900'>수집하는 정보</h2> | ||
| <p>{APP_NAME}은(는) 서비스 제공을 위해 아래 정보를 수집·저장합니다.</p> | ||
| <ul className='list-disc space-y-1 pl-5'> | ||
| <li>계정: 이메일 주소 (Supabase Auth)</li> | ||
| <li>사용자 콘텐츠: 관심종목, 메모·링크·매매 기록</li> | ||
| <li>기술 정보: 로그인 세션 토큰 (기기 로컬 저장)</li> | ||
| </ul> | ||
| </section> | ||
|
|
||
| <section className='flex flex-col gap-3 text-base leading-7 text-zinc-700'> | ||
| <h2 className='text-lg font-semibold text-zinc-900'>이용 목적</h2> | ||
| <ul className='list-disc space-y-1 pl-5'> | ||
| <li>동일 계정으로 모바일 입력·웹 검색을 연결</li> | ||
| <li>종목 타임라인 저장·조회·삭제</li> | ||
| <li>인증 및 보안 (RLS로 본인 데이터만 접근)</li> | ||
| </ul> | ||
| </section> | ||
|
|
||
| <section className='flex flex-col gap-3 text-base leading-7 text-zinc-700'> | ||
| <h2 className='text-lg font-semibold text-zinc-900'>보관·처리 위탁</h2> | ||
| <p> | ||
| 데이터는 Supabase(Postgres, Auth)에 저장됩니다. 차트는 TradingView embed(WebView)를 사용하며, 차트 제공자는 | ||
| 별도 정책이 적용될 수 있습니다. | ||
| </p> | ||
| </section> | ||
|
|
||
| <section className='flex flex-col gap-3 text-base leading-7 text-zinc-700'> | ||
| <h2 className='text-lg font-semibold text-zinc-900'>제3자 제공</h2> | ||
| <p>사용자 데이터를 판매하거나 광고 목적으로 제공하지 않습니다.</p> | ||
| </section> | ||
|
|
||
| <section className='flex flex-col gap-3 text-base leading-7 text-zinc-700'> | ||
| <h2 className='text-lg font-semibold text-zinc-900'>계정 삭제</h2> | ||
| <p> | ||
| 앱·웹 설정에서 계정을 삭제할 수 있습니다. 삭제 시 tickers·entries 등 사용자 데이터는 함께 제거되며 복구할 수 | ||
| 없습니다. | ||
| </p> | ||
| </section> | ||
|
|
||
| <section className='flex flex-col gap-3 text-base leading-7 text-zinc-700'> | ||
| <h2 className='text-lg font-semibold text-zinc-900'>문의</h2> | ||
| <p>개인정보 관련 문의: 레포 이슈 또는 프로젝트 maintainer 이메일로 연락해 주세요.</p> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 실제 문의 수단을 표시하세요.
🤖 Prompt for AI Agents |
||
| </section> | ||
|
|
||
| <Link href='/' className='text-sm text-zinc-600 underline hover:text-zinc-900'> | ||
| 홈으로 | ||
| </Link> | ||
| </div> | ||
| </main> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| 'use server'; | ||
|
|
||
| import { redirect } from 'next/navigation'; | ||
|
|
||
| import { createClient } from '@/lib/supabase/server'; | ||
|
|
||
| export const deleteAccount = async () => { | ||
| const supabase = await createClient(); | ||
| const { error } = await supabase.rpc('delete_own_account'); | ||
| if (error) { | ||
| throw new Error(error.message); | ||
| } | ||
|
|
||
| await supabase.auth.signOut(); | ||
| redirect('/'); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { redirect } from 'next/navigation'; | ||
|
|
||
| import { SettingsView } from '@/components/settings-view'; | ||
| import { createClient } from '@/lib/supabase/server'; | ||
|
|
||
| export default async function SettingsPage() { | ||
| const supabase = await createClient(); | ||
| const { | ||
| data: { user }, | ||
| } = await supabase.auth.getUser(); | ||
|
|
||
| if (!user?.email) { | ||
| redirect('/login?next=/settings'); | ||
| } | ||
|
|
||
| return <SettingsView email={user.email} />; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: scs0209/ticker-journal
Length of output: 5939
🏁 Script executed:
Repository: scs0209/ticker-journal
Length of output: 2005
🏁 Script executed:
Repository: scs0209/ticker-journal
Length of output: 8442
계정 삭제 성공과 로그아웃 오류를 분리하세요.
deleteOwnAccount는@supabase/supabase-js클라이언트의supabase.auth.signOut()오류를 RPC 성공 후에도 throw합니다. 그러면apps/mobile/app/settings.tsx의catch가삭제 실패를 표시하고router.replace('/login')을 실행하지 않습니다. 로그아웃 오류와 계정 삭제 성공을 분리하고, 삭제 완료 후 로그인 화면으로 이동하는 흐름을 보장하세요.🤖 Prompt for AI Agents