diff --git a/app/_layout.tsx b/app/_layout.tsx
index e49a771..a408175 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -127,6 +127,7 @@ function AuthGate() {
const group = segmentList[0]
const screen = segmentList[1]
const inAuthGroup = group === '(auth)'
+ const inOAuthCallback = group === 'oauth'
const hasOnboardingToken =
typeof onboardingToken === 'string' && onboardingToken.trim().length > 0
const isLoginRoute = inAuthGroup && screen === 'login'
@@ -134,6 +135,10 @@ function AuthGate() {
const isOnboardingRoute = inAuthGroup && screen === 'onboarding'
const isMidSignupRoute = isAgreementRoute || isOnboardingRoute
+ if (inOAuthCallback) {
+ return
+ }
+
if (isAuthenticated && inAuthGroup) {
router.replace('/(tabs)')
return
@@ -145,11 +150,11 @@ function AuthGate() {
}
if (isLoginRoute || !inAuthGroup) {
- router.replace('/(auth)/agreement')
+ router.replace('/agreement')
return
}
- router.replace('/(auth)/agreement')
+ router.replace('/agreement')
return
}
@@ -203,6 +208,7 @@ function RootLayoutNav() {
+
{/* Works detail screen — header managed by Stack.Screen inside the screen */}
diff --git a/app/oauth/x.tsx b/app/oauth/x.tsx
new file mode 100644
index 0000000..e025cc3
--- /dev/null
+++ b/app/oauth/x.tsx
@@ -0,0 +1,105 @@
+import { useEffect, useRef } from 'react'
+import { ActivityIndicator, Alert, StyleSheet, Text, View } from 'react-native'
+import { useLocalSearchParams, useRouter } from 'expo-router'
+
+import { useXLogin } from '../../src/features/auth/hooks'
+import {
+ clearPendingXOAuthSession,
+ getPendingXOAuthSession,
+ X_OAUTH_CONFIG,
+} from '../../src/features/auth/lib/xOAuth'
+import { C, Typography } from '../../src/theme'
+
+export default function XOAuthCallbackScreen() {
+ const params = useLocalSearchParams<{
+ code?: string
+ state?: string
+ error?: string
+ error_description?: string
+ }>()
+ const router = useRouter()
+ const xLoginMutation = useXLogin()
+ const handledRef = useRef(false)
+
+ useEffect(() => {
+ if (handledRef.current) return
+ handledRef.current = true
+
+ const completeLogin = async () => {
+ const code = Array.isArray(params.code) ? params.code[0] : params.code
+ const state = Array.isArray(params.state) ? params.state[0] : params.state
+ const oauthError = Array.isArray(params.error)
+ ? params.error[0]
+ : params.error
+ const oauthErrorDescription = Array.isArray(params.error_description)
+ ? params.error_description[0]
+ : params.error_description
+
+ console.log('[X OAuth Callback] params:', {
+ hasCode: !!code,
+ state,
+ error: oauthError,
+ errorDescription: oauthErrorDescription,
+ })
+
+ if (oauthError) {
+ await clearPendingXOAuthSession()
+ Alert.alert('로그인 실패', 'X 로그인이 취소되었거나 실패했어요.')
+ router.replace('/(auth)/login')
+ return
+ }
+
+ if (!code || !state) {
+ await clearPendingXOAuthSession()
+ Alert.alert('로그인 실패', 'X 로그인 응답이 올바르지 않아요.')
+ router.replace('/(auth)/login')
+ return
+ }
+
+ const pendingSession = await getPendingXOAuthSession()
+
+ if (!pendingSession) {
+ Alert.alert('로그인 실패', 'X 로그인 세션이 만료되었어요. 다시 시도해주세요.')
+ router.replace('/(auth)/login')
+ return
+ }
+
+ if (pendingSession.state !== state) {
+ await clearPendingXOAuthSession()
+ Alert.alert('로그인 실패', 'X 로그인 검증에 실패했어요. 다시 시도해주세요.')
+ router.replace('/(auth)/login')
+ return
+ }
+
+ await clearPendingXOAuthSession()
+ xLoginMutation.mutate({
+ code,
+ redirectUri: X_OAUTH_CONFIG.redirectUri,
+ codeVerifier: pendingSession.codeVerifier,
+ })
+ }
+
+ void completeLogin()
+ }, [params, router, xLoginMutation])
+
+ return (
+
+
+ X 로그인 처리 중입니다.
+
+ )
+}
+
+const styles = StyleSheet.create({
+ screen: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 12,
+ backgroundColor: C.card,
+ },
+ text: {
+ ...Typography.body2Medium,
+ color: C.text,
+ },
+})
diff --git a/assets/common/cardshare/image-gallery-saved.svg b/assets/common/cardshare/image-gallery-saved.svg
new file mode 100644
index 0000000..92a7fbc
--- /dev/null
+++ b/assets/common/cardshare/image-gallery-saved.svg
@@ -0,0 +1,20 @@
+
diff --git a/assets/icons/common/comment-dropdown.svg b/assets/icons/common/comment-dropdown.svg
index 15a46b8..46c000d 100644
--- a/assets/icons/common/comment-dropdown.svg
+++ b/assets/icons/common/comment-dropdown.svg
@@ -1,4 +1,20 @@
-