Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ android {

// ๋กœ์ปฌ ๊ธฐ๋ณธ๊ฐ’(์ˆ˜๋™ ๊ด€๋ฆฌ์šฉ)
versionCode 15
versionName "1.1.0"
versionName "1.2.2"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL",
"\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
Expand Down
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"expo": {
"name": "๋‹ค์‹œ",
"slug": "dasii",
"version": "1.3.0",
"version": "1.4.0",
"orientation": "portrait",
"icon": "./assets/images/img_logo.png",
"scheme": "dasii",
Expand Down
11 changes: 11 additions & 0 deletions app/(tabs)/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ export default function Home() {

return (
<SafeAreaView className='flex-1 bg-white' edges={['top']}>
{/* TODO: ์ž„์‹œ ์„ฑ๋ถ„ ๋ฆฌ์ŠคํŠธ ๋ฒ„ํŠผ (์ถ”ํ›„ ์‚ญ์ œ) */}
<View className='px-6 mb-4'>
<Pressable
className='bg-blue-500 rounded-lg py-3 items-center'
onPress={() => router.push('/ingredient')}
>
<Text className='text-white font-n-bd'>
ingredient ํŽ˜์ด์ง€ ์ด๋™ (์ž„์‹œ)
</Text>
</Pressable>
</View>
<ScrollView className='flex-1'>
<View className='flex-row items-center justify-between px-6 pt-4'>
<LogoIcon width={80} height={30} />
Expand Down
1 change: 1 addition & 0 deletions app/(tabs)/mypage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { clearTokens, getAccessToken } from '@/lib/authToken';
import Constants from 'expo-constants';
import { useFocusEffect, useRouter } from 'expo-router';
import { useCallback, useState } from 'react';

import { ScrollView, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

Expand Down
32 changes: 8 additions & 24 deletions app/+not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import { Link, Stack } from "expo-router";
import { StyleSheet } from "react-native";

import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
import { Link, Stack } from 'expo-router';
import { Text, View } from 'react-native';

export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<ThemedView style={styles.container}>
<ThemedText type="title">This screen does not exist.</ThemedText>
<Link href="/" style={styles.link}>
<ThemedText type="link">Go to home screen!</ThemedText>
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20 }}>
<Text>This screen does not exist.</Text>
<Link href='/'>
<Text>Go to home screen!</Text>
</Link>
</ThemedView>
</View>
</>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
link: {
marginTop: 15,
paddingVertical: 15,
},
});
Empty file.
151 changes: 151 additions & 0 deletions app/ingredient/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
'use client';

import ArrowLeftIcon from '@/assets/icons/ic_arrow_left.svg';
import ArrowRightIcon from '@/assets/icons/ic_arrow_right.svg';
import HomeIcon from '@/assets/icons/ic_home.svg';
import SearchBar from '@/components/common/SearchBar';
import Navigation from '@/components/layout/Navigation';
import colors from '@/constants/color';
import { useRouter } from 'expo-router';
import { useState } from 'react';
import { Pressable, ScrollView, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

// --- Mock Data ---
const hotIngredients = [
{ id: 1, name: '๊ฐ€๋ฅด์‹œ๋‹ˆ์•„ ์บ„๋ณด์ง€์•„ ์ถ”์ถœ๋ฌผ' },
{ id: 2, name: '๋ฐ€ํฌ์”จ์Šฌ์ถ”์ถœ๋ฌผ' },
{ id: 3, name: '๋…น์ฐจ์ถ”์ถœ๋ฌผ' },
];

const ingredientList = [
{ id: 4, name: '๊ฐ€๋ฅด์‹œ๋‹ˆ์•„ ์บ„๋ณด์ง€์•„ ์ถ”์ถœ๋ฌผ' },
{ id: 5, name: '๋ฐ€ํฌ์”จ์Šฌ์ถ”์ถœ๋ฌผ' },
{ id: 6, name: '๋…น์ฐจ์ถ”์ถœ๋ฌผ' },
{ id: 7, name: '์ฝ”์—”์ž์ž„Q10' },
{ id: 8, name: '๋น„ํƒ€๋ฏผC (์•„์Šค์ฝ”๋ฅด๋ธŒ์‚ฐ)' },
{ id: 9, name: '์ฝœ๋ผ๊ฒ ํŽฉํƒ€์ด๋“œ' },
{ id: 10, name: 'ํ”„๋กœ๋ฐ”์ด์˜คํ‹ฑ์Šค' },
{ id: 11, name: '๋ฃจํ…Œ์ธ' },
{ id: 12, name: '์˜ค๋ฉ”๊ฐ€3' },
];

function IngredientRow({
name,
onPress,
}: {
name: string;
onPress: () => void;
}) {
return (
<Pressable
onPress={onPress}
className='flex-row items-center justify-between px-4 py-4 bg-white active:bg-gray-50'
>
<Text className='text-sm text-gray-800'>{name}</Text>
<ArrowRightIcon width={12} height={12} fill={colors.gray[900]} />
</Pressable>
);
}

export default function IngredientGuidePage() {
const router = useRouter();
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');

const handleSearch = () => {
setSearchQuery(inputValue);
};

const handleChangeText = (text: string) => {
setInputValue(text);
if (text === '') setSearchQuery('');
};

const filteredHot = hotIngredients.filter((i) =>
searchQuery ? i.name.includes(searchQuery) : true,
);
const filteredList = ingredientList.filter((i) =>
searchQuery ? i.name.includes(searchQuery) : true,
);

const hasResults = filteredHot.length > 0 || filteredList.length > 0;

return (
<SafeAreaView className='bg-white flex-1'>
{/* ํ—ค๋” */}
<Navigation
title='์„ฑ๋ถ„ ๊ฐ€์ด๋“œ'
left={<ArrowLeftIcon width={18} height={18} fill={colors.gray[900]} />}
onLeftPress={() => router.back()}
right={
<Pressable onPress={() => router.push('/home')}>
<HomeIcon width={18} height={18} fill={colors.gray[900]} />
</Pressable>
}
onRightPress={() => router.push('/')}
/>

{/* ๊ฒ€์ƒ‰๋ฐ” */}
<SearchBar
value={inputValue}
onChangeText={handleChangeText}
onSubmit={handleSearch}
placeholder='์„ฑ๋ถ„, ์ œํ’ˆ๋ช…์œผ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด์„ธ์š”!'
/>

{/* ์Šคํฌ๋กค ์˜์—ญ */}
<ScrollView className='flex-1' showsVerticalScrollIndicator={false}>
{filteredHot.length > 0 && (
<View className='mb-2'>
<View className='px-4 pt-5 pb-2'>
<Text className='text-lg font-n-eb'>์š”์ฆ˜ ๋œจ๋Š” ์„ฑ๋ถ„ ๐Ÿ”ฅ</Text>
</View>
{filteredHot.map((item, index) => (
<View key={item.id}>
<IngredientRow
name={item.name}
onPress={() => {
router.push({
pathname: '/ingredient/[id]/ingredientDetail',
params: { id: item.id },
});
}}
/>
</View>
))}
</View>
)}
<View className='h-[0.5px] bg-gray-200 mx-4' />
{filteredList.length > 0 && (
<View className='mb-2'>
<View className='px-4 pt-5 pb-2'>
<Text className='text-lg font-n-eb '>์„ฑ๋ถ„ ๋ฆฌ์ŠคํŠธ</Text>
</View>
{filteredList.map((item, index) => (
<View key={item.id}>
<IngredientRow
name={item.name}
onPress={() => {
router.push({
pathname: '/ingredient/[id]/ingredientDetail',
params: { id: item.id },
});
}}
/>
</View>
))}
</View>
)}

{searchQuery && !hasResults && (
<View className='flex-1 items-center py-10'>
<Text className='text-gray-400 font-medium text-center'>
{`'${searchQuery}'์— ๋Œ€ํ•œ\n๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.`}
</Text>
</View>
)}
</ScrollView>
</SafeAreaView>
);
}
161 changes: 161 additions & 0 deletions components/common/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import MagnifierIcon from '@/assets/icons/ic_magnifier.svg';
import CloseIcon from '@/assets/icons/ic_x.svg';
import colors from '@/constants/color';
import { useRef, useState } from 'react';
import {
FlatList,
LayoutAnimation,
Pressable,
Text,
TextInput,
View,
} from 'react-native';

interface SearchBarProps {
placeholder?: string;
value: string;
onChangeText: (text: string) => void;
onFocus?: () => void;
onBlur?: () => void;
onSubmit?: () => void;
autoFocus?: boolean;
recentSearches?: string[];
onRecentSearchPress?: (search: string) => void;
onRemoveRecentSearch?: (search: string) => void;
onClearAllRecentSearches?: () => void;
onClearList?: () => void;
}

export default function SearchBar({
placeholder = '์„ฑ๋ถ„, ์ œํ’ˆ๋ช…์œผ๋กœ ๊ฒ€์ƒ‰ํ•ด๋ณด์„ธ์š”!',
value,
onChangeText,
onSubmit,
onFocus,
onBlur,
autoFocus = false,
recentSearches,
onRecentSearchPress,
onRemoveRecentSearch,
onClearAllRecentSearches,
onClearList,
}: SearchBarProps) {
const inputRef = useRef<TextInput>(null);
const [isFocused, setIsFocused] = useState(false);

const handleSearchSubmit = () => {
inputRef.current?.blur();
onSubmit?.();
};

const handleRecentSearchPress = (search: string) => {
setIsFocused(false);
inputRef.current?.blur();
onRecentSearchPress?.(search);
};

const showRecentSearches =
isFocused && recentSearches && recentSearches.length > 0;

return (
<View className='pt-1.5'>
<View className='px-4'>
<View className='flex-row items-center bg-gray-50 rounded-full py-[9px] px-[20px] mb-[15px]'>
<TextInput
ref={inputRef}
className='flex-1 text-b-sm font-n-rg py-0'
placeholder={placeholder}
placeholderTextColor={colors.gray[400]}
value={value}
onChangeText={onChangeText}
onFocus={() => {
LayoutAnimation.configureNext(
LayoutAnimation.Presets.easeInEaseOut,
);
setIsFocused(true);
onFocus?.();
}}
onBlur={() => {
LayoutAnimation.configureNext(
LayoutAnimation.Presets.easeInEaseOut,
);
setIsFocused(false);
onBlur?.();
}}
autoFocus={autoFocus}
onSubmitEditing={handleSearchSubmit}
returnKeyType='search'
/>
{value.length > 0 && (
<Pressable
className='ml-2 bg-gray-400 rounded-full p-[3px]'
onPress={() => {
onChangeText('');
onClearList?.();
}}
>
<CloseIcon width={13} height={13} color={colors.gray[0]} />
</Pressable>
)}
<Pressable className='ml-4' onPress={handleSearchSubmit}>
<MagnifierIcon
width={18}
height={18}
color={value !== '' ? colors.gray[900] : colors.gray[400]}
/>
</Pressable>
</View>
</View>

<View className='w-full border-[0.5px] border-gray-100 mb-4' />

{showRecentSearches && (
<View className='px-4 mb-4'>
<View>
<View className='flex-row justify-between items-center mb-3'>
<Text className='text-base font-n-bd text-gray-900'>
์ตœ๊ทผ ๊ฒ€์ƒ‰
</Text>
<Pressable onPress={onClearAllRecentSearches}>
<Text className='text-sm text-gray-500'>์ „์ฒด์‚ญ์ œ</Text>
</Pressable>
</View>

<FlatList
data={recentSearches}
horizontal
keyExtractor={(item, index) => `${item}-${index}`}
showsHorizontalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={{ width: 8 }} />}
contentContainerStyle={{ paddingVertical: 4 }}
renderItem={({ item }) => (
<View className='flex-row items-center bg-white border-[0.5px] border-gray-200 rounded-full pl-4 pr-2 py-1'>
<Pressable
onPress={() => handleRecentSearchPress(item)}
className='flex-1'
>
<Text className='text-sm text-gray-900' numberOfLines={1}>
{item}
</Text>
</Pressable>

<Pressable
onPress={() => onRemoveRecentSearch?.(item)}
className='p-1 ml-2 mr-1'
hitSlop={8}
>
<CloseIcon
width={16}
height={16}
color={colors.gray[700]}
/>
</Pressable>
</View>
)}
/>
</View>
</View>
)}
</View>
);
}
Loading