Rental Application (React & Spring boot Microservice) - 37: 대여서비스(2)

yellow_note·2021년 10월 13일
0
post-thumbnail

#1 대여흐름

앞서 카프카 메시지 브로커를 이용해 post-service와 rental-service 연동을 진행해 보았습니다. 그러면 실제로 사용자가 대여 버튼을 눌렀을 때, 어떤 이벤트들이 발생하는지 그림을 통해 살펴 보고 이를 바탕으로 실제 구현을 진행해보겠습니다.

1) user-a는 대여 버튼을 클릭합니다.

2) post-service에서는 버튼 이벤트 시 REST 요청을 받고, 메시지 브로커에 대여 생성을 위한 메시지를 발행합니다.

3) rental-service는 kafka를 구독하고 있다가 해당 메시지가 발행되면 메시지를 받습니다.

4) 우선, 대여 관련 데이터를 생성하는데 이 때의 상태 값은 pending입니다.

5) 데이터 트랜잭션이 정상적으로 수행되면 user-b에게 대여 관련 메시지를 보냅니다.

6) user-b는 대여를 수락할지 거절할지에 관한 메시지를 rental-service에 전송합니다.

7) 수락한다면 status는 being으로 바뀌고, 그렇지 않다면 데이터는 삭제됩니다.

이러한 흐름으로 대여 서비스를 수정하고, 구현하도록 하겠습니다.

#2 MyPage

대여 요청에 관한 메시지는 MyPage에서 볼 수 있도록 만들 예정이니 MyPage 컴포넌트를 다음과 같이 수정하겠습니다.

  • ./src/components/user/MyPageForm.js
...

    return(
        <>
            { user &&
                <MyPageFormBlock>
                    ...
                    <LineBlock>
                        <HalfLeftLine>
                            <TextBox>
                                <Link to="/user/rental-request">
                                    대여 요청
                                </Link>
                            </TextBox>
                        </HalfLeftLine>
                        ...
                    </LineBlock>
                    ...
                </MyPageFormBlock>
            }
        </>
    );
};

export default withRouter(MyPageForm);
  • ./src/pages/RequestPage.js
import React from 'react';
import HeaderTemplate from '../components/common/HeaderTemplate';
import RequestListContainer from '../components/user/RequestListContainer';

const RequestPage = () => {
    return(
        <>
            <HeaderTemplate />
            <RequestListContainer />
        </>
    );
};

export default RequestPage
  • ./src/components/user/RequestCard.js
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';
import ButtonContainer from './ButtonContainer';

const WhiteBox = styled.div`
    box-shadow: 0 0 8px rgba(0, 0, 0, 0.025);
    padding: 2rem;
    width: 40%;
    background: white;
    border-radius: 2px;
    margin-top: 2rem;
    margin-bottom: 2rem;
`;

const Content = styled.div`
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    font-size: 1.5rem;
    font-weight: bold;
    border-bottom: 1px solid ${palette.gray[2]};
    padding-bottom: 1.5rem;
`;

const Footer = styled.div`
    width: 100%;
    display: flex;
    justify-content: flex-end;
`;

const RequestCard = ({ item, i }) => {
    const onAccept = () => {
        
    };

    const onDecline = () => {

    };

    return(
        <WhiteBox>
            <Content>
                { item.borrower } 님이 대여 요청을 하였습니다.
            </Content>
            <Footer>
                <ButtonContainer onAccept={ onAccept }
                                 onDecline={ onDecline }
                />
            </Footer>
        </WhiteBox>
    );
};

export default RequestCard;
  • ./src/components/user/RequestListContainer.js
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';
import RequestCard from './RequestCard';

const Box = styled.div`
    width: 100%;
    height: 100vh;
    overflow-x: hidden;
    overflow-y: auto;
    background: ${palette.gray[2]};
    border-radius: 2px;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
`;

const RequestListContainer = () => {
    const dummyData = [
        {"id": 1, "borrower": "aaa", "created_at": null, "owner": "bbb", "post_id": 1, "price": 10000, "rental_id": 1, "start_date": null, "end_date": null, "status": "PENDING"},
        {"id": 1, "borrower": "aaa", "created_at": null, "owner": "bbb", "post_id": 1, "price": 10000, "rental_id": 1, "start_date": null, "end_date": null, "status": "PENDING"}
    ];

    return(
        <Box>
            {
                dummyData.map((item, i) => {
                    return <RequestCard item={ item }/>
                })
            }
        </Box>
    );
};

