📝 Day 02 - 220527
- useState, useEffect, useRef
- Axios 비동기 요청 (API)
- SeoulMap, CitySelect 데이터 연동
- 프로젝트 ver. 1 - 완성
import styled from 'styled-components';
import colors from '../lib/styles/colors';
import { useEffect, useState } from 'react';
import { ReactComponent as GeoICO } from '../assets/geo.svg';
import { Link, useParams } from 'react-router-dom';
import { cities } from '../lib/cities/cityName';
const CitySelectBlock = styled.div`
height: 50px;
width: 100%;
display: flex;
`;
const GeoWrapper = styled.div`
display: flex;
margin-right: 80px;
align-items: center;
svg {
width: 20px;
fill: ${colors.gray[2]};
margin-right: 8px;
}
h3 {
color: ${colors.gray[2]};
font-weight: 400;
font-size: 24px;
letter-spacing: 0.02em;
}
`;
const Select = styled.select`
cursor: pointer;
width: 180px;
height: 50px;
border-radius: 8px;
font-size: 18px;
text-align: center;
border: none;
border-bottom: 2px solid ${colors.gray[2]};
margin-right: 20px;
&:active,
&:hover {
background-color: ${colors.gray[0]};
}
`;
const StyledForm = styled.form`
display: flex;
`;
const StyledLink = styled(Link)`
cursor: pointer;
background-color: ${colors.green[2]};
color: #fff;
padding: 10px 20px;
border-radius: 10px;
font-size: 17px;
&:hover {
background-color: ${colors.green[1]};
}
`;
const CitySelect = () => {
const { city } = useParams();
console.log(cities[city]);
const [select, setSelect] = useState(0);
const onChangeSelect = (e) => {
setSelect(e.target.value);
};
useEffect(() => {
setSelect(city);
}, [city]);
return (
<CitySelectBlock>
<GeoWrapper>
<GeoICO />
<h3>Seoul</h3>
</GeoWrapper>
<StyledForm>
<Select className="hello" value={select} onChange={onChangeSelect}>
{cities.map((city, i) => (
<option key={city} value={i}>
{city}
</option>
))}
</Select>
<StyledLink to={`/pm/${select}`}>조회</StyledLink>
</StyledForm>
</CitySelectBlock>
);
};
export default CitySelect;
useState로 현재 select의 value를 관리한다.
-> onChange 일때마다 setSelect 되도록.
params를 불러와 해당 도시가 바뀐 경우에 select 값을 params로 바꿔줌.
-> useEffect를 이용함. 첫 렌더링시 + params 업데이트시
import styled from 'styled-components';
import { ReactComponent as PmLevelICO } from '../assets/pmLevel.svg';
import colors from '../lib/styles/colors';
const LevelInfoBlock = styled.div`
width: 400px;
border-radius: 15px;
border: 2.5px solid #000;
text-align: center;
margin-bottom: 20px;
.level-title {
padding: 8px 0;
font-size: 20px;
border-bottom: 2.5px solid #000;
margin-bottom: 14px;
}
`;
const LevelIcons = styled.div`
width: 260px;
height: 150px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin: 0 auto;
margin-bottom: 14px;
position: relative;
svg {
width: 40px;
cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
transform: scale(1.1);
+ .level-detail {
display: block;
}
}
}
.level-detail {
position: absolute;
display: none;
background-color: ${colors.trans[0]};
color: #fff;
font-size: 14px;
padding: 8px 18px;
border-radius: 5px;
.pm-title {
font-size: 15px;
font-weight: 500;
margin-right: 9px;
}
}
.very-bad {
stroke: ${colors.level[0]};
}
.bad {
stroke: ${colors.level[1]};
}
.soso {
stroke: ${colors.level[2]};
}
.good {
stroke: ${colors.level[3]};
}
.desc {
width: 60px;
font-size: 15px;
letter-spacing: -0.03em;
margin-right: 10px;
margin-left: 6px;
text-align: left;
font-weight: 500;
}
`;
const LevelInfo = () => {
return (
<LevelInfoBlock>
<h3 className="level-title">미세먼지 Level</h3>
<LevelIcons>
<PmLevelICO className="very-bad" />
<div className="level-detail">
<span className="pm-title">PM 10</span>151~
<br />
<span className="pm-title">PM 2.5</span>76~
</div>
<span className="desc">매우 나쁨</span>
<PmLevelICO className="bad" />
<div className="level-detail">
<span className="pm-title">PM 10</span>81~150
<br />
<span className="pm-title">PM 2.5</span>36~75
</div>
<span className="desc">나쁨</span>
<PmLevelICO className="soso" />
<div className="level-detail">
<span className="pm-title">PM 10</span>31~80
<br />
<span className="pm-title">PM 2.5</span>16~35
</div>
<span className="desc">보통</span>
<PmLevelICO className="good" />
<div className="level-detail">
<span className="pm-title">PM 10</span>0~30
<br />
<span className="pm-title">PM 2.5</span>0~15
</div>
<span className="desc">좋음</span>
</LevelIcons>
</LevelInfoBlock>
);
};
export default LevelInfo;
import styled from 'styled-components';
import colors from '../lib/styles/colors';
import { ReactComponent as EarthICO } from '../assets/earth.svg';
import Modal from './Modal';
import { useState } from 'react';
const ModalButtonBlock = styled.div`
margin-top: 16px;
`;
const StyledButton = styled.button`
display: flex;
justify-content: center;
align-items: center;
width: 400px;
border-radius: 10px;
padding: 8px 0;
text-align: canter;
background-color: ${colors.green[2]};
color: #fff;
font-size: 20px;
font-weight: 500;
letter-spacing: 0.03em;
cursor: pointer;
svg {
width: 26px;
margin-left: 10px;
}
&:hover {
background-color: ${colors.green[1]};
}
`;
const ModalButton = () => {
const [visible, setVisible] = useState(false);
const onOpenModal = () => {
setVisible(true);
};
const onCloseModal = () => {
setVisible(false);
};
return (
<ModalButtonBlock>
<StyledButton onClick={onOpenModal}>
For Earth
<EarthICO />
</StyledButton>
<Modal
visible={visible}
randomIndex={Math.floor(Math.random() * 6)}
onCloseModal={onCloseModal}
/>
</ModalButtonBlock>
);
};
export default ModalButton;
import { useEffect, useRef } from 'react';
import styled from 'styled-components';
import PmLevelICO from '../assets/pmLevel.svg';
import verybad from '../assets/photos/sky1.jpg';
import bad from '../assets/photos/sky2.jpg';
import soso from '../assets/photos/sky3.jpg';
import good from '../assets/photos/sky4.jpg';
const PmDetailImageBlock = styled.div`
position: relative;
width: 340px;
height: 230px;
overflow: hidden;
margin: 0 auto;
margin-bottom: 16px;
/* &::after {
content: '';
display: block;
position: absolute;
width: 30px;
height: 30px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-image: url(${PmLevelICO});
background-position: center center;
background-size: contain;
background-repeat: no-repeat;
} */
`;
const PmDetailImage = ({ level }) => {
const imgPath = useRef(null);
useEffect(() => {
console.log(level, imgPath);
switch (level) {
case '매우 나쁨':
imgPath.current = verybad;
return;
case '나쁨':
imgPath.current = bad;
return;
case '보통':
imgPath.current = soso;
return;
case '좋음':
imgPath.current = good;
return;
default:
imgPath.current = soso;
}
}, [level]);
return (
<PmDetailImageBlock>
{imgPath.current && (
<img src={imgPath.current} alt="sky" className="sky-img" />
)}
</PmDetailImageBlock>
);
};
export default PmDetailImage;
useRef
로 이미지 경로를 저장할 것임. (imgPath.current)
-> img태그의 src속성으로 넣어줌.
PmDetail
에서 props로 보내준 level에 따라 이미지가 달라지도록 함.
(switch문)
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
import PmDetailImage from './PmDetailImage';
import PmDetailText from './PmDetailText';
import axios from 'axios';
import { useEffect, useState } from 'react';
import colors from '../lib/styles/colors';
const PmDetailBlock = styled.div`
width: 400px;
min-height: 425px;
border-radius: 15px;
border: 2.5px solid #000;
text-align: center;
.detail-title {
padding: 8px 0;
font-size: 20px;
border-bottom: 2.5px solid #000;
margin-bottom: 14px;
}
.error-msg {
padding: 150px 0;
font-size: 22px;
.error-point {
background-color: #ffea2d;
padding: 0 8px;
font-size: 24px;
font-weight: 700;
}
b {
display: inline-block;
background-color: ${colors.green[2]};
color: #fff;
padding: 2px 8px;
border-radius: 6px;
}
}
`;
const PmDetail = () => {
const [level, setLevel] = useState(null);
const [pm10, setPm10] = useState(0);
const [pm25, setPm25] = useState(0);
const { city } = useParams();
// '좋음', '보통', '나쁨', '매우나쁨' - API 데이터에서 가져오기
// response.RealtimeCityAir.row[도시인덱스].IDEX_NM
useEffect(() => {
if (city === 'undefined') return;
axios
.get(
'http://openapi.seoul.go.kr:8088/6d4d776b466c656533356a4b4b5872/json/RealtimeCityAir/1/99'
)
.then((response) => {
setLevel(response.data.RealtimeCityAir.row[city].IDEX_NM);
setPm10(response.data.RealtimeCityAir.row[city].PM10);
setPm25(response.data.RealtimeCityAir.row[city].PM25);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [city]);
return (
<PmDetailBlock>
{city !== 'undefined' ? (
<>
<h3 className="detail-title">Info</h3>
<PmDetailImage level={level} />
<PmDetailText level={level} city={city} pm10={pm10} pm25={pm25} />
</>
) : (
<>
<h3 className="detail-title">Info</h3>
<p className="error-msg">
<span className="error-point">지역을 선택</span>한 후, <b>조회</b>를
눌러주세요.
</p>
</>
)}
</PmDetailBlock>
);
};
export default PmDetail;
useParams 로 현재 어떤 구인지 city 데이터를 가져온 후,
city가 'undefined' 라면 axios 요청을 하지 않도록 함.
-> UseEffect에서 필터링 한번 해줌.
axios.get(url).then( res => setState(res.data.~~))
를 해줌
❗️ 추후 에러 로그를 따로 업로드 하겠지만,
https
환경에서http
를 요청하여 mixed content 에러가 발생한다.
-> 백엔드도 구현하여 API를 백엔드 단에서 Http 요청을 하고, 미들웨어를 사용해야할 것 같다.
-> ver 2 에서 redux 구현하면서 백엔드도 같이 해볼 예정임. (RESTapi)
✅ 참고로, localhost 환경에서는 mixed content 에러가 발생하지 않지만,
배포시Https
로 배포하기 때문에 에러가 발생함.
import styled from 'styled-components';
import map from '../assets/map.png';
import colors from '../lib/styles/colors';
import { mapPosition } from '../lib/cities/cityCord';
import { useParams } from 'react-router-dom';
const SeoulMapBlock = styled.div`
display: flex;
align-items: center;
.map-wrap {
position: relative;
}
.city-area-${(props) => props.current} {
&::after {
content: '';
display: block;
width: 40px;
height: 40px;
background-color: ${colors.level[0]};
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
`;
const CityArea = styled.div`
cursor: pointer;
position: absolute;
top: ${(props) => props.top}px;
left: ${(props) => props.left}px;
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
&::after {
content: '';
display: block;
width: 25px;
height: 25px;
background-color: ${colors.gray[2]};
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: all 0.2s ease-in-out;
}
&:hover {
&::after {
width: 30px;
height: 30px;
}
}
`;
const SeoulImg = styled.img`
display: block;
width: 700px;
height: 700px;
position: relative;
`;
const SeoulMap = () => {
const { city } = useParams();
console.log(city);
return (
<SeoulMapBlock current={city}>
<div className="map-wrap">
<SeoulImg src={map} alt="seoul map" />
{mapPosition.map((p) => (
<CityArea
key={p.index}
top={p.top}
left={p.left}
width={p.width}
height={p.height}
city={p.city}
className={`city-area-${p.index}`}
/>
))}
</div>
</SeoulMapBlock>
);
};
export default SeoulMap;
props
를 이용한 스타일링. (top, left, width, height){
.city-area-${(props) => props.current} {
&::after {
content: '';
display: block;
width: 40px;
height: 40px;
background-color: ${colors.level[0]};
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}