jh-mac 7 сар өмнө
parent
commit
f867ba5d22

+ 5 - 3
app/_layout.tsx

@@ -1,5 +1,7 @@
-import { Stack } from "expo-router";
+import { Stack } from 'expo-router';
 
-export default function RootLayout() {
-  return <Stack />;
+export default function Layout() {
+  return (
+    <Stack screenOptions={{ headerShown: false }} />
+  );
 }

+ 137 - 0
app/backup

@@ -0,0 +1,137 @@
+import React, { useState } from 'react';
+import {
+  View,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  FlatList,
+  StyleSheet,
+} from 'react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { useRouter } from 'expo-router';
+
+interface Song {
+  id: string;
+  title: string;
+  artist: string;
+}
+
+const MOCK_SONGS: Song[] = [
+  { id: '1234', title: '좋은 날', artist: '아이유' },
+  { id: '5678', title: '취중진담', artist: '김동률' },
+  { id: '9101', title: 'LOVE DIVE', artist: 'IVE' },
+  { id: '1121', title: 'Permission to Dance', artist: 'BTS' },
+];
+
+export default function MainScreen() {
+  const [keyword, setKeyword] = useState('');
+  const [results, setResults] = useState<Song[]>([]);
+  const router = useRouter();
+
+  const handleSearch = () => {
+    const filtered = MOCK_SONGS.filter(
+      song =>
+        song.title.includes(keyword) ||
+        song.artist.includes(keyword)
+    );
+    setResults(filtered);
+  };
+
+  const handleAddToFavorites = async (song: Song) => {
+    const stored = await AsyncStorage.getItem('favorites');
+    const parsed = stored ? JSON.parse(stored) : [];
+    await AsyncStorage.setItem('favorites', JSON.stringify([...parsed, song]));
+  };
+
+  return (
+    <View style={styles.container}>
+      <View style={styles.navbar}>
+        <TouchableOpacity onPress={() => {}}>
+          <Text style={styles.icon}>👤</Text>
+        </TouchableOpacity>
+        <TouchableOpacity onPress={() => {}}>
+          <Text style={styles.icon}>⭐️</Text>
+        </TouchableOpacity>
+        <TouchableOpacity onPress={() => {}}>
+          <Text style={styles.icon}>⚙️</Text>
+        </TouchableOpacity>
+      </View>
+
+      {/* 타이틀 */}
+      <Text style={styles.title}>Sing Song</Text>
+
+      {/* 검색 영역 */}
+      <TextInput
+        placeholder="제목 또는 가수를 입력하세요"
+        value={keyword}
+        onChangeText={setKeyword}
+        style={styles.input}
+      />
+      <TouchableOpacity onPress={handleSearch} style={styles.searchButton}>
+        <Text style={styles.searchText}>검색</Text>
+      </TouchableOpacity>
+
+      {/* 검색 결과 리스트 */}
+      <FlatList
+        data={results}
+        keyExtractor={(item) => item.id}
+        renderItem={({ item }) => (
+          <TouchableOpacity onPress={() => handleAddToFavorites(item)}>
+            <View style={styles.item}>
+              <Text>{item.id} - {item.title} ({item.artist})</Text>
+            </View>
+          </TouchableOpacity>
+        )}
+        ListEmptyComponent={<Text>검색 결과가 없습니다.</Text>}
+      />
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    padding: 20,
+    backgroundColor: '#fff',
+  },
+  navbar: {
+    flexDirection: 'row',
+    justifyContent: 'flex-end',
+    gap: 16,
+    marginBottom: 8,
+  },
+  icon: {
+    fontSize: 20,
+  },
+  title: {
+    fontSize: 24,
+    fontWeight: 'bold',
+    marginBottom: 16,
+  },
+  settings: {
+    fontSize: 20,
+  },
+  input: {
+    borderWidth: 1,
+    borderColor: '#ccc',
+    borderRadius: 8,
+    padding: 8,
+    marginBottom: 10,
+  },
+  searchButton: {
+    backgroundColor: '#007AFF',
+    padding: 10,
+    borderRadius: 8,
+    alignItems: 'center',
+    marginBottom: 16,
+  },
+  searchText: {
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  item: {
+    padding: 12,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+});

+ 30 - 6
app/main.tsx

@@ -1,14 +1,38 @@
-import { View, Text, StyleSheet } from 'react-native';
+import React, { useState } from 'react';
+import { View, Text, SafeAreaView, StyleSheet } from 'react-native';
+import SearchScreen from '@/app/seach.tsx';
+import BottomTabBar from '@/components/BottomTabBar';
 
 export default function MainScreen() {
+  const [tab, setTab] = useState<'search' | 'favorites' | 'settings'>('search');
+
+  const renderContent = () => {
+    switch (tab) {
+      case 'search':
+        return <SearchScreen />;
+      case 'favorites':
+        return <Text>⭐ 즐겨찾기 화면</Text>;
+      case 'settings':
+        return <Text>👤 내 설정 화면</Text>;
+    }
+  };
+
   return (
-    <View style={styles.container}>
-      <Text style={styles.text}>🎉 메인 화면입니다 (게스트 진입 완료)</Text>
-    </View>
+    <SafeAreaView style={styles.container}>
+      <View style={styles.content}>{renderContent()}</View>
+      <BottomTabBar activeTab={tab} onTabChange={setTab} />
+    </SafeAreaView>
   );
 }
 
 const styles = StyleSheet.create({
-  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
-  text: { fontSize: 20, fontWeight: 'bold' },
+  container: {
+    flex: 1,
+    backgroundColor: '#fff',
+  },
+  content: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
 });

+ 211 - 0
app/seach.tsx

@@ -0,0 +1,211 @@
+import React, { useState } from 'react';
+import {
+  View,
+  Text,
+  TextInput,
+  TouchableOpacity,
+  FlatList,
+  StyleSheet,
+  Dimensions,
+  KeyboardAvoidingView,
+  Platform,
+  Modal,
+} from 'react-native';
+import { fetchSongs } from '@/service/api';
+import { addToFavoritesStorage } from '@/service/storage';
+import { Song, Criteria } from '@/types/song';
+
+interface Option {
+  label: string;
+  value: Criteria;
+}
+
+export default function SearchScreen() {
+  const [keyword, setKeyword] = useState<string>('');
+  const [criteria, setCriteria] = useState<Criteria>('title');
+  const [results, setResults] = useState<Song[]>([]);
+  const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
+
+  const options: Option[] = [
+    { label: '제목', value: 'title' },
+    { label: '가수', value: 'artist' },
+  ];
+
+  const handleSearch = async (): Promise<void> => {
+    const filtered: Song[] = await fetchSongs(criteria, keyword);
+    setResults(filtered);
+  };
+
+  const handleAddToFavorites = async (song: Song): Promise<void> => {
+    const success: boolean = await addToFavoritesStorage(song);
+    if (!success) {
+      console.log('Failed to add to favorites');
+    }
+  };
+
+  const handleSelectOption = (value: Criteria): void => {
+    setCriteria(value);
+    setIsDropdownVisible(false);
+  };
+
+  return (
+    <KeyboardAvoidingView
+      style={styles.container}
+      behavior={Platform.OS === 'ios' ? 'padding' : undefined}
+    >
+      <View style={styles.searchRow}>
+        <View style={styles.dropdownWrapper}>
+          <TouchableOpacity
+            style={styles.dropdownButton}
+            onPress={() => setIsDropdownVisible(!isDropdownVisible)}
+          >
+            <Text style={styles.dropdownText}>
+              {options.find((opt) => opt.value === criteria)?.label || '선택'}
+            </Text>
+            <Text style={styles.dropdownArrow}>▼</Text>
+          </TouchableOpacity>
+
+          <Modal
+            transparent
+            visible={isDropdownVisible}
+            animationType="fade"
+            onRequestClose={() => setIsDropdownVisible(false)}
+          >
+            <TouchableOpacity
+              style={styles.modalOverlay}
+              onPress={() => setIsDropdownVisible(false)}
+            >
+              <View style={styles.dropdownMenu}>
+                {options.map((option) => (
+                  <TouchableOpacity
+                    key={option.value}
+                    style={styles.dropdownItem}
+                    onPress={() => handleSelectOption(option.value)}
+                  >
+                    <Text style={styles.dropdownItemText}>{option.label}</Text>
+                  </TouchableOpacity>
+                ))}
+              </View>
+            </TouchableOpacity>
+          </Modal>
+        </View>
+
+        <TextInput
+          placeholder="검색어 입력"
+          value={keyword}
+          onChangeText={(text: string) => setKeyword(text)}
+          style={styles.input}
+        />
+
+        <TouchableOpacity onPress={handleSearch} style={styles.searchButton}>
+          <Text style={styles.searchText}>검색</Text>
+        </TouchableOpacity>
+      </View>
+
+      <FlatList<Song>
+        data={results}
+        keyExtractor={(item) => item.id}
+        renderItem={({ item }) => (
+          <TouchableOpacity onPress={() => handleAddToFavorites(item)}>
+            <View style={styles.item}>
+              <Text>
+                {item.id} - {item.title} ({item.artist})
+              </Text>
+            </View>
+          </TouchableOpacity>
+        )}
+        ListEmptyComponent={<Text>검색 결과가 없습니다.</Text>}
+        contentContainerStyle={{ flexGrow: 1 }}
+      />
+    </KeyboardAvoidingView>
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    padding: 16,
+  },
+  searchRow: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 16,
+    width: '100%',
+  },
+  dropdownWrapper: {
+    width: Dimensions.get('window').width * 0.3 - 24,
+    marginRight: 8,
+  },
+  dropdownButton: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    borderWidth: 1,
+    borderColor: '#ccc',
+    borderRadius: 8,
+    paddingHorizontal: 10,
+    paddingVertical: 10,
+    height: 40,
+    backgroundColor: '#fff',
+  },
+  dropdownText: {
+    fontSize: 16,
+    color: '#000',
+  },
+  dropdownArrow: {
+    fontSize: 12,
+    color: '#000',
+  },
+  modalOverlay: {
+    flex: 1,
+    backgroundColor: 'rgba(0, 0, 0, 0.2)',
+    justifyContent: 'flex-start',
+    paddingTop: 120,
+    paddingHorizontal: 16,
+  },
+  dropdownMenu: {
+    backgroundColor: '#fff',
+    borderRadius: 8,
+    width: Dimensions.get('window').width * 0.3 - 24,
+    elevation: 5,
+    shadowColor: '#000',
+    shadowOffset: { width: 0, height: 2 },
+    shadowOpacity: 0.2,
+    shadowRadius: 4,
+  },
+  dropdownItem: {
+    paddingVertical: 10,
+    paddingHorizontal: 10,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+  },
+  dropdownItemText: {
+    fontSize: 16,
+    color: '#000',
+  },
+  input: {
+    flex: 1,
+    height: 40,
+    borderWidth: 1,
+    borderColor: '#ccc',
+    borderRadius: 8,
+    paddingHorizontal: 8,
+    marginRight: 8,
+  },
+  searchButton: {
+    backgroundColor: '#007AFF',
+    paddingHorizontal: 8,
+    paddingVertical: 10,
+    borderRadius: 8,
+  },
+  searchText: {
+    color: '#fff',
+    fontWeight: 'bold',
+  },
+  item: {
+    padding: 12,
+    borderBottomWidth: 1,
+    borderBottomColor: '#eee',
+    width: Dimensions.get('window').width - 32,
+  },
+});

