diff --git a/package-lock.json b/package-lock.json index e671b5d..2702efe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "jotai": "^2.11.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.4.0", @@ -1848,14 +1849,14 @@ "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4519,6 +4520,27 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jotai": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.11.0.tgz", + "integrity": "sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index b7c1b38..fdf69c8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "jotai": "^2.11.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.4.0", diff --git a/src/components/Building.tsx b/src/components/Building.tsx index bd1f1ee..b75ba86 100644 --- a/src/components/Building.tsx +++ b/src/components/Building.tsx @@ -1,12 +1,12 @@ import styled from "@emotion/styled"; import Divider from "./Divider.tsx"; -import { buildingData, BuildingInfo } from "./data/buildingData.ts"; +import { buildingData } from "./data/buildingData.ts"; import Overflow from "./Overflow.tsx"; +import { useAtom } from "jotai"; +import { selectedBuildingAtom } from "../store/building.ts"; -interface BuildingProps { - onBuildingClick: (building: BuildingInfo) => void; -} -const Building: React.FC = ({ onBuildingClick }) => { +const Building = () => { + const [, setSelectedBuilding] = useAtom(selectedBuildingAtom); return ( <> @@ -20,7 +20,7 @@ const Building: React.FC = ({ onBuildingClick }) => { <> onBuildingClick(building)} + onClick={() => setSelectedBuilding(building)} > diff --git a/src/components/BuildingDetail.tsx b/src/components/BuildingDetail.tsx index 6242d2f..11d3f17 100644 --- a/src/components/BuildingDetail.tsx +++ b/src/components/BuildingDetail.tsx @@ -1,9 +1,17 @@ import styled from "@emotion/styled"; import { BuildingInfo, FacilityInfo } from "./data/buildingData"; import Divider from "./Divider"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import FacilityItem from "./FacilityItem.tsx"; import Overflow from "./Overflow.tsx"; +import { FaAngleLeft } from "react-icons/fa6"; +import { useAtom } from "jotai"; +import { + isPanelOpenAtom, + markFacilityAtom, + selectedBuildingAtom, +} from "../store/building.ts"; +import { BackButton } from "./Buttons.tsx"; interface BuildingDetailProps { building: BuildingInfo; onFacilityClick?: (facility: FacilityInfo) => void; @@ -13,24 +21,35 @@ const BuildingDetail: React.FC = ({ onFacilityClick, }) => { const [selectedFloor, setSelectedFloor] = useState(null); - const [selectedType, setSelectedType] = useState(null); + const [, setSelectedBuilding] = useAtom(selectedBuildingAtom); + const [markFacility, setMarkFacility] = useAtom(markFacilityAtom); + const [isPanelOpen] = useAtom(isPanelOpenAtom); const handleTypeChange = (type: number) => { - setSelectedType(Number(type)); - if (selectedType == type) { - setSelectedType(null); + if (markFacility === type) { + setMarkFacility(null); + } else { + setMarkFacility(type); } }; const handleFloorChange = (event: React.ChangeEvent) => { setSelectedFloor(event.target.value); }; + useEffect(() => { + setSelectedFloor(""); + }, [building]); return ( <> + {isPanelOpen && ( + setSelectedBuilding(null)}> + + + )} {building.name}

{building.name}

운영 시간: {building.time}

- + @@ -49,19 +68,19 @@ const BuildingDetail: React.FC = ({ @@ -77,8 +96,8 @@ const BuildingDetail: React.FC = ({ <> {selectedFloor === facility.floor && ( <> - {selectedType ? ( - facility.type === selectedType && ( + {markFacility ? ( + facility.type === markFacility && ( <> @@ -96,8 +115,8 @@ const BuildingDetail: React.FC = ({ ) : ( <> {" "} - {selectedType ? ( - facility.type === selectedType && ( + {markFacility ? ( + facility.type === markFacility && ( <> @@ -160,7 +179,7 @@ const Button = styled.button` selected && ` background: rgb(0, 51, 99, 0.5); - `}// + `} `; const Image = styled.img` diff --git a/src/components/LikeButton.tsx b/src/components/Buttons.tsx similarity index 65% rename from src/components/LikeButton.tsx rename to src/components/Buttons.tsx index 8455a3d..b8c20a7 100644 --- a/src/components/LikeButton.tsx +++ b/src/components/Buttons.tsx @@ -17,4 +17,15 @@ const LikeButton = styled.button` color: white; } `; -export default LikeButton; +const BackButton = styled.button` + position: absolute; + top: 10px; + left: 10px; + background: transparent; + border: none; + cursor: pointer; + color: white; + +`; + +export { LikeButton, BackButton }; diff --git a/src/components/FacilityDetail.tsx b/src/components/FacilityDetail.tsx index 69b877f..9a0d75e 100644 --- a/src/components/FacilityDetail.tsx +++ b/src/components/FacilityDetail.tsx @@ -1,17 +1,26 @@ import React from "react"; import { FacilityInfo } from "./data/buildingData"; import styled from "@emotion/styled"; -import LikeButton from "./LikeButton"; +import { BackButton, LikeButton } from "./Buttons"; import Divider from "./Divider"; import { IoMdHeart } from "react-icons/io"; - +import { FaAngleLeft } from "react-icons/fa6"; +import { facilityAtom, isPanelOpenAtom } from "../store/building.ts"; +import { useAtom } from "jotai"; interface FacilityDetailProps { facility: FacilityInfo; } const FacilityDetail: React.FC = ({ facility }) => { + const [, setFacility] = useAtom(facilityAtom); + const [isPanelOpen] = useAtom(isPanelOpenAtom); return ( <> + {isPanelOpen && ( + setFacility(null)}> + + + )} <h2> @@ -77,12 +86,13 @@ const ReviewContainer = styled.div` `; const Container = styled.div` width: 400px; - padding: 20px 30px; + padding: 20px 40px; + overflow: hidden; `; const Title = styled.div` display: flex; align-items: center; - margin-top: 10px; + margin-top: 20px; `; const Building = styled.p` margin-left: 10px; diff --git a/src/components/FacilityItem.tsx b/src/components/FacilityItem.tsx index c5544ce..aedbb9d 100644 --- a/src/components/FacilityItem.tsx +++ b/src/components/FacilityItem.tsx @@ -1,5 +1,5 @@ import styled from "@emotion/styled"; -import LikeButton from "./LikeButton.tsx"; +import { LikeButton } from "./Buttons.tsx"; import { FacilityInfo } from "./data/buildingData.ts"; interface FacilityItemProps { facility?: FacilityInfo | null; diff --git a/src/components/HomeBoard.tsx b/src/components/HomeBoard.tsx index 254149d..fc1b743 100644 --- a/src/components/HomeBoard.tsx +++ b/src/components/HomeBoard.tsx @@ -1,42 +1,62 @@ import styled from "@emotion/styled"; -import { useState } from "react"; import { IoIosArrowBack, IoIosArrowForward } from "react-icons/io"; import Building from "./Building"; -import { BuildingInfo, FacilityInfo } from "./data/buildingData.ts"; +import { FacilityInfo } from "./data/buildingData.ts"; import BuildingDetail from "./BuildingDetail.tsx"; import FacilityDetail from "./FacilityDetail.tsx"; +import { useAtom } from "jotai"; +import { + facilityAtom, + isPanelOpenAtom, + selectedBuildingAtom, + markFacilityAtom, +} from "../store/building.ts"; -interface HomeBoardProps { - onBuildingClick: (building: BuildingInfo) => void; - selectedBuilding: BuildingInfo | null; - isPanelOpen: boolean; - setIsPanelOpen: (isPanelOpen: boolean) => void; -} -const HomeBoard: React.FC<HomeBoardProps> = ({ - selectedBuilding, - onBuildingClick, - isPanelOpen, - setIsPanelOpen, -}) => { - const [facility, setFacility] = useState<FacilityInfo | null>(null); +const HomeBoard: React.FC = () => { + const [isPanelOpen, setIsPanelOpen] = useAtom(isPanelOpenAtom); + const [facility, setFacility] = useAtom(facilityAtom); + const [selectedBuilding] = useAtom(selectedBuildingAtom); + const [markFacility, setMarkFacility] = useAtom(markFacilityAtom); const toggleMenu = () => { setIsPanelOpen(!isPanelOpen); }; const handleFacilityClick = (facility: FacilityInfo) => { setFacility(facility); }; + const handleMarkFacility = (selectedMarkFacility: number) => { + setMarkFacility(Number(selectedMarkFacility)); + if (markFacility === selectedMarkFacility) { + setMarkFacility(null); + } + }; return ( <> <Panel isOpen={isPanelOpen}> <MarkList> <li> - <Mark> 화장실</Mark> + <Mark + onClick={() => handleMarkFacility(1)} + selected={markFacility === 1} + > + {" "} + 화장실 + </Mark> </li> <li> - <Mark>정수기</Mark> + <Mark + onClick={() => handleMarkFacility(2)} + selected={markFacility === 2} + > + 정수기 + </Mark> </li> <li> - <Mark>카페</Mark> + <Mark + onClick={() => handleMarkFacility(3)} + selected={markFacility === 3} + > + 카페 + </Mark> </li> </MarkList> <Button onClick={toggleMenu}> @@ -59,7 +79,7 @@ const HomeBoard: React.FC<HomeBoardProps> = ({ onFacilityClick={handleFacilityClick} /> ) : ( - <Building onBuildingClick={onBuildingClick} /> + <Building /> )} </Container> </Panel> @@ -95,16 +115,28 @@ const Panel = styled.div<PanelProps>` transition: width 0.3s ease-out; box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 4px 0px; `; -const Mark = styled.button` +interface MarkProps { + selected: boolean; +} +const Mark = styled.button<MarkProps>` background: white; width: 120px; height: 40px; border-radius: 20px; font-size: 15px; - border: none; + border: none;å cursor: pointer; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); padding: 10px; + cursor: pointer; + &:hover { + background: #B0C0CF; + } + ${({ selected }) => + selected && + ` + background: #B0C0CF; + `} `; const Button = styled.button` position: absolute; diff --git a/src/components/data/buildingData.ts b/src/components/data/buildingData.ts index 6c687f2..dc11343 100644 --- a/src/components/data/buildingData.ts +++ b/src/components/data/buildingData.ts @@ -104,6 +104,23 @@ export const buildingData: BuildingInfo[] = [ coordinates: { lat: 37.55167104651813, lng: 126.926557030475 }, floors: [1, 2, 3, 4, 5, 6, 7, 8], facilities: [ + { + building: "와우관", + floor: "7", + type: 3, + name: "카페트리", + like: 4, + dislike: 2, + reviewCount: 1, + review: [ + { + contents: "안녕하세요", + user: "컴공생", + date: "2021.09.01", + like: 0, + }, + ], + }, { building: "와우관", floor: "3", diff --git a/src/components/map/Kakaomap.tsx b/src/components/map/Kakaomap.tsx index 16d0633..c338f17 100644 --- a/src/components/map/Kakaomap.tsx +++ b/src/components/map/Kakaomap.tsx @@ -1,5 +1,11 @@ import { useEffect } from "react"; import { buildingData, BuildingInfo } from "../data/buildingData"; +import { useAtom } from "jotai"; +import { + facilityAtom, + markFacilityAtom, + selectedBuildingAtom, +} from "../../store/building.ts"; declare global { interface Window { @@ -10,15 +16,32 @@ declare global { const { kakao } = window; interface KakaomapProps { - selectedBuilding: BuildingInfo | null; onBuildingClick: (building: BuildingInfo) => void; } -const Kakaomap: React.FC<KakaomapProps> = ({ - selectedBuilding, - onBuildingClick, -}) => { +const Kakaomap: React.FC<KakaomapProps> = ({ onBuildingClick }) => { + const [selectedBuilding] = useAtom(selectedBuildingAtom); + const [, setFacility] = useAtom(facilityAtom); + const [markFacility] = useAtom(markFacilityAtom); const addMarkers = (map: any) => { - { + if (markFacility) { + buildingData.forEach((building) => { + building.facilities?.forEach((facility) => { + if (facility.type === markFacility) { + const markerPosition = new kakao.maps.LatLng( + building.coordinates.lat, + building.coordinates.lng + ); + const marker = new kakao.maps.Marker({ + position: markerPosition, + }); + marker.setMap(map); + kakao.maps.event.addListener(marker, "click", () => { + onBuildingClick(building); + }); + } + }); + }); + } else { buildingData.forEach((building) => { const markerPosition = new kakao.maps.LatLng( building.coordinates.lat, @@ -30,6 +53,7 @@ const Kakaomap: React.FC<KakaomapProps> = ({ marker.setMap(map); kakao.maps.event.addListener(marker, "click", () => { onBuildingClick(building); + setFacility(null); }); }); } @@ -38,13 +62,12 @@ const Kakaomap: React.FC<KakaomapProps> = ({ useEffect(() => { const container = document.getElementById("map"); const options = { - center: new kakao.maps.LatLng(37.552635722509, 126.92436042413), - level: 1, + center: new kakao.maps.LatLng(37.55087078580574, 126.92555912211695), + level: 2, }; const map = new kakao.maps.Map(container, options); addMarkers(map); - // 선택된 건물이 변경될 때 지도를 해당 위치로 이동 if (selectedBuilding) { const moveLatLon = new kakao.maps.LatLng( selectedBuilding.coordinates.lat, @@ -52,7 +75,7 @@ const Kakaomap: React.FC<KakaomapProps> = ({ ); map.setCenter(moveLatLon); } - }, [selectedBuilding]); + }, [selectedBuilding, markFacility]); return ( <div diff --git a/src/components/pages/Homepage.tsx b/src/components/pages/Homepage.tsx index 62cae8f..9d0f29c 100644 --- a/src/components/pages/Homepage.tsx +++ b/src/components/pages/Homepage.tsx @@ -2,18 +2,17 @@ import styled from "@emotion/styled"; import HomeBoard from "../HomeBoard.tsx"; import Kakaomap from "../map/Kakaomap.tsx"; import NavBar from "../nav/NavBar.tsx"; -import { useState } from "react"; import { BuildingInfo } from "../data/buildingData.ts"; +import { isPanelOpenAtom, selectedBuildingAtom } from "../../store/building.ts"; +import { useAtom } from "jotai"; const HomePageWrapper = styled.div` position: relative; width: calc(100vw - 70px); height: 100vh; `; const HomePage = () => { - const [selectedBuilding, setSelectedBuilding] = useState<BuildingInfo | null>( - null - ); - const [isPanelOpen, setIsPanelOpen] = useState(false); + const [, setSelectedBuilding] = useAtom(selectedBuildingAtom); + const [, setIsPanelOpen] = useAtom(isPanelOpenAtom); const handleBuildingClick = (building: BuildingInfo) => { setSelectedBuilding(building); setIsPanelOpen(true); @@ -22,16 +21,8 @@ const HomePage = () => { <> <NavBar /> <HomePageWrapper> - <Kakaomap - selectedBuilding={selectedBuilding} - onBuildingClick={handleBuildingClick} - /> - <HomeBoard - selectedBuilding={selectedBuilding} - onBuildingClick={handleBuildingClick} - isPanelOpen={isPanelOpen} - setIsPanelOpen={setIsPanelOpen} - /> + <Kakaomap onBuildingClick={handleBuildingClick} /> + <HomeBoard /> </HomePageWrapper> </> ); diff --git a/src/store/building.ts b/src/store/building.ts new file mode 100644 index 0000000..a9fc357 --- /dev/null +++ b/src/store/building.ts @@ -0,0 +1,10 @@ +import { atom } from "jotai"; +import { + BuildingInfo, + FacilityInfo, +} from "../components/data/buildingData.tsx"; + +export const selectedBuildingAtom = atom<BuildingInfo | null>(null); +export const isPanelOpenAtom = atom<boolean>(false); +export const facilityAtom = atom<FacilityInfo | null>(null); +export const markFacilityAtom = atom<number | null>(null);