feat: 회원가입 화면 생성

This commit is contained in:
2026-01-01 19:23:19 +09:00
parent 8829a44b88
commit 014cb61734
6 changed files with 185 additions and 26 deletions

View File

@@ -1,12 +1,17 @@
import React from "react"; import React from "react";
import { Text, type TextProps, type TextStyle } from "react-native"; import {
Text,
type StyleProp,
type TextProps,
type TextStyle,
} from "react-native";
import { Theme } from "../../theme/theme"; import { Theme } from "../../theme/theme";
type Variant = "logo" | "title" | "body" | "muted" | "error"; type Variant = "logo" | "title" | "body" | "muted" | "error";
type Props = TextProps & { type Props = TextProps & {
variant?: Variant; variant?: Variant;
style?: TextStyle | TextStyle[]; style?: StyleProp<TextStyle>;
}; };
export default function AppText({ variant = "body", style, ...props }: Props) { export default function AppText({ variant = "body", style, ...props }: Props) {

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Pressable, type PressableProps, StyleSheet, View } from "react-native"; import { Pressable, type PressableProps, StyleSheet } from "react-native";
import { Theme } from "../../theme/theme"; import { Theme } from "../../theme/theme";
import AppText from "./AppText"; import AppText from "./AppText";
@@ -12,6 +12,7 @@ export default function Button({
title, title,
variant = "primary", variant = "primary",
style, style,
disabled,
...props ...props
}: Props) { }: Props) {
const isPrimary = variant === "primary"; const isPrimary = variant === "primary";
@@ -19,42 +20,42 @@ export default function Button({
return ( return (
<Pressable <Pressable
{...props} {...props}
disabled={disabled}
style={({ pressed }) => [ style={({ pressed }) => [
styles.base, styles.base,
isPrimary ? styles.primary : styles.secondary, isPrimary ? styles.primary : styles.secondary,
pressed &&
// pressed 스타일(비활성화면 적용 안 함)
!disabled &&
pressed &&
(isPrimary ? styles.primaryPressed : styles.secondaryPressed), (isPrimary ? styles.primaryPressed : styles.secondaryPressed),
// disabled 스타일
disabled && styles.disabled,
typeof style === "function" ? style({ pressed }) : style, typeof style === "function" ? style({ pressed }) : style,
]} ]}
> >
<View> <AppText
<AppText style={[
style={{ styles.text,
textAlign: "center", isPrimary ? styles.textPrimary : styles.textSecondary,
fontWeight: Theme.FontWeight.bold, disabled && styles.textDisabled,
color: isPrimary ? Theme.Colors.text : Theme.Colors.text, ]}
}} >
> {title}
{title} </AppText>
</AppText>
</View>
</Pressable> </Pressable>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
height: 48, paddingVertical: Theme.Spacing.md,
paddingVertical: 0,
paddingHorizontal: Theme.Spacing.lg,
borderRadius: Theme.Radius.md, borderRadius: Theme.Radius.md,
alignItems: "center", // 가로 가운데 alignItems: "center",
justifyContent: "center", // 세로 가운데 justifyContent: "center",
}, minHeight: 48,
text: {
textAlign: "center",
fontWeight: Theme.FontWeight.bold,
lineHeight: 20,
}, },
primary: { primary: {
backgroundColor: Theme.Colors.primary, backgroundColor: Theme.Colors.primary,
@@ -70,4 +71,22 @@ const styles = StyleSheet.create({
secondaryPressed: { secondaryPressed: {
opacity: 0.9, opacity: 0.9,
}, },
disabled: {
opacity: 0.5,
},
text: {
textAlign: "center",
fontWeight: Theme.FontWeight.bold,
},
textPrimary: {
color: Theme.Colors.text,
},
textSecondary: {
color: Theme.Colors.text,
},
textDisabled: {
color: Theme.Colors.mutedText,
},
}); });

View File

@@ -1,9 +1,11 @@
import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { createNativeStackNavigator } from "@react-navigation/native-stack";
import React from "react"; import React from "react";
import LoginScreen from "../screens/LoginScreen"; import LoginScreen from "../screens/LoginScreen";
import SignUpScreen from "../screens/SignUpScreen";
export type AuthStackParamList = { export type AuthStackParamList = {
Login: undefined; Login: undefined;
SignUp: undefined;
}; };
const Stack = createNativeStackNavigator<AuthStackParamList>(); const Stack = createNativeStackNavigator<AuthStackParamList>();
@@ -12,6 +14,10 @@ export default function AuthStack() {
return ( return (
<Stack.Navigator screenOptions={{ headerShown: false }}> <Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} /> <Stack.Screen name="Login" component={LoginScreen} />
<Stack.Group screenOptions={{ presentation: "modal" }}>
<Stack.Screen name="SignUp" component={SignUpScreen} />
</Stack.Group>
</Stack.Navigator> </Stack.Navigator>
); );
} }

View File

@@ -1,7 +1,9 @@
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { Button, StyleSheet, View } from "react-native"; import { Button, StyleSheet, View } from "react-native";
import { useAuth } from "../store/auth";
export default function HomeScreen() { export default function HomeScreen() {
const { signOut } = useAuth();
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Button <Button
@@ -12,6 +14,7 @@ export default function HomeScreen() {
console.log(entries); console.log(entries);
}} }}
/> />
<Button title="logout" onPress={() => signOut()}></Button>
</View> </View>
); );
} }

