Skip to content
Open
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
15 changes: 8 additions & 7 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
"slug": "oba_frontend",
"version": "1.0.0",
"orientation": "portrait",
"platforms": ["ios", "android", "web"],
"platforms": [
"ios",
"android",
"web"
],
"scheme": "myapp",

"ios": {
"supportsTablet": true
},

"android": {
"adaptiveIcon": {
"backgroundColor": "#ffffff",
Expand All @@ -19,12 +21,10 @@
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},

"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},

"plugins": [
"expo-router",
[
Expand All @@ -38,9 +38,10 @@
"backgroundColor": "#000000"
}
}
]
],
"expo-web-browser",
"expo-secure-store"
],

"experiments": {
"typedRoutes": true,
"reactCompiler": true
Expand Down
6 changes: 4 additions & 2 deletions app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// oba_frontend/app/(auth)/_layout.tsx
import { Stack } from "expo-router";
import AppBackground from "../components/AppBackground"; // 경로 확인 필요
// ✅ 여기는 한 단계 위로 올라가야 하므로 "../" 가 맞습니다.
import AppBackground from "../components/AppBackground";

export default function AuthLayout() {
return (
<>
<AppBackground />
<Stack screenOptions={{ headerShown: false }} />
<Stack screenOptions={{ headerShown: false, contentStyle: { backgroundColor: 'transparent' } }} />
</>
);
}
87 changes: 64 additions & 23 deletions app/(auth)/login.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,87 @@
import { View, Text, TouchableOpacity, Image, StyleSheet, useWindowDimensions } from "react-native";
// oba_frontend/app/(auth)/login.tsx

import { View, Text, TouchableOpacity, Image, StyleSheet, useWindowDimensions, Alert } from "react-native";
import { useRouter } from "expo-router";
import * as WebBrowser from "expo-web-browser";
import * as Linking from "expo-linking";
import * as SecureStore from "expo-secure-store";

WebBrowser.maybeCompleteAuthSession();

export default function Login() {
const { width, height } = useWindowDimensions();
const router = useRouter();

const BACKEND_URL = "http://dev.onebitearticle.com:9000";
const REDIRECT_URI = "exp://192.168.219.101:8081/--/oauth/callback";
Comment on lines +15 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

하드코딩된 URL과 IP 주소는 환경 설정으로 분리해야 합니다.

BACKEND_URLREDIRECT_URI에 하드코딩된 IP 주소(192.168.219.101)는 다른 개발 환경이나 기기에서 작동하지 않습니다. 환경 변수 또는 설정 파일로 분리하는 것을 권장합니다.

🔧 환경 변수 사용 예시
-  const BACKEND_URL = "http://dev.onebitearticle.com:9000";
-  const REDIRECT_URI = "exp://192.168.219.101:8081/--/oauth/callback"; 
+  const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || "http://dev.onebitearticle.com:9000";
+  const REDIRECT_URI = Linking.createURL("oauth/callback");

Linking.createURL을 사용하면 현재 환경에 맞는 redirect URI를 자동으로 생성합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const BACKEND_URL = "http://dev.onebitearticle.com:9000";
const REDIRECT_URI = "exp://192.168.219.101:8081/--/oauth/callback";
const BACKEND_URL = process.env.EXPO_PUBLIC_BACKEND_URL || "http://dev.onebitearticle.com:9000";
const REDIRECT_URI = Linking.createURL("oauth/callback");
🤖 Prompt for AI Agents
In `@app/`(auth)/login.tsx around lines 15 - 16, BACKEND_URL and REDIRECT_URI are
hardcoded; move them to environment/config and use a runtime-generated redirect
URI: replace the constant BACKEND_URL with a value read from env/config (e.g.,
process.env.BACKEND_URL or app config) and replace REDIRECT_URI with a call to
Linking.createURL(...) (or equivalent environment-aware helper) so the redirect
is generated per environment; update any usages of BACKEND_URL and REDIRECT_URI
in this module to read from the new env/config values and remove the hardcoded
IP.


const handleLogin = async (provider: string) => {
try {
const authUrl = `${BACKEND_URL}/oauth2/authorization/${provider.toLowerCase()}`;
console.log(`📡 [Login] 시작: ${authUrl}`);

// 브라우저 열기
const result = await WebBrowser.openAuthSessionAsync(authUrl, REDIRECT_URI);

if (result.type === "success" && result.url) {
console.log("🔗 [WebBrowser] 결과 URL:", result.url);

const { queryParams } = Linking.parse(result.url);

// 🚨 [수정 완료] 로그에 찍힌 이름 그대로 사용!
const accessToken = queryParams?.access_token;
const refreshToken = queryParams?.refresh_token;

if (accessToken) {
console.log("✅ [Login] 토큰 획득 성공!");

// 문자열인지 확인 후 저장 (배열일 경우 첫 번째 요소 사용)
const accessStr = Array.isArray(accessToken) ? accessToken[0] : accessToken;
const refreshStr = Array.isArray(refreshToken) ? refreshToken[0] : refreshToken;

await SecureStore.setItemAsync("accessToken", accessStr);
if (refreshStr) {
await SecureStore.setItemAsync("refreshToken", refreshStr);
}

Alert.alert("환영합니다!", "성공적으로 로그인되었습니다.", [
{ text: "시작하기", onPress: () => router.replace("/(tabs)") },
]);
} else {
console.log("⚠️ 토큰이 없습니다. 파라미터:", queryParams);
}
Comment on lines +50 to +52
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

토큰 획득 실패 시 사용자에게 피드백이 없습니다.

result.type === "success"이지만 토큰이 없는 경우, 콘솔 로그만 남기고 사용자에게 알림이 없어 로그인이 실패한 것처럼 보일 수 있습니다.

🐛 사용자 피드백 추가
        } else {
          console.log("⚠️ 토큰이 없습니다. 파라미터:", queryParams);
+         Alert.alert("로그인 실패", "인증 정보를 받아오지 못했습니다. 다시 시도해주세요.");
        }
🤖 Prompt for AI Agents
In `@app/`(auth)/login.tsx around lines 50 - 52, The code path in
app/(auth)/login.tsx that handles result.type === "success" falls back to only
console.log when no token is present (console.log("⚠️ 토큰이 없습니다. 파라미터:",
queryParams)), leaving users without feedback; update the success-but-no-token
branch to show a clear user-facing notification (e.g., via existing toast/error
UI or an error banner) and optionally log the diagnostic info for debugging,
referencing the same check that inspects result.type and queryParams in the
login handler so users see a message like "로그인에 실패했습니다: 토큰을 받지 못했습니다" and
developers still get the console/error details.

} else {
console.log("❌ [Login] 취소됨/실패:", result.type);
}

} catch (error) {
console.error("❌ 로그인 에러:", error);
Alert.alert("오류", "로그인 중 문제가 발생했습니다.");
}
};

return (
<View style={styles.container}>
{/* 로고 / 캐릭터 */}
<Image
source={require("../../assets/knight/hand.png")}
style={[{ width: width * 0.55, height: height * 0.23, marginBottom: 16 }]}
resizeMode="contain"
/>

{/* 타이틀 */}
<Text style={styles.title}>한입기사</Text>
<Text style={styles.subtitle}>One Bite Article</Text>

{/* 소셜 로그인 버튼 영역 */}
<View style={styles.btnWrap}>
<TouchableOpacity style={[styles.btn, styles.google]}>
<TouchableOpacity style={[styles.btn, styles.google]} onPress={() => handleLogin("google")}>
<Image source={require("../../assets/icons/google.png")} style={styles.icon} />
<Text style={styles.btnText}>구글로 로그인</Text>
</TouchableOpacity>

<TouchableOpacity style={[styles.btn, styles.kakao]}>
<TouchableOpacity style={[styles.btn, styles.kakao]} onPress={() => handleLogin("kakao")}>
<Image source={require("../../assets/icons/kakao-talk.png")} style={styles.icon} />
<Text style={[styles.btnText, { color: "#3B1E1E" }]}>카카오로 로그인</Text>
</TouchableOpacity>

<TouchableOpacity style={[styles.btn, styles.naver]}>
<TouchableOpacity style={[styles.btn, styles.naver]} onPress={() => handleLogin("naver")}>
<Image source={require("../../assets/icons/naver.png")} style={styles.icon} />
<Text style={styles.btnText}>네이버로 로그인</Text>
</TouchableOpacity>
Expand All @@ -37,24 +91,11 @@ export default function Login() {
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 16,
},
container: { flex: 1, justifyContent: "center", alignItems: "center", paddingHorizontal: 16 },
title: { fontSize: 32, fontWeight: "800", color: "#333" },
subtitle: { fontSize: 16, color: "#666", marginTop: 4, marginBottom: 42 },
btnWrap: { width: "85%", gap: 14, alignItems: "center" },
btn: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 14,
borderRadius: 12,
gap: 10,
justifyContent: "center",
width: "100%",
},
btn: { flexDirection: "row", alignItems: "center", paddingVertical: 14, borderRadius: 12, gap: 10, justifyContent: "center", width: "100%" },
icon: { width: 20, height: 20, resizeMode: "contain" },
google: { backgroundColor: "#FFF", borderWidth: 1, borderColor: "#DDD" },
kakao: { backgroundColor: "#FEE500" },
Expand Down
54 changes: 15 additions & 39 deletions app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,20 @@
// oba_frontend/app/_layout.tsx
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { View } from "react-native";
import "react-native-reanimated";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
// oba_frontend/app/(tabs)/_layout.tsx

// ✅ 수정됨: 올바른 경로로 변경 (./article/components -> ./components)
import AppBackground from "../components/AppBackground";
const MyTheme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
background: "transparent",
},
};
import { Tabs } from "expo-router";
import React from "react";

export default function RootLayout() {
export default function TabLayout() {
return (
<SafeAreaProvider>
<ThemeProvider value={MyTheme}>
<SafeAreaView style={{ flex: 1, backgroundColor: "transparent" }}>
<View style={{ flex: 1, backgroundColor: "transparent" }}>
{/* 배경 컴포넌트 적용 */}
<AppBackground />

<View style={{ flex: 1, backgroundColor: "transparent" }}>
<Stack screenOptions={{ headerShown: false }}>
{/* 탭 화면 */}
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
{/* 로그인(인증) 화면 */}
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
{/* 기사 상세 화면 */}
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
</Stack>
</View>
</View>
</SafeAreaView>
<StatusBar style="auto" />
</ThemeProvider>
</SafeAreaProvider>
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: { display: 'none' }
}}
>
<Tabs.Screen name="index" options={{ title: "홈" }} />
<Tabs.Screen name="wrongArticles/index" options={{ title: "오답노트" }} />
<Tabs.Screen name="report/index" options={{ title: "리포트" }} />
<Tabs.Screen name="my/index" options={{ title: "마이" }} />
</Tabs>
);
}
Loading