Skip to content

Commit d153289

Browse files
authored
Refactor/14 (#17)
* feat: 푸시알림 서버 전송 * feat: 프로필 정보 업데이트 및 조회 api 적용 * feat: 로그인 세션관리 추가 * fix: 에러 해결 * fix: 로그인/회원가입 창 잘림 해결 * fix: UI 수정 * fix: 2026년 이전 데이터 조회 불가 * feat: textinput UI 개선 * chore: 버전 업데이트 1.0.5
1 parent b915917 commit d153289

File tree

9 files changed

+562
-39
lines changed

9 files changed

+562
-39
lines changed

app.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
99
// ===== App 기본 정보 =====
1010
name: '운세한장',
1111
slug: 'dailyfate',
12-
version: '1.0.4',
12+
version: '1.0.5',
1313

1414
// ===== 환경 변수 =====
1515
extra: {

app/index.tsx

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react';
2-
import { ActivityIndicator, Alert, View } from 'react-native';
2+
import { ActivityIndicator, Alert, Platform, View } from 'react-native';
33
import AsyncStorage from '@react-native-async-storage/async-storage';
44

55
import { useNavigation } from 'expo-router';
@@ -13,27 +13,44 @@ import SettingsSheet from '@/components/SettingsSheet';
1313
import LoginScreen from '@/components/LoginScreen';
1414
import { useAuth } from '@/providers/AuthProvider';
1515
import { registerForPushNotificationsAsync } from '@/services/pushNotifications';
16-
import { ProfileApiError, updateUserProfile } from '@/services/userProfileService';
16+
import { updatePushToken } from '@/services/pushTokenService';
17+
import {
18+
fetchUserProfile,
19+
ProfileApiError,
20+
updateUserProfile,
21+
} from '@/services/userProfileService';
1722

1823
const HAS_ONBOARDED_KEY = 'hasOnboarded';
1924
const USER_SETTINGS_KEY = 'userSettings';
25+
const HAS_LOGGED_IN_KEY = 'hasLoggedIn';
2026

2127
export default function Home() {
2228
const navigation = useNavigation();
2329

2430
const [bootLoading, setBootLoading] = useState(true);
2531
const [currentDate, setCurrentDate] = useState(new Date());
2632
const [hasOnboarded, setHasOnboarded] = useState(false);
33+
const [hasLoggedIn, setHasLoggedIn] = useState(false);
2734
const [userSettings, setUserSettings] = useState<UserSettings | null>(null);
2835
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
2936
const [needsProfileSetup, setNeedsProfileSetup] = useState(false);
3037
const [needsOnboarding, setNeedsOnboarding] = useState(false);
3138
const [profileSaving, setProfileSaving] = useState(false);
39+
const [profileLoading, setProfileLoading] = useState(false);
40+
const [forceLogin, setForceLogin] = useState(false);
41+
const [pushToken, setPushToken] = useState<string | null>(null);
3242

3343
const { isBootstrapping: authBootstrapping, isSignedIn, signOut } = useAuth();
3444

45+
const shouldShowLogin = forceLogin || (!isSignedIn && !hasLoggedIn);
46+
3547
const canLoadFortune =
36-
isSignedIn && !!userSettings && !needsProfileSetup && (!needsOnboarding || hasOnboarded);
48+
!shouldShowLogin &&
49+
(isSignedIn || hasLoggedIn) &&
50+
!!userSettings &&
51+
!needsProfileSetup &&
52+
!profileLoading &&
53+
(!needsOnboarding || hasOnboarded);
3754

3855
const {
3956
fortune,
@@ -45,13 +62,15 @@ export default function Home() {
4562
useEffect(() => {
4663
const load = async () => {
4764
try {
48-
const [onboardedRaw, settingsRaw] = await Promise.all([
65+
const [onboardedRaw, settingsRaw, loggedInRaw] = await Promise.all([
4966
AsyncStorage.getItem(HAS_ONBOARDED_KEY),
5067
AsyncStorage.getItem(USER_SETTINGS_KEY),
68+
AsyncStorage.getItem(HAS_LOGGED_IN_KEY),
5169
]);
5270

5371
if (onboardedRaw === 'true') setHasOnboarded(true);
5472
if (settingsRaw) setUserSettings(JSON.parse(settingsRaw));
73+
if (loggedInRaw === 'true') setHasLoggedIn(true);
5574
} catch (error) {
5675
console.warn('Failed to load saved state', error);
5776
} finally {
@@ -63,27 +82,101 @@ export default function Home() {
6382
}, []);
6483

6584
useEffect(() => {
85+
let active = true;
86+
6687
const registerPushToken = async () => {
6788
try {
6889
const token = await registerForPushNotificationsAsync();
6990
if (token) {
7091
console.log('Expo push token:', token);
92+
if (active) {
93+
setPushToken(token);
94+
}
7195
}
7296
} catch (error) {
7397
console.warn('Failed to register for push notifications', error);
7498
}
7599
};
76100

77101
registerPushToken();
102+
103+
return () => {
104+
active = false;
105+
};
78106
}, []);
79107

108+
useEffect(() => {
109+
if (!isSignedIn || !pushToken) return;
110+
111+
const platform = Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : null;
112+
if (!platform) return;
113+
114+
const syncPushToken = async () => {
115+
try {
116+
const result = await updatePushToken({ pushToken, platform });
117+
console.log('Push token synced:', result);
118+
} catch (error) {
119+
console.warn('Failed to sync push token', error);
120+
}
121+
};
122+
123+
syncPushToken();
124+
}, [isSignedIn, pushToken]);
125+
80126
useEffect(() => {
81127
if (isSignedIn) return;
82128
setIsSettingsOpen(false);
83129
setNeedsProfileSetup(false);
84130
setNeedsOnboarding(false);
131+
setProfileLoading(false);
85132
}, [isSignedIn]);
86133

134+
const requireLogin = useCallback(() => {
135+
setForceLogin(true);
136+
setIsSettingsOpen(false);
137+
void signOut();
138+
}, [signOut]);
139+
140+
useEffect(() => {
141+
if (!isSignedIn) return;
142+
143+
let active = true;
144+
setProfileLoading(true);
145+
146+
const loadProfile = async () => {
147+
try {
148+
const profile = await fetchUserProfile();
149+
if (!active) return;
150+
if (profile) {
151+
setNeedsProfileSetup(false);
152+
setUserSettings(profile);
153+
await AsyncStorage.setItem(USER_SETTINGS_KEY, JSON.stringify(profile));
154+
} else {
155+
setNeedsProfileSetup(true);
156+
setUserSettings(null);
157+
await AsyncStorage.removeItem(USER_SETTINGS_KEY);
158+
}
159+
} catch (error) {
160+
if (!active) return;
161+
if (error instanceof ProfileApiError && error.status === 401) {
162+
Alert.alert('로그인이 필요합니다', '다시 로그인해주세요.');
163+
requireLogin();
164+
return;
165+
}
166+
const message = error instanceof Error ? error.message : '프로필을 불러오지 못했어요.';
167+
Alert.alert('프로필 조회 실패', message);
168+
} finally {
169+
if (active) setProfileLoading(false);
170+
}
171+
};
172+
173+
loadProfile();
174+
175+
return () => {
176+
active = false;
177+
};
178+
}, [isSignedIn, requireLogin]);
179+
87180
// Header: show only when main screen is active
88181
useLayoutEffect(() => {
89182
navigation.setOptions({ headerShown: false });
@@ -94,19 +187,29 @@ export default function Home() {
94187
await AsyncStorage.setItem(HAS_ONBOARDED_KEY, 'true');
95188
};
96189

190+
const persistHasLoggedIn = useCallback(async () => {
191+
setHasLoggedIn(true);
192+
await AsyncStorage.setItem(HAS_LOGGED_IN_KEY, 'true');
193+
}, []);
194+
97195
const persistUserSettings = async (settings: UserSettings) => {
98196
setUserSettings(settings);
99197
await AsyncStorage.setItem(USER_SETTINGS_KEY, JSON.stringify(settings));
100198
};
101199

102200
const handleSignUpSuccess = useCallback(() => {
201+
void persistHasLoggedIn();
202+
setForceLogin(false);
103203
setNeedsProfileSetup(true);
104204
setNeedsOnboarding(true);
105-
}, []);
205+
}, [persistHasLoggedIn]);
106206

107207
const handleSignInSuccess = useCallback(() => {
108-
setNeedsProfileSetup(true);
109-
}, []);
208+
void persistHasLoggedIn();
209+
setForceLogin(false);
210+
setNeedsProfileSetup(false);
211+
setProfileLoading(true);
212+
}, [persistHasLoggedIn]);
110213

111214
const handleOnboardingComplete = () => {
112215
setNeedsOnboarding(false);
@@ -123,7 +226,7 @@ export default function Home() {
123226
} catch (error) {
124227
if (error instanceof ProfileApiError && error.status === 401) {
125228
Alert.alert('로그인이 필요합니다', '다시 로그인해주세요.');
126-
await signOut();
229+
requireLogin();
127230
return;
128231
}
129232
const message = error instanceof Error ? error.message : '프로필을 저장하지 못했어요.';
@@ -136,12 +239,12 @@ export default function Home() {
136239
useEffect(() => {
137240
if (!error) return;
138241
if (error.status === 401) {
139-
signOut();
242+
requireLogin();
140243
Alert.alert('로그인이 필요합니다', '다시 로그인해주세요.');
141244
return;
142245
}
143246
Alert.alert('운세를 불러오지 못했어요', error.message);
144-
}, [error, signOut]);
247+
}, [error, requireLogin]);
145248

146249
const handleNextDay = useCallback(() => {
147250
setCurrentDate((prev) => {
@@ -159,15 +262,15 @@ export default function Home() {
159262
});
160263
}, []);
161264

162-
if (bootLoading || authBootstrapping) {
265+
if (bootLoading || authBootstrapping || (profileLoading && !forceLogin)) {
163266
return (
164267
<View className="flex-1 items-center justify-center bg-stone-200">
165268
<ActivityIndicator size="large" color="#191F28" />
166269
</View>
167270
);
168271
}
169272

170-
if (!isSignedIn) {
273+
if (shouldShowLogin) {
171274
return (
172275
<LoginScreen
173276
onSignUpSuccess={handleSignUpSuccess}
@@ -216,6 +319,8 @@ export default function Home() {
216319
await persistUserSettings(nextSettings);
217320
setIsSettingsOpen(false);
218321
}}
322+
onLogout={requireLogin}
323+
onUnauthorized={requireLogin}
219324
/>
220325
)}
221326
</View>

src/components/CalendarPage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const clamp = (value: number, min: number, max: number) => Math.min(Math.max(val
3030
const dayKey = (value: Date) =>
3131
value.getFullYear() * 10000 + (value.getMonth() + 1) * 100 + value.getDate();
3232
const isBeforeDay = (left: Date, right: Date) => dayKey(left) < dayKey(right);
33+
const MIN_FORTUNE_DATE = new Date(2026, 0, 1);
3334
const EDGE_TRIGGER_RATIO = 0.22;
3435
const TAP_MOVE_TOLERANCE = 10;
3536
const TAP_MAX_DURATION_MS = 280;
@@ -88,6 +89,7 @@ const CalendarPage: React.FC<Props> = ({
8889
};
8990
}, [bounceAnim]);
9091

92+
const canGoPrev = () => isBeforeDay(MIN_FORTUNE_DATE, date);
9193
const canGoNext = () => isBeforeDay(date, new Date());
9294

9395
const handleTear = () => {
@@ -184,7 +186,7 @@ const CalendarPage: React.FC<Props> = ({
184186
});
185187

186188
const handlePrevTap = () => {
187-
if (isTearing) return;
189+
if (isTearing || !canGoPrev()) return;
188190
onPrev();
189191
};
190192

src/components/LoginScreen.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ interface Props {
1212
onSignInSuccess?: () => void;
1313
}
1414

15+
const INPUT_STYLE = {
16+
includeFontPadding: false,
17+
minHeight: 64,
18+
};
19+
1520
const LoginScreen: React.FC<Props> = ({ onSignUpSuccess, onSignInSuccess }) => {
1621
const { signIn, signUp, isLoading, error, clearError } = useAuth();
1722
const [mode, setMode] = useState<AuthMode>('signIn');
@@ -126,8 +131,10 @@ const LoginScreen: React.FC<Props> = ({ onSignUpSuccess, onSignInSuccess }) => {
126131
placeholderTextColor="#9ca3af"
127132
autoCapitalize="none"
128133
autoCorrect={false}
129-
className="rounded-xl bg-gray-100 px-4 py-5 text-xl text-gray-900"
134+
textAlignVertical="center"
135+
className="rounded-xl bg-gray-100 px-4 text-2xl text-gray-900"
130136
textContentType="username"
137+
style={INPUT_STYLE}
131138
/>
132139
</View>
133140
<View className="gap-4">
@@ -138,8 +145,10 @@ const LoginScreen: React.FC<Props> = ({ onSignUpSuccess, onSignInSuccess }) => {
138145
placeholder="⦁⦁⦁⦁⦁⦁⦁⦁⦁"
139146
placeholderTextColor="#9ca3af"
140147
secureTextEntry
141-
className="rounded-xl bg-gray-100 px-4 py-5 text-xl text-gray-900"
148+
textAlignVertical="center"
149+
className="rounded-xl bg-gray-100 px-4 text-2xl text-gray-900"
142150
textContentType="password"
151+
style={INPUT_STYLE}
143152
/>
144153
</View>
145154
</View>

0 commit comments

Comments
 (0)