BIN
assets/background.png


+ 53 - 0
components/BottomTabBar.tsx

@@ -0,0 +1,53 @@
+import React from 'react';
+import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
+import { FontAwesome } from '@expo/vector-icons';
+
+type TabType = 'search' | 'favorites' | 'settings';
+
+interface BottomTabBarProps {
+  activeTab: TabType;
+  onTabChange: (tab: TabType) => void;
+}
+
+export default function BottomTabBar({ activeTab, onTabChange }: BottomTabBarProps) {
+  return (
+    <View style={styles.tabBar}>
+      <TouchableOpacity onPress={() => onTabChange('search')} style={styles.tabItem}>
+        <FontAwesome name="search" size={24} color={activeTab === 'search' ? '#007AFF' : '#666'} />
+        <Text style={activeTab === 'search' ? styles.activeText : styles.inactiveText}>노래검색</Text>
+      </TouchableOpacity>
+      <TouchableOpacity onPress={() => onTabChange('favorites')} style={styles.tabItem}>
+        <FontAwesome name="star" size={24} color={activeTab === 'favorites' ? '#007AFF' : '#666'} />
+        <Text style={activeTab === 'favorites' ? styles.activeText : styles.inactiveText}>즐겨찾기</Text>
+      </TouchableOpacity>
+      <TouchableOpacity onPress={() => onTabChange('settings')} style={styles.tabItem}>
+        <FontAwesome name="user" size={24} color={activeTab === 'settings' ? '#007AFF' : '#666'} />
+        <Text style={activeTab === 'settings' ? styles.activeText : styles.inactiveText}>마이페이지</Text>
+      </TouchableOpacity>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+  tabBar: {
+    flexDirection: 'row',
+    justifyContent: 'space-around',
+    paddingVertical: 12,
+    paddingBottom: 0,
+    backgroundColor: '#fff',
+    borderTopWidth: 0,
+  },
+  tabItem: {
+    alignItems: 'center',
+  },
+  activeText: {
+    fontSize: 12,
+    color: '#007AFF',
+    marginTop: 4,
+  },
+  inactiveText: {
+    fontSize: 12,
+    color: '#666',
+    marginTop: 4,
+  },
+});