View File

@@ -1,9 +1,12 @@
import { useNavigation } from "@react-navigation/native";
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 { 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";
import { AuthStackParamList } from "../navigation/AuthStack";
import { useAuth } from "../store/auth"; import { useAuth } from "../store/auth";
import { Theme } from "../theme/theme"; import { Theme } from "../theme/theme";
@@ -12,6 +15,8 @@ export default function LoginScreen() {
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 =
useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
return ( return (
<SafeAreaView style={styles.safe}> <SafeAreaView style={styles.safe}>
@@ -60,7 +65,7 @@ export default function LoginScreen() {
<Pressable <Pressable
onPress={() => { onPress={() => {
console.log("feawf"); navigation.navigate("SignUp");
}} }}
> >
<AppText variant="muted" style={styles.signupLine}> <AppText variant="muted" style={styles.signupLine}>

View File

@@ -0,0 +1,121 @@
import { useNavigation } from "@react-navigation/native";
import React, { useMemo, useState } from "react";
import { 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 SignUpScreen() {
const navigation = useNavigation();
const [loginId, setLoginId] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirm, setPasswordConfirm] = useState("");
const { canSubmit, errorText } = useMemo(() => {
const idOk = loginId.trim().length >= 4;
const pwOk = password.length >= 8;
const match = password.length > 0 && password === passwordConfirm;
const can = idOk && pwOk && match;
let err = "";
if (loginId.length > 0 && !idOk)
err = "Username must be at least 4 characters.";
else if (password.length > 0 && !pwOk)
err = "Password must be at least 8 characters.";
else if (passwordConfirm.length > 0 && !match)
err = "Passwords do not match.";
return { canSubmit: can, errorText: err };
}, [loginId, password, passwordConfirm]);
const onSignUp = async () => {
navigation.goBack();
};
return (
<SafeAreaView style={styles.safe}>
<View style={styles.container}>
<AppText variant="title">Create your account</AppText>
<AppText variant="muted">Start listening in minutes</AppText>
<View style={{ height: Theme.Spacing.lg }} />
<Input
placeholder="Username"
autoCapitalize="none"
autoCorrect={false}
textContentType="username"
value={loginId}
onChangeText={setLoginId}
returnKeyType="next"
/>
<View style={{ height: Theme.Spacing.sm }} />
<Input
placeholder="Password"
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
textContentType="newPassword"
value={password}
onChangeText={setPassword}
returnKeyType="next"
/>
<View style={{ height: Theme.Spacing.sm }} />
<Input
placeholder="Confirm password"
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
textContentType="newPassword"
value={passwordConfirm}
onChangeText={setPasswordConfirm}
returnKeyType="done"
onSubmitEditing={() => {
if (canSubmit) onSignUp();
}}
/>
{errorText ? (
<AppText variant="muted" style={styles.error}>
{errorText}
</AppText>
) : null}
<View style={{ height: Theme.Spacing.lg }} />
<Button title="Sign up" disabled={!canSubmit} onPress={onSignUp} />
<View style={{ height: Theme.Spacing.sm }} />
<Button
title="Back"
variant="secondary"
onPress={() => navigation.goBack()}
/>
</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,
},
error: {
textAlign: "center",
color: Theme.Colors.mutedText,
},
});