위코드에서 마지막으로 진행한 2차 프로젝트 클론 코딩.
트립어드바이저(Trip advisor)
프로젝트 소개🙌
- 전 세계 여행자들의 859만 개의 숙박 시설, 음식점, 체험, 항공권, 크루즈 여행에 대한 7억 95천만 건 이상의 리뷰와 의견을 검색하여 모든 여행이 최적의 여행이 되도록 도와주는 Tripadvisor 홈페이지 클론코딩.
개발기간📆
- 2020년 8월 3일 ~ 2020년 8월 14일(12일간)
팀원🐙
프론트엔드
- [10기 권한성]
- [10기 김경배]
- [10기 송수호]
백엔드
- [10기 김도환]
- [10기 김병준]
기술 스택 및 구현 기능
1)기술 스택
- React.js
- React Router
- RESTful API
- multipart/form-data
2)구현 기능 상세 설명
[메인 페이지]
- 메인페이지 브라우저 스크롤 이벤트함수를 작성하여 nav 검색창에 적용
- 메인페이지 slick.js를 활용한 슬라이드 기능
- State값을 변경하여 좋아요 on/off 기능
[포스트 리뷰 페이지]
- fetch post메소드와 multipart-form을 적용하여 서버에 파일/텍스트 업로드 기능구현
[리뷰 페이지]
- fetch get메소드와 multipart-form을 적용하여 서버에 저장된 파일/이미지 내려받아 feed를 추가하는 기능구현
[회원가입/로그인 페이지]
- Fetch 함수를 통한 백엔드 통신연결
- Name 메소드를 통한 입력값 컨트롤.
- 입력값의 길이를 비교하여 버튼색깔 다르게 구현 및 조건에 따른 경고창 구현.
- 구글api, 카카오톡 api를 통한 sns로그인 기능 구현.
[호텔 상세 페이지]
- 리액트 useState, useEffect를 이용한 state값 통제
- Slick slider를 통한 이미지 슬라이더 구현.
- Overflow:hidden을 통해서 내용을 보이는 부분 통제.
[호텔 리스트 페이지]
- 페이지네이션 기능 구현
- calendar 라이브러리 구현(datepicker)
- 이미지 슬라이더 기능 구현
- fetch시 데이터 받아오는동안 loading 이미지 적용
- 정렬순서, 체크인/아웃 날짜, 부대시설(checkbox)를 통해 3중 필터링 구현
[지도 페이지]
- 구글 지도 Api 사용하여 백엔드로부터 받아온 좌표값을 통해 마커 생성 구현
- 호텔 평점에 해당하는 필터링 기능 구현
- marker 클릭시 해당하는 아이템 연결
github.com/kasumil/10-NoMac-frontend
내가 담당한 부분은
이전에 고민하던 부분중에 마지막인 더보기 부분의 아랫부분의 사진을 방향키(slick-slider) 버튼을 클릭하면 아래에 있던더 작은 사진도 같이 호버되는 것처럼 구현하고 싶었는데, 아쉬웠다.
또한 전체보기 사진을 누르면 해당 modal popup이 구현되어 이미지를 한번 더 펼쳐보는거였으면 더 좋았을텐데 아쉽기도 하고....이건 시간이 부족해서 만들지 못했다.
한페이지만 더 만들어서 클릭으로 modal을 작동시켜 slick을 구현하면 멋있을텐데..
여러모로 기능의 욕심을 안두는 편이었는데, 이번에 작업하면서 욕심을 갖게 되는 편이었다.
import React, { useState } from "react";
import styled from "styled-components";
import { withRouter } from "react-router-dom";
import { hotelListUrl } from "../../Config.js";
function EmailLogin(props, visible) {
const [ userInfo, setName ] = useState({email:"", password:""});
const inputValuedetector = (e) => {
const { name, value} = e.target
setName({...userInfo, [ name ] : value })
};
const submit = () => {
const { email, password } = userInfo
fetch(`${hotelListUrl}/account/sign-in`, {
method: 'POST',
body: JSON.stringify({ email, password })
})
.then(res => res.json())
.then(res => {
if (res.token) {
localStorage.setItem("tripadvisor-token", res.token)
alert("로그인을 환영합니다")
props.history.push('/')
} else {
alert('이메일과 비밀번호를 확인해주십시오')
};
})
}
const isValidEmail = !userInfo.email.length ||
(userInfo.email.length && userInfo.email.includes("@"));
return(
<>
<ModalOverlay visible={visible} />
<ModalWrapper tabIndex="-1" visible={visible}>
<ModalIner tabIndex="0">
<div className="modalOutline">
<LoginImageContainer>
<LoginImgtag src="https://static.tacdn.com/img2/brand_refresh/Tripadvisor_lockup_horizontal_registered.svg" />
</LoginImageContainer>
<SignInput>
<div className="signupContainer">
<div className="welcome">환영합니다 - 로그인하세요!</div>
<label className="welcome label">이메일 주소</label>
<input onChange={inputValuedetector}
className="inputValue"
type='text'
name="email"
placeholder="이메일" />
<label className="welcome label">패스워드</label>
<input onChange={inputValuedetector}
className="inputValue"
type='password'
name="password"
placeholder="비밀번호" />
<div onClick={submit}
className="inputValue logIntitle"
type="buttton">
로그인
</div>
<div className="passWordFindertitle">
<span className="passwordfinder">
패스워드 찾기
</span>
</div>
<div className="passWordFindertitle">
계정이 없으신가요?
<span onClick={()=>props.setMode("signup")}
className="passwordfinder">
가입하기
</span>
</div>
<div className="passWordFindertitle">
대신 카카오톡이나 Google을 사용하고 싶으세요?
<span onClick={()=>props.setMode("social")}
className="passwordfinder">
돌아가기
</span>
</div>
</div>
<ContiNue>
계속 진행할 경우, 트립어드바이저의
<span className="textDeco">
개인정보 취급방침
</span>
및
<span className="textDeco Cookie">
쿠키 정책
</span>
에 동의한 것으로 간주됩니다.
</ContiNue>
</SignInput>
<div className={ isValidEmail
? 'noneinputError'
: 'inputError'
} >
이메일 주소로써 유효하지 않거나 저희쪽에서 메일을 발송할 수 없는 문자열을 포함하고있습니다.
</div>
</div>
<button onClick={props.closeModal}
className="returnmain"
tabIndex="0">
X
</button>
</ModalIner>
</ModalWrapper>
</>
)
}
const ModalOverlay = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: 999;
`;
const ModalWrapper = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
overflow: auto;
outline: 0;
display: flex;
justify-content: center;
align-items: center;
`;
const ModalIner = styled.div`
width: 420px;
height: 666px;
padding: 40px 45px 30px;
box-sizing: border-box;
position: relative;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5);
background-color: white;
.modalOutline {
width: 100%;
.inputError {
position: absolute;
display: block;
color: red;
background-color: white;
font-family: 굴림, gulim, sans-serif;
font-size: 12px;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
padding: 18px;
line-height: 16px;
width: 316px;
height: 68px;
z-index: 5;
left: 51px;
right: auto;
top: 113px;
bottom: auto;
}
.noneinputError {
display: none;
color: white;
}
}
.returnmain {
position: absolute;
z-index: 1;
top: 0;
right: 0;
text-align: center;
width: 48px;
height: 48px;
font-size: 25px;
cursor: pointer;
}
`;
const LoginImageContainer = styled.div`
width: 328px;
`;
const LoginImgtag = styled.img`
width: 210px;
height: 32px;
margin-right: 20px;
`;
const SignInput = styled.div`
.signupContainer {
width: 328px;
height: 440px;
display: flex;
flex-direction: column;
.welcome {
color: black;
font-size: 16px;
font-family: "굴림, gulim, sans-serif";
margin: 36px 0 25px;
}
.label {
font-size: 14px;
font-weight: 600;
line-height: 18px;
text-align: left;
margin: 0;
}
.inputValue {
background-color: white;
height: 48px;
margin: 8px 0 16px;
padding: 4px 4px 4px 8px;
border: 2px solid #e0e0e0;
border-radius: 12px;
}
.logIntitle {
color: white;
background: black;
width: 100%;
font-size: 14px;
font-weight: 700;
margin: 20px 0;
padding: 14px 16px 8px;
border: 1px solid transparent;
line-height: 18px;
text-align: center;
&:hover {
background: #525252;
}
}
.passWordFindertitle {
margin-bottom: 12px;
.passwordfinder {
font-size: 12px;
font-family: "굴림, gulim, sans-serif";
font-weight: 600;
color: black;
line-height: 16px;
margin-left: 5px;
&:hover {
text-decoration: underline;
}
}
}
}
`;
const ContiNue = styled.div`
width: 328px;
height: 32px;
font-size: 12px;
font-family: "굴림, gulim, sans-serif";
.textDeco {
text-decoration: underline;
color: black;
text-decoration-style: solid;
font-weight: 600;
line-height: 16px;
margin: 0 5px;
}
.Cookie {
margin: 0 0 0 5px;
}
`;
export default withRouter(EmailLogin);
import React, { useState } from "react";
import styled from "styled-components";
import { withRouter } from "react-router-dom";
import { hotelListUrl } from "../../Config";
function Signup(props, visible) {
const [ userInfo, setName ] = useState({email:"",password:"",name:""});
const inputValuedetector = (e) => {
const { name, value } = e.target;
setName({ ...userInfo, [name]: value });
};
const submit = () => {
const { email, password, name } = userInfo;
fetch(`${hotelListUrl}/account/sign-up`, {
method: "POST",
body: JSON.stringify({ email, password, name }),
})
.then((res) => res.json)
.then((res) => {
if (res) {
alert("회원가입을 환영합니다");
props.history.push("/");
} else {
alert("이메일과 비밀번호를 확인해주십시오");
}
});
};
const isValidEmail =
!userInfo.email.length ||
(userInfo.email.length && userInfo.email.includes("@"));
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper tabIndex="-1" visible={visible}>
<ModalIner tabIndex="0">
<div className="modalOutline">
<LoginImageContainer>
<LoginImgtag src="https://static.tacdn.com/img2/brand_refresh/Tripadvisor_lockup_horizontal_registered.svg" />
</LoginImageContainer>
<SignInput>
<div className="signupContainer">
<div className="signupBody">
<div className="welcome">바로 회원가입하기 - 무료입니다!</div>
<label className="welcome label">이메일 주소</label>
<input
onChange={inputValuedetector}
className="email_input"
type="text"
name="email"
placeholder="이메일"
/>
<label className="welcome label">패스워드 생성하기</label>
<input
onChange={inputValuedetector}
className="email_input"
type="password"
name="password"
placeholder="비밀번호"
/>
<label className="welcome label">닉네임 생성하기</label>
<input
onChange={inputValuedetector}
className="email_input"
type="text"
name="name"
placeholder="닉네임"
/>
<div onClick={submit} className="logIntitle" type="button">
가입하기
</div>
</div>
<div className="passWordFindertitle">
이미 계정이 있으신가요?
<span
onClick={() => props.setMode("email")}
className="passwordfinder"
>
로그인
</span>
</div>
<div className="passWordFindertitle">
대신 카카오톡이나 Google을 사용하고 싶으세요?
<span
onClick={() => props.setMode("social")}
className="passwordfinder"
>
돌아가기
</span>
</div>
</div>
<div className="inputboxTitle">
<input className="inputbot" type="checkbox"></input>
<label>
트립어드바이저의 여행 특가, 팁, 새로운 기능에 대한 정보 메일을
수신하겠습니다. 수신을 원치 않을 때는 해지할 수 있습니다.
</label>
</div>
</SignInput>
<div className={isValidEmail ? "noneinputError" : "inputError"}>
이메일 주소로써 유효하지 않거나 저희쪽에서 메일을 발송할 수 없는
문자열을 포함하고있습니다.
</div>
</div>
<button
onClick={props.closeModal}
className="returnmain"
tabIndex="0"
>
X
</button>
</ModalIner>
</ModalWrapper>
</>
);
}
const ModalOverlay = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: 999;
`;
const ModalWrapper = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
overflow: auto;
outline: 0;
display: flex;
justify-content: center;
align-items: center;
`;
const ModalIner = styled.div`
width: 420px;
height: 666px;
padding: 40px 45px 30px;
box-sizing: border-box;
position: relative;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5);
background-color: white;
.modalOutline {
width: 100%;
.inputError {
position: absolute;
display: block;
color: red;
background-color: white;
font-family: "굴림, gulim, sans-serif";
font-size: 12px;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
padding: 18px;
line-height: 16px;
width: 316px;
height: 68px;
z-index: 5;
left: 51px;
right: auto;
top: 113px;
bottom: auto;
}
.noneinputError {
display: none;
color: white;
}
}
.returnmain {
position: absolute;
z-index: 1;
top: 0;
right: 0;
text-align: center;
width: 48px;
height: 48px;
font-size: 25px;
cursor: pointer;
}
`;
const LoginImageContainer = styled.div`
width: 328px;
`;
const LoginImgtag = styled.img`
width: 210px;
height: 32px;
margin-right: 20px;
`;
const SignInput = styled.div`
.signupContainer {
width: 100%;
height: 440px;
.signupBody {
display: flex;
flex-direction: column;
width: 100%;
height: 380px;
.welcome {
color: black;
font-size: 16px;
font-family: "굴림, gulim, sans-serif";
margin: 36px 0 25px;
}
.label {
font-size: 14px;
font-weight: 600;
line-height: 18px;
text-align: left;
margin: 0;
}
.email_input {
background-color: white;
height: 50px;
margin: 8px 0 16px;
padding: 4px 4px 4px 8px;
border: 2px solid #e0e0e0;
border-radius: 12px;
}
.logIntitle {
color: white;
background: black;
width: 100%;
height: 48px;
font-size: 14px;
font-weight: 700;
margin: 20px 0;
padding: 14px 16px 8px;
border-radius: 12px;
border: 1px solid transparent;
line-height: 18px;
text-align: center;
&:hover {
background: #525252;
}
}
}
.passWordFindertitle {
margin-bottom: 12px;
.passwordfinder {
font-size: 12px;
font-family: "굴림, gulim, sans-serif";
font-weight: 600;
color: black;
line-height: 16px;
margin-left: 5px;
&:hover {
text-decoration: underline;
}
}
}
}
`;
export default withRouter(Signup);
import React, { useState } from "react";
import styled from "styled-components";
import KakaoLogin from "react-kakao-login";
import { GoogleLogin } from "react-google-login";
import { withRouter } from "react-router-dom";
import { hotelListUrl } from "../../Config";
import { ClientId } from "../../Config";
import { Jskey } from "../../Config";
function SocialLogin(props, visible) {
const [id, setId] = useState("");
const [name, setName] = useState("");
// 구글 로그인
const clickGoogleBtn = (res) => {
fetch(`${hotelListUrl}/sign-in/google`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
email: res.profileObj.email,
name: res.profileObj.name,
}),
})
.then((res) => res.json())
.then((res) => {
if (res) {
localStorage.setItem("kakao-token", res.token);
alert("로그인을 환영합니다");
props.history.push("/");
} else {
alert("아이디와 비밀번호를 확인해주세요.");
}
});
};
// 카카오 로그인
const clickKakaoBtn = (res) => {
fetch(`${hotelListUrl}/account/sign-in/kakao`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
access_token: res.response.access_token,
}),
})
.then((res) => res.json())
.then((res) => {
if (res.access_token) {
localStorage.setItem("kakao-token", res.access_token);
alert("로그인을 환영합니다");
props.history.push("/");
} else {
alert("아이디와 비밀번호를 확인해주세요.");
}
});
};
const responseFail = (err) => {
console.error(err);
};
return (
<>
<ModalOverlay visible={visible} />
<ModalWrapper tabIndex="-1" visible={visible}>
<ModalIner tabIndex="0">
<div>
<LoginImageContainer>
<LoginImgtag src="https://static.tacdn.com/img2/brand_refresh/Tripadvisor_lockup_horizontal_registered.svg" />
</LoginImageContainer>
<LogInnput>
<LoginInputMargin>
<div className="centerMargin">
<GoogleBtn
clientId={`${ClientId}`}
onSuccess={clickGoogleBtn}
onFailure={responseFail}
>
<Socialspan>Google로 계속하기</Socialspan>
</GoogleBtn>
<KakaoBtn
jsKey={`${Jskey}`}
onSuccess={clickKakaoBtn}
onFailure={responseFail}
getProfile="true"
>
<SocialImg src="https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcTI2AI6Bz3gkrH3VCq4azeIg-CXoxxvsar2Og&usqp=CAU" />
<Socialspan>카카오톡 계정으로 계속하기</Socialspan>
</KakaoBtn>
<OR>
<BorderLine>
<Topspan>또는</Topspan>
</BorderLine>
</OR>
<SocialBtn onClick={() => props.setMode("email")}>
<SocialImg src="https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcQvsq86NX1qN68M4GUeh5k8h6KM4pADF86mEw&usqp=CAU" />
<Socialspan>이메일로 계속하기</Socialspan>
</SocialBtn>
</div>
<ContiNue>
계속 진행할 경우, 트립어드바이저의
<span className="textDeco">개인정보 취급방침</span>
및
<span className="textDeco">쿠키 정책</span>에 동의한 것으로
간주됩니다.
</ContiNue>
</LoginInputMargin>
</LogInnput>
</div>
<button
onClick={props.closeModal}
className="returnmain"
tabIndex="0"
>
X
</button>
</ModalIner>
</ModalWrapper>
</>
);
}
const ModalOverlay = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: 999;
`;
const ModalWrapper = styled.div`
box-sizing: border-box;
display: ${(props) => (props.visible ? "block" : "none")};
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
overflow: auto;
outline: 0;
display: flex;
justify-content: center;
align-items: center;
`;
const ModalIner = styled.div`
width: 420px;
height: 666px;
padding: 40px 45px 30px;
box-sizing: border-box;
position: relative;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.5);
background-color: white;
.returnmain {
position: absolute;
z-index: 1;
top: 0;
right: 0;
text-align: center;
width: 48px;
height: 48px;
font-size: 25px;
cursor: pointer;
}
`;
const LoginImageContainer = styled.div`
width: 328px;
`;
const LoginImgtag = styled.img`
width: 210px;
height: 32px;
margin-right: 20px;
`;
const LogInnput = styled.div`
width: 329px;
height: 467px;
`;
const LoginInputMargin = styled.div`
width: 328px;
height: 48px;
.centerMargin {
margin: 50px 0;
position: relative;
}
`;
const GoogleBtn = styled(GoogleLogin)`
width: 328px !important;
height: 48px !important;
margin-bottom: 16px !important;
border-radius: 12px !important;
box-shadow: none !important;
border: 2px solid rgba(0, 0, 0, 0.2) !important;
color: black !important;
background-color: "white" !important;
display: flex !important;
align-items: center !important;
div {
width: 20px;
height: 20px;
margin: 0 0 0 24px !important;
padding: 0 !important;
}
span {
padding: 0 !important;
}
`;
const KakaoBtn = styled(KakaoLogin)`
width: 328px;
height: 48px;
margin-bottom: 16px;
border-radius: 12px;
box-shadow: none;
border: 2px solid rgba(0, 0, 0, 0.2);
color: black;
background-color: "white";
display: flex;
align-items: center;
`;
const SocialBtn = styled.div`
width: 328px;
height: 48px;
margin-bottom: 16px;
border-radius: 12px;
box-shadow: none;
border: 2px solid rgba(0, 0, 0, 0.2);
color: black;
background-color: "white";
display: flex;
align-items: center;
`;
const SocialImg = styled.img`
width: 20px;
height: 20px;
margin-left: 24px;
`;
const Socialspan = styled.span`
color: black;
width: auto;
font-size: 14px;
font-family: "굴림, gulim, sans-serif";
margin: 0 12px 0 50px;
`;
const OR = styled.div`
padding: 8px 0;
text-align: center;
`;
const BorderLine = styled.div`
border-top: 1px solid #e0e0e0;
content: none;
display: block;
`;
const Topspan = styled.span`
position: relative;
top: -8px;
font-weight: 600;
font-size: 10px;
line-height: 14px;
color: #8c8c8c;
padding: 0 6px;
background-color: white;
`;
const ContiNue = styled.div`
width: 328px;
height: 32px;
font-size: 12px;
font-family: "굴림, gulim, sans-serif";
.textDeco {
text-decoration: underline;
color: black;
text-decoration-style: solid;
font-weight: 600;
line-height: 16px;
}
`;
export default withRouter(SocialLogin);
구조상으로 바로 로그인을 완료하면 내 페이제에서는 X버튼을 안누르고도 Modal 기능이 꺼지고 나가지는데, 왜 시연할때는 안꺼지는건지 이해를 못하겠다.;;;
내꺼에선 정상작동 되었는데 왜 남의것으로 합쳐서 시연할땐 작동이 안된건지 참.... -_-
다음으로는 호텔(Detail 페이지).
아쉬움이 느껴지는 페이지였다.
호텔 인트로듀스에서 minipicture에서 smallbigImg와 verysmall부분이 있는데, 하나를 클릭시 다른 하나도 작동시켜야하는것이라 어려워서 4시간정도 고생하다가 결국 마감시간때문에 포기.
괜히 건드렸다가 다른 것도 터질까봐...
또 하나 전체 height를 능동형으로 더보기 버튼을 클릭하면 늘어나게 구현하려고 했는데, 무엇이 잘못되었는지....
height 부분이 더보기로 확장해서 봤을땐, 안늘어난다.... 이거 일일히 찾아야해서 먹였는데도 무엇인지 걸려서 안늘어남...
시간때문에 이것도 포기하고 고정으로 먹였다.
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import Hotelpageheader from "./Hotelpageheader";
import Hotelpicture from "./Hotelpicture";
import HotelIntroduce from "./HotelIntroduce";
import { hotelListUrl } from "../../Config";
import Nav from "../../Components/Nav";
function Hotelpage() {
const [ hotels, setHotel ] = useState([]);
useEffect(() => {
const pageNumber = sessionStorage.getItem("pageNumber")
// console.log(pageNumber)
fetch(`${hotelListUrl}/hotel/${pageNumber}`)
// fetch(`${hotelListUrl}/hotel/1`)
.then(res => res.json())
.then(res => {
console.log(`${hotelListUrl}/hotel/${pageNumber}`)
setHotel(res.data )
})
}, []);
return(
<div>
{hotels.map((hotel, index) => (
<section key={index}>
<Nav />
<Hotelpageheader idx={index} hotel={hotel}/>
<HotelBox>
<Hotelpicture idx={index} hotel={hotel} />
<HotelIntroduce idx={index} hotel={hotel} />
</HotelBox>
</section>
))};
</div>
)
}
const HotelBox = styled.div`
width: 100%;
height: 100%;
margin-bottom: 12px;
padding: 0 24px;
display: flex;
align-items: center;
flex-direction: column;
background-color:#f2f2f2;
`;
export default Hotelpage;
import React, { useState } from "react";
import styled from "styled-components";
import { Link, withRouter } from "react-router-dom";
import { IoMdPin } from "react-icons/io";
import { IoMdHeartEmpty } from "react-icons/io";
import { FiShare } from "react-icons/fi";
import { BsFillHeartFill } from "react-icons/bs";
function Hotelpageheader({idx, hotel}) {
const [ savePoint , setSavepoint ] = useState(idx.hotel);
const [ onMouse, setOnMouse ] = useState( false );
const { kind } = hotel.hotel_rating
const circleReturn =(kind) => {
if ( kind > 4.5 ) {
return "●●●●●"
}
if ( kind > 3.5 ) {
return "●●●●○"
}
if ( kind > 2.5) {
return "●●●○○"
}
if (kind >1.5) {
return "●●○○○"
}
if (kind > 0.5) {
return "●○○○○"
}
}
return(
<>
<HotelpageheaderDIV>
{/* 비용절약 광고창탭 */}
<HeaderBanner>
<SavePricetitle>
<Reducead>
<ReduceADimgWrap>
<div className="wrap">
<ReduceADimg src="https://static.tacdn.com/img2/brand_refresh/Tripadvisor_lockup_horizontal.svg" />
</div>
<ReduceADdescription>
<span><b>비용을 절약하세요.</b>트립어드바이저는 호텔 최저가를 찾기 위해 200개 이상의 사이트를 비교합니다.</span>
</ReduceADdescription>
</ReduceADimgWrap>
</Reducead>
</SavePricetitle>
</HeaderBanner>
{/* 여기부터 호텔 이름 */}
<HotelpageName>
<HotelpageSubtitleHigh>
<h1 className="hotelpageSubBigtitle">{hotel.name}
<div className="hotelpageSubEnglishtitle">
{hotel.english_name}
</div>
</h1>
</HotelpageSubtitleHigh>
<ReviwPoint>
<Link to="null">
<span className="rate">
{circleReturn(hotel.hotel_rating)}
</span>
<span className="marginleft">
{hotel.review_count_msg}
</span>
</Link>
</ReviwPoint>
</HotelpageName>
<HotelpageSubtileUnder>
<HotelPageSpace>
<HotelPageSubtitleUnderLeft>
<div>
<Locationbarwrap>
<IoMdPin className="locationImg" />
<span className="locationName">
{hotel.address}
</span>
</Locationbarwrap>
</div>
</HotelPageSubtitleUnderLeft>
<HotelPageSubtitleUnderRight>
<HeartSave onMouseEnter={() => setOnMouse(true)} onMouseLeave={() => setOnMouse(false)}>
{onMouse
? <BsFillHeartFill className="redheart" />
: <IoMdHeartEmpty className="heart" />}
<div className="save">저장</div>
</HeartSave>
<div className="shareBoxMargin">
<div className="sharebox">
<FiShare className="shareImg" />
<span className="share">
공유
</span>
</div>
</div>
</HotelPageSubtitleUnderRight>
</HotelPageSpace>
</HotelpageSubtileUnder>
</HotelpageheaderDIV>
</>
)
}
// 호텔 헤더
const HotelpageheaderDIV = styled.div`
max-width:1280px;
width: 100%;
height: 284px;
padding: 0 24px;
margin: 0 auto;
`;
const HeaderBanner = styled.div`
width: 100%;
height: 100px;
display: flex;
justify-content: center;
`;
const SavePricetitle = styled.div`
width: 100%;
margin: 0;
padding: 5px 0;
`;
const Reducead = styled.div`
width: 970px;
height: 66px;
padding: 16px 0;
margin: 12px auto 0;
align-items: center;
background-color: #faf1ed;
`;
const ReduceADimgWrap = styled.div`
display: flex;
justify-content: center;
align-items:center;
height: 100%;
.wrap {
width: 232px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
}
`;
const ReduceADimg = styled.img`
width: 164px;
padding: 0 12px 0 0;
top: 3px;
`;
const ReduceADdescription = styled.div`
width: 76%;
height: 49px;
font-size: 18px;
padding: 0 15px;
border-left: 1px solid white;
text-align: left;
`;
// 호텔헤더 이름
const HotelpageName = styled.div`
width: 100%;
height: 107px;
padding: 12px 12px 0;
flex:none;
`;
const HotelpageSubtitleHigh = styled.div`
width: 100%;
height: 71px;
.hotelpageSubBigtitle {
width: 800px;
height: 71px;
font-size: 32px;
line-height: 36px;
font-weight: 700;
color: black;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
margin: 0;
}
.hotelpageSubEnglishtitle {
width: 100%;
height: 35px;
font-size: 32px;
line-height: 36px;
font-weight: 700;
color: black;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
}
`;
const ReviwPoint = styled.div`
width: 100%;
height: 30px;
margin: 4px 0 0;
display: flex;
.rate{
width: 80px;
height: 20px;
font-size: 30px;
color: green;
}
.marginleft{
width: 95px;
height: 30px;
font-size: 14px;
color: #474747;
font-family: "Arial,Tahoma,Bitstream Vera Sans,sans-serif";
margin : 0 0 0 5px;
}
`;
const HotelpageSubtileUnder = styled.div`
width: 100%;
height: 38px;
padding: 0 12px 12px;
`;
// 저장,공유, 호텔방문 flex먹인 곳.
const HotelPageSpace = styled.div`
width: 100%;
height: 32px;
display: flex;
justify-content: space-between;
align-items: center;
`;
const HotelPageSubtitleUnderRight = styled.div`
width: 128px;
height: 22px;
color: black;
display: flex;
flex-wrap: wrap;
/* 공유박스 */
.shareBoxMargin{
width: 64px;
height: 22px;
margin-left: 8px;
padding-left: 8px;
border-left: 1px solid #e0e0e0;
.sharebox {
width: 56px;
height: 22px;
display: flex;
align-items: center;
.shareImg{
width: 20px;
height: 20px;
}
.share{
width: 36px;
height: 20px;
padding-left: 4px;
font-size: 16px;
font-family:Arial, Tahoma, "Bitstream Vera Sans", sans-serif;
}
}
}
`;
// 하트,저장 부분
const HeartSave = styled.div`
width: 55px;
height: 20px;
cursor: pointer;
display: flex;
justify-items: space-between;
.heart {
width: 20px;
height: 20px;
}
.redheart {
width: 20px;
height: 20px;
color: red;
}
.save{
width: 34px;
font-size: 16px;
font-family:Arial, Tahoma, "Bitstream Vera Sans", sans-serif;
height: 20px;
margin-left: 4px;
&:hover {
text-decoration:underline;
}
}
`;
const HotelPageSubtitleUnderLeft = styled.div`
display: flex;
`;
const Locationbarwrap = styled.div`
width: auto;
height: 25px;
display: flex;
align-items: center;
.locationImg {
width: 18px;
height: 18px;
}
.locationName{
width: auto;
height: 17px;
font-size: 14px;
font-weight: 400;
color: #474747;
border-bottom: 1px dotted #e0e0e0;
text-decoration: none;
&:hover{
text-decoration: underline;
}
}
`;
export default withRouter(Hotelpageheader);
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import { withRouter } from "react-router-dom";
import { FiCamera } from "react-icons/fi";
import { FaBed, FaRegComments } from "react-icons/fa";
import { GiMountaintop, GiKnifeFork } from "react-icons/gi";
import { BsArrowsAngleExpand } from "react-icons/bs";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
function Hotelpicture({idx, hotel}) {
const [ mouse, setMouse ] = useState(false);
const [sliderIndex,setSliderIndex] = useState(0)
const pictureBundle =() => {
idx.history.push("")
}
const settings = {
infinite: false,
speed: 500,
slidesToShow: 1,
slidesToScroll: 1,
}
return(
<>
{/* 호텔 내용 */}
<HotelPictures>
<HotelPicturePadding>
<LeftPicture>
<BigPicture>
<BigPictureInterior
onMouseEnter={() => setMouse(true)}
onMouseLeave={() => setMouse(false)}
>
<BigPictureImglistWrap>
<BigPictureImglist>
<CustomSlider {...settings}>
{hotel.image.map(hotel => {
return(
<div>
<img alt="" src={hotel}/>
</div>
)
})}
</CustomSlider>
</BigPictureImglist>
</BigPictureImglistWrap>
<DisplayButton onMouse={mouse} onClick={pictureBundle}>
<BsArrowsAngleExpand className="margin"/>
전체보기
</DisplayButton>
<Picturetitle>
<FiCamera className='picture' />
<PictureAll>모든 사진 보기
<Picturecount>(20)</Picturecount>
</PictureAll>
</Picturetitle>
</BigPictureInterior>
</BigPicture>
</LeftPicture>
<PictureColumn>
<HighBox bgImg={hotel.image[1]}>
<GiMountaintop className="position"/>
<PositionName>주변 환경사진</PositionName>
</HighBox>
<UnderBox bgImg={hotel.image[4]}>
<FaBed className="position"/>
<PositionName>객실 및 스위트</PositionName>
</UnderBox>
</PictureColumn>
<div className="rightColumn">
<RightHighBox bgImg={hotel.image[5]}>
<FaRegComments className="position"/>
<PositionName>라운지</PositionName>
</RightHighBox>
{/* 언더박스는 사진모음 */}
<RightUnderBox bgImg={hotel.image[6]}>
<GiKnifeFork className="position"/>
<PositionName>식사</PositionName>
</RightUnderBox>
</div>
</HotelPicturePadding>
</HotelPictures>
</>
)
}
// 사진전체 사이즈
const HotelPictures = styled.div`
width: 1280px;
height: 501px;
`;
//사진 전체 패딩
const HotelPicturePadding = styled.div`
width: 100%;
padding: 12px;
display: flex;
flex-direction: wrap;
.rightColumn{
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 5px;
cursor:pointer;
}
`;
//우측 주변환경사진, 객실및 스위트 컬럼
const PictureColumn = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
cursor:pointer;
`;
//우측 주변 환경사진
const HighBox = styled.div`
width : 330px;
height: 235px;
background-color: #e0e0e0;
font-size: 16px;
opacity: 0.6;
font-weight: 700;
text-shadow: -1px 0 #000,
0 1px #000,
1px 0 #000,
0 -1px #000;
background-image: url(${props => props.bgImg });
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-size: cover;
&:hover{
opacity: 0.7;
}
.position {
width: 36px;
height: 36px;
}
`;
// 우측 4개 사진의 내부 글자
const PositionName = styled.span`
height: 32px;
`;
// 우측 객실 및 스위트 부분
const UnderBox =styled.div`
width : 330px;
height: 235px;
background-color: #e0e0e0;
font-size: 16px;
opacity: 0.6;
font-weight: 700;
text-shadow: -1px 0 #000,
0 1px #000,
1px 0 #000,
0 -1px #000;
background-image: url(${props => props.bgImg });
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-size: cover;
&:hover{
opacity: 0.7;
}
.position {
width: 36px;
height: 36px;
}
`;
// 우측 라운지 부분
const RightHighBox = styled.div`
width : 330px;
height: 235px;
background-color: #e0e0e0;
font-size: 16px;
opacity: 0.6;
font-weight: 700;
text-shadow: -1px 0 #000,
0 1px #000,
1px 0 #000,
0 -1px #000;
background-image: url(${props => props.bgImg });
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-size: cover;
&:hover{
opacity: 0.7;
}
.position {
width: 36px;
height: 36px;
}
`;
// 우측 식사 부분
const RightUnderBox = styled.div`
width : 330px;
height: 235px;
background-color: #e0e0e0;
font-size: 16px;
opacity: 0.6;
font-weight: 700;
text-shadow: -1px 0 #000,
0 1px #000,
1px 0 #000,
0 -1px #000;
background-image: url(${props => props.bgImg });
color: white;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-size: cover;
&:hover{
opacity: 0.7;
}
.position {
width: 36px;
height: 36px;
}
`;
//사진 왼쪽 wrap
const LeftPicture = styled.div`
margin-right: 5px;
width: 582px;
height: 475px;
`;
//사진 왼쪽 큰 사진
const BigPicture = styled.div`
width: 582px;
height: 380px;
`;
// 사진위에 있는 전체보기, 옆으로 이동버튼
const BigPictureInterior = styled.div`
position: absolute;
width: 582px;
height: 474px;
`;
const DisplayButton = styled.div`
display: ${props => props.onMouse ? "flex" : "none"};
width: 148px;
height: 60px;
font-size: 20px;
top: 210px;
left: 220px;
padding: 0 18px;
position: absolute;
color : white;
align-items: center;
justify-content: center;
background-color: rgba(74,74,74, .6);
&:hover{
background-color: rgba(0,0,0,.5);
}
.margin{
margin-right: 5px;
}
`;
// 사진 div
const BigPictureImglistWrap =styled.div`
background-color: black;
list-style: none;
width: auto;
`;
// 사진 ul
const BigPictureImglist = styled.ul`
opacity: 1;
position: relative;
&:hover{
opacity: 0.8;
}
`;
// 슬라이더 부분
const CustomSlider = styled(Slider)`
width: 582px;
height: 100%;
.slick-disabled {
display: none !important;
}
img{
width: 582px;
height: 472px;
}
.slick-prev {
position: absolute;
color: white;
background-color: rgba(74,74,74, .6);
width: 80px;
height: 100px;
left: 0;
top: 240px;
border-radius: 5px;
z-index: 5;
&:hover{
background-color: rgba(0,0,0,.5);
}
}
.slick-prev:before{
width: 30px !important;
height: 30px !important;
font-size: 30px !important;
}
.slick-next {
position: absolute;
color: white;
background-color: rgba(74,74,74, .6);
width: 80px;
height: 100px;
right: 0;
top: 240px;
border-radius: 5px;
&:hover{
background-color: rgba(0,0,0,.5);
}
}
.slick-next:before{
width: 30px !important;
height: 30px !important;
font-size: 30px !important;
}
`
//사진 위에 있는 타이틀
const Picturetitle = styled.div`
font-size: 16px;
bottom: 0;
width: 100%;
z-index: 5;
padding: 16px 16px 8px 16px;
font-weight: 400;
font-family: 'Arial, Tahoma, "Bitstream Vera Sans", sans-serif';
display: flex;
align-items: center;
text-align: left;
color: white;
position: absolute;
.picture {
width: 32px;
height: 36px;
margin-right: 10px;
}
`;
const PictureAll =styled.span`
width: auto;
height: auto;
font-weight: 700;
top: -2px;
&:hover {
text-decoration: underline;
}
`;
const Picturecount = styled.span`
width: auto;
height: auto;
font-size: 16px;
top: -2px;
`;
export default withRouter(Hotelpicture);
제일 힘들었던 introduce 부분.
오른쪽 설명 type 함수들의 값을 전체를 긁어왔다고 생각했는데, 호텔마다 시설의 표기 종류가 달라서....
간혹 빈칸부분들이 있어 이미지를 구현 못하는 부분이 있었다.;;;;
import React, { useState } from "react";
import styled from "styled-components";
import { Link, withRouter } from "react-router-dom";
import { RiParkingBoxLine, RiWifiLine, RiHandHeartLine } from "react-icons/ri";
import { FaHiking, FaBath, FaBaby } from "react-icons/fa";
import { FiCoffee, FiTv } from "react-icons/fi";
import { MdCardTravel, MdSmokeFree } from "react-icons/md";
import { IoIosBed } from "react-icons/io";
import { GiKnifeFork } from "react-icons/gi";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
function HotelIntroduce({idx,hotel}) {
const [ savepoint , setSavepoint ] = useState(hotel)
const [ moretext, setMortext ] = useState(false)
const { kind } = hotel.hotel_rating
const ratingReturn = (kind) => {
if ( kind > 4.5 ) {
return "아주좋음"
}
if ( kind > 3.5 ) {
return "좋음"
}
if ( kind > 2.5) {
return "보통"
}
if (kind >1.5) {
return "별로"
}
if (kind > 0.5) {
return "최악"
}
};
const circleReturn =(kind) => {
if ( kind > 4.5 ) {
return "●●●●●"
}
if ( kind > 3.5 ) {
return "●●●●○"
}
if ( kind > 2.5) {
return "●●●○○"
}
if (kind >1.5) {
return "●●○○○"
}
if (kind > 0.5) {
return "●○○○○"
}
}
const Splitcontent = hotel.content
const SplitMap = Splitcontent.split(".")
const settings = {
infinite: false,
slidesToShow: 1,
slidesToScroll: 1,
}
const Typefacilities =(el) => {
const facilities = {
"무료 주차" : <RiParkingBoxLine />,
"무료 초고속 인터넷(Wi-Fi)": <RiWifiLine />,
"온수 욕조": <FaBath />,
"무료 조식": <FiCoffee />,
"하이킹": <FaHiking/>,
"베이비시팅": <FaBaby/>,
"마사지":<RiHandHeartLine/>,
"수하물 보관": <MdCardTravel/>
}
return (
<div className="typeBox">
{facilities[el]}
<div className="marginLeft">{el}</div>
</div>
)
};
const Typeamenities =(el) => {
const Amenities = {
"목욕 가운" : <IoIosBed />,
"벽난로" : <IoIosBed />,
"하우스키핑" : <IoIosBed />,
"옷장/벽장" : <IoIosBed />,
"커피/티 메이커" : <FiCoffee />,
"평면 TV" : <FiTv />,
"소파 침대" : <IoIosBed />,
"욕실/샤워실" : <FaBath />,
"다리미" : <IoIosBed />,
"전용 욕실" : <IoIosBed />,
"모닝콜 서비스/알람 시계" : <IoIosBed />,
"전자레인지" : <GiKnifeFork />,
"냉장고" : <GiKnifeFork />,
"전기 주전자" : <GiKnifeFork />,
"DVD/CD 플레이어" : <FiTv />,
"전기 담요" : <IoIosBed />,
"무료 세면도구" : <FaBath />,
"헤어드라이어" : <FaBath />
}
return (
<div className="typeBox">
{Amenities[el]}
<div className="marginLeft">{el}</div>
</div>
)
}
const Typeroom =(el) => {
const roomtypes = {
"금연실": <MdSmokeFree />,
"스위트": <IoIosBed />,
"패밀리 룸": <IoIosBed />
}
return (
<div className="typeBox">
{roomtypes[el]}
<div className="marginLeft">{el}</div>
</div>
)
}
return(
<Flex>
<HotelIntroduceDIV>
<div>
<IntroduceTitle>
<h2 className="Introduce">소개</h2>
</IntroduceTitle>
<HotelContent>
<HotelContentLeft>
<ReviewPoint>
<ReviewNumber>{Number(hotel.hotel_rating).toFixed(1)}</ReviewNumber>
<Link to="null" className="Reviews">
<ReviewGood>{ratingReturn(hotel.hotel_rating)}</ReviewGood>
<div className="flexList">
<ReviewNumberConverter>{circleReturn(hotel.hotel_rating)}</ReviewNumberConverter>
<ReviweAccount>1445건의 리뷰</ReviweAccount>
</div>
</Link>
</ReviewPoint>
<PointContent>
<div className="rate">{circleReturn(hotel.place)}</div>
<div className="title">장소</div>
</PointContent>
<PointContent>
<div className="rate">{circleReturn(hotel.cleanliness)}</div>
<div className="title">청결도</div>
</PointContent>
<PointContent>
<div className="rate">{circleReturn(hotel.service)}</div>
<div className="title">서비스</div>
</PointContent>
<PointPrice>
<div className="rate">{circleReturn(hotel.price)}</div>
<div className="title">가격</div>
</PointPrice>
<FoldingContent>
<ul className={moretext? "foldingMargin": "foldingMargin hidden"} >
{SplitMap.map((hotel) => {
return(
<li>{hotel}</li>
)})}
</ul>
<div className="foldBtn">
<div className={moretext? "btnHidden": "btnDisplay"} onClick={() => setMortext(true)}>더보기</div>
<div className={moretext? "btnDisplay": "btnHidden"} onClick={() => setMortext(false)}>덜 보기</div>
</div>
</FoldingContent>
<MiniPicture>
<CustomSlider {...settings}>
{hotel.image.map(hotel =>{
return (
<div>
<img className="smallBigimg" src={hotel} alt="" />
</div>
)})}
</CustomSlider>
<Overflow>
{hotel.image.map(hotel=>{
return(
<div>
<img className="verySmallimg" src={hotel} alt="" />
</div>
)
})}
</Overflow>
</MiniPicture>
</HotelContentLeft>
<HotelContentRight>
<HotelFistContainer>
<div className="firstTitle">편의 시설</div>
<Hotelfacilities>
<div className='elControl'>{hotel.facilities.map(e => Typefacilities(e))}</div>
</Hotelfacilities>
</HotelFistContainer>
<div className="dummyBTN">더보기</div>
<HotelSecondContainer>
<div className="secondTitle">객실 특징</div>
<Amanities>
<div className='elControl'>{hotel.amanities.map(e => Typeamenities(e))}</div>
</Amanities>
</HotelSecondContainer>
<div className="dummyBTN">더보기</div>
<HotelThirdContainer>
<div className="thirdTitle">객실유형</div>
<Roomtype>
<div className='elControl'>{hotel.roomtypes.map(e => Typeroom(e))}</div>
</Roomtype>
</HotelThirdContainer>
</HotelContentRight>
</HotelContent>
</div>
</HotelIntroduceDIV>
<Ad>
<div className="adCenter">
<img className="adimg" src="/images/adImg.png" alt="광고창" />
</div>
</Ad>
</Flex>
)
}
export default withRouter(HotelIntroduce);
const Flex = styled.div`
display: flex;
`;
const HotelIntroduceDIV = styled.div`
max-width: 1280px;
width: 814px;
height: 1250px;
margin: 24px 0 12px 0;
padding: 24px;
background-color: white;
`;
const IntroduceTitle = styled.div`
width: 100%;
height: 49px;
padding-bottom: 18px;
border-bottom: 1px solid gray;
.Introduce{
width: 100%;
height: 30px;
font-size: 28px;
font-weight: 700;
line-height: 30px;
color: black;
font-family: 'Arial, Tahoma, Bitstream Vera Sans, sans-serif';
}
`;
// 호텔 소개 내용 전체
const HotelContent = styled.div`
width: 100%;
/* height: 100%; */
display : flex;
`;
//소개 왼쪽
const HotelContentLeft = styled.div`
width: 50%;
/* height: auto; */
padding: 0 23px 0 12px;
position: relative;
`;
const ReviewPoint = styled.div`
width: 100%;
margin: 14px 0;
display: flex;
.Reviews{
cursor: pointer;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-items: center;
.flexList{
display: flex;
align-items: center;
}
}
`;
const ReviewNumber = styled.span`
color: black;
font-family: 'Arial, Tahoma, Bitstream Vera Sans, sans-serif';
font-size: 48px;
font-weight: 700;
margin-right: 8px;
width: 90px;
`;
const ReviewGood = styled.div`
color: black;
font-family: 'Arial, Tahoma, Bitstream Vera Sans, sans-serif';
font-size: 16px;
font-weight: 700;
width: 100%;
height: 32px;
line-height: 22px;
display: flex;
align-items: flex-end;
`;
const ReviewNumberConverter = styled.div`
width: 90px;
height: 30px;
font-size: 30px;
color: green;
`;
const ReviweAccount = styled.div`
width: 95px;
height: 30px;
color: black;
font-size: 14px;
font-family: 'Arial, Tahoma, Bitstream Vera Sans, sans-serif';
margin-left: 5px;
display: flex;
align-items: flex-end;
`;
const PointContent = styled.div`
width: 100%;
height: 30px;
margin-bottom: 3px;
display: flex;
.rate {
width: 80px;
height: 30px;
font-size: 30px;
color: green;
}
.title{
color: #2c2c2c;
font-size: 14px;
margin-left: 20px;
font-family: 'Arial, Tahoma, Bitstream Vera Sans, sans-serif';
display: flex;
align-items: flex-end;
font-weight: bold;
}
`;
const PointPrice = styled.div`
width: 100%;
height: 30px;
margin-bottom: 24px;
display: flex;
.rate {
width: 80px;
height: 20px;
font-size: 30px;
color: green;
}
.title {
color: #2c2c2c;
font-size: 14px;
margin-left: 20px;
font-weight: 400;
font-family: 'Arial, Tahoma, Bitstream Vera Sans, sans-serif';
display: flex;
align-items: flex-end;
font-weight: bold;
}
`;
const FoldingContent = styled.div`
width: 100%;
height: auto;
.foldingMargin{
font-size: 16px;
li{
margin-bottom: 12px;
}
}
.hidden {
overflow: hidden;
max-height: 175px;
}
.btnDisplay{
display: block;
cursor: pointer;
width: 48px;
border-bottom: 1px dotted #e0e0e0;
margin-right: 4px;
font-size: 14px;
line-height: 18px;
color: #474747;
font-family: "Arial, Tahoma, Bitstream Vera Sans, sans-serif";
}
.btnHidden{
display: none;
}
`;
const MiniPicture = styled.div`
width: 348px;
position: absolute;
cursor: pointer;
.smallBigimg{
width: 348px;
height: 272px;
}
li{
width:100%;
height: 52px;
display: flex;
flex-wrap: wrap;
}
`;
const CustomSlider = styled(Slider)`
width: 348px;
height: 100%;
.slick-disabled {
display: none !important;
}
.slick-prev {
position: absolute;
color: white;
background-color: rgba(74,74,74, .6);
width: 80px;
height: 100px;
left: 0;
border-radius: 5px;
z-index: 5;
&:hover{
background-color: rgba(0,0,0,.5);
}
}
.slick-prev:before{
width: 30px !important;
height: 30px !important;
font-size: 30px !important;
}
.slick-next {
position: absolute;
color: white;
background-color: rgba(74,74,74, .6);
width: 80px;
height: 100px;
right: 0;
border-radius: 5px;
&:hover{
background-color: rgba(0,0,0,.5);
}
}
.slick-next:before{
width: 30px !important;
height: 30px !important;
font-size: 30px !important;
}
`;
const Overflow = styled.div`
width: 350px;
height: 52px;
display: flex;
overflow: hidden;
.verySmallimg{
width: 48px;
height: 50px;
margin: 1px;
&:hover{
opacity: 0.7;
}
}
`;
//소개 오른쪽
const HotelContentRight = styled.div`
height: auto;
.dummyBTN {
color: black;
font-size: 14px;
font-weight: 700;
cursor: pointer;
line-height: 18px;
display: block;
&:hover{
text-decoration:underline;
}
}
`;
const HotelFistContainer = styled.div`
width: 370px;
height: 260px;
.firstTitle{
color: #2c2c2c;
margin-bottom: 16px;
padding-top: 20px;
font-size: 16px;
font-weight: 700;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
}
`;
const Hotelfacilities = styled.div`
width: 370px;
height: auto;
.elControl {
display: flex;
flex-wrap: wrap;
font-size: 14px;
height: auto;
}
.typeBox{
width: 185px;
height: 36px;
font-size: 17px;
color: #474747;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
margin-bottom: 16px;
display: flex;
align-items: center;
svg{
width: 30px;
height: 26px;
}
.marginLeft{
width: 100%;
height: 100%;
margin-left: 3px;
line-height: 18px;
display: flex;
align-items: center;
}
}
`;
const HotelSecondContainer = styled.div`
width: 100%;
height: 255px;
.secondTitle{
color: black;
margin: 20px 0 16px;
padding: 20px 0 0;
font-size: 16px;
font-weight: 700;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
}
.elControl {
display: flex;
flex-wrap: wrap;
font-size: 14px;
height: auto;
}
.typeBox{
width: 185px;
height: 36px;
font-size: 17px;
color: #474747;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
margin-bottom: 16px;
display: flex;
align-items: center;
svg{
width: 30px;
height: 26px;
}
}
.marginLeft{
width: 100%;
height: 100%;
margin-left: 3px;
line-height: 18px;
display: flex;
align-items: center;
}
`;
const Amanities = styled.div`
width: 370px;
height: 180px;
`;
const Roomtype = styled.div`
width: 370px;
height: 180px;
`;
const HotelThirdContainer = styled.div`
.thirdTitle{
color: black;
margin: 20px 0 16px;
padding: 20px 0 0;
font-size: 16px;
font-weight: 700;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
}
.elControl {
display: flex;
flex-wrap: wrap;
font-size: 14px;
height: auto;
}
.typeBox{
width: 185px;
height: 36px;
font-size: 17px;
color: #474747;
font-family: 'Arial,Tahoma,Bitstream Vera Sans,sans-serif';
margin-bottom: 16px;
display: flex;
align-items: center;
svg{
width: 30px;
height: 26px;
}
}
.marginLeft{
width: 100%;
height: 100%;
margin-left: 3px;
line-height: 18px;
display: flex;
align-items: center;
}
`;
// 광고창 ad
const Ad =styled.div`
width: 419px;
height: 1014px;
padding: 24px 12px 0 12px;
.adCenter {
width: 100%;
height: auto;
display: flex;
justify-content: center;
.adimg{
width: 300px;
height: 700px;
}
}
`;
- 후기 -
이번 프로젝트는 1차에 비해서 난이도도 상승하였고,
그만큼 여러모로 고민을 많이하게되고, 기능 구현을 위해서 노력을 많이하게 된 프로젝트였다.
그만큼 기능을 구현하려다보니 이전에 없던 욕심도 생기게 된 프로젝트였다.
그렇기에 내가 하다보니 기능을 구현하려다가 포기하게 되었는데, 기능을 포기하여 아쉬움을 느꼈던 프로젝트였다.
마지막으로 위코드에서 생활도 이제 끝내었다.
내일부터 기업에 가서 (무급)인턴쉽을 행해야 하는데, 내가 잘 할 수 있을까? 라는 고민도 많이하고 있다.
기본적으로 같이 가는 사람들이 잘하는 팀이다보니 아무래도 걱정할 수 밖에 없는 거 같은데,
멘토들이 말했듯이 사람은 뛰어가는데, 속도가 있어서 남이 엄청 빠르다고 나도 그 속도로 뛴다면
단거리는 같이 뛸 수 있어도 장거리에서는 그들과 같이 갈수없다고 하듯이 나만의 속도로 뛴다면
언젠가는 그들과 같이 뛰거나 오히려 뛰어넘을 수 있지 않을까? 라고 생각한다.
토끼와 거북이처럼 말이다.
기억하고 싶은 코드....?
의외로 난 다른 사람들과 다르게 함수부분이 인상 깊었다.
이 부분중에서 내가 만든 함수는 제일 첫 부분.
기초적인 리턴 방식이었다.
하지만, 이걸 도움없이 내가 만들어서 작동시켰다는데 있어 매우 뿌듯.
두번째 함수는 멘토의 도움을 받았다.
처음에는 내가 1번째처럼 일일히 리턴하는 방식으로 했더니 거진 100줄 가까이 되는 코드가 나오던데....
그걸 보고 멘토님이 만들어주셨다.
어차피 인자로 해당 값이 들어온다면 그걸 그냥 일일히 리턴하지말고 프로퍼티로 작성하여
연결해서 리턴하라고...
마지막으로 이전에도 설명했던 switch문이다.
내가 만든부분은 아니다.
파트-멘토 조인호님이 만들었는데, 내가 기존에는 path로 통제하던걸 그렇게 하면 안된다고 말씀하시면서
이렇게 만들어줬는데, 이해하기가 어렵다..
작동로직은 이해했는데, 왜 이렇게 작성하는가를 잘 모르겠다.
요게 왜 인상적이냐? 하면 내가 만든 로그인 컴포넌트를 모두 nav바에서 컨트롤하고
해당 x버튼을 누르면 나가기가 되는데, 이때 해당 위치에서 꺼질 수 있다는 것이 매우 인상깊었다.
const circleReturn =(kind) => {
if ( kind > 4.5 ) {
return "●●●●●"
}
if ( kind > 3.5 ) {
return "●●●●○"
}
if ( kind > 2.5) {
return "●●●○○"
}
if (kind >1.5) {
return "●●○○○"
}
if (kind > 0.5) {
return "●○○○○"
}
}
const Typefacilities =(el) => {
const facilities = {
"무료 주차" : <RiParkingBoxLine />,
"무료 초고속 인터넷(Wi-Fi)": <RiWifiLine />,
"온수 욕조": <FaBath />,
"무료 조식": <FiCoffee />,
"하이킹": <FaHiking/>,
"베이비시팅": <FaBaby/>,
"마사지":<RiHandHeartLine/>,
"수하물 보관": <MdCardTravel/>
}
return (
<div className="typeBox">
{facilities[el]}
<div className="marginLeft">{el}</div>
</div>
)
};
const modalHandler = (mode) => {
switch (mode) {
case "social":
return <SocialLogin setMode={setLoginMode} closeModal={closeModal} />;
case "email":
return <EmailLogin setMode={setLoginMode} closeModal={closeModal} />;
case "signup":
return <SignUp setMode={setLoginMode} closeModal={closeModal} />;
default:
return <SocialLogin setMode={setLoginMode} closeModal={closeModal} />;
}
};
'코딩 > 위코드 코딩학습' 카테고리의 다른 글
[위코드] TIL(Today I am Learned) -29(기업협업) (0) | 2020.08.21 |
---|---|
[위코드] TIL(Today I am Learned) -28(anser) (0) | 2020.08.20 |
[위코드] TIL(Today I am learned) -26 (0) | 2020.08.12 |
[위코드] TIL(Today I am learned) -25 (0) | 2020.08.12 |
[위코드] TIL(Today I am learned) -24(위코드의 마지막 주말) (0) | 2020.08.09 |