+ 27 - 0
data/song.json

@@ -0,0 +1,27 @@
+[
+  {"id": "1", "title": "좋은 날", "artist": "아이유"},
+  {"id": "2", "title": "좋은 날", "artist": "아이유"},
+  {"id": "3", "title": "좋은 날", "artist": "아이유"},
+  {"id": "4", "title": "좋은 날", "artist": "아이유"},
+  {"id": "5", "title": "좋은 날", "artist": "아이유"},
+  {"id": "6", "title": "좋은 날", "artist": "아이유"},
+  {"id": "7", "title": "좋은 날", "artist": "아이유"},
+  {"id": "8", "title": "좋은 날", "artist": "아이유"},
+  {"id": "9", "title": "좋은 날", "artist": "아이유"},
+  {"id": "10", "title": "좋은 날", "artist": "아이유"},
+  {"id": "11", "title": "좋은 날", "artist": "아이유"},
+  {"id": "12", "title": "좋은 날", "artist": "아이유"},
+  {"id": "13", "title": "좋은 날", "artist": "아이유"},
+  {"id": "14", "title": "좋은 날", "artist": "아이유"},
+  {"id": "15", "title": "좋은 날", "artist": "아이유"},
+  {"id": "16", "title": "좋은 날", "artist": "아이유"},
+  {"id": "17", "title": "좋은 날", "artist": "아이유"},
+  {"id": "18", "title": "좋은 날", "artist": "아이유"},
+  {"id": "19", "title": "좋은 날", "artist": "아이유"},
+  {"id": "20", "title": "좋은 날", "artist": "아이유"},
+  {"id": "21", "title": "좋은 날", "artist": "아이유"},
+  {"id": "22", "title": "좋은 날", "artist": "아이유"},
+  {"id": "23", "title": "취중진담", "artist": "김동률"},
+  {"id": "24", "title": "LOVE DIVE", "artist": "IVE"},
+  {"id": "25", "title": "Permission to Dance", "artist": "BTS"}
+]

