Browse Source

즐겨찾기 기능 구현

jh-mac 7 months ago
parent
commit
6b447ee2bc
5 changed files with 298 additions and 23 deletions
  1. 187 5
      app/favorites.tsx
  2. 2 3
      app/seach.tsx
  3. 101 0
      service/async-storage.ts
  4. 0 15
      service/storage.ts
  5. 8 0
      types/favorites.ts

+ 187 - 5
app/favorites.tsx

@@ -1,19 +1,201 @@
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import {
   View,
   Text,
   StyleSheet,
+  TouchableOpacity,
+  FlatList,
+  TextInput,
+  Modal,
+  Alert,
 } from 'react-native';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { AntDesign } from '@expo/vector-icons';
+import { Song } from '@/types/song';
+import {loadFavoriteFolders, saveFavoriteFolders} from "@/service/async-storage";
+
+interface Folder {
+  id: string;
+  name: string;
+  songs: Song[];
+  expanded: boolean;
+}
 
 export default function FavoriteScreen() {
+  const [folders, setFolders] = useState<Folder[]>([]);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [newFolderName, setNewFolderName] = useState('');
   
+  useEffect(() => {
+    loadFavoriteFolders().then((folders) => setFolders(folders))
+  }, []);
+
+  const toggleFolder = (id: string) => {
+    const updated = folders.map((f) =>
+      f.id === id ? { ...f, expanded: !f.expanded } : f
+    );
+    setFolders(updated);
+    saveFavoriteFolders(updated);
+  };
+
+  const addFolder = () => {
+    if (!newFolderName.trim()) return;
+
+    const newFolder: Folder = {
+      id: Date.now().toString(),
+      name: newFolderName.trim(),
+      songs: [],
+      expanded: false,
+    };
+    const updated = [...folders, newFolder];
+    setFolders(updated);
+    saveFavoriteFolders(updated);
+    setNewFolderName('');
+    setModalVisible(false);
+  };
+
+  const deleteFolder = (id: string) => {
+    Alert.alert('삭제 확인', '이 폴더를 삭제하시겠습니까?', [
+      { text: '취소', style: 'cancel' },
+      {
+        text: '삭제',
+        style: 'destructive',
+        onPress: () => {
+          const updated = folders.filter((f) => f.id !== id);
+          setFolders(updated);
+          saveFavoriteFolders(updated);
+        },
+      },
+    ]);
+  };
+
+  const renderFolder = ({ item }: { item: Folder }) => (
+    <View style={styles.folderContainer}>
+      <TouchableOpacity
+        onPress={() => toggleFolder(item.id)}
+        style={styles.folderHeader}
+      >
+        <Text style={styles.folderTitle}>{item.name}</Text>
+        <View style={{ flexDirection: 'row' }}>
+          <TouchableOpacity onPress={() => deleteFolder(item.id)}>
+            <AntDesign name="delete" size={20} color="gray" style={{ marginLeft: 10 }} />
+          </TouchableOpacity>
+          <AntDesign
+            name={item.expanded ? 'up' : 'down'}
+            size={16}
+            color="black"
+            style={{ marginLeft: 10 }}
+          />
+        </View>
+      </TouchableOpacity>
+
+      {item.expanded &&
+        item.songs.map((song) => (
+          <View key={song.no} style={styles.songItem}>
+            <Text style={styles.songText}>
+              🎵 {song.title} - {song.singer}
+            </Text>
+          </View>
+        ))}
+    </View>
+  );
+
   return (
-    <View>
-      <Text>⭐즐겨찾기</Text>
+    <View style={styles.container}>
+      <Text style={styles.title}>⭐ 즐겨찾기</Text>
+      <FlatList
+        data={folders}
+        renderItem={renderFolder}
+        keyExtractor={(item) => item.id}
+        contentContainerStyle={{ paddingBottom: 80 }}
+      />
+
+      <TouchableOpacity
+        style={styles.addButton}
+        onPress={() => setModalVisible(true)}
+      >
+        <Text style={styles.addButtonText}>+ 새 폴더 추가</Text>
+      </TouchableOpacity>
+
+      {/* 폴더 추가 모달 */}
+      <Modal visible={modalVisible} transparent animationType="slide">
+        <View style={styles.modalOverlay}>
+          <View style={styles.modalContent}>
+            <Text style={{ fontWeight: 'bold', marginBottom: 10 }}>폴더 이름 입력</Text>
+            <TextInput
+              placeholder="예: 나만의 명곡"
+              value={newFolderName}
+              onChangeText={setNewFolderName}
+              style={styles.input}
+            />
+            <View style={{ flexDirection: 'row', marginTop: 10 }}>
+              <TouchableOpacity
+                style={[styles.modalButton, { backgroundColor: '#ccc' }]}
+                onPress={() => setModalVisible(false)}
+              >
+                <Text>취소</Text>
+              </TouchableOpacity>
+              <TouchableOpacity
+                style={[styles.modalButton, { backgroundColor: '#4CAF50' }]}
+                onPress={addFolder}
+              >
+                <Text style={{ color: 'white' }}>확인</Text>
+              </TouchableOpacity>
+            </View>
+          </View>
+        </View>
+      </Modal>
     </View>
   );
 }
 
 const styles = StyleSheet.create({
-  
-});
+  container: { flex: 1, padding: 16 },
+  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 12 },
+  folderContainer: { marginBottom: 12, borderBottomWidth: 1, borderColor: '#ddd' },
+  folderHeader: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingVertical: 8,
+    alignItems: 'center',
+  },
+  folderTitle: { fontSize: 16, fontWeight: 'bold' },
+  songItem: { paddingLeft: 16, paddingVertical: 4 },
+  songText: { fontSize: 14 },
+  addButton: {
+    position: 'absolute',
+    bottom: 20,
+    left: 20,
+    right: 20,
+    backgroundColor: '#007AFF',
+    padding: 16,
+    borderRadius: 10,
+    alignItems: 'center',
+  },
+  addButtonText: { color: 'white', fontWeight: 'bold' },
+  modalOverlay: {
+    flex: 1,
+    backgroundColor: '#00000099',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  modalContent: {
+    width: 300,
+    backgroundColor: 'white',
+    borderRadius: 10,
+    padding: 16,
+  },
+  input: {
+    borderWidth: 1,
+    borderColor: '#ddd',
+    borderRadius: 6,
+    padding: 8,
+  },
+  modalButton: {
+    flex: 1,
+    padding: 10,
+    marginHorizontal: 5,
+    borderRadius: 6,
+    alignItems: 'center',
+  },
+});

