seach.tsx 6.0 KB

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