+ 96 - 0
docs/기능명세서/MainScreen.md

@@ -0,0 +1,96 @@
+# 기능 명세서: 메인 화면 (`main.tsx`)
+
+## 📌 개요
+
+인트로 화면에서 "게스트로 시작하기"를 선택한 사용자에게 표시되는 **홈 화면**입니다. 사용자는 노래 검색 및 즐겨찾기 기능을 사용할 수 있으며, 환경설정 및 후원 기능도 포함될 예정입니다.
+
+---
+
+## 🛠️ 사용 기술/라이브러리
+
+| 기술/라이브러리                     | 설명                                   |
+| ---------------------------- | ------------------------------------ |
+| React Native                 | 기본 컴포넌트(View, Text, ScrollView 등) 사용 |
+| Expo Router                  | 파일 기반 라우팅 (`main.tsx`)               |
+| TypeScript                   | 정적 타입 검사                             |
+| AsyncStorage                 | 즐겨찾기 등 사용자 로컬 데이터 저장에 사용             |
+| React Navigation             | 스택 내비게이션 구성 예정                       |
+| FlatList                     | 노래 리스트 및 페이지네이션 구현에 사용               |
+| Gesture Handler & Reanimated | 스와이프 기능 구현                           |
+| StyleSheet 또는 Tailwind CSS   | UI 스타일 구성                            |
+
+---
+
+## 🧭 사용자 흐름
+
+1. 사용자가 인트로 화면에서 "게스트로 시작하기" 선택 시 홈 화면(`main.tsx`)으로 진입
+2. 상단에는 앱 타이틀 및 환경설정 버튼 표시
+3. 메인 영역에서 다음 기능들을 사용 가능:
+
+    * 노래방 곡 검색 기능
+    * 카테고리별 즐겨찾기 등록 및 관리
+    * 공지사항 확인 영역
+    * 후원(Donate) 버튼 표시
+
+---
+
+## 📱 UI 구성 요소
+
+| 요소         | 유형                                 | 설명                                     |
+| ---------- | ---------------------------------- | -------------------------------------- |
+| 상단 바       | `View`, `Text`, `TouchableOpacity` | 앱 타이틀, 환경설정 아이콘 포함                     |
+| 노래 검색 영역   | `TextInput`, `Button`              | 제목/가수 키워드 검색 입력 및 검색 실행                |
+| 검색 결과 리스트  | `FlatList`                         | 곡번호, 제목, 가수 형태로 표시. 스와이프 가능한 페이지네이션 포함 |
+| 즐겨찾기/폴더 영역 | `ScrollView`, `TouchableOpacity`   | 관심사별 노래 그룹핑 저장 UI                      |
+| 공지사항 영역    | `View`, `Text`                     | 상단 또는 리스트 상단에 고정 공지 형태로 노출             |
+| Donate 버튼  | `TouchableOpacity`                 | 하단 고정 혹은 사이드 버튼으로 후원 요청 액션 연결 예정       |
+
+---
+
+## ⚙️ 개발 세부 명세
+
+### 노래방 번호 검색 기능
+
+* 키워드 입력 시 제목 또는 가수 기준으로 로컬에서 검색
+* 검색 버튼 클릭 시 `FlatList`에 검색 결과 표시
+* 스와이프 기반 페이지 전환 구현 예정
+
+### 즐겨찾기/카테고리 기능
+
+* 폴더(또는 태그) 기반으로 노래 저장 가능
+* 검색 리스트 아이템을 눌러 해당 카테고리에 저장 가능
+* 저장 구조는 AsyncStorage에 JSON 형태로 저장
+
+### 공지사항 기능
+
+* 공지 텍스트를 별도 영역 또는 검색창 위에 노출
+* 서버 연동 가능성이 있으므로, 후속 작업에 대비하여 컴포넌트 분리 구조로 설계
+
+### 상단 네비게이션 바 UI
+* 👤, ⭐️, ⚙️ 아이콘을 오른쪽 끝 정렬
+* 앱 타이틀(Sing Song)은 네비게이션 바 아래에 위치
+
+### 환경설정 버튼
+
+* 우측 상단에 위치
+* 아이콘 버튼으로 표시하며, 현재는 비동작 처리
+
+### 후원(Donate) 버튼
+
+* 화면 하단 고정 또는 Floating 버튼 형태로 배치 고려
+* 클릭 시 안내 모달 또는 외부 링크 이동 (후속 개발 예정)
+
+---
+
+## ✅ 체크리스트
+
+* [x] 기본 레이아웃 구성
+* [x] 환경설정 버튼 UI 추가
+* [x] 노래 검색 영역 구성
+* [x] FlatList를 이용한 결과 리스트 렌더링
+* [x] 카테고리/즐겨찾기 UI 기본 틀 마련
+* [ ] 스와이프 기반 페이지 전환 기능
+* [ ] 공지사항 및 Donate 영역 UI 추가 및 기획
+* [ ] 상태 저장을 위한 AsyncStorage 연동
+* [ ] 검색, 저장, 불러오기 로직 구현
+* [ ] UI 반응형 대응 및 스타일링 마무리