export default RequestListContainer;
  • ./src/components/user/ButtonContainer.js
import React from 'react';
import styled from 'styled-components';
import FullButton from '../common/FullButton';

const CustomFullButton = styled(FullButton)`
    margin-right: 25px;
    margin-left: 25px;
    width: 200px;
    &:hover {
        margin-right: 25px;
        margin-left: 25px;
        width: 200px;
    }
`;

const ButtonContainer = ({
    onAccept, 
    onDecline
}) => {
    return(
        <>
            <CustomFullButton onClick={ onAccept }>
                수락하기
            </CustomFullButton>
            <CustomFullButton red
                              onClick={ onDecline }                
            >
                거절하기
            </CustomFullButton>
        </>
    );
};

export default ButtonContainer;

ui를 완성했으니 잘 나오는지 확인을 해보겠습니다.

대여 요청 카드가 잘 나오는 모습을 볼 수 있습니다. 그러면 rental-service를 수정하고 클릭 이벤트를 작성하도록 하겠습니다.

#3 rental-service

  • ./controller/RentalController
...

@RestController
@Slf4j
@RequestMapping("/")
public class RentalController {
    ...

    @PostMapping("/complete-rental")
    public ResponseEntity<?> completeRental(@RequestBody RequestComplete vo) {
        log.info("Rental Service's Controller Layer :: Call createRental Method!");

        if(!vo.isAcceptance()) {
            rentalService.decline(vo.getRentalId());

            return ResponseEntity.status(HttpStatus.OK).body("Your request is declined");
        }

        RentalDto rental = rentalService.completeRental(vo.getRentalId());
        ResponseRental responseRental = ResponseRental.builder()
                                                      .rentalId(rental.getRentalId())
                                                      .postId(rental.getPostId())
                                                      .price(rental.getPrice())
                                                      .owner(rental.getOwner())
                                                      .borrower(rental.getBorrower())
                                                      .startDate(rental.getStartDate())
                                                      .endDate(rental.getEndDate())
                                                      .status(rental.getStatus())
                                                      .createdAt(rental.getCreatedAt())
                                                      .build();

        return ResponseEntity.status(HttpStatus.CREATED).body(responseRental);
    }

    @GetMapping("/{rentalId}/rental")
    public ResponseEntity<?> getRentalByRentalId(@PathVariable("rentalId") String rentalId) {
        log.info("Rental Service's Controller Layer :: Call getRentalByRentalId Method!");

        RentalDto rentalDto = rentalService.getRentalByRentalId(rentalId);

        return ResponseEntity.status(HttpStatus.OK).body(ResponseRental.builder()
                                                                       .rentalId(rentalDto.getRentalId())
                                                                       .postId(rentalDto.getPostId())
                                                                       .price(rentalDto.getPrice())
                                                                       .owner(rentalDto.getOwner())
                                                                       .borrower(rentalDto.getBorrower())
                                                                       .startDate(rentalDto.getStartDate())
                                                                       .endDate(rentalDto.getEndDate())
                                                                       .status(rentalDto.getStatus())
                                                                       .createdAt(rentalDto.getCreatedAt())
                                                                       .build());
    }

    @GetMapping("/{owner}/my_rentals")
    public ResponseEntity<?> getRentalsByOwner(@PathVariable("owner") String owner) {
        log.info("Rental Service's Controller Layer :: Call getMyRentalByUserId Method!");

        Iterable<RentalDto> rentalList = rentalService.getRentalsByOwner(owner);
        List<ResponseRental> result = new ArrayList<>();

        rentalList.forEach(v -> {
            result.add(ResponseRental.builder()
                                     .rentalId(v.getRentalId())
                                     .postId(v.getPostId())
                                     .price(v.getPrice())
                                     .owner(v.getOwner())
                                     .borrower(v.getBorrower())
                                     .startDate(v.getStartDate())
                                     .endDate(v.getEndDate())
                                     .status(v.getStatus())
                                     .createdAt(v.getCreatedAt())
                                     .build());
        });

        return ResponseEntity.status(HttpStatus.OK).body(result);
    }

