diff --git a/src/components/ui/AudioBookCard.tsx b/src/components/ui/AudioBookCard.tsx
new file mode 100644
index 0000000..9462894
--- /dev/null
+++ b/src/components/ui/AudioBookCard.tsx
@@ -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 (
+
+ {/* Top row */}
+
+
+
+ {title}
+
+
+
+ {meta}
+
+
+
+
+
+
+ {status === "GENERATING" ? (
+
+ ) : (
+
+ )}
+
+ {status === "GENERATING" ? (
+
+ ) : status === "READY" ? (
+
+ ) : null}
+
+
+
+ {/* Middle: generating progress */}
+ {status === "GENERATING" ? (
+
+
+
+
+
+ {typeof progress === "number"
+ ? `${Math.round(progress * 100)}% 진행`
+ : "작업 중…"}
+
+
+ ) : null}
+
+ {/* Bottom: failed actions */}
+ {showRetry ? (
+
+
+ 생성에 실패했어요. 다시 시도할까요?
+
+
+ 재시도
+
+
+ ) : null}
+
+ );
+}
+
+function Badge({
+ text,
+ bg,
+ color,
+}: {
+ text: string;
+ bg: string;
+ color: string;
+}) {
+ return (
+
+ {text}
+
+ );
+}
+
+function PlayPill() {
+ return (
+
+ ▶︎
+
+ );
+}
+
+function ProgressBar({
+ progress,
+ accent,
+}: {
+ progress?: number;
+ accent: string;
+}) {
+ const pct =
+ typeof progress === "number"
+ ? Math.max(0, Math.min(1, progress))
+ : undefined;
+
+ return (
+
+
+
+ );
+}
+
+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,
+ },
+});
diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx
new file mode 100644
index 0000000..d2a38dc
--- /dev/null
+++ b/src/components/ui/Card.tsx
@@ -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 & {
+ style?: StyleProp;
+ wrapperStyle?: StyleProp;
+ 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) => (
+
+ {hasAccent && (
+
+ )}
+
+ {pressed && !disabled && (
+
+ )}
+
+ {children}
+
+ );
+
+ if (onPress) {
+ return (
+
+ [
+ styles.pressableClip,
+ pressed && !disabled && styles.pressed,
+ ]}
+ android_ripple={{ color: "rgba(0,0,0,0.06)" }}
+ >
+ {({ pressed }) => Inner(pressed)}
+
+
+ );
+ }
+
+ return (
+
+ {Inner(false)}
+
+ );
+}
+
+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,
+ },
+});
diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx
index 0361f85..8d4d287 100644
--- a/src/screens/HomeScreen.tsx
+++ b/src/screens/HomeScreen.tsx
@@ -1,5 +1,7 @@
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 { Colors } from "../theme/colors";
@@ -7,6 +9,16 @@ export default function HomeScreen() {
const { signOut } = useAuth();
return (
+
+ feafwea
+ feafwea
+
+
fewfeaw
fewfeaw
@@ -20,6 +32,7 @@ const styles = StyleSheet.create({
backgroundColor: Colors.bg,
alignContent: "center",
justifyContent: "center",
+ padding: 8,
},
row: {
flexDirection: "row",
diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx
index b5912f7..2c26e74 100644
--- a/src/screens/LoginScreen.tsx
+++ b/src/screens/LoginScreen.tsx
@@ -1,8 +1,10 @@
+import { AntDesign, Ionicons } from "@expo/vector-icons";
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 { Platform, 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";
@@ -11,13 +13,26 @@ import { useAuth } from "../store/auth";
import { Theme } from "../theme/theme";
export default function LoginScreen() {
- const { signIn } = useAuth();
+ const { signIn /*, signInWithGoogle, signInWithApple */ } = useAuth();
const [loginId, setLoginId] = useState("");
const [password, setPassword] = useState("");
const canSubmit = loginId.trim().length > 0 && password.length > 0;
+
const navigation =
useNavigation>();
+ const onGooglePress = () => {
+ // TODO: 구현 후 연결
+ // signInWithGoogle();
+ console.log("Google login");
+ };
+
+ const onApplePress = () => {
+ // TODO: 구현 후 연결
+ // signInWithApple();
+ console.log("Apple login");
+ };
+
return (
@@ -57,10 +72,53 @@ export default function LoginScreen() {