feat: 로그인 화면 및 공통 속성 생성

This commit is contained in:
2025-12-31 18:40:46 +09:00
parent 822cd55c93
commit 7400363466
17 changed files with 689 additions and 17 deletions

View File

@@ -0,0 +1,27 @@
import React from "react";
import { Text, type TextProps, type TextStyle } from "react-native";
import { Theme } from "../../theme/theme";
type Variant = "logo" | "title" | "body" | "muted" | "error";
type Props = TextProps & {
variant?: Variant;
style?: TextStyle | TextStyle[];
};
export default function AppText({ variant = "body", style, ...props }: Props) {
const base: TextStyle = {
color: Theme.Colors.text,
fontSize: Theme.FontSize.md,
};
const variants: Record<Variant, TextStyle> = {
logo: { fontSize: Theme.FontSize.xxl, fontWeight: Theme.FontWeight.bold },
title: { fontSize: Theme.FontSize.xl, fontWeight: Theme.FontWeight.bold },
body: { fontSize: Theme.FontSize.md },
muted: { color: Theme.Colors.mutedText, fontSize: Theme.FontSize.sm },
error: { color: Theme.Colors.danger, fontSize: Theme.FontSize.sm },
};
return <Text {...props} style={[base, variants[variant], style]} />;
}

View File

@@ -0,0 +1,73 @@
import React from "react";
import { Pressable, type PressableProps, StyleSheet, View } from "react-native";
import { Theme } from "../../theme/theme";
import AppText from "./AppText";
type Props = PressableProps & {
title: string;
variant?: "primary" | "secondary";
};
export default function Button({
title,
variant = "primary",
style,
...props
}: Props) {
const isPrimary = variant === "primary";
return (
<Pressable
{...props}
style={({ pressed }) => [
styles.base,
isPrimary ? styles.primary : styles.secondary,
pressed &&
(isPrimary ? styles.primaryPressed : styles.secondaryPressed),
typeof style === "function" ? style({ pressed }) : style,
]}
>
<View>
<AppText
style={{
textAlign: "center",
fontWeight: Theme.FontWeight.bold,
color: isPrimary ? Theme.Colors.text : Theme.Colors.text,
}}
>
{title}
</AppText>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
base: {
height: 48,
paddingVertical: 0,
paddingHorizontal: Theme.Spacing.lg,
borderRadius: Theme.Radius.md,
alignItems: "center", // 가로 가운데
justifyContent: "center", // 세로 가운데
},
text: {
textAlign: "center",
fontWeight: Theme.FontWeight.bold,
lineHeight: 20,
},
primary: {
backgroundColor: Theme.Colors.primary,
},
primaryPressed: {
backgroundColor: Theme.Colors.primaryPressed,
},
secondary: {
backgroundColor: Theme.Colors.surface,
borderWidth: 1,
borderColor: Theme.Colors.border,
},
secondaryPressed: {
opacity: 0.9,
},
});

View File

@@ -0,0 +1,32 @@
import React from "react";
import { StyleSheet, TextInput, type TextInputProps, View } from "react-native";
import { Theme } from "../../theme/theme";
type Props = TextInputProps;
export default function Input(props: Props) {
return (
<View style={styles.wrap}>
<TextInput
{...props}
placeholderTextColor={Theme.Colors.placeholder}
style={[styles.input, props.style]}
/>
</View>
);
}
const styles = StyleSheet.create({
wrap: {
borderWidth: 1,
borderColor: Theme.Colors.border,
borderRadius: Theme.Radius.md,
backgroundColor: Theme.Colors.inputBg,
},
input: {
paddingHorizontal: Theme.Spacing.md,
paddingVertical: Theme.Spacing.md,
color: Theme.Colors.text,
fontSize: Theme.FontSize.md,
},
});

View File

@@ -0,0 +1,17 @@
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import React from "react";
import HomeScreen from "../screens/HomeScreen";
export type AppStackParamList = {
Home: undefined;
};
const Stack = createNativeStackNavigator<AppStackParamList>();
export default function AppStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
);
}

View File

@@ -0,0 +1,10 @@
import React from "react";
import AppStack from "./AppStack";
import AuthStack from "./AuthStack";
export default function AuthGate() {
// const { isAuthed } = useAuth();
const isAuthed = true;
// return <AuthStack />;
return isAuthed ? <AppStack /> : <AuthStack />;
}