    @GetMapping("/{borrower}/borrow_rentals")
    public ResponseEntity<?> getRentalsByBorrower(@PathVariable("borrower") String borrower) {
        log.info("Rental Service's Controller Layer :: Call getBorrowRentalByUserId Method!");

        Iterable<RentalDto> rentalList = rentalService.getRentalsByBorrower(borrower);
        List<ResponseRental> result = new ArrayList<>();

        rentalList.forEach(v -> {
            result.add(ResponseRental.builder()
                                     .rentalId(v.getRentalId())
                                     .postId(v.getPostId())
                                     .price(v.getPrice())
                                     .owner(v.getOwner())
                                     .borrower(v.getBorrower())
                                     .startDate(v.getStartDate())
                                     .endDate(v.getEndDate())
                                     .status(v.getStatus())
                                     .createdAt(v.getCreatedAt())
                                     .build());
        });

        return ResponseEntity.status(HttpStatus.OK).body(result);
    }

    @GetMapping("/{owner}/request-rentals")
    public ResponseEntity<?> getRentalsByPending(@PathVariable("owner") String owner) {
        log.info("Rental Service's Controller Layer :: Call getRentalsByPending Method!");

        Iterable<RentalDto> rentalList = rentalService.getRentalsByPending(owner);
        List<ResponseRental> result = new ArrayList<>();

        rentalList.forEach(v -> {
            result.add(ResponseRental.builder()
                                     .rentalId(v.getRentalId())
                                     .postId(v.getPostId())
                                     .price(v.getPrice())
                                     .owner(v.getOwner())
                                     .borrower(v.getBorrower())
                                     .startDate(v.getStartDate())
                                     .endDate(v.getEndDate())
                                     .status(v.getStatus())
                                     .createdAt(v.getCreatedAt())
                                     .build());
        });

        return ResponseEntity.status(HttpStatus.OK).body(result);
    }
}

변경되거나 추가된 메서드는 다음과 같습니다.

1) completeRental
대여에 대한 최종적인 처리를 하는 메서드입니다. 전달받는 vo의 acceptance 값을 판별해 false라면 PENDING상태인 데이터를 제거하고, 그렇지 않다면 BEING상태로 바꾸기 위해 서비스를 호출합니다.

2) getRentalsByPending
앞서 UI에서 작성한 대여 요청 데이터들을 불러오기 위한 메서드입니다.

  • ./status/RentalStatus
package com.microservices.rentalservice.status;

public enum RentalStatus {
    BEING_RENTAL,
    PENDING_RENTAL,
    EXPIRED_RENTAL
}

대여 상태를 가지고 있는 열거형 데이터입니다.

그리고 RenalEntity, RentalDto, RetnalResponse에 status라는 이름의 속성을 추가하도록 하겠습니다.

  • ./service/RentalServiceImpl
...

@Service
@Slf4j
public class RentalServiceImpl implements RentalService {
    ...

    @Transactional
    @Override
    public RentalDto completeRental(String rentalId) {
        log.info("Rental Service's Service Layer :: Call completeRental write Method!");

        RentalEntity rentalEntity = rentalRepository.findByRentalId(rentalId);

        rentalEntity.setStatus(RentalStatus.BEING_RENTAL.name());

        rentalRepository.save(rentalEntity);

        return RentalDto.builder()
                        .rentalId(rentalEntity.getRentalId())
                        .postId(rentalEntity.getPostId())
                        .price(rentalEntity.getPrice())
                        .owner(rentalEntity.getOwner())
                        .borrower(rentalEntity.getBorrower())
                        .startDate(rentalEntity.getStartDate())
                        .endDate(rentalEntity.getEndDate())
                        .status(rentalEntity.getStatus())
                        .createdAt(rentalEntity.getCreatedAt())
                        .build();
    }

    @Transactional
    @Override
    public RentalDto getRentalByRentalId(String rentalId) {
        log.info("Rental Service's Service Layer :: Call getRentalByRentalId Method!");

        RentalEntity rentalEntity = rentalRepository.findByRentalId(rentalId);

        return RentalDto.builder()
                        .rentalId(rentalEntity.getRentalId())
                        .postId(rentalEntity.getPostId())
                        .price(rentalEntity.getPrice())
                        .owner(rentalEntity.getOwner())
                        .borrower(rentalEntity.getBorrower())
                        .startDate(rentalEntity.getStartDate())
                        .endDate(rentalEntity.getEndDate())
                        .status(rentalEntity.getStatus())
                        .createdAt(rentalEntity.getCreatedAt())
                        .build();
    }

