not-a-gardener의 첫 페이지는 로그인 페이지다. 로그인 이후에 헤더, 푸터, 사이드바가 있는 메인 페이지가 보인다.
이에 헤더, 푸터, 컨텐츠, 사이드바 컴포넌트를 합쳐 DefaultLayout을 만들었다.
너무 길어질 것 같아 코드는 생략할 것이지만 주요 로직을 기록하면 다음과 같다.
Component | 설명 |
---|---|
AppContent | 전체 식물 리스트 컴포넌트, 식물 추가 컴포넌트 등 주요 컴포넌트들이 들어올 자리 routes라는 객체 배열에 메인 컴포넌트를 path, name, element로 정리 AppContent는 이 routes 배열을 토대로 <Route> 를 만든다. |
AppSideBar | 사이드바 nav: 사이드바에 연결해놓은 컴포넌트를 정리해놓은 배열 사이드바에 props로 넘겨 사이드바의 아이템을 만든다 |
AppHeader, AppFooter | 그냥... 헤더, 푸터 |
그리고 위의 네 가지 컴포넌트를 조합하여 <DefaultLayout>
컴포넌트를 만든 뒤,
App.js에서 "/garden/*" 주소에 연결했다.
"/garden"으로 시작하는 요청은 로그인 필터를 거치는 요청이므로,
인증/인가를 거친 유저만이 DefaultLayout 페이지에 접근할 수 있을 것이다.
이 프로젝트는 accessToken을 헤더에 담아 보내며 로그인을 유지한다. 그런데 새로고침을 하면 로그인이 풀리는 오류가 발생했다.
구글링 끝에 Axios 인터셉터를 통해 해결하기로 결정했다.
이번에도 코드에 대한 설명은 주석으로 대신한다.
import axios from "axios";
// axios 인스턴스 생성
const authAxios = axios.create({
baseURL: 'http://localhost:3000',
timeout: 1000
})
// request 인터셉터 작성
authAxios.interceptors.request.use(
function (config) {
const accessToken = localStorage.getItem("login");
// local storage에 accessToken 값이 있다면 헤더에 넣어준다.
if(accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
export default authAxios;
이제 인가가 필요한 Request에는 authAxios 인스턴스를 받아 사용하면 된다.
요청을 보내면 Axios 인터셉터가 이를 미리 가로채 헤더에 토큰을 넣어줄 것이다.
이전 포스팅에서 다룬 로직은 생략하겠다.
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import authAxios from '../requestInterceptor'
import WaterModal from './WaterModal'
import ModifyPlant from '../plant/Plant'
import DeletePlant from '../plant/DeletePlant'
const GardenMain = () => {
const [plantList, setPlantList] = useState([{
plantNo: ''
, plantName: ''
, averageWateringPeriod: ''
, wateringCode: ''
, fertilizingCode: ''
}]);
const [visible, setVisible] = useState(false);
const [clickedPlant, setClickedPlant] = useState(0);
// 백엔드에서 식물 리스트를 받아온다
useEffect(() => {
authAxios.get("/garden", "")
.then((res) => {
console.log("res.data", res.data);
setPlantList(res.data);
})
.catch(error => console.log(error))
}, [])
// 물주기 모달 여는 함수
const openModal = (plantNo) => {
setVisible(!visible);
setClickedPlant(plantNo);
}
// 물주기 모달 닫는 함수 -> 모달 컴포넌트 props로 넘겨줌
const closeModal = () => {
setVisible(false);
}
// 삭제 모달 state
const [ deleteVisible, setDeleteVisible ] = useState(false);
// 삭제 모달 여는 함수
const deletePlant = (plantNo) => {
setDeleteVisible(true);
setClickedPlant(plantNo);
}
// 삭제 모달 닫는 함수
const closeDeleteModal = () => {
setDeleteVisible(false)
}
// 삭제 시 plantNo가 일치하지 않는 원소만 추출해서 새로운 배열 만듦
const onRemove = () => {
setPlantList(plantList.filter(plant => plant.plantNo !== clickedPlant))
}
return (
<>
<CRow>
{plantList.map((plant, idx) => {
const color = ["primary", "warning", "danger", "success"];
let message = "";
/* wateringCode, fertilizingCode에 따른 메시지 도출 로직*/
const modifyUrl = `/garden/modify-plant/${plant.plantNo}`;
return (
<CCol sm={6} lg={3}>
<WaterModal visible={visible} clickedPlant={clickedPlant} closeModal={closeModal} />
<CWidgetStatsA
className="mb-4"
color={color[plant.wateringCode % 4]} // 일단 4로 나눈 나머지로 해결
value={
<div onClick={() => {openModal(plant.plantNo)}}>
<span role="img" aria-label="herb">🌿 </span>
{plant.plantName}{' '}
<span role="img" aria-label="herb">🌿</span>
<div className="fs-6 fw-normal">
<div>{plant.plantSpecies}</div>
<div>
{message}
</div>
</div>
</div>
}
action={
<CDropdown alignment="end">
<CDropdownToggle color="transparent" caret={false} className="p-0">
<CIcon icon={cilOptions} className="text-high-emphasis-inverse" />
</CDropdownToggle>
<CDropdownMenu>
<Link to={modifyUrl} component={ModifyPlant}>
<CDropdownItem>상세 정보</CDropdownItem>
</Link>
<div onClick={() => {deletePlant(plant.plantNo)}}>
<CDropdownItem>삭제</CDropdownItem>
<DeletePlant deleteVisible={deleteVisible} clickedPlant={clickedPlant} closeDeleteModal={closeDeleteModal} onRemove={onRemove}/>
</div>
<CDropdownItem disabled>Disabled action</CDropdownItem>
</CDropdownMenu>
</CDropdown>
}
icon={<CIcon icon={cilOptions} className="text-high-emphasis-inverse" />}
/>
</CCol>
)
})}
</CRow>
</>
)
}
export default GardenMain
GardenMain 컴포넌트의 자식인 WaterModal
import React, { useState, useEffect } from 'react'
import authAxios from './requestInterceptor';
const WaterModal = (props) => {
// props.closeModal로 부모 컴포넌트의 state 변경 함수 넘겨줌
const { visible, clickedPlant } = props;
console.log("clickedPlant", clickedPlant);
const [fertilized, setFertilized] = useState("N");
// 비료를 줬어요 버튼을 누르면 fertilized 값에 'Y'를 저장한다.
const onClick = () => {
setFertilized("Y");
submit();
}
const submit = () => {
authAxios.post("/garden/water", {plantNo: clickedPlant, fertilized: fertilized})
.then((res) => {
// response가 도착하면 props로 받은 함수를 사용하여,
// 부모 컴포넌트의 visible 값을 변경한다.
// == 모달이 닫힌다.
props.closeModal();
})
.catch(error => console.log(error));
}
return (
<CModal visible={visible} onClose={props.closeModal}>
<CModalHeader>
<CModalTitle>물을 주셨나요?</CModalTitle>
</CModalHeader>
<CModalFooter>
<CButton color="primary" onClick={submit}>
네, 물을 줬어요 💧
</CButton>
<CButton color="warning" onClick={onClick}>
비료를 줬어요 🍗
</CButton>
</CModalFooter>
</CModal>
)
}
export default WaterModal
GardenMain에서 props로 visible(모달 state 값), clickedPlant(클릭한 식물의 plantNo), closeModal(부모인 Garden 컴포넌트의 state인 visible 값을 제어할 함수)를 넘겨준다.
그저 DTO를 받아 서비스로 넘겨줄 뿐이므로 생략
@Service
@Slf4j
public class WateringServiceImpl implements WateringService {
@Autowired
private WateringDao wateringDao;
@Autowired
private PlantDao plantDao;
@Override
public int addWatering(WaterDto waterDto) {
// 새로운 물주기 정보 DB에 저장
wateringDao.addWatering(waterDto.toEntity());
// plantNo로 물주기 코드를 계산해 반환
return getWateringPeriodCode(waterDto.getPlantNo());
}
@Override
public int getWateringPeriodCode(int plantNo){
// 이전 물주기와 비교
int prevWateringPeriod = plantDao.getPlantOne(plantNo).getAverageWateringPeriod();
// DB에서 마지막 물주기 row 2개 들고와서 주기 계산
// == 방금 며칠만에 물준 건지 계산
List<Watering> recentWateringList = wateringDao.getRecentTwoWateringDays(plantNo);
if(recentWateringList.size() == 1){
// 물주기 기록이 오직 한 개인 경우
// TODO plant 테이블의 create_date과 비교하거나, 데이터 두 개 모일 때까지 기다리거나!
return 0;
}
int tmpPeriod = Period.between(recentWateringList.get(1).getWateringDate(), recentWateringList.get(0).getWateringDate()).getDays();
if(prevWateringPeriod > tmpPeriod){
// 물주기가 짧아진 경우 -> DB에 반영
plantDao.updateAverageWateringPeriod(plantNo, tmpPeriod);
return -1;
} else if (prevWateringPeriod == tmpPeriod){
// 물주기 똑같음
return 0;
} else {
// 물주기 길어짐
// 인간의 게으름 혹은 환경 문제이므로 DB 반영하지 않음
return 1;
}
}
}
@Override
public Watering addWatering(Watering watering) {
return wateringRepository.save(watering);
}
@Override
public List<Watering> getRecentTwoWateringDays(int plantNo) {
return wateringRepository.findTop2ByPlantNoOrderByWateringNoDesc(plantNo);
}