Browse Source

로딩 기능 적용

jh-mac 7 months ago
parent
commit
acdcb45b2e
5 changed files with 117 additions and 32 deletions
  1. 35 22
      app/seach.tsx
  2. 27 0
      components/Loading.tsx
  3. 9 0
      constants/apiFields.ts
  4. 43 7
      service/api.ts
  5. 3 3
      types/song.ts

+ 35 - 22
app/seach.tsx

@@ -15,6 +15,8 @@ import {
 import { fetchSongs } from '@/service/api';
 import { addToFavoritesStorage } from '@/service/storage';
 import { Song, Criteria } from '@/types/song';
+import { API_FIELDS } from '@/constants/apiFields';
+import Loading from '@/components/Loading';
 
 interface Option {
   label: string;
@@ -23,19 +25,26 @@ interface Option {
 
 export default function SearchScreen() {
   const [keyword, setKeyword] = useState<string>('');
-  const [criteria, setCriteria] = useState<Criteria>('title');
+  const [criteria, setCriteria] = useState<Criteria>(API_FIELDS.TITLE);
   const [results, setResults] = useState<Song[]>([]);
   const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
+  const [isLoading, setIsLoading] = useState<boolean>(false); // 로딩 상태 추가
 
   const options: Option[] = [
-    { label: '제목', value: 'title' },
-    { label: '가수', value: 'artist' },
+    { label: '제목', value: API_FIELDS.TITLE },
+    { label: '가수', value: API_FIELDS.SINGER },
   ];
 
   const handleSearch = async (): Promise<void> => {
+    if (!keyword.trim()) return; // 빈 검색어 처리
+    setIsLoading(true); // 검색 시작 시 로딩 시작
     Keyboard.dismiss();
-    const filtered: Song[] = await fetchSongs(criteria, keyword);
-    setResults(filtered);
+    try {
+      const filtered: Song[] = await fetchSongs(criteria, keyword);
+      setResults(filtered);
+    } finally {
+      setIsLoading(false); // 검색 완료 시 로딩 종료
+    }
   };
 
   const handleAddToFavorites = async (song: Song): Promise<void> => {
@@ -96,9 +105,9 @@ export default function SearchScreen() {
           placeholder="검색어 입력"
           value={keyword}
           onChangeText={(text: string) => setKeyword(text)}
-          onSubmitEditing={handleSearch} // 엔터 키 입력 시 검색 실행
+          onSubmitEditing={handleSearch}
           style={styles.input}
-          returnKeyType="search" // 키보드의 엔터 키를 "검색" 버튼으로 표시
+          returnKeyType="search"
         />
 
         <TouchableOpacity onPress={handleSearch} style={styles.searchButton}>
@@ -106,21 +115,25 @@ export default function SearchScreen() {
         </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 }}
-      />
+      {isLoading ? (
+        <Loading /> // 로딩 중일 때 Loading 컴포넌트 표시
+      ) : (
+        <FlatList<Song>
+          data={results}
+          keyExtractor={(item) => item.no}
+          renderItem={({ item }) => (
+            <TouchableOpacity onPress={() => handleAddToFavorites(item)}>
+              <View style={styles.item}>
+                <Text>
+                  {item.no} - {item.title} ({item.singer})
+                </Text>
+              </View>
+            </TouchableOpacity>
+          )}
+          ListEmptyComponent={<Text>검색 결과가 없습니다.</Text>}
+          contentContainerStyle={{ flexGrow: 1 }}
+        />
+      )}
     </KeyboardAvoidingView>
   );
 }

+ 27 - 0
components/Loading.tsx

@@ -0,0 +1,27 @@
+import React from 'react';
+import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
+
+const Loading = () => {
+  return (
+    <View style={styles.container}>
+      <ActivityIndicator size="large" color="#007AFF" />
+      <Text style={styles.text}>로딩 중...</Text>
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'rgba(0, 0, 0, 0.2)',
+  },
+  text: {
+    marginTop: 10,
+    fontSize: 16,
+    color: '#000',
+  },
+});
+
+export default Loading;

+ 9 - 0
constants/apiFields.ts

@@ -0,0 +1,9 @@
+export const API_FIELDS = {
+  TITLE: 'title' as const,
+  SINGER: 'singer' as const,
+  NO: 'no' as const,
+  BRAND: 'brand' as const,
+  COMPOSER: 'composer' as const,
+  LYRICIST: 'lyricist' as const,
+  RELEASE: 'release' as const,
+};

+ 43 - 7
service/api.ts

@@ -1,14 +1,50 @@
-import songData from '@/data/song.json';
-import { Song, Criteria } from '@/types/song';
+/* src/service/api.ts */
+import {Criteria, Song} from '@/types/song';
+import {API_FIELDS} from '@/constants/apiFields';
+
+// 띄어쓰기를 무시하는 정규화 함수
+const normalizeText = (text: string): string => text.replace(/\s/g, '').toLowerCase();
+
+// API 응답 데이터 타입 정의
+interface ApiSongResponse {
+  [key: string]: string; // 동적 키를 위해 인덱스 시그니처 사용
+  [API_FIELDS.BRAND]: string;
+  [API_FIELDS.NO]: string;
+  [API_FIELDS.TITLE]: string;
+  [API_FIELDS.SINGER]: string;
+  [API_FIELDS.COMPOSER]: string;
+  [API_FIELDS.LYRICIST]: string;
+  [API_FIELDS.RELEASE]: string;
+}
 
 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;
+    if (!keyword.trim()) return []; // 빈 검색어 처리
+
+    // 검색 키워드 정규화 (띄어쓰기 제거)
+    const normalizedKeyword = normalizeText(keyword);
+
+    // criteria에 따라 적절한 엔드포인트 선택
+    const endpoint =
+      criteria === 'title'
+        ? `https://api.manana.kr/karaoke/song/${normalizedKeyword}.json`
+        : `https://api.manana.kr/karaoke/singer/${normalizedKeyword}.json`;
+
+    const response = await fetch(endpoint);
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+
+    const data: ApiSongResponse[] = await response.json();
+
+    // API 응답 데이터를 Song 타입으로 변환 (키를 상수로 정의)
+    return data.map((item) => ({
+      [API_FIELDS.NO]: item[API_FIELDS.NO],
+      [API_FIELDS.TITLE]: item[API_FIELDS.TITLE],
+      [API_FIELDS.SINGER]: item[API_FIELDS.SINGER],
+    }));
   } catch (error) {
     console.error('Error fetching songs:', error);
     return [];
   }
-};
+};

+ 3 - 3
types/song.ts

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