    @Transactional
    @Override
    public Iterable<RentalDto> getRentalsByOwner(String owner) {
        log.info("Rental Service's Service Layer :: Call getRentalsByOwner Method!");

        Iterable<RentalEntity> rentals = rentalRepository.findAllByOwner(owner);
        List<RentalDto> rentalList = new ArrayList<>();

        rentals.forEach(v -> {
            rentalList.add(RentalDto.builder()
                                    .rentalId(v.getRentalId())
                                    .postId(v.getPostId())
                                    .price(v.getPrice())
                                    .owner(v.getOwner())
                                    .borrower(v.getBorrower())
                                    .startDate(v.getStartDate())
                                    .endDate(v.getEndDate())
                                    .status(v.getStatus())
                                    .createdAt(v.getCreatedAt())
                                    .build());
        });

        return rentalList;
    }

    @Transactional
    @Override
    public Iterable<RentalDto> getRentalsByBorrower(String borrower) {
        log.info("Rental Service's Service Layer :: Call getRentalsByBorrower Method!");

        Iterable<RentalEntity> rentals = rentalRepository.findAllByBorrower(borrower);
        List<RentalDto> rentalList = new ArrayList<>();

        rentals.forEach(v -> {
            rentalList.add(RentalDto.builder()
                                    .rentalId(v.getRentalId())
                                    .postId(v.getPostId())
                                    .price(v.getPrice())
                                    .owner(v.getOwner())
                                    .borrower(v.getBorrower())
                                    .startDate(v.getStartDate())
                                    .endDate(v.getEndDate())
                                    .status(v.getStatus())
                                    .createdAt(v.getCreatedAt())
                                    .build());
        });

        return rentalList;
    }

    @Transactional
    @Override
    public void decline(String rentalId) {
        log.info("Rental Service's Service Layer :: Call decline write Method!");

        RentalEntity rentalEntity = rentalRepository.findByRentalId(rentalId);

        rentalRepository.delete(rentalEntity);
    }

    @Transactional
    @Override
    public Iterable<RentalDto> getRentalsByPending(String owner) {
        log.info("Rental Service's Service Layer :: Call getRentalsByPending write Method!");

        Iterable<RentalEntity> rentals = rentalRepository.findRentalsByPending(owner);
        List<RentalDto> rentalList = new ArrayList<>();

        rentals.forEach(v -> {
            rentalList.add(RentalDto.builder()
                                    .rentalId(v.getRentalId())
                                    .postId(v.getPostId())
                                    .price(v.getPrice())
                                    .owner(v.getOwner())
                                    .borrower(v.getBorrower())
                                    .startDate(v.getStartDate())
                                    .endDate(v.getEndDate())
                                    .status(v.getStatus())
                                    .createdAt(v.getCreatedAt())
                                    .build());
        });

        return rentalList;
    }
}

서비스의 메서드들을 살펴보겠습니다.

1) createRental: status에 PENDING값을 부여하고 데이터베이스에 저장합니다.

2) completeRental: 대여에 대한 수락 여부를 위한 메서드로 status를 BEING으로 변경하고, 업데이트합니다.

3) decline: 대여에 대한 거절을 위한 메서드로 데이터를 삭제합니다.

4) getRentalsByPending: 대여 요청을 불러오는 메서드입니다.

  • ./message/KafkaConsumer
package com.microservices.rentalservice.message;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microservices.rentalservice.entity.RentalEntity;
import com.microservices.rentalservice.repository.RentalRepository;
import com.microservices.rentalservice.status.RentalStatus;
import com.microservices.rentalservice.util.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Service
@Slf4j
public class KafkaConsumer {
    RentalRepository rentalRepository;

    @Autowired
    public KafkaConsumer(RentalRepository rentalRepository) {
        this.rentalRepository = rentalRepository;
    }

    @KafkaListener(topics="rental-topic")
    public void requestRental(String kafkaMessage) {
        log.info("Kafka Message : " + kafkaMessage);

        Map<Object, Object> map = new HashMap<>();
        ObjectMapper mapper = new ObjectMapper();

        try {
            map = mapper.readValue(kafkaMessage, new TypeReference<Map<Object, Object>>() {});
        } catch(JsonProcessingException ex) {
            ex.printStackTrace();
        }

        RentalEntity rentalEntity = RentalEntity.builder()
                                                .rentalId(UUID.randomUUID().toString())
                                                .postId(Long.parseLong(String.valueOf(map.get("postId"))))
                                                .owner((String)map.get("owner"))
                                                .borrower((String)map.get("borrower"))
                                                .price(Long.parseLong(String.valueOf(map.get("price"))))
                                                .startDate((String)map.get("startDate"))
                                                .endDate((String)map.get("endDate"))
                                                .status(RentalStatus.PENDING_RENTAL.name())
                                                .createdAt(DateUtil.dateNow())
                                                .build();

        rentalRepository.save(rentalEntity);
    }
}

