📝 Day 01 - 220526
- 환경 setting
- 기본 라우팅 (페이지 구현)
- 컴포넌트 작성 (with 가상 데이터)
- styled-components 스타일링
$ yarn create react-app mise
이번 PM 프로젝트에 맞는 파비콘을 제작하여 적용함.
$ yarn add axios redux react-redux redux-actions styled-components
App.js
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Pm from './pages/Pm';
const App = () => {
return (
<>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/pm/:city" element={<Pm />} />
</Routes>
</>
);
};
export default App;
Path | Component |
---|---|
/ | <Home /> |
/pm/:city | <Pm /> |
-> useParams ({city})
import Footer from '../components/Footer';
import Header from '../components/Header';
import ContentDefault from '../components/ContentDefault';
const Home = () => {
return (
<div>
<Header />
<ContentDefault />
<Footer />
</div>
);
};
export default Home;
import { useParams } from 'react-router-dom';
import Header from '../components/Header';
import Content from '../components/Content';
import Footer from '../components/Footer';
// /pm/:city
const Pm = () => {
const { city } = useParams();
console.log(city);
return (
<div>
<Header />
<Content />
<Footer />
</div>
);
};
export default Pm;
CitySelect.jsx
Content.jsx
ContentDefault.jsx
Footer.jsx
Header.jsx
LevelInfo.jsx
Modal.jsx
ModalButton.jsx
PmDefault.jsx
PmDetail.jsx
PmDetailImage.jsx
PmDetailText.jsx
SeoulMap.jsx
import styled from 'styled-components';
import colors from '../lib/styles/colors';
import { ReactComponent as MaskICO } from '../assets/mask.svg';
import { Link } from 'react-router-dom';
const HeaderBlock = styled.div`
width: 100%;
height: 80px;
background: ${colors.green[2]};
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
justify-content: center;
svg {
width: 40px;
fill: #fff;
}
.header-logo {
font-size: 32px;
font-weight: 400;
letter-spacing: 0.07em;
line-height: 40px;
color: #fff;
&:hover {
color: ${colors.trans[1]};
.header-point {
color: #fff;
}
}
.header-point {
font-weight: 700;
font-size: 48px;
}
}
`;
const Header = () => {
return (
<HeaderBlock>
<MaskICO />
<Link to="/">
<h1 className="header-logo">
<span className="header-point">P</span>ut on a{' '}
<span className="header-point">M</span>ask!
</h1>
</Link>
</HeaderBlock>
);
};
export default Header;
import styled from 'styled-components';
import CitySelect from './CitySelect';
import SeoulMap from './SeoulMap';
import LevelInfo from './LevelInfo';
import PmDefault from './PmDefault';
import ModalButton from './ModalButton';
const ContentDefaultBlock = styled.div`
display: flex;
padding: 20px 100px;
justify-content: center;
`;
const CityBlock = styled.div`
display: flex;
flex-direction: column;
margin-right: 120px;
`;
const InfoBlock = styled.div`
display: flex;
flex-direction: column;
`;
const ContentDefault = () => {
return (
<ContentDefaultBlock>
<CityBlock>
<CitySelect />
<SeoulMap />
</CityBlock>
<InfoBlock>
<LevelInfo />
<PmDefault />
<ModalButton />
</InfoBlock>
</ContentDefaultBlock>
);
};
export default ContentDefault;
import styled from 'styled-components';
import colors from '../lib/styles/colors';
const FooterBlock = styled.div`
width: 100%;
height: 40px;
background-color: ${colors.gray[0]};
color: ${colors.gray[2]};
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 100px;
.links {
display: flex;
li {
margin-right: 60px;
}
}
`;
const Ancher = styled.a`
font-size: 15px;
font-weight: 700;
color: ${colors.green[1]};
&:hover {
color: ${colors.green[0]};
}
`;
const Footer = () => {
return (
<FooterBlock>
<h3>© thisisyjin</h3>
<ul className="links">
<li>
<Ancher href="https://velog.io/@thisisyjin">blog</Ancher>
</li>
<li>
<Ancher href="https://github.com/thisisyjin">github</Ancher>
</li>
<li>
<Ancher href="mailto:thisisyjin@naver.com">contact</Ancher>
</li>
</ul>
</FooterBlock>
);
};
export default Footer;
import styled from 'styled-components';
import CitySelect from './CitySelect';
import SeoulMap from './SeoulMap';
import LevelInfo from './LevelInfo';
import ModalButton from './ModalButton';
import PmDetail from './PmDetail';
const ContentBlock = styled.div`
display: flex;
padding: 20px 100px;
justify-content: center;
`;
const CityBlock = styled.div`
display: flex;
flex-direction: column;
margin-right: 120px;
`;
const InfoBlock = styled.div`
display: flex;
flex-direction: column;
`;
const Content = () => {
return (
<ContentBlock>
<CityBlock>
<CitySelect />
<SeoulMap />
</CityBlock>
<InfoBlock>
<LevelInfo />
<PmDetail />
<ModalButton />
</InfoBlock>
</ContentBlock>
);
};
export default Content;
import styled from 'styled-components';
import colors from '../lib/styles/colors';
import { ReactComponent as GeoICO } from '../assets/geo.svg';
import { Link } from 'react-router-dom';
// 임시 데이터
export const cities = [
'강남구',
'강동구',
'강서구',
'강북구',
'관악구',
'광진구',
'구로구',
'금천구',
'노원구',
'동대문구',
'도봉구',
'동작구',
'마포구',
'서대문구',
'성동구',
'성북구',
'서초구',
'송파구',
'영등포구',
'용산구',
'양천구',
'은평구',
'종로구',
'중구',
'중랑구',
];
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: 135px;
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 = () => {
return (
<CitySelectBlock>
<GeoWrapper>
<GeoICO />
<h3>Seoul</h3>
</GeoWrapper>
<StyledForm>
<Select className="hello">
{cities.map((city, i) => (
<option key={city} value={i}>
{city}
</option>
))}
</Select>
<StyledLink
// select의 value대로 이동 (인덱스)
to={`/pm/0`}
>
조회
</StyledLink>
</StyledForm>
</CitySelectBlock>
);
};
export default CitySelect;
import styled from 'styled-components';
import map from '../assets/map.png';
import colors from '../lib/styles/colors';
// 지도 좌표 예시
const mapPosition = [
{ index: 0, city: '강남구', left: 455, top: 509, width: 83, height: 49 },
{ index: 1, city: '강동구', left: 578, top: 368, width: 95, height: 39 },
{ index: 2, city: '강서구', left: 38, top: 311, width: 93, height: 75 },
{ index: 3, city: '강북구', left: 381, top: 173, width: 70, height: 49 },
{ index: 4, city: '관악구', left: 264, top: 550, width: 78, height: 65 },
{ index: 5, city: '광진구', left: 505, top: 356, width: 44, height: 72 },
{ index: 6, city: '구로구', left: 97, top: 485, width: 98, height: 41 },
{ index: 7, city: '금천구', left: 194, top: 543, width: 42, height: 79 },
{ index: 8, city: '노원구', left: 487, top: 104, width: 60, height: 113 },
{ index: 9, city: '동대문구', left: 454, top: 295, width: 56, height: 57 },
{ index: 10, city: '도봉구', left: 410, top: 103, width: 60, height: 58 },
{ index: 11, city: '동작구', left: 270, top: 470, width: 66, height: 33 },
{ index: 12, city: '마포구', left: 201, top: 342, width: 60, height: 47 },
{ index: 13, city: '서대문구', left: 256, top: 299, width: 60, height: 34 },
{ index: 14, city: '성동구', left: 419, top: 367, width: 68, height: 47 },
{ index: 15, city: '성북구', left: 387, top: 251, width: 76, height: 49 },
{ index: 16, city: '서초구', left: 369, top: 491, width: 59, height: 75 },
{ index: 17, city: '송파구', left: 522, top: 452, width: 71, height: 44 },
{ index: 18, city: '영등포구', left: 213, top: 421, width: 42, height: 71 },
{ index: 19, city: '용산구', left: 314, top: 398, width: 76, height: 47 },
{ index: 20, city: '양천구', left: 106, top: 428, width: 79, height: 34 },
{ index: 21, city: '은평구', left: 245, top: 172, width: 68, height: 94 },
{ index: 22, city: '종로구', left: 326, top: 243, width: 42, height: 90 },
{ index: 23, city: '중구', left: 343, top: 348, width: 66, height: 35 },
{ index: 24, city: '중랑구', left: 515, top: 254, width: 56, height: 65 },
];
const SeoulMapBlock = styled.div`
display: flex;
align-items: center;
.map-wrap {
position: relative;
}
`;
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%);
}
&:hover {
&::after {
background-color: ${colors.level[0]};
}
}
`;
const SeoulImg = styled.img`
display: block;
width: 700px;
height: 700px;
position: relative;
`;
const SeoulMap = () => {
return (
<SeoulMapBlock>
<div className="map-wrap">
<SeoulImg src={map} alt="seoul map" />
{mapPosition.map((p) => (
<CityArea
key={p.left}
top={p.top}
left={p.left}
width={p.width}
height={p.height}
city={p.city}
/>
))}
</div>
</SeoulMapBlock>
);
};
export default SeoulMap;
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: 280px;
height: 150px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
margin: 0 auto;
margin-bottom: 14px;
.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: 14px;
letter-spacing: -0.03em;
margin-right: 10px;
margin-left: 6px;
text-align: left;
}
`;
const LevelInfo = () => {
return (
<LevelInfoBlock>
<h3 className="level-title">미세먼지 Level</h3>
<LevelIcons>
<PmLevelICO className="very-bad" />
<span className="desc">매우 나쁨</span>
<PmLevelICO className="bad" />
<span className="desc">나쁨</span>
<PmLevelICO className="soso" />
<span className="desc">보통</span>
<PmLevelICO className="good" />
<span className="desc">좋음</span>
</LevelIcons>
</LevelInfoBlock>
);
};
export default LevelInfo;
import styled from 'styled-components';
import PmDetailImage from './PmDetailImage';
import PmDetailText from './PmDetailText';
// 임시 데이터
// API 데이터 요청
const city = 0; // 배열 Index로. - 0은 강남구.
const level = '좋음'; // '좋음', '보통', '나쁨', '매우나쁨' - API 데이터에서 가져오기
// response.RealtimeCityAir.row[도시인덱스].IDEX_NM
const PmDetailBlock = styled.div`
width: 400px;
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;
}
`;
const PmDetail = () => {
return (
<PmDetailBlock>
<h3 className="detail-title">Info</h3>
<PmDetailImage level={level} />
<PmDetailText level={level} city={city} />
</PmDetailBlock>
);
};
export default PmDetail;
import { useEffect, useRef } from 'react';
import styled from 'styled-components';
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`
width: 340px;
height: 230px;
overflow: hidden;
margin: 0 auto;
margin-bottom: 16px;
`;
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, imgPath]);
return (
<PmDetailImageBlock>
{imgPath.current && (
<img src={imgPath.current} alt="sky" className="sky-img" />
)}
</PmDetailImageBlock>
);
};
export default PmDetailImage;
import styled from 'styled-components';
import { cities } from './CitySelect'; // 인덱스로 어느 구인지 찾기
import colors from '../lib/styles/colors';
const PmDetailTextBlock = styled.div`
margin-bottom: 18px;
`;
const DateAreaWrapper = styled.div`
font-size: 17px;
letter-spacing: 0.03em;
.month {
font-weight: 500;
}
.date {
font-weight: 500;
}
.cityName {
font-size: 19px;
font-weight: 700;
}
`;
const PmInfo = styled.div`
margin-bottom: 10px;
.pm-level {
display: inline-block;
font-size: 20px;
font-weight: 500;
padding: 3px 6px;
border-radius: 8px;
color: #fff;
background-color: ${(props) =>
props.level === '매우 나쁨'
? colors.level[0]
: props.level === '나쁨'
? colors.level[1]
: props.level === '보통'
? colors.level[2]
: colors.level[3]};
}
`;
const MaskInfo = styled.div`
color: ${colors.green[2]};
font-weight: 700;
`;
const PmDetailText = ({ city, level }) => {
const month = new Date().getMonth() + 1;
const date = new Date().getDate();
return (
<PmDetailTextBlock>
<DateAreaWrapper>
<span className="month">{month}</span>월{' '}
<span className="date">{date}</span>일{' '}
<span className="cityName">{cities[city]}</span>의
</DateAreaWrapper>
<PmInfo level={level}>
미세먼지 정도는 <span className="pm-level">{level}</span> 입니다.
</PmInfo>
<MaskInfo>
{level === '매우 나쁨' || level === '나쁨'
? '마스크를 착용하세요.'
: '맑은 하루가 되겠네요!'}
</MaskInfo>
</PmDetailTextBlock>
);
};
export default PmDetailText;
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 styled from 'styled-components';
import colors from '../lib/styles/colors';
import { ReactComponent as PlantICO } from '../assets/plant.svg';
import { ReactComponent as CloseICO } from '../assets/close.svg';
const textArr = [
'대중교통 이용하기 🚍',
'친환경 세제 사용하기 🧼',
'친환경 인증 마크 제품 구매하기 🌱',
'일회용품 사용 자제하기 🥤',
'사용하지 않는 콘센트를 뽑아 에너지 절약하기 🔌',
'육류 섭취 줄이고 유기농 식품 섭취하기 🥩',
];
const FullScreen = styled.div`
position: fixed;
z-index: 50;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: ${colors.trans[0]};
display: flex;
justify-content: center;
align-items: center;
`;
const ModalBlock = styled.div`
position: relative;
width: 850px;
background-color: #fff;
padding: 25px 45px;
border-radius: 10px;
text-align: center;
.modal-title {
font-size: 20px;
margin-bottom: 20px;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 30px;
margin-right: 8px;
}
.modal-content {
font-size: 20px;
font-weight: 500;
}
}
.close {
cursor: pointer;
width: 30px;
position: absolute;
top: 20px;
right: 20px;
fill: ${colors.gray[2]};
}
`;
const Modal = ({ randomIndex, visible, onCloseModal }) => {
if (!visible) return null;
return (
<FullScreen>
<ModalBlock>
<h3 className="modal-title">
<PlantICO /> Today's For Earth
</h3>
<p className="modal-content">{textArr[randomIndex]}</p>
<CloseICO className="close" onClick={onCloseModal} />
</ModalBlock>
</FullScreen>
);
};
export default Modal;
-> result / modal / button