diff --git a/app.json b/app.json
index 31ded9c..1e26c50 100644
--- a/app.json
+++ b/app.json
@@ -29,6 +29,17 @@
"backgroundColor": "#6C5CE7"
},
"intentFilters": [
+ {
+ "action": "VIEW",
+ "data": [
+ {
+ "scheme": "intelfit",
+ "host": "*",
+ "pathPrefix": "/auth/kakao"
+ }
+ ],
+ "category": ["BROWSABLE", "DEFAULT"]
+ },
{
"action": "VIEW",
"data": [
diff --git a/src/screens/auth/KakaoOnboardingScreen.tsx b/src/screens/auth/KakaoOnboardingScreen.tsx
index 0f15f32..590a1c1 100644
--- a/src/screens/auth/KakaoOnboardingScreen.tsx
+++ b/src/screens/auth/KakaoOnboardingScreen.tsx
@@ -11,6 +11,7 @@ import {
Alert,
ActivityIndicator,
Modal,
+ SafeAreaView,
} from "react-native";
import { Picker } from "@react-native-picker/picker";
import { authAPI } from "../../services";
@@ -43,6 +44,15 @@ const experienceLevelOptions = [
{ label: "고급자", value: "ADVANCED" },
];
+const healthConcernOptions = [
+ { label: "의지 부족", value: "WILLPOWER" },
+ { label: "근육의 자극", value: "MUSCLE_STIMULATION" },
+ { label: "루틴 짜기 어려움", value: "ROUTINE_DIFFICULTY" },
+ { label: "올바른 운동 자세", value: "CORRECT_FORM" },
+ { label: "식단 관리", value: "DIET_MANAGEMENT" },
+ { label: "기타", value: "OTHER" },
+];
+
const KakaoOnboardingScreen = ({ navigation }: any) => {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
@@ -70,6 +80,7 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
const [healthGoalModalVisible, setHealthGoalModalVisible] = useState(false);
const [experienceLevelModalVisible, setExperienceLevelModalVisible] =
useState(false);
+ const [healthConcernModalVisible, setHealthConcernModalVisible] = useState(false);
const handleChange = (name: string, value: string) => {
setFormData((prev) => ({ ...prev, [name]: value }));
@@ -205,11 +216,15 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
};
return (
-
-
+
+
+
신체정보를 입력해주세요
@@ -318,12 +333,14 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
}}
style={styles.modalPicker}
itemStyle={styles.pickerItemStyle}
+ dropdownIconColor="#ffffff"
>
{generateYearOptions().map((year) => (
))}
@@ -349,12 +366,14 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
}}
style={styles.modalPicker}
itemStyle={styles.pickerItemStyle}
+ dropdownIconColor="#ffffff"
>
{generateMonthOptions().map((month) => (
))}
@@ -368,6 +387,7 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
}}
style={styles.modalPicker}
itemStyle={styles.pickerItemStyle}
+ dropdownIconColor="#ffffff"
>
{(() => {
if (tempPickerValue.year && tempPickerValue.month) {
@@ -401,6 +421,7 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
key={day}
label={String(day).padStart(2, "0")}
value={String(day)}
+ color={Platform.OS === "android" ? "#ffffff" : undefined}
/>
))}
@@ -727,23 +748,23 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
- {/* 운동 경험 수준 (선택) */}
+ {/* 헬스 고민 (선택) */}
setExperienceLevelModalVisible(true)}
+ onPress={() => setHealthConcernModalVisible(true)}
>
opt.value === formData.experienceLevel
+ formData.fitnessConcerns
+ ? healthConcernOptions.find(
+ (opt) => opt.value === formData.fitnessConcerns
)?.label || ""
: ""
}
- placeholder="운동 경험 수준 (선택)"
+ placeholder="헬스 고민"
placeholderTextColor="rgba(255, 255, 255, 0.7)"
editable={false}
pointerEvents="none"
@@ -751,17 +772,17 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
- {/* 운동 경험 수준 선택 모달 */}
+ {/* 헬스 고민 선택 모달 */}
setExperienceLevelModalVisible(false)}
+ onRequestClose={() => setHealthConcernModalVisible(false)}
>
setExperienceLevelModalVisible(false)}
+ onPress={() => setHealthConcernModalVisible(false)}
>
{
>
setExperienceLevelModalVisible(false)}
+ onPress={() => setHealthConcernModalVisible(false)}
>
취소
- 운동 경험 수준 선택
+ 헬스 고민 선택
- {experienceLevelOptions.map((option) => (
+ {healthConcernOptions.map((option) => (
{
- handleChange("experienceLevel", option.value);
- setExperienceLevelModalVisible(false);
+ handleChange("fitnessConcerns", option.value);
+ setHealthConcernModalVisible(false);
}}
>
@@ -808,18 +829,6 @@ const KakaoOnboardingScreen = ({ navigation }: any) => {
-
- {/* 헬스 고민 (선택) */}
-
- handleChange("fitnessConcerns", text)}
- placeholderTextColor="rgba(255, 255, 255, 0.7)"
- multiline
- />
-
{
)}
-
+
+
);
};
@@ -843,10 +853,13 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: "#252525",
},
+ keyboardView: {
+ flex: 1,
+ },
scrollContent: {
flexGrow: 1,
paddingHorizontal: 20,
- paddingTop: 60,
+ paddingTop: Platform.OS === "ios" ? 20 : 40,
paddingBottom: 40,
},
header: {
@@ -886,6 +899,11 @@ const styles = StyleSheet.create({
fontWeight: "400",
color: "#ffffff",
},
+ textAreaInput: {
+ height: 120,
+ paddingTop: 16,
+ paddingBottom: 16,
+ },
birthDateButtonContainer: {
width: "100%",
},
@@ -962,9 +980,12 @@ const styles = StyleSheet.create({
modalPicker: {
width: "100%",
height: 150,
+ backgroundColor: "#252525",
},
pickerItemStyle: {
color: "#ffffff",
+ fontSize: 18,
+ fontWeight: "400",
},
genderOptionContainer: {
paddingHorizontal: 20,
diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx
index 36ced02..6419db2 100644
--- a/src/screens/auth/LoginScreen.tsx
+++ b/src/screens/auth/LoginScreen.tsx
@@ -10,9 +10,12 @@ import {
ScrollView,
Alert,
ActivityIndicator,
+ Modal,
+ SafeAreaView,
} from "react-native";
import * as Linking from "expo-linking";
import * as WebBrowser from "expo-web-browser";
+import { WebView } from "react-native-webview";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { colors } from "../../theme/colors";
import { authAPI } from "../../services";
@@ -39,6 +42,8 @@ const LoginScreen = ({ navigation }: any) => {
password?: string;
}>({});
const [loading, setLoading] = useState(false);
+ const [showKakaoWebView, setShowKakaoWebView] = useState(false);
+ const [kakaoLoginUrl, setKakaoLoginUrl] = useState("");
const handleChange = (name: string, value: string) => {
setFormData((prev) => ({
@@ -107,7 +112,20 @@ const LoginScreen = ({ navigation }: any) => {
console.log("🔗 [카카오 로그인] 딥링크 처리 시작:", url);
console.log("🔗 [카카오 로그인] 플랫폼:", Platform.OS);
- const parsed = Linking.parse(url);
+ // 카카오 네이티브 스킴(kakaob46c5ece88946636902899138451ac5e://oauth)을 intelfit://auth/kakao로 변환
+ let processedUrl = url;
+ if (url.includes("kakaob46c5ece88946636902899138451ac5e://oauth")) {
+ // 카카오 스킴에서 쿼리 파라미터 추출
+ const urlObj = new URL(url.replace("kakaob46c5ece88946636902899138451ac5e", "http"));
+ const params = urlObj.searchParams;
+ const code = params.get("code");
+ if (code) {
+ processedUrl = `intelfit://auth/kakao?code=${code}`;
+ console.log("🔗 [카카오 로그인] 카카오 스킴을 intelfit 스킴으로 변환:", processedUrl);
+ }
+ }
+
+ const parsed = Linking.parse(processedUrl);
const code = parsed.queryParams?.code as string | undefined;
const accessToken = parsed.queryParams?.accessToken as string | undefined;
const refreshToken = parsed.queryParams?.refreshToken as
@@ -251,9 +269,15 @@ const LoginScreen = ({ navigation }: any) => {
const checkInitialUrl = async () => {
try {
const initialUrl = await Linking.getInitialURL();
- if (initialUrl && initialUrl.includes("intelfit://auth/kakao")) {
- console.log("🔗 [카카오 로그인] 초기 딥링크 발견:", initialUrl);
- handleKakaoDeepLink(initialUrl);
+ if (initialUrl) {
+ // intelfit://auth/kakao 또는 kakaob46c5ece88946636902899138451ac5e://oauth 스킴 처리
+ if (
+ initialUrl.includes("intelfit://auth/kakao") ||
+ initialUrl.includes("kakaob46c5ece88946636902899138451ac5e://oauth")
+ ) {
+ console.log("🔗 [카카오 로그인] 초기 딥링크 발견:", initialUrl);
+ handleKakaoDeepLink(initialUrl);
+ }
}
} catch (error) {
console.error("❌ [카카오 로그인] 초기 URL 확인 실패:", error);
@@ -265,7 +289,13 @@ const LoginScreen = ({ navigation }: any) => {
// 🔹 ② redirect_uri 가로채기 (가장 중요)
const subscription = Linking.addEventListener("url", async ({ url }) => {
console.log("🔗 [카카오 로그인] 딥링크 수신:", url);
- handleKakaoDeepLink(url);
+ // intelfit://auth/kakao 또는 kakaob46c5ece88946636902899138451ac5e://oauth 스킴 처리
+ if (
+ url.includes("intelfit://auth/kakao") ||
+ url.includes("kakaob46c5ece88946636902899138451ac5e://oauth")
+ ) {
+ handleKakaoDeepLink(url);
+ }
});
// 앱이 이미 열려있을 때 딥링크 처리
@@ -444,218 +474,91 @@ const LoginScreen = ({ navigation }: any) => {
// ✅ 방법 2: openAuthSessionAsync 사용 (Expo Go에서도 작동, 딥링크 리다이렉트 감지 가능)
const KAKAO_CLIENT_ID = "99baee411cc547822f138712b19b032c";
+ // 백엔드 콜백 URL 사용 (백엔드에서 딥링크로 리다이렉트해야 함)
const REDIRECT_URI = "https://www.intelfits.com/api/auth/kakao/callback";
const loginUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${KAKAO_CLIENT_ID}&redirect_uri=${encodeURIComponent(
REDIRECT_URI
)}&response_type=code&scope=profile_nickname,profile_image`;
console.log("🔵 [카카오 로그인] WebBrowser 방식 사용");
+ console.log("🔵 [카카오 로그인] Platform:", Platform.OS);
+ console.log("🔵 [카카오 로그인] REDIRECT_URI:", REDIRECT_URI);
console.log("🔵 [카카오 로그인] URL:", loginUrl);
// 딥링크 스킴 설정 (앱 내부 브라우저에서 열리도록)
+ // 백엔드 콜백에서 이 딥링크로 리다이렉트해야 함
const deepLinkScheme = "intelfit://auth/kakao";
console.log("🔵 [카카오 로그인] 딥링크 스킴:", deepLinkScheme);
- let result;
-
- // 안드로이드에서는 openBrowserAsync 사용 (앱 내부 브라우저)
- if (Platform.OS === "android") {
+ // iOS와 안드로이드 플랫폼별 처리
+ if (Platform.OS === "ios") {
+ // iOS는 openAuthSessionAsync 사용 (결과를 직접 받음)
+ let result;
try {
- console.log("🔵 [카카오 로그인] 안드로이드 - openBrowserAsync 사용 (앱 내부 브라우저)");
- await WebBrowser.openBrowserAsync(loginUrl, {
- enableBarCollapsing: false,
- showInRecents: false,
- // 안드로이드에서 앱 내부 브라우저 사용
- toolbarColor: "#000000",
- controlsColor: "#ffffff",
- });
- // openBrowserAsync는 딥링크를 자동으로 처리하지 않으므로
- // Linking 이벤트 리스너가 처리하도록 함
- setLoading(false);
- return;
+ console.log("🔵 [카카오 로그인] iOS - openAuthSessionAsync 사용");
+ result = await WebBrowser.openAuthSessionAsync(
+ loginUrl,
+ deepLinkScheme,
+ {
+ preferEphemeralSession: false,
+ }
+ );
+ console.log("🔵 [카카오 로그인] openAuthSessionAsync 결과:", result);
} catch (browserError: any) {
- console.error("❌ [카카오 로그인] openBrowserAsync 에러:", browserError);
+ console.error("❌ [카카오 로그인] WebBrowser 에러:", browserError);
Alert.alert("오류", "카카오 로그인 페이지를 열 수 없습니다.");
setLoading(false);
return;
}
- }
-
- // iOS는 openAuthSessionAsync 사용
- try {
- result = await WebBrowser.openAuthSessionAsync(
- loginUrl,
- deepLinkScheme,
- {
- preferEphemeralSession: false,
- }
- );
- } catch (browserError: any) {
- console.error("❌ [카카오 로그인] WebBrowser 에러:", browserError);
- Alert.alert("오류", "카카오 로그인 페이지를 열 수 없습니다.");
- setLoading(false);
- return;
- }
-
- // iOS와 안드로이드 모두 result 처리
- if (result) {
- console.log("🔵 [카카오 로그인] 결과:", result);
-
- // openAuthSessionAsync가 성공적으로 딥링크를 받은 경우
- if (result.type === "success" && result.url) {
- console.log("🔵 [카카오 로그인] 딥링크 URL 받음:", result.url);
-
- // 기존 Linking 리스너가 처리하도록 이벤트 트리거
- // 또는 직접 파싱해서 처리
- const parsed = Linking.parse(result.url);
- const code = parsed.queryParams?.code as string | undefined;
- const accessToken = parsed.queryParams?.accessToken as
- | string
- | undefined;
- const refreshToken = parsed.queryParams?.refreshToken as
- | string
- | undefined;
-
- // 백엔드가 이미 처리해서 토큰을 딥링크에 포함한 경우
- if (accessToken) {
- console.log("✅ [카카오 로그인] 토큰이 딥링크에 포함됨");
- try {
- setLoading(true);
-
- // 토큰 저장
- await AsyncStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
- if (refreshToken) {
- await AsyncStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
- }
-
- // userId가 있으면 저장
- const userId = parsed.queryParams?.userId as string | undefined;
- if (userId) {
- await AsyncStorage.setItem("userId", userId);
- }
- // membershipType 저장 (기본값 FREE)
- const membershipType =
- (parsed.queryParams?.membershipType as string | undefined) ||
- "FREE";
- await AsyncStorage.setItem("membershipType", membershipType);
-
- // 온보딩 여부 확인 (isOnboarded 우선 확인)
- const isOnboarded = parsed.queryParams?.isOnboarded;
- const onboarded = parsed.queryParams?.onboarded;
- const shouldOnboard =
- isOnboarded === "false" || onboarded === "false";
-
- // 신규 유저 확인 (온보딩이 완료된 경우에만)
- const newUser = parsed.queryParams?.newUser;
- const isNewUser = newUser === "true";
-
- console.log("✅ [카카오 로그인] 토큰 저장 완료");
- if (shouldOnboard || isNewUser) {
- navigation.replace("KakaoOnboarding");
- } else {
- navigation.replace("Main");
- }
- } catch (error: any) {
- console.error("❌ [카카오 로그인] 토큰 저장 실패:", error);
- Alert.alert(
- "로그인 실패",
- error.message || "토큰 저장 중 오류가 발생했습니다."
- );
- } finally {
+ // iOS는 result를 직접 처리
+ if (result) {
+ console.log("🔵 [카카오 로그인] 결과:", result);
+
+ if (result.type === "success" && result.url) {
+ console.log("🔵 [카카오 로그인] 딥링크 URL 받음:", result.url);
+ // iOS는 result.url을 직접 처리
+ await handleKakaoDeepLink(result.url);
+ } else if (result.type === "cancel") {
+ console.log("⚠️ [카카오 로그인] 사용자가 취소함");
+ Alert.alert("알림", "카카오 로그인이 취소되었습니다.");
setLoading(false);
- }
- return;
- }
-
- // 인증 코드가 있는 경우 (백엔드 API 호출 필요)
- if (code) {
- console.log(
- "🔵 [카카오 로그인] 인증 코드 받음, 백엔드 API 호출 시작"
- );
- try {
- setLoading(true);
-
- // 👉 서버 API 호출
- const res = await fetch(
- "https://www.intelfits.com/api/auth/kakao/login",
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ code }),
- }
- );
-
- console.log("🔵 [카카오 로그인] 백엔드 응답 상태:", res.status);
- const data = await res.json();
- console.log("🔵 [카카오 로그인] 백엔드 응답 데이터:", data);
-
- if (!res.ok) {
- throw new Error(data.message || "카카오 로그인에 실패했습니다");
- }
-
- // 토큰 저장
- if (data.accessToken) {
- await AsyncStorage.setItem(ACCESS_TOKEN_KEY, data.accessToken);
- }
- if (data.refreshToken) {
- await AsyncStorage.setItem(REFRESH_TOKEN_KEY, data.refreshToken);
- }
- if (data.userId) {
- await AsyncStorage.setItem("userId", String(data.userId));
- }
- if (data.membershipType) {
- await AsyncStorage.setItem("membershipType", data.membershipType);
- } else {
- await AsyncStorage.setItem("membershipType", "FREE");
- }
-
- // 신규 유저/기존 유저 확인 (newUser 값 우선 확인)
- const isNewUser = data.newUser === true;
- if (isNewUser) {
- navigation.replace("KakaoOnboarding");
- } else {
- navigation.replace("Main");
- }
- } catch (error: any) {
- console.error("카카오 로그인 처리 실패:", error);
- const errorMessage =
- error.message || "카카오 로그인에 실패했습니다";
- if (typeof window !== "undefined" && (window as any).alert) {
- (window as any).alert(`로그인 실패\n${errorMessage}`);
- } else {
- Alert.alert("로그인 실패", errorMessage);
- }
- } finally {
+ } else if (result.type === "dismiss") {
+ console.log("⚠️ [카카오 로그인] 브라우저가 닫힘");
+ setLoading(false);
+ } else {
+ console.log("⚠️ [카카오 로그인] 예상치 못한 결과:", result);
+ Alert.alert("오류", "카카오 로그인에 실패했습니다. 다시 시도해주세요.");
setLoading(false);
}
}
- } else if (result.type === "cancel") {
- console.log("⚠️ [카카오 로그인] 사용자가 취소함");
- Alert.alert("알림", "카카오 로그인이 취소되었습니다.");
- setLoading(false);
- } else if (result.type === "dismiss") {
- console.log("⚠️ [카카오 로그인] 브라우저가 닫힘");
- // iOS에서 dismiss는 사용자가 브라우저를 닫은 경우
+ } else {
+ // 안드로이드는 WebView를 사용하여 앱 내부에서 브라우저 열기
+ try {
+ console.log("🔵 [카카오 로그인] Android - WebView 사용 (앱 내부 브라우저)");
+ console.log("🔵 [카카오 로그인] Login URL:", loginUrl);
+
+ // WebView 모달 열기
+ setKakaoLoginUrl(loginUrl);
+ setShowKakaoWebView(true);
setLoading(false);
- } else {
- console.log("⚠️ [카카오 로그인] 예상치 못한 결과:", result);
- Alert.alert("오류", "카카오 로그인에 실패했습니다. 다시 시도해주세요.");
+ } catch (browserError: any) {
+ console.error("❌ [카카오 로그인] WebBrowser 에러:", browserError);
+ console.error("❌ [카카오 로그인] 에러 상세:", JSON.stringify(browserError, null, 2));
+ Alert.alert("오류", "카카오 로그인 페이지를 열 수 없습니다.");
setLoading(false);
+ return;
}
- } else {
- // 안드로이드에서 result가 없는 경우 (Linking 이벤트 리스너가 처리)
- console.log("🔵 [카카오 로그인] 안드로이드 - Linking 이벤트 리스너가 처리하도록 대기");
- // setLoading(false는 하지 않음 - Linking 이벤트에서 처리될 때까지 대기
}
} catch (error: any) {
console.error("❌ [카카오 로그인] 에러:", error);
const errorMessage =
error.message || "카카오 로그인 페이지를 열 수 없습니다.";
Alert.alert("오류", errorMessage);
- } finally {
setLoading(false);
}
+ // 안드로이드는 Linking 이벤트 리스너가 처리하므로 finally에서 setLoading(false)를 호출하지 않음
+ // iOS는 위의 각 분기에서 이미 setLoading(false)를 호출함
};
return (
@@ -730,6 +633,90 @@ const LoginScreen = ({ navigation }: any) => {
+
+ {/* 안드로이드용 카카오 로그인 WebView 모달 */}
+ {Platform.OS === "android" && (
+ {
+ setShowKakaoWebView(false);
+ setLoading(false);
+ }}
+ >
+
+
+ {
+ setShowKakaoWebView(false);
+ setLoading(false);
+ }}
+ style={styles.webViewCloseBtn}
+ >
+ ✕
+
+ 카카오 로그인
+
+
+
+ {
+ console.log("🌐 [카카오 로그인] WebView URL:", navState.url);
+
+ // 딥링크 감지
+ if (
+ navState.url.includes("intelfit://auth/kakao") ||
+ navState.url.includes("kakaob46c5ece88946636902899138451ac5e://oauth")
+ ) {
+ console.log("🔗 [카카오 로그인] 딥링크 감지, WebView 닫기");
+ setShowKakaoWebView(false);
+ // Linking 이벤트 리스너가 처리하도록 딥링크 트리거
+ handleKakaoDeepLink(navState.url);
+ }
+ }}
+ onShouldStartLoadWithRequest={(request) => {
+ console.log("🔍 [카카오 로그인] 로드 요청:", request.url);
+
+ // 딥링크 감지
+ if (
+ request.url.includes("intelfit://auth/kakao") ||
+ request.url.includes("kakaob46c5ece88946636902899138451ac5e://oauth")
+ ) {
+ console.log("🔗 [카카오 로그인] 딥링크 감지, WebView 닫기");
+ setShowKakaoWebView(false);
+ // Linking 이벤트 리스너가 처리하도록 딥링크 트리거
+ handleKakaoDeepLink(request.url);
+ return false; // 페이지 로드 차단
+ }
+ return true;
+ }}
+ onError={(syntheticEvent) => {
+ const { nativeEvent } = syntheticEvent;
+ console.error("❌ [카카오 로그인] WebView 에러:", nativeEvent);
+ Alert.alert("오류", "카카오 로그인 페이지를 불러올 수 없습니다.");
+ setShowKakaoWebView(false);
+ setLoading(false);
+ }}
+ onHttpError={(syntheticEvent) => {
+ const { nativeEvent } = syntheticEvent;
+ console.error("❌ [카카오 로그인] HTTP 에러:", nativeEvent.statusCode, nativeEvent.url);
+ }}
+ javaScriptEnabled={true}
+ domStorageEnabled={true}
+ startInLoadingState={true}
+ scalesPageToFit={true}
+ style={{ flex: 1 }}
+ renderLoading={() => (
+
+
+ 로딩 중...
+
+ )}
+ />
+
+
+ )}
);
};
@@ -832,6 +819,51 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: "400",
},
+ webViewContainer: {
+ flex: 1,
+ backgroundColor: "#000",
+ },
+ webViewHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ backgroundColor: "#252525",
+ borderBottomWidth: 1,
+ borderBottomColor: "#333",
+ },
+ webViewCloseBtn: {
+ width: 40,
+ height: 40,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ webViewCloseText: {
+ color: "#fff",
+ fontSize: 24,
+ fontWeight: "300",
+ },
+ webViewTitle: {
+ color: "#fff",
+ fontSize: 18,
+ fontWeight: "600",
+ },
+ webViewLoading: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: "#252525",
+ },
+ webViewLoadingText: {
+ color: "#fff",
+ fontSize: 16,
+ marginTop: 12,
+ },
});
export default LoginScreen;
diff --git a/src/screens/chatbot/ChatbotScreen.tsx b/src/screens/chatbot/ChatbotScreen.tsx
index 86b0fd8..1bec95e 100644
--- a/src/screens/chatbot/ChatbotScreen.tsx
+++ b/src/screens/chatbot/ChatbotScreen.tsx
@@ -1,5 +1,5 @@
// src/screens/chatbot/ChatbotScreen.tsx
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef } from "react";
import {
View,
Text,
@@ -11,11 +11,13 @@ import {
Platform,
Alert,
ActivityIndicator,
+ Animated,
} from "react-native";
import {
SafeAreaView,
useSafeAreaInsets,
} from "react-native-safe-area-context";
+import { LinearGradient } from "expo-linear-gradient";
import { Ionicons as Icon } from "@expo/vector-icons";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { chatAPI, ChatHistoryItem } from "../../services/chatAPI";
@@ -434,32 +436,55 @@ const ChatbotScreen = ({ navigation }: any) => {
- 🤖
+
+ 🤖
+
handleQuickSelect("exercise")}
+ activeOpacity={0.7}
>
- 🏋️
- 운동 추천
+
+ 🏋️
+ 운동 추천
+
handleQuickSelect("food")}
+ activeOpacity={0.7}
>
- 🍗
- 식단 추천
+
+ 🍗
+ 식단 추천
+
handleQuickSelect("plan")}
+ activeOpacity={0.7}
>
- 📅
- 계획 수립
+
+ 📅
+ 계획 수립
+
@@ -530,26 +555,48 @@ const ChatbotScreen = ({ navigation }: any) => {
-
- {msg.text}
-
+ {msg.type === "bot" && (
+
+ 🤖
+
+ )}
+ {msg.type === "user" ? (
+
+ {msg.text}
+
+ ) : (
+
+ {msg.text}
+
+ )}
+ {msg.type === "user" && (
+
+
+
+ )}
))}
{isLoading && (
-
- ...
+
+
+ 🤖
+
+
+
+
+
+
+
+
)}
@@ -561,20 +608,39 @@ const ChatbotScreen = ({ navigation }: any) => {
0 ? Math.max(insets.bottom, 4) : 4 },
+ {
+ paddingBottom: isInTab
+ ? 2
+ : (insets.bottom > 0 ? Math.max(insets.bottom, 4) : 4)
+ },
]}
>
-
-
- ➤
-
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
@@ -646,13 +712,15 @@ const styles = StyleSheet.create({
},
title: {
fontSize: 32,
- fontWeight: "bold",
+ fontWeight: "700",
color: NEW_COLORS.text,
marginBottom: 8,
+ letterSpacing: -0.5,
},
subtitle: {
fontSize: 18,
color: NEW_COLORS.text_secondary,
+ fontWeight: "400",
},
settingsButton: {
flexDirection: "row",
@@ -710,9 +778,19 @@ const styles = StyleSheet.create({
botImageContainer: {
alignItems: "center",
marginVertical: 40,
+ padding: 20,
+ },
+ botImageGradient: {
+ width: 140,
+ height: 140,
+ borderRadius: 70,
+ justifyContent: "center",
+ alignItems: "center",
+ borderWidth: 2,
+ borderColor: NEW_COLORS.accent,
},
botEmoji: {
- fontSize: 120,
+ fontSize: 80,
},
quickActions: {
flexDirection: "row",
@@ -721,24 +799,29 @@ const styles = StyleSheet.create({
},
actionBtn: {
flex: 1,
- backgroundColor: NEW_COLORS.card_bg,
- padding: 20,
borderRadius: 20,
+ overflow: "hidden",
+ shadowColor: NEW_COLORS.accent,
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 6,
+ },
+ actionBtnGradient: {
+ padding: 20,
alignItems: "center",
- shadowColor: "#000000",
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 4,
- elevation: 3,
+ justifyContent: "center",
+ minHeight: 100,
},
actionIcon: {
- fontSize: 32,
+ fontSize: 36,
marginBottom: 8,
},
actionText: {
fontSize: 14,
- fontWeight: "600",
- color: NEW_COLORS.text,
+ fontWeight: "700",
+ color: "#000000",
+ letterSpacing: 0.3,
},
historyButton: {
flexDirection: "row",
@@ -752,6 +835,11 @@ const styles = StyleSheet.create({
borderRadius: 24,
borderWidth: 1,
borderColor: NEW_COLORS.separator,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
},
historyButtonText: {
fontSize: 15,
@@ -802,61 +890,146 @@ const styles = StyleSheet.create({
messagesContainer: {
paddingBottom: 10,
},
+ messageWrapper: {
+ flexDirection: "row",
+ alignItems: "flex-end",
+ marginBottom: 16,
+ gap: 8,
+ },
+ userMessageWrapper: {
+ justifyContent: "flex-end",
+ },
+ botMessageWrapper: {
+ justifyContent: "flex-start",
+ },
+ botAvatar: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: NEW_COLORS.card_bg,
+ justifyContent: "center",
+ alignItems: "center",
+ borderWidth: 2,
+ borderColor: NEW_COLORS.accent,
+ },
+ botAvatarEmoji: {
+ fontSize: 20,
+ },
+ userAvatar: {
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: NEW_COLORS.accent,
+ justifyContent: "center",
+ alignItems: "center",
+ },
message: {
- maxWidth: "80%",
- padding: 12,
+ maxWidth: "75%",
+ paddingHorizontal: 16,
+ paddingVertical: 12,
borderRadius: 20,
- marginBottom: 12,
},
userMessage: {
- alignSelf: "flex-end",
- backgroundColor: NEW_COLORS.accent,
+ borderBottomRightRadius: 4,
+ shadowColor: NEW_COLORS.accent,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 4,
},
botMessage: {
- alignSelf: "flex-start",
backgroundColor: NEW_COLORS.card_bg,
+ borderBottomLeftRadius: 4,
+ borderWidth: 1,
+ borderColor: NEW_COLORS.separator,
+ shadowColor: "#000",
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ loadingMessage: {
+ paddingVertical: 16,
},
userMessageText: {
color: "#000000",
- fontSize: 16,
+ fontSize: 15,
+ lineHeight: 22,
+ fontWeight: "500",
},
botMessageText: {
color: NEW_COLORS.text,
- fontSize: 16,
+ fontSize: 15,
+ lineHeight: 22,
+ fontWeight: "400",
},
- loadingText: {
- color: NEW_COLORS.text_secondary,
- fontSize: 16,
+ typingIndicator: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 4,
+ },
+ dot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: NEW_COLORS.text_secondary,
+ },
+ dot1: {
+ opacity: 0.4,
+ },
+ dot2: {
+ opacity: 0.6,
+ },
+ dot3: {
+ opacity: 0.8,
},
chatinputContainer: {
- flexDirection: "row",
- paddingHorizontal: 10,
- paddingTop: 8,
- backgroundColor: NEW_COLORS.card_bg,
+ paddingHorizontal: 16,
+ paddingTop: 4,
+ backgroundColor: NEW_COLORS.background,
borderTopWidth: 1,
borderTopColor: NEW_COLORS.separator,
- gap: 12,
+ },
+ inputWrapper: {
+ flexDirection: "row",
+ alignItems: "flex-end",
+ gap: 10,
+ backgroundColor: NEW_COLORS.card_bg,
+ borderRadius: 28,
+ paddingHorizontal: 4,
+ paddingVertical: 4,
+ borderWidth: 1,
+ borderColor: NEW_COLORS.separator,
},
messageInput: {
flex: 1,
- backgroundColor: NEW_COLORS.background,
+ backgroundColor: "transparent",
borderRadius: 24,
- paddingHorizontal: 20,
+ paddingHorizontal: 18,
paddingVertical: 12,
- fontSize: 16,
+ fontSize: 15,
color: NEW_COLORS.text,
+ maxHeight: 100,
+ minHeight: 44,
},
sendBtn: {
- width: 48,
- height: 48,
- borderRadius: 24,
+ width: 44,
+ height: 44,
+ borderRadius: 22,
backgroundColor: NEW_COLORS.accent,
justifyContent: "center",
alignItems: "center",
+ shadowColor: NEW_COLORS.accent,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.4,
+ shadowRadius: 6,
+ elevation: 5,
},
- sendIcon: {
- color: "#000000",
- fontSize: 20,
+ sendBtnDisabled: {
+ backgroundColor: NEW_COLORS.separator,
+ opacity: 0.5,
+ shadowOpacity: 0,
+ elevation: 0,
},
});