대여 생성을 위한 카프카 컨슈머에 status값을 추가하도록 하겠습니다.

  • ./repository/RentalRepository
package com.microservices.rentalservice.repository;

import com.microservices.rentalservice.entity.RentalEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface RentalRepository extends JpaRepository<RentalEntity, Long> {
    RentalEntity findByRentalId(String rentalId);

    Iterable<RentalEntity> findAllByOwner(String owner);

    Iterable<RentalEntity> findAllByBorrower(String borrower);

    @Query(
        value = "SELECT * " +
                "FROM rentals " +
                "WHERE status = 'EXPIRED_RENTAL',
        nativeQuery = true
    )
    Iterable<RentalEntity> findAllExceptExpired();

    @Query(
        value = "SELECT * " +
                "FROM rentals " +
                "WHERE status = 'PENDING_RENTAL' AND owner = :owner,
        nativeQuery = true
    )
    Iterable<RentalEntity> findRentalsByPending(String owner);
}
  • EXPIRED상태는 후에 스케쥴러를 이용하여 대여 데이터들을 만료시킬 때 사용하도록 하겠습니다.

추가적으로 post-service에서 메서드를 추가하겠습니다.

  • ./controller/PostController
...

@RestController
@RequestMapping("/")
@Slf4j
public class PostController {
    ...

    @PostMapping("/rollback/{postId}")
    public ResponseEntity<?> rollbackPost(@PathVariable("postId") Long postId) {
        log.info("Post Service's Controller Layer :: Call rollbackPost Method!");

        postService.rollbackPost(postId);

        return ResponseEntity.status(HttpStatus.OK).body("Successfully rollback!");
    }
}

작성자가 거절 버튼을 누르면 호출될 endpoint입니다. 빌리기 버튼을 누르게 되면 자동적으로 게시글의 status값이 COMPLETE로 변하기 때문에 이를 READY 상태로 돌리기 위한 메서드입니다.

  • ./service/PostServiceImpl
...

@Service
@Slf4j
public class PostServiceImpl implements PostService {
    ....

    @Transactional
    @Override
    public void rollbackPost(Long postId) {
        log.info("Post Service's Service Layer :: Call rollbackPost Method!");

        PostEntity entity = postRepository.findPostById(postId);

        entity.setStatus(PostStatus.READY_RENTAL.name());

        postRepository.save(entity);
    }
}

post-service, rental-service에 대해서 어느 정도 구현이 완료되었습니다. 그러면 react에서 관련 redux 모듈을 만들고 실제 테스트를 진행해보도록 하겠습니다.

#5 rental redux

우선 postAPI에 다음의 엔드포인트를 추가하도록 하겠습니다.

  • ./src/lib/api/posts.js
export const createRental = ({
    postId,
    owner,
    borrower,
    price,
    startDate,
    endDate
}) => client.post('/post-service/rental', {
    postId,
    owner,
    borrower,
    price,
    startDate,
    endDate
});
  • ./src/lib/api/rental.js
import client from "./client";

export const completeRental = ({
    rentalId,
    acceptance
}) => client.post('/rental-service/complete-rental', {
    rentalId,
    acceptance
});

export const requestRentals = owner => client.get(`/rental-service/${owner}/request-rentals`);

rental-service와 연동하면서 필요한 REST API는 3개입니다. 대여 데이터를 만들기 위한 post-service의 createRental, 최종 대여 처리를 위한 compeleteRental, 대여 요청 리스트를 불러오기 위한 requestRentals 이 3가지를 리덕스 모듈로 만들어보도록 하겠습니다.

  • ./src/modules/rental.js
import { createAction, handleActions } from "redux-actions";
import createRequestSaga, { createRequestActionTypes } from "../lib/createRequestSaga";
import * as rentalAPI from '../lib/api/rental';
import * as postAPI from '../lib/api/posts';
import { takeLatest } from "@redux-saga/core/effects";

const INITIALIZE = 'rental/INITIALIZE';

const CHANGE_FIELD = 'rental/CHANGE_FIELD';

const [
    CREATE_RENTAL,
    CREATE_RENTAL_SUCCESS,
    CREATE_RENTAL_FAILURE,
] = createRequestActionTypes('rental/CREATE_RENTAL');

