From 46697d2857dd4a9513446e574d73547d4020f6a9 Mon Sep 17 00:00:00 2001 From: ahcgnoej Date: Mon, 9 Sep 2024 18:01:16 +0900 Subject: [PATCH] =?UTF-8?q?Feat=20:=20=EA=B8=B0=EC=98=A8=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=A5=B8=20=EC=9D=8C=EC=8B=9D=20=EC=B6=94=EC=B2=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/App.jsx | 2 + src/Components/Food/Food.css | 51 +++++++++++++ src/Components/Food/Food.jsx | 72 +++++++++++++++++++ src/Components/Food/GetCurrentTemperature.jsx | 51 +++++++++++++ src/Components/Food/ListArea.jsx | 38 ++++++++++ src/apis/naverApi.js | 54 ++++++++++++++ vite.config.js | 9 +++ 8 files changed, 279 insertions(+) create mode 100644 src/Components/Food/Food.css create mode 100644 src/Components/Food/Food.jsx create mode 100644 src/Components/Food/GetCurrentTemperature.jsx create mode 100644 src/Components/Food/ListArea.jsx create mode 100644 src/apis/naverApi.js diff --git a/package.json b/package.json index 387b490..eee7548 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", + "proxy": "https://openapi.naver.com", "scripts": { "dev": "vite", "build": "vite build", @@ -13,6 +14,7 @@ "dependencies": { "axios": "^1.7.6", "normalize.css": "^8.0.1", + "prpo-types": "^0.0.1-security", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/App.jsx b/src/App.jsx index 2723df6..2e0a296 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,9 +1,11 @@ import Header from '@components/Header/Header' +import Food from '@/Components/Food/Food' const App = () => { return ( <>
+ ) } diff --git a/src/Components/Food/Food.css b/src/Components/Food/Food.css new file mode 100644 index 0000000..d3c1d86 --- /dev/null +++ b/src/Components/Food/Food.css @@ -0,0 +1,51 @@ +.container{ + width: 95%; + height: auto; + margin: 0 auto; + border-radius: 20px; + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + overflow-x: auto; +} +.container::-webkit-scrollbar{ + display: none; +} + +.container div { + border-radius: 20px; + width: 250px; + height: 180px; + background-color: #FFF; + display: flex; + flex-direction: column; + flex:0 0 auto; + margin-bottom: 20px; + text-align: center; + align-items: center; + justify-content: space-around; + padding-bottom: 10px; + +} + +.recommendation { + width: 90%; + margin: 0 auto; + display: flex; + flex-direction: row; + justify-content: space-between; +} +.recommendation p { + color: #000; + font-weight: bold; +} + +.main_container { + width:90%; + height: 270px; + background-color:rgba(233, 216, 225, 0.3); + margin: 0 auto; + border-radius: 20px; + +} \ No newline at end of file diff --git a/src/Components/Food/Food.jsx b/src/Components/Food/Food.jsx new file mode 100644 index 0000000..0cb4043 --- /dev/null +++ b/src/Components/Food/Food.jsx @@ -0,0 +1,72 @@ +import React, { useState, useEffect } from 'react' +import ListArea from './ListArea' +import GetCurrentTemperature from './GetCurrentTemperature' +import './Food.css' + +function Food() { + const [search, setSearch] = useState('') + const [temperature, setTemperature] = useState(null) + const [recommendation, setRecommendation] = useState(null) + + const food = { + cold: ['찌개', '떡볶이', '국밥', '라면', '백반', '전골', '해장국'], + hot: ['냉면', '메밀', '초계국수', '물회', '삼계탕'], + normal: ['치킨', '삼겹살', '찌개', '비빔밥', '회', '갈비', '카레', '덮밥', '짜장면', '찜닭'], + } + + // 기온에 따라 cold, hot, normal로 분할 + const getRandomRecommendation = (temp) => { + let recommendations + if (temp <= 20) { + recommendations = food.cold + } else if (temp >= 21 && temp <= 27) { + recommendations = food.normal + } else if (temp >= 28) { + recommendations = food.hot + } else { + return '' + } + return recommendations[Math.floor(Math.random() * recommendations.length)] + } + + //기온 업데이트 + const handleTemperatureUpdate = (temp) => { + setTemperature(temp) + if (recommendation === null) { + setRecommendation(getRandomRecommendation(temp)) + } + } + + // 다른 음식 추천이 가능하도록 함 + const handleRefresh = () => { + if (temperature !== null) { + setRecommendation(getRandomRecommendation(temperature)) + } + } + + // 검색 내용으로 쓰임 + useEffect(() => { + if (recommendation) { + setSearch('강남' + recommendation) + } + }, [recommendation]) + + return ( +
+
+ {recommendation && ( +
+
+

오늘은 {recommendation} 어떠세요?

+
+

다른 음식 볼래요!

+
+ )} +
+ + +
+ ) +} + +export default Food diff --git a/src/Components/Food/GetCurrentTemperature.jsx b/src/Components/Food/GetCurrentTemperature.jsx new file mode 100644 index 0000000..2106d48 --- /dev/null +++ b/src/Components/Food/GetCurrentTemperature.jsx @@ -0,0 +1,51 @@ +import React, { useState, useEffect } from 'react' +import { 단기예보조회 } from '../../apis/apis.js' + +const GetCurrentTemperature = ({ onTemperatureUpdate }) => { + const [temperature, setTemperature] = useState(null) + const NX = 61 + const NY = 126 + + const filterByCategory = (category) => (data) => data.category === category + const formatForecastTime = (time) => time.slice(0, 2) + const foramtValueName = ({ fcstTime, fcstValue, ...rest }) => ({ + 시간: formatForecastTime(fcstTime), + 온도: fcstValue, + ...rest, + }) + const getLatestItem = (items) => { + if (items.length === 0) return null + const sortedItems = items.sort((a, b) => new Date(`${a.날짜} ${a.시간}`) - new Date(`${b.날짜} ${b.시간}`)) + return sortedItems[sortedItems.length - 1] + } + + useEffect(() => { + const fetchData = async () => { + try { + const response = await 단기예보조회(NX, NY) + console.log('API 응답:', response) + + if (response.data.response.body.items) { + const items = response.data.response.body.items.item.filter(filterByCategory('TMP')).map(foramtValueName) + const latestItem = getLatestItem(items) + if (latestItem) { + setTemperature(latestItem.온도) + onTemperatureUpdate(latestItem.온도) + } else { + throw new Error('데이터가 없습니다.') + } + } else { + throw new Error('API 응답 형식이 잘못되었습니다.') + } + } catch (err) { + setError('온도 조회 중 오류 발생: ' + err.message) + } finally { + setLoading(false) + } + } + + fetchData() + }, [onTemperatureUpdate]) +} + +export default GetCurrentTemperature diff --git a/src/Components/Food/ListArea.jsx b/src/Components/Food/ListArea.jsx new file mode 100644 index 0000000..0283611 --- /dev/null +++ b/src/Components/Food/ListArea.jsx @@ -0,0 +1,38 @@ +import searchFood from '../../apis/naverApi' +import './Food.css' + +const ListArea = ({ search }) => { + const { loading, data, error } = searchFood(search) + + if (loading) { + return

로딩중...

+ } + + if (error) { + return

에러가 발생했습니다

+ } + + if (!data || data.length === 0) { + return

데이터를 불러오지 못했습니다.

+ } + + return ( +
+ {data.map((item) => ( +
+

{item.title.split('').join('').split('').join('')}

+

{item.address}

+ + 음식점 보러 가기 + +
+ ))} +
+ ) +} + +export default ListArea diff --git a/src/apis/naverApi.js b/src/apis/naverApi.js new file mode 100644 index 0000000..b14de27 --- /dev/null +++ b/src/apis/naverApi.js @@ -0,0 +1,54 @@ +import { useReducer, useEffect } from 'react'; +import axios from 'axios'; + +const NAVER_ID = import.meta.env.VITE_NAVER_ID; +const NAVER_SECRET = import.meta.env.VITE_NAVER_SECRET; + +//reducer 함수로 action 타입에 따라 상태 업데이트 +const reducer = (state, action) => { + switch(action.type) { + case 'LOADING': + return { loading: true, data: null, error: null }; + case 'SUCCESS': + return { loading: false, data: action.data, error: null }; + case 'ERROR': + return { loading: false, data: null, error: action.error }; + default: + return state; + } +} + +//검색 결과 가져오기 +const searchFood = (search) => { + const [state, dispatch] = useReducer(reducer, { + loading: false, + data: null, + error: null + }); + + const fetchData = async () => { + try { + dispatch({ type: 'LOADING' }); + const { data: { items } } = await axios.get( + 'api/v1/search/local.json', { + params: { query: search + '맛집', display: 5 }, + headers: { + 'X-Naver-Client-Id': NAVER_ID, + 'X-Naver-Client-Secret': NAVER_SECRET + }, + }); + dispatch({ type: 'SUCCESS', data: items }); + } catch (err) { + console.error('API Error:', err); + dispatch({ type: 'ERROR', error: err }); + } + }; + + useEffect(() => { + fetchData(); + }, [search]); + + return state; +} + +export default searchFood; diff --git a/vite.config.js b/vite.config.js index 5f52522..7eee37f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,4 +14,13 @@ export default defineConfig({ }, extensions: ['.js', '.jsx'], }, + server: { + proxy: { + '/api': { + target: 'https://openapi.naver.com', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + }, + }, + }, })