View File

@@ -0,0 +1,17 @@
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import React from "react";
import LoginScreen from "../screens/LoginScreen";
export type AuthStackParamList = {
Login: undefined;
};
const Stack = createNativeStackNavigator<AuthStackParamList>();
export default function AuthStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} />
</Stack.Navigator>
);
}

View File

@@ -0,0 +1,18 @@
import { StyleSheet, Text, View } from "react-native";
export default function HomeScreen() {
return (
<View style={styles.container}>
<Text>feawfewa</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "red",
alignContent: "center",
justifyContent: "center",
},
});

View File

@@ -0,0 +1,74 @@
import React from "react";
import { Pressable, StyleSheet, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import AppText from "../components/ui/AppText";
import Button from "../components/ui/Button";
import Input from "../components/ui/Input";
import { Theme } from "../theme/theme";
export default function LoginScreen() {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.container}>
<AppText variant="title">Your personal growth podcast</AppText>
<AppText variant="muted">Sign in to continue</AppText>
<View style={{ height: Theme.Spacing.lg }} />
<Input
placeholder="Username"
autoCapitalize="none"
autoCorrect={false}
textContentType="username"
/>
<View style={{ height: Theme.Spacing.sm }} />
<Input
placeholder="Password"
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
textContentType="password"
/>
<View style={{ height: Theme.Spacing.lg }} />
<Button title="Continue" onPress={() => {}} />
<View style={{ height: Theme.Spacing.md }} />
<Pressable
onPress={() => {
console.log("feawf");
}}
>
<AppText variant="muted" style={styles.signupLine}>
Don&apos;t have an account?{" "}
<AppText style={styles.signupLink}>Sign up</AppText>
</AppText>
</Pressable>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: {
flex: 1,
backgroundColor: Theme.Colors.bg,
},
container: {
flex: 1,
padding: Theme.Spacing.xl,
justifyContent: "center",
gap: Theme.Spacing.sm,
},
signupLine: {
textAlign: "center",
},
signupLink: {
color: Theme.Colors.primary,
fontWeight: Theme.FontWeight.bold,
},
});

31
src/store/auth.tsx Normal file
View File

@@ -0,0 +1,31 @@
import React, { createContext, useContext, useMemo, useState } from "react";
export type AuthContextValue = {
isAuthed: boolean;
login: () => void;
logout: () => void;
};
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isAuthed, setIsAuthed] = useState<boolean>(false);
console.log("isAuthed: " + isAuthed);
const value = useMemo<AuthContextValue>(
() => ({
isAuthed,
login: () => setIsAuthed(true),
logout: () => setIsAuthed(false),
}),
[isAuthed]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}

22
src/theme/colors.ts Normal file
View File

@@ -0,0 +1,22 @@
export const Colors = {
// base
bg: "#0B1220",
surface: "#111827",
card: "#0F172A",
// text
text: "#FFFFFF",
mutedText: "#9CA3AF",
placeholder: "#6B7280",
// brand
primary: "#4F46E5",
primaryPressed: "#4338CA",
// status
danger: "#EF4444",
// lines
border: "#22304A",
inputBg: "#0B1220",
} as const;

6
src/theme/radius.ts Normal file
View File

@@ -0,0 +1,6 @@
export const Radius = {
sm: 10,
md: 12,
lg: 16,
xl: 20,
} as const;

8
src/theme/spacing.ts Normal file
View File

@@ -0,0 +1,8 @@
export const Spacing = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
"2xl": 24,
} as const;

12
src/theme/theme.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Colors } from "./colors";
import { Radius } from "./radius";
import { Spacing } from "./spacing";
import { FontSize, FontWeight } from "./typography";
export const Theme = {
Colors,
Spacing,
Radius,
FontSize,
FontWeight,
} as const;

14
src/theme/typography.ts Normal file
View File

@@ -0,0 +1,14 @@
export const FontSize = {
sm: 13,
md: 15,
lg: 18,
xl: 22,
xxl: 26,
} as const;
export const FontWeight = {
regular: "400",
medium: "500",
semibold: "600",
bold: "700",
} as const;