const [
    COMPLETE,
    COMPLETE_SUCCESS,
    COMPLETE_FAILURE
] = createRequestActionTypes('rental/COMPLETE');

const [
    REQUEST_RENTALS,
    REQUEST_RENTALS_SUCCESS,
    REQUEST_RENTALS_FAILURE,
] = createRequestActionTypes('rental/REQUEST_RENTALS');

export const createRental = createAction(CREATE_RENTAL, ({
    postId,
    owner,
    borrower,
    price,
    startDate,
    endDate
}) => ({
    postId,
    owner,
    borrower,
    price,
    startDate,
    endDate
}));

export const completeRental = createAction(COMPLETE, ({
    acceptance,
    rentalId
}) => ({
    acceptance,
    rentalId
}));

export const requestRentals = createAction(REQUEST_RENTALS, owner => owner);

export const intialize = createAction(INITIALIZE);

export const changeField = createAction(CHANGE_FIELD, ({
    key,
    value
}) => ({
    key,
    value
}));

const createRentalSaga = createRequestSaga(CREATE_RENTAL, postAPI.createRental);
const completeRentalSaga = createRequestSaga(COMPLETE, rentalAPI.completeRental);
const requestRentalsSaga = createRequestSaga(REQUEST_RENTALS, rentalAPI.requestRentals);

export function* rentalSaga() {
    yield takeLatest(CREATE_RENTAL, createRentalSaga);
    yield takeLatest(COMPLETE, completeRentalSaga);
    yield takeLatest(REQUEST_RENTALS, requestRentalsSaga);
};

const initialState = {
    acceptance: '',
    rentalId: '',
    owner: '',
    message: '',
    rental: null,
    rentals: null,
    rentalError: null,
};

const rental = handleActions(
    {
        [INITIALIZE]: state => initialState,
        [CHANGE_FIELD]: (state, { payload: { key, value }}) => ({
            ...state,
            [key]: value
        }),
        [CREATE_RENTAL_SUCCESS]: (state, { payload: message }) => ({
            ...state,
            message,
        }),
        [CREATE_RENTAL_FAILURE]: (state, { payload: rentalError }) => ({
            ...state,
            rentalError
        }),
        [COMPLETE_SUCCESS]: (state, { payload: rental }) => ({
            ...state,
            rental,
        }),
        [COMPLETE_FAILURE]: (state, { payload: rentalError }) => ({
            ...state,
            rentalError
        }),
        [REQUEST_RENTALS_SUCCESS]: (state, { payload: rentals }) => ({
            ...state,
            rentals
        }),
        [REQUEST_RENTALS_FAILURE]: (state, { payload: rentalError }) => ({
            ...state,
            rentalError
        }),
    },
    initialState,
);

export default rental;

대여 생성을 위해 message, 대여 처리를 위해 acceptance, rentalId를 위한 state 그리고 반환값을 위한 rental이 있고, 요청 리스트를 위해 owner 그리고 반환값을 위한 rentals를 state로 만들었습니다.

  • ./src/modules/index.js
import { combineReducers } from "redux";
import { all } from 'redux-saga/effects';
import auth, { authSaga } from './auth';
import user, { userSaga } from "./user";
import write, { writeSaga } from "./write";
import post, { postSaga } from "./post";
import postList, { postListSaga } from "./postList";
import writeComment, { writeCommentSaga } from "./writeComment";
import send, { sendSaga } from './send';
import messageList, { messageListSaga } from "./messageList";
import loading from './loading';
import rental, { rentalSaga } from "./rental";

const rootReducer = combineReducers(
    {
        ...
        rental,
    },
);

export function* rootSaga() {
    yield all([
        ...
        rentalSaga(),
    ]);
}

export default rootReducer;

post 리덕스 모듈, api에 메서드를 추가하겠습니다.

  • ./src/lib/api/posts.js
...

export const rollbackStatus = postId => client.post(`/post-service/rollback/${postId}`);
  • ./src/modules/post.js
...

const [
    ROLLBACK_POST,
    ROLLBACK_POST_SUCCESS,
    ROLLBACK_POST_FAILURE,
] = 'post/ROLLBACK_POST';

...
const rollbackPostSaga = createRequestSaga(ROLLBACK_POST, postsAPI.rollbackStatus);

export function* postSaga() {
    yield takeLatest(READ_POST, readPostSaga);
    yield takeLatest(ROLLBACK_POST, rollbackPostSaga);
}

const initialState = {
    post: null,
    error: null,
    message: null,
};

