seach.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import React, { useState } from 'react';
  2. import {
  3. View,
  4. Text,
  5. TextInput,
  6. TouchableOpacity,
  7. FlatList,
  8. StyleSheet,
  9. Dimensions,
  10. KeyboardAvoidingView,
  11. Platform,
  12. Modal,
  13. Keyboard,
  14. } from 'react-native';
  15. import { fetchSongs } from '@/service/api';
  16. import { addToFavoritesStorage } from '@/service/storage';
  17. import { Song, Criteria } from '@/types/song';
  18. import { API_FIELDS } from '@/constants/apiFields';
  19. import Loading from '@/components/Loading';
  20. interface Option {
  21. label: string;
  22. value: Criteria;
  23. }
  24. export default function SearchScreen() {
  25. const [keyword, setKeyword] = useState<string>('');
  26. const [criteria, setCriteria] = useState<Criteria>(API_FIELDS.TITLE);
  27. const [results, setResults] = useState<Song[]>([]);
  28. const [isDropdownVisible, setIsDropdownVisible] = useState<boolean>(false);
  29. const [isLoading, setIsLoading] = useState<boolean>(false); // 로딩 상태 추가
  30. const options: Option[] = [
  31. { label: '제목', value: API_FIELDS.TITLE },
  32. { label: '가수', value: API_FIELDS.SINGER },
  33. ];
  34. const handleSearch = async (): Promise<void> => {
  35. if (!keyword.trim()) return; // 빈 검색어 처리
  36. setIsLoading(true); // 검색 시작 시 로딩 시작
  37. Keyboard.dismiss();
  38. try {
  39. const filtered: Song[] = await fetchSongs(criteria, keyword);
  40. setResults(filtered);
  41. } finally {
  42. setIsLoading(false); // 검색 완료 시 로딩 종료
  43. }
  44. };
  45. const handleAddToFavorites = async (song: Song): Promise<void> => {
  46. const success: boolean = await addToFavoritesStorage(song);
  47. if (!success) {
  48. console.log('Failed to add to favorites');
  49. }
  50. };
  51. const handleSelectOption = (value: Criteria): void => {
  52. setCriteria(value);
  53. setIsDropdownVisible(false);
  54. };
  55. return (
  56. <KeyboardAvoidingView
  57. style={styles.container}
  58. behavior={Platform.OS === 'ios' ? 'padding' : undefined}
  59. >
  60. <View style={styles.searchRow}>
  61. <View style={styles.dropdownWrapper}>
  62. <TouchableOpacity
  63. style={styles.dropdownButton}
  64. onPress={() => setIsDropdownVisible(!isDropdownVisible)}
  65. >
  66. <Text style={styles.dropdownText}>
  67. {options.find((opt) => opt.value === criteria)?.label || '선택'}
  68. </Text>
  69. <Text style={styles.dropdownArrow}>▼</Text>
  70. </TouchableOpacity>
  71. <Modal
  72. transparent
  73. visible={isDropdownVisible}
  74. animationType="fade"
  75. onRequestClose={() => setIsDropdownVisible(false)}
  76. >
  77. <TouchableOpacity
  78. style={styles.modalOverlay}
  79. onPress={() => setIsDropdownVisible(false)}
  80. >
  81. <View style={styles.dropdownMenu}>
  82. {options.map((option) => (
  83. <TouchableOpacity
  84. key={option.value}
  85. style={styles.dropdownItem}
  86. onPress={() => handleSelectOption(option.value)}
  87. >
  88. <Text style={styles.dropdownItemText}>{option.label}</Text>
  89. </TouchableOpacity>
  90. ))}
  91. </View>
  92. </TouchableOpacity>
  93. </Modal>
  94. </View>
  95. <TextInput
  96. placeholder="검색어 입력"
  97. value={keyword}
  98. onChangeText={(text: string) => setKeyword(text)}
  99. onSubmitEditing={handleSearch}
  100. style={styles.input}
  101. returnKeyType="search"
  102. />
  103. <TouchableOpacity onPress={handleSearch} style={styles.searchButton}>
  104. <Text style={styles.searchText}>검색</Text>
  105. </TouchableOpacity>
  106. </View>
  107. {isLoading ? (
  108. <Loading /> // 로딩 중일 때 Loading 컴포넌트 표시
  109. ) : (
  110. <FlatList<Song>
  111. data={results}
  112. keyExtractor={(item) => item.no}
  113. renderItem={({ item }) => (
  114. <TouchableOpacity onPress={() => handleAddToFavorites(item)}>
  115. <View style={styles.item}>
  116. <Text>
  117. {item.no} - {item.title} ({item.singer})
  118. </Text>
  119. </View>
  120. </TouchableOpacity>
  121. )}
  122. ListEmptyComponent={<Text>검색 결과가 없습니다.</Text>}
  123. contentContainerStyle={{ flexGrow: 1 }}
  124. />
  125. )}
  126. </KeyboardAvoidingView>
  127. );
  128. }
  129. const styles = StyleSheet.create({
  130. container: {
  131. flex: 1,
  132. padding: 16,
  133. },
  134. searchRow: {
  135. flexDirection: 'row',
  136. alignItems: 'center',
  137. marginBottom: 16,
  138. width: '100%',
  139. },
  140. dropdownWrapper: {
  141. width: Dimensions.get('window').width * 0.3 - 24,
  142. marginRight: 8,
  143. },
  144. dropdownButton: {
  145. flexDirection: 'row',
  146. alignItems: 'center',
  147. justifyContent: 'space-between',
  148. borderWidth: 1,
  149. borderColor: '#ccc',
  150. borderRadius: 8,
  151. paddingHorizontal: 10,
  152. paddingVertical: 10,
  153. height: 40,
  154. backgroundColor: '#fff',
  155. },
  156. dropdownText: {
  157. fontSize: 16,
  158. color: '#000',
  159. },
  160. dropdownArrow: {
  161. fontSize: 12,
  162. color: '#000',
  163. },
  164. modalOverlay: {
  165. flex: 1,
  166. backgroundColor: 'rgba(0, 0, 0, 0.2)',
  167. justifyContent: 'flex-start',
  168. paddingTop: 120,
  169. paddingHorizontal: 16,
  170. },
  171. dropdownMenu: {
  172. backgroundColor: '#fff',
  173. borderRadius: 8,
  174. width: Dimensions.get('window').width * 0.3 - 24,
  175. elevation: 5,
  176. shadowColor: '#000',
  177. shadowOffset: { width: 0, height: 2 },
  178. shadowOpacity: 0.2,
  179. shadowRadius: 4,
  180. },
  181. dropdownItem: {
  182. paddingVertical: 10,
  183. paddingHorizontal: 10,
  184. borderBottomWidth: 1,
  185. borderBottomColor: '#eee',
  186. },
  187. dropdownItemText: {
  188. fontSize: 16,
  189. color: '#000',
  190. },
  191. input: {
  192. flex: 1,
  193. height: 40,
  194. borderWidth: 1,
  195. borderColor: '#ccc',
  196. borderRadius: 8,
  197. paddingHorizontal: 8,
  198. marginRight: 8,
  199. },
  200. searchButton: {
  201. backgroundColor: '#007AFF',
  202. paddingHorizontal: 8,
  203. paddingVertical: 10,
  204. borderRadius: 8,
  205. },
  206. searchText: {
  207. color: '#fff',
  208. fontWeight: 'bold',
  209. },
  210. item: {
  211. padding: 12,
  212. borderBottomWidth: 1,
  213. borderBottomColor: '#eee',
  214. width: Dimensions.get('window').width - 32,
  215. },
  216. });