+ 0 - 42
docs/기능명세서/main.md

@@ -1,42 +0,0 @@
-# 기능 명세서: 메인 화면 (main.tsx)
-
-## 📌 개요
-인트로 화면에서 `"게스트로 시작하기"`를 선택한 사용자에게 표시되는 메인 화면입니다. 현재는 간단한 텍스트 안내만 있으며, 이후 노래방 기능들이 확장될 공간입니다.
-
----
-
-## 🛠️ 사용 기술/라이브러리
-
-| 기술/라이브러리    | 설명                         |
-|---------------------|------------------------------|
-| React Native        | View 및 Text 컴포넌트 사용   |
-| Expo Router         | 파일 기반 라우팅 (`main.tsx`) |
-| TypeScript          | 정적 타입 설정               |
-| StyleSheet API      | 기본 스타일 구성             |
-
----
-
-## 📱 UI 구성 요소
-
-| 요소      | 유형      | 설명                                       |
-|-----------|-----------|--------------------------------------------|
-| `View`    | 레이아웃   | 화면 중앙 정렬, 배경 등 컨테이너 역할         |
-| `Text`    | 텍스트    | "메인 화면입니다" 메시지 출력                  |
-| `StyleSheet` | 스타일 | 가운데 정렬 및 텍스트 강조 스타일링 적용       |
-
----
-
-## ⚙️ 개발 세부 명세
-
-- `View` 컴포넌트로 전체 화면을 차지하며 중앙 정렬 적용
-- `Text`를 통해 메인 화면 진입 메시지를 강조 표시
-- 이후 이 영역에 마이크 입력, 노래 추천, 즐겨찾기 등 기능 확장 예정
-
----
-
-## ✅ 체크리스트
-
-- [x] 텍스트 메시지 정상 출력
-- [x] 중앙 정렬 확인
-- [x] Expo Router 기반의 라우팅 정상 동작 확인
-- [ ] 기능 확장 전 초기 상태 UI 구성 완료