const post = handleActions(
    {
        ...
        [ROLLBACK_POST_SUCCESS]: (state, { payload: message }) => ({
            ...state,
            message,
        }),
        [ROLLBACK_POST_FAILURE]: (state, { payload: error }) => ({
            ...state,
            error,
        }),
    },
    initialState,
);

export default post;

리덕스 모듈을 만들었으니 이를 활용해보도록 하겠습니다.

  • ./src/components/user/RequestContainer.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';
import RequestCard from './RequestCard';
import { requestRentals } from '../../modules/rental';

const Box = styled.div`
    width: 100%;
    height: 100vh;
    overflow-x: hidden;
    overflow-y: auto;
    background: ${palette.gray[2]};
    border-radius: 2px;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
`;

const NotData = styled.div`
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.5rem;
`;

const RequestListContainer = () => {
    const dispatch = useDispatch();
    const { 
        rentals,
        nickname 
    } = useSelector(({ 
        rental,
        user
    }) => ({ 
        rentals: rental.rentals,
        nickname: user.user.nickname,
    }));

    useEffect(() => {
        dispatch(requestRentals(nickname));
    }, [dispatch, nickname]);

    return(
        <Box>
            {
                rentals ?
                rentals.map((item, i) => {
                    return <RequestCard item={ item }/>
                }) : 
                <NotData>
                    대여 요청이 존재하지 않습니다!
                </NotData>
            }
        </Box>
    );
};

export default RequestListContainer;

useSelector를 이용하여 state 값에 접근하고, requestRentals 메서드를 호출하여 요청 리스트를 불러오도록 합니다. 여기서 nickname은 owner값에 들어가게 됩니다.

  • ./src/components/user/RequestCard.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changeField, completeRental, initialize, intialize } from '../../modules/rental';
import styled from 'styled-components';
import palette from '../../lib/styles/palettes';
import ButtonContainer from './ButtonContainer';
import { rollbackPost } from '../../modules/post';

...

const RequestCard = ({ item, i }) => {
    const dispatch = useDispatch()
    const {
        acceptance,
        rentalId,
        rental
    } = useSelector(({ rental }) => ({
        acceptance: rental.acceptance,
        rentalId: rental.rentalId,
        rental: rental.rental
    }));
    const onAccept = () => {
        dispatch(changeField({
            key: 'acceptance',
            value: true
        }));
    };

    const onDecline = () => {
        dispatch(changeField({
            key: 'acceptance',
            value: false
        }));
    };

    useEffect(() => {
        dispatch(changeField({
            key: 'rentalId',
            value: item.rentalId
        }));
    }, [dispatch, item]);

    useEffect(() => {
        if(acceptance === true) {
            dispatch(completeRental({
                acceptance,
                rentalId
            }));

            dispatch(initialize());
        }

        if(acceptance === false) {
            const postId = item.postId;

            dispatch(completeRental({
                acceptance,
                rentalId
            }));

            dispatch(initialize());

            dispatch(rollbackPost(postId));
        }
    }, [dispatch, acceptance, rentalId, item]);

    return(
        <WhiteBox>
            <Content>
                { item.borrower } 님이 대여 요청을 하였습니다.
            </Content>
            <Footer>
                <ButtonContainer onAccept={ onAccept }
                                 onDecline={ onDecline }
                />
            </Footer>
        </WhiteBox>
    );
};

export default RequestCard;

그리고 수락과 거절 이벤트에서 각각 acceptance 값을 true와 false로 변경해주고 state값에 접근하여 rentalId값을 가져와 completeRental메서드를 실행합니다.

  • ./src/components/posts/PostViewer.js
...

const PostViewer = ({
    post, 
    error, 
    loading
}) => {
    const dispatch = useDispatch();
    const { nickname } = useSelector(({ user }) => ({ nickname: user.user.nickname }));
    const onRental = () => {
        const { 
            postId,
            writer,
            price,
            startDate,
            endDate
        } = post;

        dispatch(createRental({
            postId,
            writer,
            nickname,
            price,
            startDate,
            endDate
        }));
    };

    ...

    return(
        <PostViewerBlock>
            ...
                    <RentalArea>        
                        <RentalButton onClick={ onRental }>
                            빌리기
                        </RentalButton>
                    </RentalArea>
                </PostNav>
            }
            ...
        </PostViewerBlock>
    );
};

export default PostViewer;

PostViewer의 빌리기 버튼에 대한 이벤트를 만들었습니다.

