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 { Text, type TextProps, type TextStyle } from "react-native";
import {
Text,
type StyleProp,
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[];
style?: StyleProp<TextStyle>;
};
export default function AppText({ variant = "body", style, ...props }: Props) {

View File

@@ -1,5 +1,5 @@
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 AppText from "./AppText";
@@ -12,6 +12,7 @@ export default function Button({
title,
variant = "primary",
style,
disabled,
...props
}: Props) {
const isPrimary = variant === "primary";
@@ -19,42 +20,42 @@ export default function Button({
return (
<Pressable
{...props}
disabled={disabled}
style={({ pressed }) => [
styles.base,
isPrimary ? styles.primary : styles.secondary,
pressed &&
// pressed 스타일(비활성화면 적용 안 함)
!disabled &&
pressed &&
(isPrimary ? styles.primaryPressed : styles.secondaryPressed),
// disabled 스타일
disabled && styles.disabled,
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>
<AppText
style={[
styles.text,
isPrimary ? styles.textPrimary : styles.textSecondary,
disabled && styles.textDisabled,
]}
>
{title}
</AppText>
</Pressable>
);
}
const styles = StyleSheet.create({
base: {
height: 48,
paddingVertical: 0,
paddingHorizontal: Theme.Spacing.lg,
paddingVertical: Theme.Spacing.md,
borderRadius: Theme.Radius.md,
alignItems: "center", // 가로 가운데
justifyContent: "center", // 세로 가운데
},
text: {
textAlign: "center",
fontWeight: Theme.FontWeight.bold,
lineHeight: 20,
alignItems: "center",
justifyContent: "center",
minHeight: 48,
},
primary: {
backgroundColor: Theme.Colors.primary,
@@ -70,4 +71,22 @@ const styles = StyleSheet.create({
secondaryPressed: {
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 React from "react";
import LoginScreen from "../screens/LoginScreen";
import SignUpScreen from "../screens/SignUpScreen";
export type AuthStackParamList = {
Login: undefined;
SignUp: undefined;
};
const Stack = createNativeStackNavigator<AuthStackParamList>();
@@ -12,6 +14,10 @@ export default function AuthStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Group screenOptions={{ presentation: "modal" }}>
<Stack.Screen name="SignUp" component={SignUpScreen} />
</Stack.Group>
</Stack.Navigator>
);
}

View File

@@ -1,7 +1,9 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { Button, StyleSheet, View } from "react-native";
import { useAuth } from "../store/auth";
export default function HomeScreen() {
const { signOut } = useAuth();
return (
<View style={styles.container}>
<Button
@@ -12,6 +14,7 @@ export default function HomeScreen() {
console.log(entries);
}}
/>
<Button title="logout" onPress={() => signOut()}></Button>
</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 { 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 { AuthStackParamList } from "../navigation/AuthStack";
import { useAuth } from "../store/auth";
import { Theme } from "../theme/theme";
@@ -12,6 +15,8 @@ export default function LoginScreen() {
const [loginId, setLoginId] = useState("");
const [password, setPassword] = useState("");
const canSubmit = loginId.trim().length > 0 && password.length > 0;
const navigation =
useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
return (
<SafeAreaView style={styles.safe}>
@@ -60,7 +65,7 @@ export default function LoginScreen() {
<Pressable
onPress={() => {
console.log("feawf");
navigation.navigate("SignUp");
}}
>
<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,
},
});