diff --git a/src/app/travel-place/page.tsx b/src/app/travel-place/page.tsx new file mode 100644 index 0000000..28a8bfd --- /dev/null +++ b/src/app/travel-place/page.tsx @@ -0,0 +1,158 @@ +"use client"; +import TravelPlaceItem, { + TravelPlaceProps, +} from "@/components/travel-place/TravelPlaceItem"; +import TravelPlaceList from "@/components/travel-place/TravelPlaceList"; +import React, { useState, useEffect } from "react"; + +export default function TravelPlacePage() { + const [travelPlaces, setTravelPlaces] = useState([]); + + useEffect(() => { + async function loadTravelPlaces() { + const data = await fetchPlaces(); + setTravelPlaces(data); + } + + loadTravelPlaces(); + }, []); + + return ( +
+ +
+ ); +} + +function fetchPlaces(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(travelPlaceDatas); + }, 1000); + }); +} + +const travelPlaceDatas = [ + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2021%2F12%2F29%2F61cc26b285028.jpg&w=828&q=75", + types: ["호텔", "리조트"], + name: "럭셔리 호텔", + areas: ["해운대", "바다 전망"], + ratings: [4.7, 3200], + sleepSalePrice: 130000, + sleepCheckInTime: "15:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2020%2F06%2F12%2F5ee2ce314b98e.jpg&w=640&q=75", + types: ["게스트하우스"], + name: "코지 게스트하우스", + areas: ["광안리", "해변 근처"], + ratings: [4.3, 250], + borrowOriginalPrice: 70000, + borrowSalePrice: 65000, + borrowMaximumTime: "6시간", + sleepOriginalPrice: 80000, + sleepSalePrice: 75000, + sleepCheckInTime: "14:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2021%2F05%2F07%2F6094c1a7028c3.jpg&w=640&q=75", + types: ["펜션", "해변"], + name: "해변 펜션", + areas: ["송정", "해변가"], + ratings: [4.6, 1230], + sleepSalePrice: 95000, + sleepCheckInTime: "16:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Fadimg_new%2F74709%2F517878%2Fdfae1e96f6e96c3ba705b638ad6e2b18.jpg&w=640&q=75", + types: ["호스텔", "도심"], + name: "모던 호스텔", + areas: ["서면", "시내 중심"], + ratings: [4.1, 210], + borrowSalePrice: 45000, + borrowMaximumTime: "12시간", + sleepOriginalPrice: 60000, + sleepSalePrice: 55000, + sleepCheckInTime: "15:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2020%2F11%2F12%2F5facd1c4769bd.jpg&w=640&q=75", + types: ["리조트", "풀빌라"], + name: "오션뷰 리조트", + areas: ["기장", "오션뷰"], + ratings: [4.8, 340], + borrowOriginalPrice: 180000, + borrowSalePrice: 170000, + borrowMaximumTime: "8시간", + sleepOriginalPrice: 220000, + sleepSalePrice: 200000, + sleepCheckInTime: "14:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2016%2F05%2F17%2F573ade296aabc.jpg&w=640&q=75", + types: ["민박", "산촌"], + name: "산속의 작은 민박", + areas: ["강원도", "산속"], + ratings: [4.2, 150], + sleepOriginalPrice: 50000, + sleepSalePrice: 45000, + sleepCheckInTime: "14:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2023%2F03%2F31%2F6426a6b54e93f.jpg&w=640&q=75", + types: ["B&B", "전원"], + name: "전원 B&B", + areas: ["전주", "시골"], + ratings: [4.5, 300], + borrowOriginalPrice: 60000, + borrowSalePrice: 55000, + borrowMaximumTime: "4시간", + sleepOriginalPrice: 70000, + sleepSalePrice: 65000, + sleepCheckInTime: "15:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2021%2F08%2F12%2F611473fc22462.jpg&w=640&q=75", + types: ["아파트", "시티뷰"], + name: "도심 아파트 숙소", + areas: ["서울", "도심"], + ratings: [4.8, 500], + sleepOriginalPrice: 120000, + sleepSalePrice: 110000, + sleepCheckInTime: "16:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Fadimg_new%2F74000%2F23922%2F764c4e5c292f71cdb4b3d63595086807.jpg&w=640&q=75", + types: ["캠프장", "자연"], + name: "자연 속 캠프장", + areas: ["제주도", "해변가"], + ratings: [4.6, 220], + borrowOriginalPrice: 80000, + borrowSalePrice: 70000, + borrowMaximumTime: "10시간", + sleepOriginalPrice: 90000, + sleepSalePrice: 85000, + sleepCheckInTime: "17:00", + }, + { + imgUrl: + "https://www.yeogi.com/_next/image?url=https%3A%2F%2Fimage.goodchoice.kr%2Faffiliate%2F2023%2F10%2F20%2F65314d7fb6dd3.jpg&w=640&q=75", + types: ["모텔", "도심 근처"], + name: "시내 모텔", + areas: ["부산", "시내 근처"], + ratings: [3.9, 100], + sleepOriginalPrice: 40000, + sleepSalePrice: 35000, + sleepCheckInTime: "13:00", + }, +]; diff --git a/src/components/travel-place/TravelPlaceItem.tsx b/src/components/travel-place/TravelPlaceItem.tsx new file mode 100644 index 0000000..5cd1e53 --- /dev/null +++ b/src/components/travel-place/TravelPlaceItem.tsx @@ -0,0 +1,175 @@ +import Image from "next/image"; +import React from "react"; +import { formatNumberWithCommas } from "../../util/string_util"; + +export interface TravelPlaceProps { + imgUrl: string; + types: string[]; + name: string; + areas: string[]; + ratings: number[]; + borrowOriginalPrice?: number; + borrowSalePrice?: number; + borrowMaximumTime?: string; + sleepOriginalPrice?: number; + sleepSalePrice?: number; + sleepCheckInTime?: string; +} + +export default function TravelPlaceItem({ + imgUrl, + types, + name, + areas, + ratings, + borrowOriginalPrice, + borrowSalePrice, + borrowMaximumTime, + sleepOriginalPrice, + sleepSalePrice, + sleepCheckInTime, +}: TravelPlaceProps) { + return ( + // 아이템 영역 +
+ {/* 이미지 */} +
+ Picture of the author +
+ {/* 세부정보 */} +
+ {/* 기본정보 */} +
+ {/* 종류 */} +
+ {types.map((item, index) => ( + + {item} + {index < types.length - 1 && } + + ))} +
+ + {/* 이름 */} +

{name}

+ + {/* 위치 */} +
+ {areas.map((item, index) => ( + + {item} + {index < areas.length - 1 && } + + ))} +
+ + {/* 사용자 평가 */} +
+ {/* 평점 */} +
+ + {ratings[0]} +
+ {/* 평가자 수 */} +
+ {formatNumberWithCommas(ratings[1])} + 평가 +
+
+
+ + {/* 가격정보 */} +
+ {/* 대실 */} + {borrowSalePrice !== undefined && ( +
+ {/* 대실 체크인 시간 및 원가 */} +
+
+ 대실 + + 최대 {borrowMaximumTime} + +
+ {borrowOriginalPrice !== undefined ? ( +
+ + {formatNumberWithCommas(borrowOriginalPrice as number)} + + +
+ ) : ( + <> + )} +
+ {/* 대실 판매가 */} +
+ + {formatNumberWithCommas(borrowSalePrice as number)} + + +
+
+ )} + + {sleepSalePrice !== undefined && borrowSalePrice !== undefined ? ( +
+ ) : ( + <> + )} + + {/* 숙박 */} +
+ {/* 숙박 체크인 시간 및 원가 */} +
+
+ + 숙박 + + {sleepCheckInTime !== undefined && + borrowSalePrice !== undefined ? ( + + {sleepCheckInTime} 체크인 + + ) : ( + "" + )} +
+ {sleepOriginalPrice !== undefined ? ( +
+ + {formatNumberWithCommas(sleepOriginalPrice as number)} + + +
+ ) : ( + <> + )} +
+ {/* 숙박 판매가 */} +
+ + + {formatNumberWithCommas(sleepSalePrice as number)} + + +
+
+
+
+
+ ); +} diff --git a/src/components/travel-place/TravelPlaceList.tsx b/src/components/travel-place/TravelPlaceList.tsx new file mode 100644 index 0000000..085ff5b --- /dev/null +++ b/src/components/travel-place/TravelPlaceList.tsx @@ -0,0 +1,23 @@ +"use client"; +import React, { useState, useEffect } from "react"; +import TravelPlaceItem, { TravelPlaceProps } from "./TravelPlaceItem"; + +interface TravelPlaceListProps { + travelPlaces: TravelPlaceProps[]; +} + +export default function TravelPlaceList({ + travelPlaces, +}: TravelPlaceListProps) { + return ( +
+
+ {travelPlaces.map((travelPlace, index) => ( +
+ +
+
+ ))} +
+ ); +} diff --git a/src/util/string_util.ts b/src/util/string_util.ts new file mode 100644 index 0000000..96c14ea --- /dev/null +++ b/src/util/string_util.ts @@ -0,0 +1,7 @@ +export function formatNumberWithCommas(input: number | string) { + const number = typeof input === "string" ? parseFloat(input) : input; + if (isNaN(number)) { + throw new Error("Invalid input: not a number or a numeric string"); + } + return new Intl.NumberFormat().format(number); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 58b1bf3..41b02ab 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,47 +1,48 @@ -import type { Config } from 'tailwindcss'; +import type { Config } from "tailwindcss"; const pxToRem = (px: number, base = 16) => `${px / base}rem`; const range = (start: number, end: number): number[] => - Array.from({ length: end - start + 1 }, (_, index) => start + index); + Array.from({ length: end - start + 1 }, (_, index) => start + index); const config: Config = { - content: [ - './src/pages/**/*.{js,ts,jsx,tsx,mdx}', - './src/components/**/*.{js,ts,jsx,tsx,mdx}', - './src/app/**/*.{js,ts,jsx,tsx,mdx}', - ], - theme: { - extend: { - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - }, - colors: { - 'color-214': 'rgb(214,214,214)', - 'color-112': 'rgb(112,112,112)', - 'color-194': 'rgb(194,194,194)', - 'color-light-blue': 'rgb(18,115,228)', - }, - boxShadow: { - box: '0px 2px 16px rgba(0, 0, 0, 0.08)', - }, - spacing: Array.from({ length: 1000 }, (_, index) => { - const value = pxToRem(index + 1); - return { [`${index + 1}pxr`]: value }; - }).reduce((acc, obj) => ({ ...acc, ...obj }), {}), - fontSize: Array.from({ length: 1000 }, (_, index) => { - const value = pxToRem(index + 1); - return { [`${index + 1}pxr`]: value }; - }).reduce((acc, obj) => ({ ...acc, ...obj }), {}), - }, - screens: { - sm: '640px', - md: '920px', - lg: '1024px', - xl: '1280px', - }, - }, - plugins: [], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + colors: { + "color-214": "rgb(214,214,214)", + "color-112": "rgb(112,112,112)", + "color-194": "rgb(194,194,194)", + "color-orange": "rgb(255,173,10)", + "color-light-blue": "rgb(18,115,228)", + }, + boxShadow: { + box: "0px 2px 16px rgba(0, 0, 0, 0.08)", + }, + spacing: Array.from({ length: 1000 }, (_, index) => { + const value = pxToRem(index + 1); + return { [`${index + 1}pxr`]: value }; + }).reduce((acc, obj) => ({ ...acc, ...obj }), {}), + fontSize: Array.from({ length: 1000 }, (_, index) => { + const value = pxToRem(index + 1); + return { [`${index + 1}pxr`]: value }; + }).reduce((acc, obj) => ({ ...acc, ...obj }), {}), + }, + screens: { + sm: "640px", + md: "920px", + lg: "1024px", + xl: "1280px", + }, + }, + plugins: [], }; export default config;