diff --git a/eslint.config.js b/eslint.config.js index 0387e22..27c452e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,6 +20,7 @@ export default [ "react/react-in-jsx-scope": "off", // React 17 이상에서는 불필요 "react/jsx-no-target-blank": ["error", { allowReferrer: true }], // 보안 문제 해결 "@typescript-eslint/no-explicit-any": "off", + "react/prop-types": "off", }, }, ]; diff --git a/src/App.tsx b/src/App.tsx index c2276ee..7e1d97e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ function App() { } /> + diff --git a/src/components/Building.tsx b/src/components/Building.tsx index d2f788b..bd1f1ee 100644 --- a/src/components/Building.tsx +++ b/src/components/Building.tsx @@ -1,22 +1,76 @@ import styled from "@emotion/styled"; +import Divider from "./Divider.tsx"; +import { buildingData, BuildingInfo } from "./data/buildingData.ts"; +import Overflow from "./Overflow.tsx"; + +interface BuildingProps { + onBuildingClick: (building: BuildingInfo) => void; +} +const Building: React.FC = ({ onBuildingClick }) => { + return ( + <> + + + 홍익대학교 + + + + 내부건물 + {buildingData.map((building) => ( + <> + onBuildingClick(building)} + > + + + {building.name} +
운영 시간: {building.time}
+
+
+ + + ))} +
+
+ + ); +}; +export default Building; const Container = styled.div` + width: 400px; position: relative; - padding: 45px; - overflow: hidden; + padding: 20px 30px; `; const Title = styled.div` + margin: 10px 0px; width: 300px; - font-size: 30px; + font-size: 35px; font-weight: 600; `; -const Building = () => { - return ( - <> - - 홍익대학교 - - - ); -}; -export default Building; +const SubTitle = styled.div` + font-size: 25px; + font-weight: 500; + margin-bottom: 10px; +`; +const BuildingItem = styled.a` + display: flex; + margin: 20px 0px; + color: black; + cursor: pointer; +`; +const Image = styled.img` + width: 100px; + height: 100px; + margin-right: 20px; +`; +const Detail = styled.div` + display: flex; + flex-direction: column; + gap: 10px; +`; +const Name = styled.div` + font-size: 20px; + font-weight: 500; +`; diff --git a/src/components/BuildingDetail.tsx b/src/components/BuildingDetail.tsx new file mode 100644 index 0000000..6242d2f --- /dev/null +++ b/src/components/BuildingDetail.tsx @@ -0,0 +1,187 @@ +import styled from "@emotion/styled"; +import { BuildingInfo, FacilityInfo } from "./data/buildingData"; +import Divider from "./Divider"; +import { useState } from "react"; +import FacilityItem from "./FacilityItem.tsx"; +import Overflow from "./Overflow.tsx"; +interface BuildingDetailProps { + building: BuildingInfo; + onFacilityClick?: (facility: FacilityInfo) => void; +} +const BuildingDetail: React.FC = ({ + building, + onFacilityClick, +}) => { + const [selectedFloor, setSelectedFloor] = useState(null); + const [selectedType, setSelectedType] = useState(null); + const handleTypeChange = (type: number) => { + setSelectedType(Number(type)); + if (selectedType == type) { + setSelectedType(null); + } + }; + const handleFloorChange = (event: React.ChangeEvent) => { + setSelectedFloor(event.target.value); + }; + return ( + <> + + {building.name} + +

{building.name}

+

운영 시간: {building.time}

+ + + {building.floors.map((floor) => ( + + ))} + +
+ + + + {selectedFloor ? `${selectedFloor}층 내부 시설` : "내부 시설"} + + + + + + + + {building.facilities?.map((facility) => ( + <> + onFacilityClick?.(facility)} + > + {selectedFloor ? ( + <> + {selectedFloor === facility.floor && ( + <> + {selectedType ? ( + facility.type === selectedType && ( + <> + + + + ) + ) : ( + <> + + + + )} + + )} + + ) : ( + <> + {" "} + {selectedType ? ( + facility.type === selectedType && ( + <> + + + + ) + ) : ( + <> + + + + )} + + )} + + + ))} + +
+ + ); +}; + +export default BuildingDetail; + +const FacilityItems = styled.a` + display: flex; + flex-direction: column; + cursor: pointer; +`; + +const Facilities = styled.div` + display: flex; + gap: 10px; + margin-bottom: 20px; +`; +const DetailTitle = styled.p` + font-size: 20px; + font-weight: 500; + margin-bottom: 10px; +`; +interface ButtonProps { + selected: boolean; +} +const Button = styled.button` + border: none; + background: #a7a7a7; + color: white; + padding: 5px 10px; + border-radius: 10px; + font-size: 17px; + font-weight: 300; + width: 84px; + cursor: pointer; + flex-shrink: 0; + white-space: nowrap; + &:hover { + background: rgb(0, 51, 99, 0.5); + } + ${({ selected }) => + selected && + ` + background: rgb(0, 51, 99, 0.5); + `}// +`; + +const Image = styled.img` + width: 100%; + height: 200px; + object-fit: cover; +`; +const Container = styled.div` + width: 400px; + padding: 20px 30px; +`; +const DropDown = styled.select` + border: none; + margin: 10px 0px; + width: 100px; + padding: 4px 15px; + text-align: left; + border-radius: 10px; + font-size: 13.5px; + font-weight: 300; + background: #a7a7a7; + color: white; + appearance: none; +`; diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx new file mode 100644 index 0000000..5ee4db2 --- /dev/null +++ b/src/components/Divider.tsx @@ -0,0 +1,12 @@ +import styled from "@emotion/styled"; +interface DividerProps { + size: boolean; +} +const Divider = styled.div` + width: 100%; + height: ${({ size }) => (size ? "10px" : "1px")}; + background: #e9e9e9; + margin-bottom: 10px; +`; + +export default Divider; diff --git a/src/components/FacilityDetail.tsx b/src/components/FacilityDetail.tsx new file mode 100644 index 0000000..69b877f --- /dev/null +++ b/src/components/FacilityDetail.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import { FacilityInfo } from "./data/buildingData"; +import styled from "@emotion/styled"; +import LikeButton from "./LikeButton"; +import Divider from "./Divider"; +import { IoMdHeart } from "react-icons/io"; + +interface FacilityDetailProps { + facility: FacilityInfo; +} + +const FacilityDetail: React.FC = ({ facility }) => { + return ( + <> + + + <h2> + {facility.floor}층 {facility.name} + </h2> + <Building>{facility.building} </Building> + + + 좋아요 {facility.like}개 + 싫어요 {facility.dislike}개 + + + + + +

리뷰 {facility.reviewCount}개

+
+ + + 등록 + + + {facility.review.map((review) => ( + <> + + + +
{review.contents}
+ + + {review.like} + +
+
+
+ {review.user} +
+
+ {review.date} +
+
+
+ + ))} +
+ + ); +}; + +export default FacilityDetail; +const ReviewContainer = styled.div` + padding: 0px 10px; + margin-bottom: 10px; +`; +const Container = styled.div` + width: 400px; + padding: 20px 30px; +`; +const Title = styled.div` + display: flex; + align-items: center; + margin-top: 10px; +`; +const Building = styled.p` + margin-left: 10px; + color: #a7a7a7; +`; +const Like = styled.div` + display: flex; + gap: 10px; + margin: 15px 0px 10px 0px; +`; +const Review = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 10px; + margin-bottom: 5px; + font-weight: 300; +`; +const Input = styled.input` + width: 83%; + height: 35px; + border: none; + background: #f2f2f2; + border-radius: 5px; + padding: 0px 10px; +`; +const ReviewButton = styled.button` + border: none; + width: 15%; + border-radius: 5px; + background: #8099b1; + color: white; + &:hover { + background: #003363; + } +`; +const ReviewInput = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: 25px; + margin-top: 20px; +`; diff --git a/src/components/FacilityItem.tsx b/src/components/FacilityItem.tsx new file mode 100644 index 0000000..c5544ce --- /dev/null +++ b/src/components/FacilityItem.tsx @@ -0,0 +1,71 @@ +import styled from "@emotion/styled"; +import LikeButton from "./LikeButton.tsx"; +import { FacilityInfo } from "./data/buildingData.ts"; +interface FacilityItemProps { + facility?: FacilityInfo | null; +} +const FacilityItem: React.FC = ({ facility }) => { + return ( + <> + {facility ? ( + <> + + + {facility.floor}층 {facility.name} + + + 저장 + + 아직까지 작성된 리뷰가 없습니다! + + 좋아요 {facility.like}개 + 싫어요 {facility.dislike}개 + + + ) : ( +