+ 2 - 3
app/seach.tsx

@@ -11,10 +11,9 @@ import {
   Platform,
   Modal,
   Keyboard,
-  StatusBar
 } from 'react-native';
 import { fetchSongs } from '@/service/api';
-import { addToFavoritesStorage } from '@/service/storage';
+import { addToFavoritesStorage } from '@/service/async-storage';
 import { Song, Criteria } from '@/types/song';
 import { API_FIELDS } from '@/constants/apiFields';
 import Loading from '@/components/Loading';
@@ -125,7 +124,7 @@ export default function SearchScreen() {
           data={results}
           keyExtractor={(item) => item.no}
           renderItem={({ item }) => (
-            <TouchableOpacity onPress={() => handleAddToFavorites(item)}>
+            <TouchableOpacity onPress={() => {}}>
               <View style={styles.item}>
                 <Text>
                   {item.no} - {item.title} ({item.singer})

+ 101 - 0
service/async-storage.ts

@@ -0,0 +1,101 @@
+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;
+  }
+};
+
+export interface FavoriteFolder {
+  id: string;
+  name: string;
+  songs: Song[];
+  expanded: boolean;
+}
+
+const STORAGE_KEY = 'favoritesFolders';
+
+// 폴더 저장
+export const saveFavoriteFolders = async (folders: FavoriteFolder[]): Promise<void> => {
+  try {
+    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(folders));
+  } catch (error) {
+    console.error('Error saving favorite folders:', error);
+  }
+};
+
+// 폴더 불러오기
+export const loadFavoriteFolders = async (): Promise<FavoriteFolder[]> => {
+  try {
+    const stored = await AsyncStorage.getItem(STORAGE_KEY);
+    return stored ? JSON.parse(stored) : [];
+  } catch (error) {
+    console.error('Error loading favorite folders:', error);
+    return [];
+  }
+};
+
+// 폴더 초기화
+export const clearFavoriteFolders = async (): Promise<void> => {
+  try {
+    await AsyncStorage.removeItem(STORAGE_KEY);
+  } catch (error) {
+    console.error('Error clearing favorite folders:', error);
+  }
+};
+
+// 🔹 폴더에 노래 추가
+export const addSongToFolder = async (
+  folderId: string,
+  song: Song
+): Promise<FavoriteFolder[] | null> => {
+  try {
+    const folders = await loadFavoriteFolders();
+    const updated = folders.map((folder) => {
+      if (folder.id === folderId) {
+        const isDuplicate = folder.songs.some((s) => s.no === song.no);
+        if (!isDuplicate) {
+          return { ...folder, songs: [...folder.songs, song] };
+        }
+      }
+      return folder;
+    });
+    await saveFavoriteFolders(updated);
+    return updated;
+  } catch (error) {
+    console.error('Error adding song to folder:', error);
+    return null;
+  }
+};
+
+// 🔸 폴더에서 노래 삭제
+export const removeSongFromFolder = async (
+  folderId: string,
+  songNo: string
+): Promise<FavoriteFolder[] | null> => {
+  try {
+    const folders = await loadFavoriteFolders();
+    const updated = folders.map((folder) => {
+      if (folder.id === folderId) {
+        return {
+          ...folder,
+          songs: folder.songs.filter((s) => s.no !== songNo),
+        };
+      }
+      return folder;
+    });
+    await saveFavoriteFolders(updated);
+    return updated;
+  } catch (error) {
+    console.error('Error removing song from folder:', error);
+    return null;
+  }
+};

+ 0 - 15
service/storage.ts

@@ -1,15 +0,0 @@
-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;
-  }
-};

+ 8 - 0
types/favorites.ts

@@ -0,0 +1,8 @@
+import {Song} from "@/types/song";
+
+export interface Folder {
+  id: string;
+  name: string;
+  songs: Song[];
+  expanded: boolean;
+}