From 00b5af0ed55626b75f33d4309a2d3095b3cdc88d Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 02:43:55 +0900 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20eslintrc=20console=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=20=EC=82=AD=EC=A0=9C=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eslintrc console 경고 삭제 --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index b035402..4a78f79 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,7 @@ "rules": { //"no-var": "warn", "no-multiple-empty-lines": "warn", - "no-console": ["warn", { "allow": ["warn", "error"] }], + //"no-console": ["warn", { "allow": ["warn", "error"] }], "eqeqeq": "warn", "dot-notation": "warn", "no-unused-vars": "warn", From c31ea23ba14b037811063e0761238bccfb4a301c Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 02:46:35 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20axios,zustand=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=20=ED=9B=84=20api=20=EC=85=8B=ED=8C=85=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit axios,zustand 설치 후 api 셋팅 --- .gitignore | 2 ++ package-lock.json | 67 +++++++++++++++++++++++++++++++++++- package.json | 4 ++- src/apis/api/storeList.jsx | 68 +++++++++++++++++++++++++++++++++++++ src/apis/utils/index.js | 3 ++ src/apis/utils/instance.jsx | 12 +++++++ 6 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/apis/api/storeList.jsx create mode 100644 src/apis/utils/index.js create mode 100644 src/apis/utils/instance.jsx diff --git a/.gitignore b/.gitignore index 4d29575..532eddc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.env \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index eaa61eb..eb048c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-naver-maps": "^0.1.3", @@ -18,7 +19,8 @@ "react-scripts": "5.0.1", "styled-components": "^6.1.12", "styled-reset": "^4.5.2", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^4.5.5" }, "devDependencies": { "eslint-config-prettier": "^9.1.0", @@ -5446,6 +5448,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -14886,6 +14911,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -17800,6 +17830,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18804,6 +18842,33 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", + "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", + "dependencies": { + "use-sync-external-store": "1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 7a57d51..e4023b5 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.4", "react": "^18.3.1", "react-dom": "^18.3.1", "react-naver-maps": "^0.1.3", @@ -14,7 +15,8 @@ "react-scripts": "5.0.1", "styled-components": "^6.1.12", "styled-reset": "^4.5.2", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^4.5.5" }, "scripts": { "start": "react-scripts start", diff --git a/src/apis/api/storeList.jsx b/src/apis/api/storeList.jsx new file mode 100644 index 0000000..df2c182 --- /dev/null +++ b/src/apis/api/storeList.jsx @@ -0,0 +1,68 @@ +import { defaultInstance } from '../utils/instance'; + +//category 검색 가게 리스트 +export const getStoreList = async (params) => { + console.log(params); + try { + const { data, status } = await defaultInstance.get(`api/stores/search`, { params: params }); + return { + data, + status, + }; + } catch (e) { + console.log(e); + return { + error: e.response.data.detail, + status: e.response.status, + }; + } +}; +//nearby_station 검색 가게 리스트 +export const getStationStoreList = async (params) => { + try { + const { data, status } = await defaultInstance.get(`api/stores/search/nearby_station`, { params: params }); + return { + data, + status, + }; + } catch (e) { + console.log(e); + return { + error: e.response.data.detail, + status: e.response.status, + }; + } +}; +//name 검색 가게리스트 +export const getNameStoreList = async (params) => { + try { + const { data, status } = await defaultInstance.get(`api/stores/search/name`, { params: params }); + return { + data, + status, + }; + } catch (e) { + console.log(e); + return { + error: e.response.data.detail, + status: e.response.status, + }; + } +}; +// 가게 상세 정보 +export const getStoreDetail = async (id) => { + console.log('Store ID:', id); + try { + const { data, status } = await defaultInstance.get(`api/stores/${id.storeId}`); + return { + data, + status, + }; + } catch (e) { + console.log(e); + return { + error: e.response.data.detail, + status: e.response.status, + }; + } +}; diff --git a/src/apis/utils/index.js b/src/apis/utils/index.js new file mode 100644 index 0000000..92d44e2 --- /dev/null +++ b/src/apis/utils/index.js @@ -0,0 +1,3 @@ +import defaultInstance from './instance'; + +export { defaultInstance }; diff --git a/src/apis/utils/instance.jsx b/src/apis/utils/instance.jsx new file mode 100644 index 0000000..905c1d1 --- /dev/null +++ b/src/apis/utils/instance.jsx @@ -0,0 +1,12 @@ +import axios from 'axios'; +const REACT_APP_BackEndUrl = process.env.REACT_APP_BackEndUrl; // 나중에 환경변수로 바꾸기 + +const Api = axios.create({ + baseURL: REACT_APP_BackEndUrl, + headers: { + 'Content-Type': `application/json`, + }, +}); + +export const defaultInstance = Api; +//일단 어떤 데이터들 받아야하는지 정리 From 2b4bee03d0896009afc2abf401eed384bc89e6a3 Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 02:47:22 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20storeList=20=EB=A1=9C=EC=BB=ACstore?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B4=EA=B4=80=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit storeList 로컬store로 보관 --- src/store/index.js | 2 ++ src/store/useStoreList.jsx | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/store/index.js create mode 100644 src/store/useStoreList.jsx diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..a17b3ec --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,2 @@ +import useStoreList from './useStoreList'; +export { useStoreList }; diff --git a/src/store/useStoreList.jsx b/src/store/useStoreList.jsx new file mode 100644 index 0000000..bc3c253 --- /dev/null +++ b/src/store/useStoreList.jsx @@ -0,0 +1,19 @@ +import create from 'zustand'; +import { persist } from 'zustand/middleware'; + +// 초기 상태 값 설정 +const initialStoreList = []; + +const useStoreList = create( + persist( + (set) => ({ + storeList: initialStoreList, // 초기 상태 값 설정 + setStoreList: (stores) => { + set({ storeList: stores }); // 상태 업데이트 로직 수정 + }, + }), + { name: 'store-list' } + ) +); + +export default useStoreList; From 6c522c40d658f2be61549d71dbfa26cec0618a64 Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 02:48:22 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix=20:=20map=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=9D=BC=EC=84=9C=20=EB=A7=88=EC=BB=A4?= =?UTF-8?q?=EC=99=80=20=ED=81=B4=EB=9F=AC=EC=8A=A4=ED=84=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit map 리스트에 따라서 마커와 클러스터 생성 --- src/components/common/Map.jsx | 128 +++++++++------------------------- 1 file changed, 34 insertions(+), 94 deletions(-) diff --git a/src/components/common/Map.jsx b/src/components/common/Map.jsx index f4645cd..cf3acbf 100644 --- a/src/components/common/Map.jsx +++ b/src/components/common/Map.jsx @@ -1,92 +1,30 @@ -import React, { useEffect, useRef, useCallback } from 'react'; +import React, { useEffect, useRef, useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -//신대방 삼거리역 임시 데이터 설정 -> 지하철역 - -const data = [ - { - id: 1, - image: '', - keyword: '서울시 냉면', - name: '맛있는 알고리즘', - address: '서울 종로구 광화문로 1길 234 5층', - rating: 4.5, - category: '냉면', - reviewCount: '999', - nearbyStation: '2,5호선 을지로9가역 1번 출구에서 239m', - phone: '02-1234-5678', - businessHours: [ - '토: 11:30 - 21:00 20:40 라스트오더', - '일: 11:30 - 21:00 20:40 라스트오더', - '월: 정기휴무 (매주 월요일)', - '화: 11:30 - 21:00 20:40 라스트오더', - '수: 11:30 - 21:00 20:40 라스트오더', - '목: 11:30 - 21:00 20:40 라스트오더', - '금: 11:30 - 21:00 20:40 라스트오더', - ], - latitude: '37.4996', - longitude: '126.9286', - positiveKeywords: '진한 육수, 고소한 맛, 푸짐한 고명', - reviewSummary: '진한 육수와 고소한 맛, 고명이 푸짐합니다. 가격이 비싸고 면이 평범하다는 의견도 있습니다.', - positiveRatio: '68', - nagativeRatio: '32', - }, - { - id: 2, - image: '', - keyword: '서울시 냉면', - name: '맛있는 알고리즘', - address: '서울 종로구 광화문로 1길 234 5층', - rating: 4.5, - category: '냉면', - reviewCount: '999', - nearbyStation: '2,5호선 을지로9가역 1번 출구에서 239m', - phone: '02-1234-5678', - businessHours: [ - '토: 11:30 - 21:00 20:40 라스트오더', - '일: 11:30 - 21:00 20:40 라스트오더', - '월: 정기휴무 (매주 월요일)', - '화: 11:30 - 21:00 20:40 라스트오더', - '수: 11:30 - 21:00 20:40 라스트오더', - '목: 11:30 - 21:00 20:40 라스트오더', - '금: 11:30 - 21:00 20:40 라스트오더', - ], - latitude: '37.4998', - longitude: '126.9280', - positiveKeywords: '진한 육수, 고소한 맛, 푸짐한 고명', - reviewSummary: '진한 육수와 고소한 맛, 고명이 푸짐합니다. 가격이 비싸고 면이 평범하다는 의견도 있습니다.', - positiveRatio: '68', - nagativeRatio: '32', - }, - { - id: 3, - name: '해물포차꼴통2호점', - latitude: '37.4991', - longitude: '126.9289', - }, - { - id: 4, - name: '일진아구찜', - latitude: '37.4938', - longitude: '126.9246', - }, - { - id: 5, - name: '즉석 바지락손칼국수', - latitude: '37.5000', - longitude: '126.9295', - }, -]; +import { useStoreList } from '../../store'; +//import { useParams } from 'react-router-dom'; const MyMap = () => { - //const [newMap, setNewMap] = useState(); const navigate = useNavigate(); const mapElement = useRef(null); const mapRef = useRef(null); - const currentLocation = { - lat: data[0].latitude, - lng: data[0].longitude, - }; + const markersRef = useRef([]); + const { storeList } = useStoreList(); + //const { id } = useParams(); + const [currentLocation, setCurrentLocation] = useState({ + lat: '37.5665', + lng: '126.9780', + }); + useEffect(() => { + if (storeList.length > 0) { + setCurrentLocation({ + lat: storeList[0].latitude, + lng: storeList[0].longitude, + }); + } else { + console.log('Store not found'); + } + }, [storeList]); const markerClickHandler = useCallback( (id) => { navigate(`/webmap/storeDetail/${id}`, { state: { detailVisible: true } }); @@ -95,9 +33,9 @@ const MyMap = () => { ); //clustering useEffect(() => { + const markers = []; const { naver } = window; const createMarkerList = []; - //const MarkerClustering = makeMarkerClustering(naver); let mapOptions = {}; if (currentLocation.lat !== 0 && currentLocation.lng !== 0) { mapOptions = { @@ -110,15 +48,15 @@ const MyMap = () => { mapDataControl: true, scaleControl: true, maxZoom: 20, - zoom: 18, + zoom: 10, }; } mapRef.current = new naver.maps.Map(mapElement.current, mapOptions); - //setNewMap(map); + markersRef.current.forEach((marker) => marker.setMap(null)); + markersRef.current = []; const addMarker = (id, name, lat, lng) => { - const { naver } = window; try { - let newMarker = new naver.maps.Marker({ + const newMarker = new naver.maps.Marker({ position: new naver.maps.LatLng(lat, lng), map: mapRef.current, title: name, @@ -127,15 +65,17 @@ const MyMap = () => { content: ``, }, }); - // console.log('process...'); - newMarker.setTitle(name); + markers[id] = newMarker; + markersRef.current.push(newMarker); createMarkerList.push(newMarker); naver.maps.Event.addListener(newMarker, 'click', () => markerClickHandler(id)); - } catch (e) {} + } catch (e) { + console.log('이거 때문이에요'); + } }; const addMarkers = () => { - for (let i = 0; i < data.length; i++) { - let markerObj = data[i]; + for (let i = 0; i < storeList.length; i++) { + let markerObj = storeList[i]; const dom_id = markerObj.id; const title = markerObj.name; const lat = markerObj.latitude; @@ -177,10 +117,10 @@ const MyMap = () => { disableClickZoom: false, gridSize: 120, icons: [htmlMarker1, htmlMarker2, htmlMarker3, htmlMarker4], - indexGenerator: [10, 100], + indexGenerator: [1, 5, 10, 100], }); }); - }, [currentLocation.lat, currentLocation.lng, markerClickHandler]); + }, [currentLocation.lat, currentLocation.lng, markerClickHandler, storeList]); return ; }; From 587fa9653f487cfa21029a15c8bd8427da3a192c Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 02:48:49 +0900 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20storeList=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit storeList 경로 변경 --- src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.jsx b/src/App.jsx index 1fb3242..8cc6049 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,7 +12,7 @@ function App() { } /> } /> - } /> + } /> } /> From f4627c1825e59c01c1b0b1a8e539489cc3582f37 Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 02:49:41 +0900 Subject: [PATCH 6/8] =?UTF-8?q?fix=20:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95=ED=95=98=EA=B3=A0=20idx->=20item?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카테고리 수정하고 idx-> item으로 수정 --- src/components/common/Category.jsx | 60 ++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/src/components/common/Category.jsx b/src/components/common/Category.jsx index 27a6586..7dfb0c1 100644 --- a/src/components/common/Category.jsx +++ b/src/components/common/Category.jsx @@ -3,26 +3,54 @@ import { Orange } from '../../color'; import { White } from '../../color'; import { useNavigate } from 'react-router-dom'; const items = [ + '한정식', + '일식당', + '양식', + '중식당', + '카페,디저트', + '베이커리', + '칼국수,만두', '냉면', - '파스타', - '삼겹살', - '스테이크', - '초밥', + '기사식당', + '한식', + '백반,가정식', + '생선구이', + '육류,고기요리', + '두부요리', + '국밥', + '주꾸미요리', + '정육식당', + '보리밥', + '요리주점', + '찌개,전골', + '닭갈비', + '맥주,호프', + '인도음식', + '카레', + '초밥,롤', + '돈가스', '떡볶이', - '빵', - '부대찌개', - '돈까스', - '수제햄버거', - '짜장면', - '마라탕', - '김치찌개', + '종합분식', + '조개요리', + '일본식라면', '덮밥', + '베트남음식', + '양꼬치', + '생선회', + '순대,순댓국', + '샤브샤브', + '이탈리아음식', + '스파게티,파스타전문', + '이자카야', + '돼지고기구이', + '태국음식', + '아시아음식', ]; const Category = (position) => { const navigate = useNavigate(); - const onClickHandler = (idx) => { - navigate(`/webmap/storeList/${idx}`, { state: { listVisible: true } }); //임시 경로 + const onClickHandler = (item) => { + navigate(`/webmap/storeList/${item}`, { state: { listVisible: true } }); //임시 경로 }; return ( @@ -30,8 +58,8 @@ const Category = (position) => { return (
{ - onClickHandler(idx); + onClick={() => { + onClickHandler(item); }} > {item} @@ -51,6 +79,8 @@ const CategoryLayout = styled.div` gap: 20px; align-items: center; justify-content: center; + overflow-y: scroll; + padding-bottom: 100px; //margin-left: 30px;*/ & > div { display: flex; From 1f418fe6c69a1ffad767bfb8eed98185cc935613 Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 02:50:45 +0900 Subject: [PATCH 7/8] =?UTF-8?q?fix=20:=20api=EC=97=90=20=EB=A7=9E=EC=B6=B0?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api에 맞춰 컴포넌트 수정 --- src/components/common/StoreCard.jsx | 32 ++-- src/components/common/StoreDetail.jsx | 118 ++++++++------- src/components/common/StoreList.jsx | 206 ++++++++++++++++++++++++-- src/components/main/MainHeader.jsx | 6 +- src/pages/Main.jsx | 31 +--- src/pages/WebMap.jsx | 115 +++++--------- 6 files changed, 320 insertions(+), 188 deletions(-) diff --git a/src/components/common/StoreCard.jsx b/src/components/common/StoreCard.jsx index d2af915..a99f71e 100644 --- a/src/components/common/StoreCard.jsx +++ b/src/components/common/StoreCard.jsx @@ -3,36 +3,39 @@ import { Grey, Orange, White } from '../../color'; import React from 'react'; import { ReactComponent as Path } from '../../assets/Icon/Path.svg'; import { ReactComponent as PathMobile } from '../../assets/Icon/Path_Mobile.svg'; - import { ReactComponent as Star } from '../../assets/Icon/Star.svg'; import { useNavigate } from 'react-router-dom'; -const StoreCard = () => { +//storeList에서 필요한 값 props로 받는다. +const StoreCard = ({ id, image, name, rating, address, positiveKeywords, storeLink }) => { const navigate = useNavigate(); const cardClickHandler = () => { - navigate('/webmap/storeDetail/1', { state: { detailVisible: true } }); + navigate(`/webmap/storeDetail/${id}`, { state: { detailVisible: true } }); + }; + const storeLinkHandler = () => { + window.location.href = storeLink; }; return ( - 맛집 대표 사진 + 맛집 대표 사진 - 맛있는 알고리즘 - - + - 위치 : 경기도 안양시 안양동 12 + 위치 : {address} -

별점

+

별점

-

5.0

+

{rating}

AI 분석결과

- 조용한, 양식, 외식, 감성, 와인, 스테이크 + {positiveKeywords}
); @@ -46,7 +49,7 @@ const StoreCardLayout = styled.div` gap: 20px; border-bottom: 1px solid ${Grey}; @media screen and (max-width: 1024px) { - max-width: 300px; + width: 300px; height: auto; } `; @@ -62,12 +65,13 @@ const ImgBox = styled.div` border-radius: 20px; } @media screen and (max-width: 1024px) { - margin: 0px; display: flex; align-items: center; justify-content: center; + width: 150px; + margin-left: 10px; & > img { - width: 100px; + width: 150px; height: 120px; } } diff --git a/src/components/common/StoreDetail.jsx b/src/components/common/StoreDetail.jsx index 375b716..9b15114 100644 --- a/src/components/common/StoreDetail.jsx +++ b/src/components/common/StoreDetail.jsx @@ -7,37 +7,28 @@ import { ReactComponent as Star } from '../../assets/Icon/Star.svg'; import { ReactComponent as Close } from '../../assets/Icon/Close.svg'; import { ReactComponent as PathMobile } from '../../assets/Icon/Path_Mobile.svg'; import { ReactComponent as SeeMore } from '../../assets/Icon/SeeMore.svg'; -const data = { - id: 1, - image: '', - keyword: '서울시 냉면', - name: '맛있는 알고리즘', - address: '서울 종로구 광화문로 1길 234 5층', - rating: 4.5, - category: '냉면', - reviewCount: '999', - nearbyStation: '2,5호선 을지로9가역 1번 출구에서 239m', - phone: '02-1234-5678', - businessHours: [ - '토: 11:30 - 21:00 20:40 라스트오더', - '일: 11:30 - 21:00 20:40 라스트오더', - '월: 정기휴무 (매주 월요일)', - '화: 11:30 - 21:00 20:40 라스트오더', - '수: 11:30 - 21:00 20:40 라스트오더', - '목: 11:30 - 21:00 20:40 라스트오더', - '금: 11:30 - 21:00 20:40 라스트오더', - ], - latitude: '33.56821', - longitude: '136.9971945', - positiveKeywords: '진한 육수, 고소한 맛, 푸짐한 고명', - reviewSummary: '진한 육수와 고소한 맛, 고명이 푸짐합니다. 가격이 비싸고 면이 평범하다는 의견도 있습니다.', - positiveRatio: '68', - nagativeRatio: '32', -}; +import { useParams } from 'react-router-dom'; +import { getStoreDetail } from '../../apis/api/storeList'; +import { useStoreList } from '../../store'; const StoreDetail = () => { + const { id } = useParams(); + const storeId = id; const location = useLocation(); const [visible, setVisible] = useState(true); + const [isLoading, setIsLoading] = useState(); + const [store, setStore] = useState(); + const [hourVisible, setHourVisible] = useState(false); + const { setStoreList } = useStoreList(); + const storeLinkHandler = () => { + window.location.href = store.store_link; + }; + useEffect(() => { + if (storeId) { + fetchStoreDetail(storeId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [storeId]); useEffect(() => { if (location.state?.detailVisible) { setVisible(true); @@ -47,40 +38,65 @@ const StoreDetail = () => { setVisible(false); //console.log(visible); }; + const toggleHours = () => { + setHourVisible(!hourVisible); + }; + const fetchStoreDetail = async (storeId) => { + setIsLoading(true); + try { + const response = await getStoreDetail({ storeId }); + const newData = response.data; + if (typeof newData.business_hours === 'string') { + try { + const jsonString = newData.business_hours.replace(/'/g, '"'); + newData.business_hours = JSON.parse(jsonString); + } catch (e) { + console.error('Failed to parse business_hours:', e); + } + } + setStore(newData); + setStoreList(newData); + } catch (error) { + console.log(error); + } + setIsLoading(false); + }; return ( - visible && ( + visible && + !isLoading && + store && ( - 맛집 대표 사진 + 맛집 대표 사진 -
{data.name}
- - +
- {data.category} + {store.category}
리뷰: - {data.reviewCount} + {store.reviews_count}
별점: - {data.rating} + {store.rating}

AI 분석 긍정 키워드

-

{data.positiveKeywords}

+

{store.positive_keywords}

@@ -92,22 +108,20 @@ const StoreDetail = () => {
주소: - {data.address} + {store.address}
전화번호: - {data.phone} + {store.phone}
영업시간: - <SeeMore /> + <SeeMore onClick={toggleHours} /> - - {data.businessHours && - data.businessHours.map((item, idx) => { - return
{item}
; - })} + + {Array.isArray(store.business_hours) && + store.business_hours.map((item, idx) =>
{item}
)}
@@ -115,17 +129,17 @@ const StoreDetail = () => { AI 분석 긍정 키워드 - {data.positiveKeywords} + {store.positive_keywords} AI분석 결과 - {data.reviewSummary} + {store.review_summary} 긍정/부정 리뷰 비율 - +
- 이 식당의 긍정 리뷰 비율은 {data.positiveRatio}%입니다. + 이 식당의 긍정 리뷰 비율은 {store.positive_ratio}%입니다. @@ -344,10 +358,12 @@ const StoreDetailBox = styled.div` } `; const Hours = styled.div` + display: flex; display: flex; flex-direction: column; @media screen and (max-width: 1024px) { - display: none; + max-width: 140px; + display: ${(props) => (props.$visible ? 'flex' : 'none')}; } `; const StoreReviewBox = styled.div` @@ -389,13 +405,13 @@ const RatingBar = styled.div` width: 80%; height: 30px; border-radius: 30px; - border: 2px solid ${Grey}; + border: 2px solid ${Orange}; background: ${White}; & > div { width: ${(props) => props.ratio}%; height: 100%; border-radius: 30px; - background-color: ${Grey}; + background-color: ${Orange}; } `; const Title = styled.p` diff --git a/src/components/common/StoreList.jsx b/src/components/common/StoreList.jsx index e4fc62f..e6cb386 100644 --- a/src/components/common/StoreList.jsx +++ b/src/components/common/StoreList.jsx @@ -2,12 +2,147 @@ import styled from 'styled-components'; import { Grey, Orange, White } from '../../color'; import { ReactComponent as SearchIcon } from './../../assets/Icon/Feather Icon.svg'; import StoreCard from './StoreCard'; +import { useEffect, useState } from 'react'; import { ReactComponent as SortReview } from '../../assets/Icon/ReviewSort.svg'; import { ReactComponent as SortPositive } from '../../assets/Icon/SortPositive.svg'; -const item = [1, 2, 3, 4, 5]; //임시 데이터 -const StoreList = () => { +import { getStoreList } from '../../apis/api/storeList'; +import { useParams } from 'react-router-dom'; +import { useStoreList } from '../../store'; + +const StoreList = ({ station }) => { + const [stores, setStores] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [page, setPage] = useState(0); + const [hasMore, setHasMore] = useState(true); + const { keyword } = useParams(); + const [storeName, setStoreName] = useState(); + const [storeCategory, setStoreCategory] = useState(); + const [stationInput, setStationInput] = useState(station); + const [sortBy, setSortBy] = useState(); + const [isNothing, setIsNothing] = useState(false); + const { setStoreList } = useStoreList(); + const fetchStoreData = async (storeCategory, storeName, station, sortBy, page) => { + setIsLoading(true); + + console.log(process.env.BackEndUrl); + setIsNothing(true); + const params = {}; + if (storeCategory) params.category = storeCategory; + if (storeName) params.name = storeName; + if (station) params.nearby_station = station; + if (sortBy) params.sortBy = sortBy; + params.pahe = page; + params.sortOrder = 'lower'; + try { + const response = await getStoreList(params); + const newData = response.data; + if (newData.length < 10) { + setHasMore(false); + setIsLoading(false); + } + if (newData.length === 0) { + setIsNothing(true); + setIsLoading(false); + return newData; //그냥 데이터가 없을 때 + } else { + setStores((prevData) => { + const newDataFiltered = newData.filter( + (newItem) => !prevData.some((prevItem) => prevItem.id === newItem.id) + ); + return [...prevData, ...newDataFiltered]; + }); + } + } catch (error) { + console.log(error); + } + setIsLoading(false); + }; + useEffect(() => { + setPage(0); + setStores([]); + fetchStoreData(storeCategory, storeName, station, sortBy, 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [station]); + useEffect(() => { + if (keyword && typeof keyword === 'string') { + setStoreCategory(keyword); + setPage(0); + setStores([]); + fetchStoreData(keyword, storeName, station, sortBy, 0); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [keyword, storeCategory]); + useEffect(() => { + const updatedStores = stores; + setStoreList(updatedStores); // stores가 바뀔 때마다 storeList를 업데이트 + }, [stores, setStoreList]); + // input 값 변경시 실행 + const nameChangeHandler = (e) => { + setStoreName(e.target.value); + }; + const nameKeyDownHandler = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + setPage(0); + setStores([]); + fetchStoreData(storeCategory, e.target.value, station, sortBy, 0); + } + }; + const stationChangeHandler = (e) => { + setStationInput(e.target.value); + }; + const stationKeyDownHandler = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (e.target.value) { + setPage(0); + setStores([]); + fetchStoreData(storeCategory, storeName, stationInput, sortBy, 0); + } + } + }; + const sortReviewClickHandler = () => { + setSortBy(); + }; + const sortPositiveClickHandler = () => { + setSortBy('positive_ratio'); + }; + useEffect(() => { + setPage(0); + setStores([]); + fetchStoreData(storeCategory, storeName, stationInput, sortBy, 0); + if (page !== 0) { + setPage(0); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sortBy]); + useEffect(() => { + hasMore && fetchStoreData(storeCategory, storeName, stationInput, sortBy, page); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + const handleObserver = (entries) => { + const target = entries[0]; + if (target.isIntersecting) { + setPage((prevPage) => prevPage + 1); + } + }; + useEffect(() => { + const observer = new IntersectionObserver(handleObserver, { + threshold: 0, + }); + const observerTarget = document.getElementById('observer'); + // 관찰 시작 + if (observerTarget) { + observer.observe(observerTarget); + } + return () => { + if (observerTarget) { + observer.unobserve(observerTarget); + } + }; + }, [page]); const onClickHandler = () => { - //... + //.. }; return ( @@ -15,22 +150,56 @@ const StoreList = () => { - + + + + + + +

정렬

- -
- {item.map((item) => { - return ; - })} + {stores && + stores.map((store) => { + return ( + + ); + })} + {isNothing && stores.length === 0 && 조건에 맞는 검색어가 없습니다. } + {isLoading &&

Loading...

} +
); }; @@ -40,8 +209,15 @@ const StoreListLayout = styled.div` left: 0; top: 0; z-index: 100; + padding-bottom: 100px; + width: 100%; background-color: ${White}; - overflow: visible; + overflow-y: scroll; + overflow-x: visible; + @media screen and (max-width: 1024px) { + width: 600px; + height: auto; + } `; const SearchBarBox = styled.div` margin-top: 20px; @@ -54,7 +230,7 @@ const SearchBarBox = styled.div` justify-content: flex-start; align-items: center; & > input { - flex: 1 1 auto; + width: 100%; height: 45px; border-radius: 30px; border: 1px solid ${Orange}; @@ -99,3 +275,11 @@ const SortBox = styled.div` } } `; + +const Alert = styled.h3` + padding: 14px; + display: flex; + font-weight: 400; + font-size: 15px; + color: ${Orange}; +`; diff --git a/src/components/main/MainHeader.jsx b/src/components/main/MainHeader.jsx index 9aec386..1056827 100644 --- a/src/components/main/MainHeader.jsx +++ b/src/components/main/MainHeader.jsx @@ -1,9 +1,7 @@ import styled from 'styled-components'; import { ReactComponent as SearchIcon } from '../../assets/Icon/Feather Icon.svg'; import { ReactComponent as DashBoard } from '../../assets/Icon/DashBoard.svg'; -import { ReactComponent as Game } from '../../assets/Icon/Game.svg'; import { ReactComponent as ArrowRight } from '../../assets/Icon/ArrowNav.svg'; - import { Orange } from '../../color'; import { useNavigate } from 'react-router-dom'; const MainHeader = () => { @@ -18,14 +16,12 @@ const MainHeader = () => { - + diff --git a/src/pages/Main.jsx b/src/pages/Main.jsx index 323c18c..1ce0360 100644 --- a/src/pages/Main.jsx +++ b/src/pages/Main.jsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { MyMap, StoreCard } from '../components/common'; +import { MyMap } from '../components/common'; import { MainHeader } from '../components/main'; import { ReactComponent as Arrow } from '../assets/Icon/Arrow.svg'; import { ReactComponent as Pizza } from '../assets/image/Pizza.svg'; @@ -48,13 +48,6 @@ const Main = () => {

view all

- -
-

인기 순위 Top

- -
- -
); @@ -117,11 +110,13 @@ const CategoryHeader = styled.div` border-bottom: solid 2px ${Grey}; margin: 20px; & > p { + width: 100px; font-weight: 600; - font-size: 23px; + font-size: 20px; } @media screen and (max-width: 1024px) { & > p { + width: 60px; font-weight: 400; font-size: 16px; } @@ -209,21 +204,3 @@ const StoreListPreview = styled.div` border-bottom: 1px solid ${Orange}; } `; - -const StoreTopList = styled.div` - display: flex; - flex-direction: column; - margin-left: 10px; - justify-content: center; - align-items: center; - margin-top: 10px; - & > div { - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; - & > p { - color: ${Orange}; - } - } -`; diff --git a/src/pages/WebMap.jsx b/src/pages/WebMap.jsx index f81eee9..b6c1625 100644 --- a/src/pages/WebMap.jsx +++ b/src/pages/WebMap.jsx @@ -1,98 +1,29 @@ import styled from 'styled-components'; -import { Category, StoreList, MobileNav } from '../components/common'; +import { Category, StoreList, MobileNav, MyMap } from '../components/common'; import { LightGrey, Orange, White } from '../color'; import { ReactComponent as SearchIcon } from '../assets/Icon/Feather Icon.svg'; import { ReactComponent as DashBoard } from '../assets/Icon/DashBoard.svg'; import { ReactComponent as Home } from '../assets/Icon/Home.svg'; import { ReactComponent as Arrow } from '../assets/Icon/Arrow.svg'; -import { MyMap } from '../components/common'; import { useState, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -const data = [ - { - id: 1, - image: '', - keyword: '서울시 냉면', - name: '맛있는 알고리즘', - address: '서울 종로구 광화문로 1길 234 5층', - rating: 4.5, - category: '냉면', - reviewCount: '999', - nearbyStation: '2,5호선 을지로9가역 1번 출구에서 239m', - phone: '02-1234-5678', - businessHours: [ - '토: 11:30 - 21:00 20:40 라스트오더', - '일: 11:30 - 21:00 20:40 라스트오더', - '월: 정기휴무 (매주 월요일)', - '화: 11:30 - 21:00 20:40 라스트오더', - '수: 11:30 - 21:00 20:40 라스트오더', - '목: 11:30 - 21:00 20:40 라스트오더', - '금: 11:30 - 21:00 20:40 라스트오더', - ], - latitude: '37.4996', - longitude: '126.9286', - positiveKeywords: '진한 육수, 고소한 맛, 푸짐한 고명', - reviewSummary: '진한 육수와 고소한 맛, 고명이 푸짐합니다. 가격이 비싸고 면이 평범하다는 의견도 있습니다.', - positiveRatio: '68', - nagativeRatio: '32', - }, - { - id: 2, - image: '', - keyword: '서울시 냉면', - name: '맛있는 알고리즘', - address: '서울 종로구 광화문로 1길 234 5층', - rating: 4.5, - category: '냉면', - reviewCount: '999', - nearbyStation: '2,5호선 을지로9가역 1번 출구에서 239m', - phone: '02-1234-5678', - businessHours: [ - '토: 11:30 - 21:00 20:40 라스트오더', - '일: 11:30 - 21:00 20:40 라스트오더', - '월: 정기휴무 (매주 월요일)', - '화: 11:30 - 21:00 20:40 라스트오더', - '수: 11:30 - 21:00 20:40 라스트오더', - '목: 11:30 - 21:00 20:40 라스트오더', - '금: 11:30 - 21:00 20:40 라스트오더', - ], - latitude: '37.4998', - longitude: '126.9280', - positiveKeywords: '진한 육수, 고소한 맛, 푸짐한 고명', - reviewSummary: '진한 육수와 고소한 맛, 고명이 푸짐합니다. 가격이 비싸고 면이 평범하다는 의견도 있습니다.', - positiveRatio: '68', - nagativeRatio: '32', - }, - { - id: 3, - name: '해물포차꼴통2호점', - latitude: '37.4991', - longitude: '126.9289', - }, - { - id: 4, - name: '일진아구찜', - latitude: '37.4938', - longitude: '126.9246', - }, - { - id: 5, - name: '즉석 바지락손칼국수', - latitude: '37.5000', - longitude: '126.9295', - }, -]; +import { useStoreList } from '../store'; const WebMap = () => { const location = useLocation(); const navigate = useNavigate(); const [isStoreList, setIsStoreList] = useState(true); const [categoryState, setIsCategoryState] = useState(location.state?.visible || false); + const [station, setStation] = useState(); + const [count, setCount] = useState(); + const { setStoreList } = useStoreList(); useEffect(() => { if (location.state?.listVisible) { setIsStoreList(true); + setCount(1); } if (!location.state?.listVisible) { setIsStoreList(false); + setCount(0); } if (location.state?.visible) { setIsCategoryState(true); @@ -114,6 +45,17 @@ const WebMap = () => { const homeClickHandler = () => { navigate('/'); }; + const onChangeHandler = (e) => { + setStation(e.target.value); + }; + const onKeyDownHandler = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + setIsStoreList(true); + setIsCategoryState(false); + setStation(station); + } + }; return ( @@ -124,14 +66,20 @@ const WebMap = () => { - {isStoreList && } + {isStoreList && } {!isStoreList && ( - +

카테고리 설정

@@ -150,7 +98,14 @@ const WebMap = () => { )} {!categoryState && ( - + )} @@ -273,7 +228,7 @@ const CategoryButton = styled.div` const MapContainer = styled.div` max-width: 100%; - flex: 1 1 auto; + flex: 1 1; margin: 0; display: flex; flex-direction: row; From b7a1411b034bcba22de541d86a1952e093db8697 Mon Sep 17 00:00:00 2001 From: hae_oo Date: Mon, 26 Aug 2024 13:36:06 +0900 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=EA=B8=8D=EC=A0=95=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B9=84=EC=9C=A8=20studycard=EC=97=90=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/StoreCard.jsx | 33 +++++++++++++++++++++-------- src/components/common/StoreList.jsx | 4 ++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/common/StoreCard.jsx b/src/components/common/StoreCard.jsx index a99f71e..2e50e31 100644 --- a/src/components/common/StoreCard.jsx +++ b/src/components/common/StoreCard.jsx @@ -6,7 +6,7 @@ import { ReactComponent as PathMobile } from '../../assets/Icon/Path_Mobile.svg' import { ReactComponent as Star } from '../../assets/Icon/Star.svg'; import { useNavigate } from 'react-router-dom'; //storeList에서 필요한 값 props로 받는다. -const StoreCard = ({ id, image, name, rating, address, positiveKeywords, storeLink }) => { +const StoreCard = ({ id, image, name, rating, address, positiveKeywords, storeLink, positiveRatio }) => { const navigate = useNavigate(); const cardClickHandler = () => { navigate(`/webmap/storeDetail/${id}`, { state: { detailVisible: true } }); @@ -29,13 +29,19 @@ const StoreCard = ({ id, image, name, rating, address, positiveKeywords, storeLi 위치 : {address} - -

별점

- -

{rating}

-
+ +
+

별점

+ +

{rating}

+
+
+

AI 리뷰 긍정비율

+

{positiveRatio}%

+
+

AI 분석결과

- {positiveKeywords} +

{positiveKeywords}

); @@ -137,12 +143,21 @@ const NameAndPath = styled.div` } } `; -const Rating = styled.div` +const ReviewBox = styled.div` display: flex; flex-direction: row; gap: 10px; justify-content: flex-start; - align-items: center; + & > div { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 10px; + } + @media screen and (max-width: 1024px) { + flex-direction: column; + } `; const Name = styled.p` diff --git a/src/components/common/StoreList.jsx b/src/components/common/StoreList.jsx index e6cb386..d30b2eb 100644 --- a/src/components/common/StoreList.jsx +++ b/src/components/common/StoreList.jsx @@ -23,8 +23,6 @@ const StoreList = ({ station }) => { const { setStoreList } = useStoreList(); const fetchStoreData = async (storeCategory, storeName, station, sortBy, page) => { setIsLoading(true); - - console.log(process.env.BackEndUrl); setIsNothing(true); const params = {}; if (storeCategory) params.category = storeCategory; @@ -57,6 +55,7 @@ const StoreList = ({ station }) => { } setIsLoading(false); }; + useEffect(() => {}); useEffect(() => { setPage(0); setStores([]); @@ -194,6 +193,7 @@ const StoreList = ({ station }) => { rating={store.rating} positiveKeywords={store.positive_keywords} storeLink={store.store_link} + positiveRatio={store.positive_ratio} /> ); })}