시설 정보가 없습니다.

+ )} + + ); +}; + +export default FacilityItem; + +const TitleItems = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +`; +const StoreButton = styled.button` + border: none; + background: #8099b1; + color: white; + padding: 2px; + width: 50px; + height: 27px; + border-radius: 10px; + font-size: 13px; + font-weight: 300; + cursor: pointer; + white-space: nowrap; + &:hover { + background: rgb(0, 51, 99, 0.8); + } +`; + +const DetailTitle = styled.p` + font-size: 20px; + font-weight: 500; + margin: 10px 0px; +`; + +const Review = styled.div` + font-size: 15px; + font-weight: 300; + color: #a7a7a7; +`; +const Like = styled.div` + display: flex; + gap: 10px; + margin: 15px 0px 10px 0px; +`; diff --git a/src/components/HomeBoard.tsx b/src/components/HomeBoard.tsx index 7f11700..254149d 100644 --- a/src/components/HomeBoard.tsx +++ b/src/components/HomeBoard.tsx @@ -2,6 +2,73 @@ 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 BuildingDetail from "./BuildingDetail.tsx"; +import FacilityDetail from "./FacilityDetail.tsx"; + +interface HomeBoardProps { + onBuildingClick: (building: BuildingInfo) => void; + selectedBuilding: BuildingInfo | null; + isPanelOpen: boolean; + setIsPanelOpen: (isPanelOpen: boolean) => void; +} +const HomeBoard: React.FC = ({ + selectedBuilding, + onBuildingClick, + isPanelOpen, + setIsPanelOpen, +}) => { + const [facility, setFacility] = useState(null); + const toggleMenu = () => { + setIsPanelOpen(!isPanelOpen); + }; + const handleFacilityClick = (facility: FacilityInfo) => { + setFacility(facility); + }; + return ( + <> + + +
  • + 화장실 +
  • +
  • + 정수기 +
  • +
  • + 카페 +
  • +
    + + + {facility ? ( + + ) : selectedBuilding ? ( + + ) : ( + + )} + +
    + + ); +}; + +export default HomeBoard; + interface PanelProps { isOpen: boolean; } @@ -52,43 +119,3 @@ const Button = styled.button` cursor: pointer; font-size: 17px; `; - -const HomeBoard: React.FC = () => { - const [isOpen, setIsOpen] = useState(false); - const toggleMenu = () => { - setIsOpen(!isOpen); - }; - return ( - <> - - -
  • - 화장실 -
  • -
  • - 정수기 -
  • -
  • - 카페 -
  • -
    - - - - -
    - - ); -}; - -export default HomeBoard; diff --git a/src/components/LikeButton.tsx b/src/components/LikeButton.tsx new file mode 100644 index 0000000..8455a3d --- /dev/null +++ b/src/components/LikeButton.tsx @@ -0,0 +1,20 @@ +import styled from "@emotion/styled"; + +const LikeButton = styled.button` + border: none; + background: rgb(0, 51, 99, 0.2); + color: black; + padding: 5px 10px; + border-radius: 10px; + font-size: 13px; + font-weight: 300; + cursor: pointer; + white-space: nowrap; + display: flex; + align-items: center; + &:hover { + background: rgb(0, 51, 99, 0.5); + color: white; + } +`; +export default LikeButton; diff --git a/src/components/Overflow.tsx b/src/components/Overflow.tsx new file mode 100644 index 0000000..dc4b263 --- /dev/null +++ b/src/components/Overflow.tsx @@ -0,0 +1,9 @@ +import styled from "@emotion/styled"; + +const Overflow = styled.div` + overflow-y: auto; + height: 100vh; + overflow-x: hidden; +`; + +export default Overflow; diff --git a/src/components/data/buildingData.ts b/src/components/data/buildingData.ts new file mode 100644 index 0000000..6c687f2 --- /dev/null +++ b/src/components/data/buildingData.ts @@ -0,0 +1,132 @@ +export interface BuildingInfo { + image: string; + name: string; + time: string; + floors: number[]; + facilities?: FacilityInfo[]; + coordinates: { lat: number; lng: number }; +} +export interface FacilityInfo { + building: string; + type: number; + floor: string; + name: string; + like: number; + dislike: number; + reviewCount: number; + review: ReviewInfo[]; +} +interface ReviewInfo { + contents: string; + user: string; + date: string; + like: number; +} +export const buildingData: BuildingInfo[] = [ + { + image: + "https://photo.hongik.ac.kr/app/board/attach/image/thumb_353_1697760691000.do", + name: "홍문관", + time: "10:00~18:00", + floors: [1, 2, 3, 4, 5, 6, 7, 8, 9], + coordinates: { lat: 37.55258227611087, lng: 126.92491801500331 }, + facilities: [ + { + building: "홍문관", + floor: "1", + type: 1, + name: "화장실", + like: 0, + dislike: 0, + reviewCount: 2, + review: [ + { + contents: "안녕하세요", + user: "컴공생", + date: "2021.09.01", + like: 0, + }, + { + contents: "깨끗해요!", + user: "자율전공샹", + date: "2025.10.01", + like: 3, + }, + ], + }, + { + building: "홍문관", + floor: "3", + type: 1, + name: "화장실", + like: 0, + dislike: 0, + reviewCount: 2, + review: [ + { + contents: "안녕하세요", + user: "컴공생", + date: "2021.09.01", + like: 0, + }, + { + contents: "깨끗해요!", + user: "자율전공샹", + date: "2025.10.01", + like: 3, + }, + ], + }, + { + building: "홍문관", + floor: "1", + type: 3, + name: "카페나무", + like: 4, + dislike: 2, + reviewCount: 1, + review: [ + { + contents: "안녕하세요", + user: "컴공생", + date: "2021.09.01", + like: 0, + }, + ], + }, + ], + }, + { + image: + "https://photo.hongik.ac.kr/app/board/attach/image/thumb_732_1700796628000.do", + name: "와우관", + time: "00:00~24:00", + coordinates: { lat: 37.55167104651813, lng: 126.926557030475 }, + floors: [1, 2, 3, 4, 5, 6, 7, 8], + facilities: [ + { + building: "와우관", + floor: "3", + type: 1, + name: "화장실", + like: 3, + dislike: 0, + reviewCount: 2, + review: [ + { + contents: "안녕하세요", + user: "컴공생", + date: "2021.09.01", + like: 0, + }, + { + contents: "깨끗해요!", + user: "자율전공샹", + date: "2025.10.01", + like: 3, + }, + ], + }, + ], + }, +]; diff --git a/src/components/map/Kakaomap.tsx b/src/components/map/Kakaomap.tsx index 3bcf003..16d0633 100644 --- a/src/components/map/Kakaomap.tsx +++ b/src/components/map/Kakaomap.tsx @@ -1,19 +1,59 @@ import { useEffect } from "react"; +import { buildingData, BuildingInfo } from "../data/buildingData"; + declare global { interface Window { kakao: any; } } + const { kakao } = window; -const Kakaomap = () => { + +interface KakaomapProps { + selectedBuilding: BuildingInfo | null; + onBuildingClick: (building: BuildingInfo) => void; +} +const Kakaomap: React.FC = ({ + selectedBuilding, + onBuildingClick, +}) => { + const addMarkers = (map: any) => { + { + buildingData.forEach((building) => { + 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); + }); + }); + } + }; + useEffect(() => { const container = document.getElementById("map"); const options = { center: new kakao.maps.LatLng(37.552635722509, 126.92436042413), level: 1, }; - new kakao.maps.Map(container, options); - }, []); + const map = new kakao.maps.Map(container, options); + addMarkers(map); + + // 선택된 건물이 변경될 때 지도를 해당 위치로 이동 + if (selectedBuilding) { + const moveLatLon = new kakao.maps.LatLng( + selectedBuilding.coordinates.lat, + selectedBuilding.coordinates.lng + ); + map.setCenter(moveLatLon); + } + }, [selectedBuilding]); + return (
    { >
    ); }; + export default Kakaomap; diff --git a/src/components/pages/Homepage.tsx b/src/components/pages/Homepage.tsx index 8fca37e..62cae8f 100644 --- a/src/components/pages/Homepage.tsx +++ b/src/components/pages/Homepage.tsx @@ -2,18 +2,36 @@ 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"; const HomePageWrapper = styled.div` position: relative; width: calc(100vw - 70px); height: 100vh; `; const HomePage = () => { + const [selectedBuilding, setSelectedBuilding] = useState( + null + ); + const [isPanelOpen, setIsPanelOpen] = useState(false); + const handleBuildingClick = (building: BuildingInfo) => { + setSelectedBuilding(building); + setIsPanelOpen(true); + }; return ( <> - - + + ); diff --git a/src/components/style/GlobalStyle.tsx b/src/components/style/GlobalStyle.tsx index 4cf0edb..b305a7d 100644 --- a/src/components/style/GlobalStyle.tsx +++ b/src/components/style/GlobalStyle.tsx @@ -10,6 +10,7 @@ const GlobalStyle = () => ( box-sizing: border-box; margin: 0px; padding: 0px; + text-decoration: none; } body { margin-left: 70px;