+ 4 - 2
package.json

@@ -12,6 +12,8 @@
   },
   "dependencies": {
     "@expo/vector-icons": "^14.1.0",
+    "@react-native-async-storage/async-storage": "^2.1.2",
+    "@react-native-picker/picker": "^2.11.0",
     "@react-navigation/bottom-tabs": "^7.3.10",
     "@react-navigation/elements": "^2.3.8",
     "@react-navigation/native": "^7.1.6",
@@ -41,9 +43,9 @@
   "devDependencies": {
     "@babel/core": "^7.25.2",
     "@types/react": "~19.0.10",
-    "typescript": "~5.8.3",
     "eslint": "^9.25.0",
-    "eslint-config-expo": "~9.2.0"
+    "eslint-config-expo": "~9.2.0",
+    "typescript": "~5.8.3"
   },
   "private": true
 }

+ 14 - 0
service/api.ts

@@ -0,0 +1,14 @@
+import songData from '@/data/song.json';
+import { Song, Criteria } from '@/types/song';
+
+export const fetchSongs = async (criteria: Criteria, keyword: string): Promise<Song[]> => {
+  try {
+    const filtered = songData.filter((song: Song) =>
+      song[criteria].toLowerCase().includes(keyword.toLowerCase())
+    );
+    return filtered;
+  } catch (error) {
+    console.error('Error fetching songs:', error);
+    return [];
+  }
+};

+ 15 - 0
service/storage.ts

@@ -0,0 +1,15 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { Song } from '@/types/song';
+
+export const addToFavoritesStorage = async (song: Song): Promise<boolean> => {
+  try {
+    const stored = await AsyncStorage.getItem('favorites');
+    const parsed: Song[] = stored ? JSON.parse(stored) : [];
+    const updatedFavorites = [...parsed, song];
+    await AsyncStorage.setItem('favorites', JSON.stringify(updatedFavorites));
+    return true;
+  } catch (error) {
+    console.error('Error adding to favorites:', error);
+    return false;
+  }
+};

+ 7 - 0
types/song.ts

@@ -0,0 +1,7 @@
+export interface Song {
+  id: string;
+  title: string;
+  artist: string;
+}
+
+export type Criteria = 'title' | 'artist';