Compare commits
3 Commits
355c9d7743
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 982e307482 | |||
| 1c60a450b9 | |||
| 81897144c6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,3 +41,4 @@ yarn-error.*
|
|||||||
/android
|
/android
|
||||||
.env.*
|
.env.*
|
||||||
.idea
|
.idea
|
||||||
|
package-lock.json
|
||||||
|
|||||||
5
app.json
5
app.json
@@ -27,6 +27,9 @@
|
|||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
}
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@react-native-google-signin/google-signin"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9221
package-lock.json
generated
9221
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,9 +19,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
"@react-native-google-signin/google-signin": "^16.1.1",
|
||||||
|
"@react-navigation/bottom-tabs": "^7.9.0",
|
||||||
"@react-navigation/native": "^7.1.26",
|
"@react-navigation/native": "^7.1.26",
|
||||||
"@react-navigation/native-stack": "^7.9.0",
|
"@react-navigation/native-stack": "^7.9.0",
|
||||||
"expo": "~54.0.30",
|
"expo": "~54.0.30",
|
||||||
|
"expo-apple-authentication": "~8.0.8",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
|||||||
@@ -14,5 +14,14 @@ export type LoginData = {
|
|||||||
export type LoginResponse = ApiResponse<LoginData>;
|
export type LoginResponse = ApiResponse<LoginData>;
|
||||||
|
|
||||||
// 회원가입 api
|
// 회원가입 api
|
||||||
export type SignUpRequest = { loginId: string; password: string };
|
export type SignUpRequest = {
|
||||||
export type SignUpResponse = { userId: number };
|
loginId: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SignUpData = {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SignUpResponse = ApiResponse<SignUpData>;
|
||||||
|
|||||||
233
src/components/ui/AudioBookCard.tsx
Normal file
233
src/components/ui/AudioBookCard.tsx
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ActivityIndicator, Pressable, StyleSheet, View } from "react-native";
|
||||||
|
import { Theme } from "../../theme/theme";
|
||||||
|
import AppText from "../ui/AppText";
|
||||||
|
import Card from "../ui/Card";
|
||||||
|
|
||||||
|
export type AudioStatus = "QUEUED" | "GENERATING" | "READY" | "FAILED";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
meta: string;
|
||||||
|
status: AudioStatus;
|
||||||
|
progress?: number;
|
||||||
|
onPress?: () => void;
|
||||||
|
onRetry?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STATUS_UI: Record<
|
||||||
|
AudioStatus,
|
||||||
|
{ label: string; accent: string; badgeBg: string; badgeText: string }
|
||||||
|
> = {
|
||||||
|
QUEUED: {
|
||||||
|
label: "대기중",
|
||||||
|
accent: "#94a3b8",
|
||||||
|
badgeBg: "rgba(148,163,184,0.15)",
|
||||||
|
badgeText: "#64748b",
|
||||||
|
},
|
||||||
|
GENERATING: {
|
||||||
|
label: "생성중",
|
||||||
|
accent: "#60a5fa",
|
||||||
|
badgeBg: "rgba(96,165,250,0.15)",
|
||||||
|
badgeText: "#3b82f6",
|
||||||
|
},
|
||||||
|
READY: {
|
||||||
|
label: "완료",
|
||||||
|
accent: "#34d399",
|
||||||
|
badgeBg: "rgba(52,211,153,0.15)",
|
||||||
|
badgeText: "#10b981",
|
||||||
|
},
|
||||||
|
FAILED: {
|
||||||
|
label: "실패",
|
||||||
|
accent: "#fb7185",
|
||||||
|
badgeBg: "rgba(251,113,133,0.15)",
|
||||||
|
badgeText: "#e11d48",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AudioBookCard({
|
||||||
|
title,
|
||||||
|
meta,
|
||||||
|
status,
|
||||||
|
progress,
|
||||||
|
onPress,
|
||||||
|
onRetry,
|
||||||
|
}: Props) {
|
||||||
|
const ui = STATUS_UI[status];
|
||||||
|
|
||||||
|
const canPress = status === "READY" && !onPress;
|
||||||
|
const showRetry = status === "FAILED" && !onRetry;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
onPress={canPress ? onPress : undefined}
|
||||||
|
disabled={!canPress}
|
||||||
|
accentColor={ui.accent}
|
||||||
|
wrapperStyle={styles.wrapper}
|
||||||
|
>
|
||||||
|
{/* Top row */}
|
||||||
|
<View style={styles.topRow}>
|
||||||
|
<View style={styles.titleBlock}>
|
||||||
|
<AppText variant="title" numberOfLines={1}>
|
||||||
|
{title}
|
||||||
|
</AppText>
|
||||||
|
<View style={{ height: Theme.Spacing.xs }} />
|
||||||
|
<AppText variant="muted" numberOfLines={1}>
|
||||||
|
{meta}
|
||||||
|
</AppText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.rightBlock}>
|
||||||
|
<Badge text={ui.label} bg={ui.badgeBg} color={ui.badgeText} />
|
||||||
|
|
||||||
|
{status === "GENERATING" ? (
|
||||||
|
<View style={{ height: Theme.Spacing.sm }} />
|
||||||
|
) : (
|
||||||
|
<View style={{ height: Theme.Spacing.sm }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === "GENERATING" ? (
|
||||||
|
<ActivityIndicator />
|
||||||
|
) : status === "READY" ? (
|
||||||
|
<PlayPill />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Middle: generating progress */}
|
||||||
|
{status === "GENERATING" ? (
|
||||||
|
<View style={styles.progressWrap}>
|
||||||
|
<View style={{ height: Theme.Spacing.sm }} />
|
||||||
|
<ProgressBar progress={progress} accent={ui.accent} />
|
||||||
|
<View style={{ height: Theme.Spacing.xs }} />
|
||||||
|
<AppText variant="muted">
|
||||||
|
{typeof progress === "number"
|
||||||
|
? `${Math.round(progress * 100)}% 진행`
|
||||||
|
: "작업 중…"}
|
||||||
|
</AppText>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Bottom: failed actions */}
|
||||||
|
{showRetry ? (
|
||||||
|
<View style={styles.bottomRow}>
|
||||||
|
<AppText variant="muted" style={{ flex: 1 }}>
|
||||||
|
생성에 실패했어요. 다시 시도할까요?
|
||||||
|
</AppText>
|
||||||
|
<Pressable onPress={onRetry} style={styles.retryBtn}>
|
||||||
|
<AppText>재시도</AppText>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
text,
|
||||||
|
bg,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
bg: string;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.badge, { backgroundColor: bg }]}>
|
||||||
|
<AppText style={{ color }}>{text}</AppText>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PlayPill() {
|
||||||
|
return (
|
||||||
|
<View style={styles.playPill}>
|
||||||
|
<AppText>▶︎</AppText>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressBar({
|
||||||
|
progress,
|
||||||
|
accent,
|
||||||
|
}: {
|
||||||
|
progress?: number;
|
||||||
|
accent: string;
|
||||||
|
}) {
|
||||||
|
const pct =
|
||||||
|
typeof progress === "number"
|
||||||
|
? Math.max(0, Math.min(1, progress))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.progressTrack}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.progressFill,
|
||||||
|
{
|
||||||
|
backgroundColor: accent,
|
||||||
|
width: pct == null ? "60%" : `${pct * 100}%`,
|
||||||
|
},
|
||||||
|
pct == null && { opacity: 0.55 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
wrapper: {
|
||||||
|
marginHorizontal: Theme.Spacing.lg,
|
||||||
|
marginTop: Theme.Spacing.md,
|
||||||
|
},
|
||||||
|
topRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: Theme.Spacing.md,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
titleBlock: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
rightBlock: {
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: Theme.Spacing.xs,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
paddingHorizontal: Theme.Spacing.sm,
|
||||||
|
paddingVertical: Theme.Spacing.xs,
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
playPill: {
|
||||||
|
paddingHorizontal: Theme.Spacing.md,
|
||||||
|
paddingVertical: Theme.Spacing.xs,
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Theme.Colors.border,
|
||||||
|
},
|
||||||
|
progressWrap: {
|
||||||
|
marginTop: Theme.Spacing.md,
|
||||||
|
},
|
||||||
|
progressTrack: {
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(148,163,184,0.18)",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
progressFill: {
|
||||||
|
height: "100%",
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
bottomRow: {
|
||||||
|
marginTop: Theme.Spacing.md,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: Theme.Spacing.sm,
|
||||||
|
},
|
||||||
|
retryBtn: {
|
||||||
|
paddingHorizontal: Theme.Spacing.md,
|
||||||
|
paddingVertical: Theme.Spacing.xs,
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Theme.Colors.border,
|
||||||
|
},
|
||||||
|
});
|
||||||
131
src/components/ui/Card.tsx
Normal file
131
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
type StyleProp,
|
||||||
|
type ViewProps,
|
||||||
|
type ViewStyle,
|
||||||
|
} from "react-native";
|
||||||
|
import { Theme } from "../../theme/theme";
|
||||||
|
|
||||||
|
type Props = Omit<ViewProps, "style"> & {
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
wrapperStyle?: StyleProp<ViewStyle>;
|
||||||
|
onPress?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
accentColor?: string;
|
||||||
|
accentWidth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Card({
|
||||||
|
style,
|
||||||
|
wrapperStyle,
|
||||||
|
onPress,
|
||||||
|
disabled,
|
||||||
|
accentColor,
|
||||||
|
accentWidth = 4,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const hasAccent = !!accentColor;
|
||||||
|
|
||||||
|
const contentPad: ViewStyle | undefined = hasAccent
|
||||||
|
? { paddingLeft: Theme.Spacing.lg + accentWidth + Theme.Spacing.sm }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const Inner = (pressed?: boolean) => (
|
||||||
|
<View
|
||||||
|
{...props}
|
||||||
|
style={[styles.inner, contentPad, disabled && styles.disabled, style]}
|
||||||
|
>
|
||||||
|
{hasAccent && (
|
||||||
|
<View
|
||||||
|
pointerEvents="none"
|
||||||
|
style={[
|
||||||
|
styles.accent,
|
||||||
|
{ backgroundColor: accentColor, width: accentWidth },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pressed && !disabled && (
|
||||||
|
<View pointerEvents="none" style={styles.pressOverlay} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onPress) {
|
||||||
|
return (
|
||||||
|
<View style={[styles.shadowWrap, wrapperStyle]}>
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
disabled={disabled}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
styles.pressableClip,
|
||||||
|
pressed && !disabled && styles.pressed,
|
||||||
|
]}
|
||||||
|
android_ripple={{ color: "rgba(0,0,0,0.06)" }}
|
||||||
|
>
|
||||||
|
{({ pressed }) => Inner(pressed)}
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.shadowWrap, wrapperStyle]}>
|
||||||
|
<View style={styles.pressableClip}>{Inner(false)}</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
shadowWrap: {
|
||||||
|
borderRadius: Theme.Radius.lg,
|
||||||
|
|
||||||
|
shadowColor: "#0B1220",
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 18,
|
||||||
|
shadowOffset: { width: 0, height: 10 },
|
||||||
|
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 안쪽: 라운드/리플/오버레이 클립 담당
|
||||||
|
pressableClip: {
|
||||||
|
borderRadius: Theme.Radius.lg,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
|
||||||
|
// 실제 카드 표면
|
||||||
|
inner: {
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
padding: Theme.Spacing.lg,
|
||||||
|
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "rgba(15, 23, 42, 0.10)", // 지금보다 살짝 진하게
|
||||||
|
},
|
||||||
|
|
||||||
|
accent: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
pressOverlay: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.04)",
|
||||||
|
},
|
||||||
|
|
||||||
|
pressed: {
|
||||||
|
transform: [{ translateY: 1 }],
|
||||||
|
},
|
||||||
|
|
||||||
|
disabled: {
|
||||||
|
opacity: 0.55,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import HomeScreen from "../screens/HomeScreen";
|
import AppTabs from "./AppTabs";
|
||||||
|
|
||||||
export type AppStackParamList = {
|
export type AppStackParamList = {
|
||||||
Home: undefined;
|
MainTabs: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<AppStackParamList>();
|
const Stack = createNativeStackNavigator<AppStackParamList>();
|
||||||
@@ -11,7 +11,7 @@ const Stack = createNativeStackNavigator<AppStackParamList>();
|
|||||||
export default function AppStack() {
|
export default function AppStack() {
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="Home" component={HomeScreen} />
|
<Stack.Screen name="MainTabs" component={AppTabs} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/navigation/AppTabs.tsx
Normal file
30
src/navigation/AppTabs.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||||
|
import React from "react";
|
||||||
|
import HomeScreen from "../screens/HomeScreen";
|
||||||
|
import { Colors } from "../theme/colors";
|
||||||
|
|
||||||
|
export type AppTabParamList = {
|
||||||
|
Library: undefined;
|
||||||
|
Home: undefined;
|
||||||
|
Settings: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab = createBottomTabNavigator<AppTabParamList>();
|
||||||
|
|
||||||
|
export default function AppTabs() {
|
||||||
|
return (
|
||||||
|
<Tab.Navigator
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
tabBarStyle: {
|
||||||
|
backgroundColor: Colors.surface,
|
||||||
|
borderTopColor: Colors.border,
|
||||||
|
},
|
||||||
|
tabBarActiveTintColor: Colors.text,
|
||||||
|
tabBarInactiveTintColor: Colors.mutedText,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab.Screen name="Home" component={HomeScreen} />
|
||||||
|
</Tab.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import { Button, StyleSheet, View } from "react-native";
|
import { Button, StyleSheet, View } from "react-native";
|
||||||
|
import AppText from "../components/ui/AppText";
|
||||||
|
import AudioBookCard from "../components/ui/AudioBookCard";
|
||||||
|
import Card from "../components/ui/Card";
|
||||||
import { useAuth } from "../store/auth";
|
import { useAuth } from "../store/auth";
|
||||||
|
import { Colors } from "../theme/colors";
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { signOut } = useAuth();
|
const { signOut } = useAuth();
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<Button
|
<Card>
|
||||||
title="Debug Storage"
|
<AppText>feafwea</AppText>
|
||||||
onPress={async () => {
|
<AppText>feafwea</AppText>
|
||||||
const keys = await AsyncStorage.getAllKeys();
|
</Card>
|
||||||
const entries = await AsyncStorage.multiGet(keys);
|
<AudioBookCard
|
||||||
console.log(entries);
|
meta="ff"
|
||||||
}}
|
title="fefe"
|
||||||
/>
|
status="READY"
|
||||||
|
progress={0.91}
|
||||||
|
></AudioBookCard>
|
||||||
<Button title="logout" onPress={() => signOut()}></Button>
|
<Button title="logout" onPress={() => signOut()}></Button>
|
||||||
|
<AppText>fewfeaw</AppText>
|
||||||
|
<AppText>fewfeaw</AppText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -22,8 +29,15 @@ export default function HomeScreen() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "red",
|
backgroundColor: Colors.bg,
|
||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: 12,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
5
src/screens/LibraryScreen.tsx
Normal file
5
src/screens/LibraryScreen.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
|
export default function LibraryScreen() {
|
||||||
|
return <View>Library</View>;
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { AntDesign, Ionicons } from "@expo/vector-icons";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Pressable, StyleSheet, View } from "react-native";
|
import { Platform, Pressable, StyleSheet, View } from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
import AppText from "../components/ui/AppText";
|
import AppText from "../components/ui/AppText";
|
||||||
import Button from "../components/ui/Button";
|
import Button from "../components/ui/Button";
|
||||||
import Input from "../components/ui/Input";
|
import Input from "../components/ui/Input";
|
||||||
@@ -11,13 +13,26 @@ import { useAuth } from "../store/auth";
|
|||||||
import { Theme } from "../theme/theme";
|
import { Theme } from "../theme/theme";
|
||||||
|
|
||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const { signIn } = useAuth();
|
const { signIn /*, signInWithGoogle, signInWithApple */ } = useAuth();
|
||||||
const [loginId, setLoginId] = useState("");
|
const [loginId, setLoginId] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const canSubmit = loginId.trim().length > 0 && password.length > 0;
|
const canSubmit = loginId.trim().length > 0 && password.length > 0;
|
||||||
|
|
||||||
const navigation =
|
const navigation =
|
||||||
useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
|
useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
|
||||||
|
|
||||||
|
const onGooglePress = () => {
|
||||||
|
// TODO: 구현 후 연결
|
||||||
|
// signInWithGoogle();
|
||||||
|
console.log("Google login");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onApplePress = () => {
|
||||||
|
// TODO: 구현 후 연결
|
||||||
|
// signInWithApple();
|
||||||
|
console.log("Apple login");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={styles.safe}>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@@ -57,10 +72,53 @@ export default function LoginScreen() {
|
|||||||
<Button
|
<Button
|
||||||
title="Continue"
|
title="Continue"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
signIn(loginId, password);
|
signIn(loginId.trim(), password);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* divider */}
|
||||||
|
<View style={{ height: Theme.Spacing.md }} />
|
||||||
|
<View style={styles.dividerRow}>
|
||||||
|
<View style={styles.dividerLine} />
|
||||||
|
<AppText variant="muted" style={styles.dividerText}>
|
||||||
|
or
|
||||||
|
</AppText>
|
||||||
|
<View style={styles.dividerLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Google */}
|
||||||
|
<Pressable
|
||||||
|
onPress={onGooglePress}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
styles.socialBtn,
|
||||||
|
pressed && styles.socialBtnPressed,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.socialRow}>
|
||||||
|
<AntDesign name="google" size={18} color={Theme.Colors.text} />
|
||||||
|
<AppText style={styles.socialText}>Continue with Google</AppText>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
{/* Apple (iOS only) */}
|
||||||
|
{Platform.OS === "ios" && (
|
||||||
|
<Pressable
|
||||||
|
onPress={onApplePress}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
styles.socialBtn,
|
||||||
|
styles.appleBtn,
|
||||||
|
pressed && styles.appleBtnPressed,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.socialRow}>
|
||||||
|
<Ionicons name="logo-apple" size={20} color="#fff" />
|
||||||
|
<AppText style={[styles.socialText, styles.appleText]}>
|
||||||
|
Continue with Apple
|
||||||
|
</AppText>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={{ height: Theme.Spacing.md }} />
|
<View style={{ height: Theme.Spacing.md }} />
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -89,6 +147,55 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
gap: Theme.Spacing.sm,
|
gap: Theme.Spacing.sm,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
dividerRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: Theme.Spacing.sm,
|
||||||
|
},
|
||||||
|
dividerLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: Theme.Colors.border,
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
dividerText: {
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
|
||||||
|
socialBtn: {
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Theme.Colors.border,
|
||||||
|
borderRadius: Theme.Radius.lg,
|
||||||
|
paddingVertical: Theme.Spacing.md,
|
||||||
|
paddingHorizontal: Theme.Spacing.lg,
|
||||||
|
backgroundColor: Theme.Colors.card,
|
||||||
|
},
|
||||||
|
socialBtnPressed: {
|
||||||
|
opacity: 0.9,
|
||||||
|
transform: [{ scale: 0.99 }],
|
||||||
|
},
|
||||||
|
socialRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: Theme.Spacing.sm,
|
||||||
|
},
|
||||||
|
socialText: {
|
||||||
|
fontWeight: Theme.FontWeight.semibold,
|
||||||
|
},
|
||||||
|
|
||||||
|
appleBtn: {
|
||||||
|
backgroundColor: "#000",
|
||||||
|
borderColor: "#000",
|
||||||
|
},
|
||||||
|
appleBtnPressed: {
|
||||||
|
opacity: 0.92,
|
||||||
|
},
|
||||||
|
appleText: {
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
|
||||||
signupLine: {
|
signupLine: {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,22 +1,68 @@
|
|||||||
|
// export const Colors = {
|
||||||
|
// // base
|
||||||
|
// bg: "#0B1220", // 깊은 네이비
|
||||||
|
// surface: "#111B2E", // 기본 표면
|
||||||
|
// card: "#162442", // 카드/리스트 아이템
|
||||||
|
|
||||||
|
// // text
|
||||||
|
// text: "#EAF0FF",
|
||||||
|
// mutedText: "#A8B3CF",
|
||||||
|
// placeholder: "#6F7AA3",
|
||||||
|
|
||||||
|
// // brand
|
||||||
|
// primary: "#2DD4BF", // 청록 포인트 (오디오앱에 잘 맞음)
|
||||||
|
// primaryPressed: "#14B8A6",
|
||||||
|
|
||||||
|
// // status
|
||||||
|
// danger: "#F87171",
|
||||||
|
|
||||||
|
// // lines
|
||||||
|
// border: "#223055",
|
||||||
|
// inputBg: "#0E1730",
|
||||||
|
// } as const;
|
||||||
|
|
||||||
|
// export const Colors = {
|
||||||
|
// // base
|
||||||
|
// bg: "#0B0B14",
|
||||||
|
// surface: "#121225",
|
||||||
|
// card: "#1A1B33",
|
||||||
|
|
||||||
|
// // text
|
||||||
|
// text: "#F1F2FF",
|
||||||
|
// mutedText: "#B0B3D6",
|
||||||
|
// placeholder: "#7A7FA8",
|
||||||
|
|
||||||
|
// // brand
|
||||||
|
// primary: "#8B5CF6",
|
||||||
|
// primaryPressed: "#7C3AED",
|
||||||
|
|
||||||
|
// // status
|
||||||
|
// danger: "#FB7185",
|
||||||
|
|
||||||
|
// // lines
|
||||||
|
// border: "#2A2D52",
|
||||||
|
// inputBg: "#0F1022",
|
||||||
|
// } as const;
|
||||||
|
|
||||||
export const Colors = {
|
export const Colors = {
|
||||||
// base
|
// base
|
||||||
bg: "#0B1220",
|
bg: "#F7F4EE",
|
||||||
surface: "#111827",
|
surface: "#FFFFFF",
|
||||||
card: "#0F172A",
|
card: "#FFFFFF",
|
||||||
|
|
||||||
// text
|
// text
|
||||||
text: "#FFFFFF",
|
text: "#1F2937",
|
||||||
mutedText: "#9CA3AF",
|
mutedText: "#6B7280",
|
||||||
placeholder: "#6B7280",
|
placeholder: "#9CA3AF",
|
||||||
|
|
||||||
// brand
|
// brand
|
||||||
primary: "#4F46E5",
|
primary: "#0EA5A4", // 청록 포인트
|
||||||
primaryPressed: "#4338CA",
|
primaryPressed: "#0B8B8A",
|
||||||
|
|
||||||
// status
|
// status
|
||||||
danger: "#EF4444",
|
danger: "#DC2626",
|
||||||
|
|
||||||
// lines
|
// lines
|
||||||
border: "#22304A",
|
border: "#E6E0D6",
|
||||||
inputBg: "#0B1220",
|
inputBg: "#F1EEE7",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user