이로써 react에서 연동이 어느 정도 완료되었습니다. 그러면 실제로 서비스들을 실행하고 테스트를 진행해보도록 하겠습니다.

#6 테스트

컨버스 게시글을 대상으로 테스트를 진행하겠습니다.


빌리기 버튼을 누른 결과 성공적으로 게시글의 상태가 바뀌고, 대여 데이터가 생성됨을 볼 수 있습니다.

거절 버튼을 눌러보도록 하겠습니다.

MariaDB [RENTALSERVICE]> select * from rentals;
Empty set (0.017 sec)

MariaDB [RENTALSERVICE]> use POSTSERVICE;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [POSTSERVICE]> select * from posts;
+---------+--------------+---------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+-----------------+--------------+-----------------+-----------------+------------------------------------------------------+--------------------------------------+--------+
| post_id | category     | content                                                                                                             | created_at          | end_date        | post_type       | rental_price | start_date      | status          | title                                                | user_id                              | writer |
+---------+--------------+---------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+-----------------+--------------+-----------------+-----------------+------------------------------------------------------+--------------------------------------+--------+
|       1 | 가전제품     | test-001                                                                                                            | 2021년 09월 17일    | Fri Sep 03 2021 | 빌려줄게요      |       100000 | Wed Sep 01 2021 | READY_RENTAL    | test-001                                             | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       2 | 도서류       | test-002                                                                                                            | 2021년 09월 17일    | Sun Sep 05 2021 | 빌려줄게요      |        10000 | Wed Sep 01 2021 | READY_RENTAL    | test-002                                             | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       3 | NULL         | 급하게 캠핑을 가야 해서 10월 첫 주 주말에 랜턴 빌려주실 분 계시면 감사하겠습니다..                                  | 2021년 09월 17일    | NULL            | 빌려주세요      |         NULL | NULL            | REQUEST_RENTAL  | 캠핑 랜턴 빌려주실 분 혹시 계신가요??                | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       4 | 가전제품     | 당분간 노트북 사용할 일이 없어 대여해드려요!                                                                        | 2021년 09월 17일    | Sun Oct 31 2021 | 빌려줄게요      |       100000 | Wed Sep 01 2021 | COMPLETE_RENTAL | 노트북 빌려드립니다!                                 | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       5 | 도서류       | 수학책 빌려드립니다!!!!                                                                                             | 2021년 09월 17일    | Thu Sep 30 2021 | 빌려줄게요      |         5000 | Wed Sep 01 2021 | READY_RENTAL    | 수학책 빌려드립니다!                                 | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       6 | 캠핑용품     | 캠핑 텐트 장기간 빌려드립니다!                                                                                      | 2021년 09월 17일    | Sun Oct 31 2021 | 빌려줄게요      |       100000 | Sat Sep 11 2021 | READY_RENTAL    | 캠핑 텐트 빌리고 싶으신 분??                         | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       7 | 가전제품     | 전자레인지 빌려드릴게요!~!~!~                                                                                       | 2021년 09월 17일    | Tue Nov 30 2021 | 빌려줄게요      |        50000 | Sat Oct 02 2021 | READY_RENTAL    | 전자레인지 빌려드릴게요!                             | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       8 | NULL         | 이번 주 주말에 축구화 빌려주실 분 계신가요???                                                                       | 2021년 09월 17일    | NULL            | 빌려주세요      |         NULL | NULL            | REQUEST_RENTAL  | 축구화 빌리고 싶습니다~                              | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
|       9 | 의류         | 새 컨버스화 빌려드립니다!                                                                                           | 2021년 09월 17일    | Thu Oct 07 2021 | 빌려줄게요      |        10000 | Sat Sep 04 2021 | READY_RENTAL    | 컨버스화 빌려드립니다                                | 06da8d9f-20b8-4fce-a5fe-2e4a4eb4e33c | asd    |
+---------+--------------+---------------------------------------------------------------------------------------------------------------------+---------------------+-----------------+-----------------+--------------+-----------------+-----------------+------------------------------------------------------+--------------------------------------+--------+
9 rows in set (0.006 sec)

rental 데이터가 삭제되고 컨버스 게시글의 상태값이 READY로 변경됨을 볼 수 있습니다.

다음 포스트에서는 스케쥴 어노테이션을 사용하여 자동적으로 현재 시간이 end_date와 같다면 대여의 상태 값을 EXPIRE로 변경해보도록 하겠습니다.

0개의 댓글

관련 채용 정보