본문 바로가기
프로젝트/아리(Ari)

[Spring Boot, Data JPA] 마이페이지 - 찜(좋아요), 협약 맺기(친구 추가)

by sepang 2022. 10. 1.

fig 1

  이제 남아있는 기능인 찜 목록, 협약 맺기 기능을 완성해보자.

찜(좋아요) 기능

fig 2-1
fig 2-2

  좋아요의 경우에는 가게 좋아요와 게시글 좋아요로 나누어진다. 대상은 다르지만 과정은 동일하므로 가게 좋아요를 기준으로 살펴보자. fig 2-1의 하트를 클릭하면 하트가 채워지고 해당 가게가 회원의 favoriteStores<Store> 리스트에 추가되고, 다시 터치하면 제거된다.

컨트롤러

@PostMapping("/favorite/toggle")
public ResponseEntity<?> addMemberFavoriteStore(@RequestParam Long storeId, Principal principal) {
    Long memberId = Long.parseLong(principal.getName());
    return memberService.toggleMemberFavoriteStore(memberId, storeId);
}

  원래는 좋아요 추가와 제거에 대한 컨트롤러와 서비스 코드를 분리하였는데 이후 하나의 api로 통일시켰다. 동일한 api지만 좋아요 추가 여부에 따라 다른 코드가 동작하도록 말이다. 컨트롤러 메소드에서는 jwt를 통해 요청보낸 회원의 id와 @RequestParam으로 받은 찜 목록 추가 대상 가게의 id를 받아서 서비스 메서드인 toggleMemberFavoriteStore()로 넘겨준다.

@Transactional
public ResponseEntity<?> toggleMemberFavoriteStore(Long memberId, Long storeId){
    /*1. 요청한 회원과 종아요(or 취소) 한 가게 객체를 가져옴*/
    Member member = getMemberInfoById(memberId);
    Store store = storeService.findStore(storeId);

    FavoriteStore favorite; // 존재하는 좋아요 정보나 새롭게 추가할 좋아요 정보가 담길 변수

    /*2. 회원이 해당 가게를 좋아요 했는지 여부를 판단 후 했다면 다음 조건 문 실행*/
    if(member.isFavoriteStore(store)) {
        favorite = favoriteRepository.findFavoriteStoreByMemberAndStore(member, store).orElseGet(null);

        member.deleteFavorite(favorite);
        favoriteRepository.delete(favorite);

        return response.success("즐겨찾기 목록에서 성공적으로 제거했습니다.");
    }

    /*3. 회원이 좋아요 하지 않았던 가게라면 회원과 가게 정보 담아 새롭게 좋아요 객체 생성 후 추가*/
    favorite = new FavoriteStore(member, store);

    member.addFavorite(favorite);
    store.addFavorite(favorite);

    favoriteRepository.save(favorite);

    return response.success("즐겨찾기 목록에 성공적으로 저장했습니다.");
}
public boolean isFavoriteStore(Store store){
    return favoriteStores.stream().map(FavoriteStore :: getStore)
            .anyMatch(findStore->  findStore.getId() == store.getId());
}

  서비스 메서드에서는 파라미터를 이용해 좋아요를 클릭한 회원과 대상 가게 객체를 가져온다. 원래는 repository 함수를 바로 사용했었는데 팀원이 단순한 기능이라도 서비스 코드에서 메소드를 거쳐서 리포지토리 메서드를 사용하는게 적절하다고 생각하여 1.에서는 다음과 같이 사용하였다. 근데 이 방식을 통일시키는게 수월하지 않아 방침을 확실히 정해야 할 것 같다.

  어쨌든 그 다음 2.에서는 가게의 찜 목록 추가 여부를 확인한다. 해당 함수는 팀원이 만들어 줬는데 나도 스트림 문법을 학습해서 적용하도록 해야겠다. isFavoriteStore()가 true를 반환했다면 찜 목록에 해당 가게가 추가되어 있다는 뜻이므로 멤버와 가게 객체를 이용해서 기존 FavoriteStore객체를 찾은 다음 이를 삭제해준다.

  isFavoriteStore()가 false를 반환하는 경우에는 좋아요 목록에 해당 가게를 추가해야 하므로 새 FavoriteStore 객체를 만들어 준 후에 이를 추가해주면 된다.

 

협약 맺기

  해당 기능은 검색을 해봐도 비슷한 내용이 잘 없어서 순전히 내 생각만으로 만들어 본 코드이다. 만들어놓고 보니 친구 추가 기능과 이후에도 활용이 가능할 것 같다.

fig 3-1

  가게를 선택해서 들어가면 해당 가게가 수신/발신한 요청과 처리된 요청을 확인할 수 있다.

협약 요청 시 자신의 가게 이름/id 가져오기

/**
 * 협약하기 버튼 누를 시 누른 사람의 가게 id/이름 가져오기
 */
@GetMapping("/store-list")
public ResponseEntity<?> getStoreNameList(Principal principal) {

    Long fromOwnerId = Long.parseLong(principal.getName());

    return storeService.getStoreNameList(fromOwnerId);
}

  해당 api는 협약 요청을 보내거나, fig 3-1의 첫번째 페이지에 들어왔을 때 필요한 가게 이름을 가져오는 api이다. 선택한 가게 객체를 가져오기 위해 id도 같이 보내줘야 한다.

public ResponseEntity<?> getStoreNameList(Long ownerId) {

    Member owner = memberRepository.findById(ownerId).orElse(null);
    List<Store> Stores = owner.getStores();
    List<StoreNameDto> results = new ArrayList<>();

    for (Store store : Stores) {
        StoreNameDto storeNameDto = new StoreNameDto(store.getId(), store.getName());
        results.add(storeNameDto);
    }

    return response.success(results, "발신자 가게 정보", HttpStatus.OK);
}

  이해하기에 크게 어렵지 않은 코드이다. 해당하는 Member 객체를 가져와서 자신의 가게가 저장된 Stores 필드를 반복문에 넣어서 각 가게의 id와 이름을 StoreNameDto에 넣어준 뒤 이것을 반환 객체 results에 넣어주면 된다.

협약 요청

fig 3-2

  fig 3-2와 같은 입력 폼을 채운 뒤 '협약 등록' 버튼을 클릭했을 때 협약 요청 api가 매핑된다.

/**
 * 협약맺기 버튼 누르면 작성 내용 테이블에 반영
 */
@PostMapping("/request")
public ResponseEntity<?> requestPartnership(@RequestBody PartnershipRequestDto requestDto) {

    return partnershipService.requestPartnership(requestDto);
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class PartnershipRequestDto {
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy/MM/dd", timezone = "/Asia/Seoul")
    private LocalDate startDate;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy/MM/dd", timezone = "/Asia/Seoul")
    private LocalDate endDate;
    private Long fromStoreId;
    private Long toStoreId;
    private String content;
    private Long articleId;
}

  요청 시 위의 dto에 있는 정보들이 담겨서 보내진다. articleId는 제휴 요청이 제휴 게시판의 게시글을 통해 이뤄지기 때문이다. dto를 받아서 바로 서비스 메서드인 requestPartnership()에 넘겨준다.

/**
 * 제휴 요청 메소드
 */
@Transactional
public ResponseEntity<?> requestPartnership(PartnershipRequestDto requestDto) {
    /*1. dto를 통해 from/to 가게 모두 가져옴*/
    Store toStore = storeRepository.findById(requestDto.getToStoreId()).orElse(null);
    Store fromStore = storeRepository.findById(requestDto.getFromStoreId()).orElse(null);

    /*2-1. dto의 정보를 기반으로 새로운 제휴 객체 생성1(fromStore)*/
    Partnership newPartnership1 = Partnership.builder()
            .store(fromStore)
            .StoreName(fromStore.getName())
            .partnerId(toStore.getId())
            .partnerName(toStore.getName())
            .startDate(requestDto.getStartDate())
            .finishDate(requestDto.getEndDate())
            .info(requestDto.getContent())
            .partnershipState(PartnershipState.WAITING)
            .article(boardRepository.findById(requestDto.getArticleId()).orElse(null))
            .isFrom(true)
            .build();

    /*2-1. dto의 정보를 기반으로 새로운 제휴 객체 생성1(toStore)*/
    Partnership newPartnership2 = Partnership.builder()
            .store(toStore)
            .StoreName(toStore.getName())
            .partnerId(fromStore.getId())
            .partnerName(fromStore.getName())
            .startDate(requestDto.getStartDate())
            .finishDate(requestDto.getEndDate())
            .info(requestDto.getContent())
            .partnershipState(PartnershipState.WAITING)
            .article(boardRepository.findById(requestDto.getArticleId()).orElse(null))
            .isFrom(false)
            .build();

    /*3. 각 가게의 제휴 리스트에 새로 만든 제휴 객체 추가*/
    toStore.getPartnershipList().add(newPartnership1);
    fromStore.getPartnershipList().add(newPartnership2);

    /*4. 각 객체의 변경사항을 db에 반영*/
    partnershipRepository.save(newPartnership1);
    partnershipRepository.save(newPartnership2);

    newPartnership1.setCounterpartId(newPartnership2.getPartnershipId());
    newPartnership2.setCounterpartId(newPartnership1.getPartnershipId());

    return response.success(null, "새로운 협약 요청", HttpStatus.OK);
}

  가장 많은 고민을 했던 부분이다. 처음에는 하나의 협약에 두개의 가게가 관련되어 있으므로, Partnership DB 테이블의 하나의 row에 두 가게를 모두 기록하려고 하였다. 그런데 이렇게 해버리면 두 가게 중 어느쪽을 외래키로 설정하기도 애매하고 Partnership 엔티티 입장에서는 Store 필드가 2개가 되므로 정상적인 DB 관계가 이뤄지지 않았다.

  그래서 생각을 바꿔서 하나의 협약이 생성되면 Partnership DB 테이블에는 각 가게에 해당하는 2개의 row를 만들기로 결정하였다. 즉 A, B 가게가 협약을 맺으면 한 row는 A의 id가 외래키로 잡히고 B 가게의 id는 long 타입 column에 저장하고, 다른 row는 B의 id가 외래키로 잡히고 A 가게의 id는 long 타입 column에 저장되는 식으로 말이다. 또한 서로의 협약 id도 저장해야 했다.

  그렇기에 코드 자체는 Partnership 객체를 2개 만들고, 이를 저장하는 간단한 흐름이다.

받은(보낸/완료된) 협약 요청

/**
 * 받은 협약의 id와 발신 가게 받아오기
 */
@GetMapping("/received")
public ResponseEntity<?> getReceivedRequestList(@RequestParam Long storeId) {

    return partnershipService.getReceivedRequestList(storeId);
}

  협약은 member가 아닌 store 간에 이뤄지므로 member 정보는 굳이 필요가 없다. 해당 가게의 주인인지 판단하는 과정도 추후에 추가해야 할 것 같다.

/**
 * 요청 받은 제휴 정보만 가져오기
 */
@Transactional
public ResponseEntity<?> getReceivedRequestList(Long storeId) {
    /*1. 해당 api를 요청한 가게 객체를 가져온 뒤 해당 객체의 제휴 리스트를 가져온다.*/
    Store store = storeRepository.findById(storeId).orElse(null);
    List<Partnership> partnershipList = store.getPartnershipList();
    List<SimplePartnershipDto> results = new ArrayList<>(); //여러개의 response dto가 담길 변수
    log.info("제휴 리스트: {}", partnershipList.isEmpty());

    /*2. 각 제휴 정보들을 확인하면서 조건에 부합하는 제휴정보를 dto로 만들어 준 뒤 results에 추가해준다. */
    for (Partnership partnership : partnershipList) {
        log.info("제휴 객체 id: {}", partnership.getPartnershipId());
        /*2-1. 제휴 정보를 확인해보고 해당 api 요청을 보낸 가게가 toStore이고 제휴 상태가 WAITING일 때 */
        if (!partnership.isFrom() && partnership.getPartnershipState() == PartnershipState.WAITING) {
            Store findStore = storeRepository.findById(partnership.getPartnerId()).orElse(null);
            SimplePartnershipDto receivedRequest = new SimplePartnershipDto(partnership.getPartnershipId(),
                    findStore.getName(), partnership.getPartnershipState(), partnership.isRead());

            results.add(receivedRequest);
        }
    }

    return response.success(results, "내가 받은 요청 리스트", HttpStatus.OK);
}
@Getter
@AllArgsConstructor
public class SimplePartnershipDto {

    private Long partnershipId;
    private String StoreName;
    private PartnershipState partnershipState;
    private boolean isRead;

    public SimplePartnershipDto(Long partnershipId, String fromStoreName) {
        this.partnershipId = partnershipId;
        this.StoreName = fromStoreName;
    }

    public SimplePartnershipDto(Long partnershipId, String fromStoreName, PartnershipState partnershipState) {
        this.partnershipId = partnershipId;
        this.StoreName = fromStoreName;
        this.partnershipState = partnershipState;
    }
}

  1.에서 api를 요청한 가게 객체를 가져온 뒤 해당 객체의 Partnership 리스트를 가져온다. 늘 하던대로 dto에 담아주고 이를 반환 객체에 추가해서 반환해주면 된다.

  다만 2.를 보면 알겠지만 몇가지 과정이 필요하다. 해당 서비스 코드는 해당 가게에게 온 아직 처리되지 않은 수신 요청만을 가져와야 하기 때문이다. 그렇기 때문에 partnership의 isFromfalse이고 PartnershipState가 WAITING인 협약 정보만을 가져와야한다. 

  보낸 요청, 완료된 요청을 가져오는 것도 이와 거의 동일하니 넘어가도록 하자.

요청 상세 확인

fig 3-3

@GetMapping("/info")
public ResponseEntity<?> getPartnershipInfo(@RequestParam Long storeId, @RequestParam Long partnershipId) {

    return partnershipService.getPartnershipInfo(storeId, partnershipId);
}
/**
 * 선택한 제휴의 상세 정보 가져오기
 */
@Transactional
public ResponseEntity<?> getPartnershipInfo(Long storeId, Long partnershipId) {

    /*1. 선택한 제휴 객체를 가져온다.*/
    Partnership partnership = partnershipRepository.findById(partnershipId).orElse(null);
    Store fromStore = storeRepository.findById(storeId).orElse(null);
    Store toStore = storeRepository.findById(partnership.getPartnerId()).orElse(null);

    /*2. 해당 객체를 detailPartnershipDto로 만들어준다.*/
    List<String> storeNames = new ArrayList<>();
    storeNames.add(fromStore.getName());
    storeNames.add(toStore.getName());
    DetailPartnershipDto detailPartnershipDto = new DetailPartnershipDto(partnershipId, partnership.getStartDate(),
            partnership.getFinishDate(), storeNames, partnership.getInfo(), partnership.getPartnershipState());

    /*3. 자신이 보낸 요청을 자신이 조회할 때에는 페이지가 다르게 렌더링 되어야 하므로 이를 구분해준다*/
    if (partnership.isFrom()) { // 자신이 보낸 요청인 경우
        detailPartnershipDto.setSentByMe(true);
    }else{
        detailPartnershipDto.setSentByMe(false);
        /*3-1. 만약 받은 요청을 클릭 했을 때 해당 요청을 처음 클릭하는 경우에는 isRead를 false로 변경하여 읽음 처리*/
        if(partnership.isRead() == false)
            partnership.changeReadStatus();
    }

    /*4. DB에 변경사항 반영*/
    partnershipRepository.save(partnership);

    return response.success(detailPartnershipDto, "제휴 요청 정보", HttpStatus.OK);
}
@Getter
@AllArgsConstructor
public class DetailPartnershipDto {

    private Long partnershipId;
    private LocalDate startDate;
    private LocalDate finishDate;
    private List<String> storeNames;
    private String content;
    private PartnershipState partnershipState;
    private boolean sentByMe;

    public DetailPartnershipDto(Long partnershipId, LocalDate startDate, LocalDate finishDate, List<String> storeNames, String content, PartnershipState partnershipState) {
        this.partnershipId = partnershipId;
        this.startDate = startDate;
        this.finishDate = finishDate;
        this.storeNames = storeNames;
        this.content = content;
        this.partnershipState = partnershipState;
    }

    public void setSentByMe(boolean sentByMe) {
        this.sentByMe = sentByMe;
    }
}

  해당 api에서는 협약 객체의 대부분의 필드 정보들을 반환해야 한다. 여기서 신경써줘야 할 것은 isFrom의 여부에 따라 프론트 단에서 fig 3-3처럼 페이지를 다르게 렌더링해야 하므로 이를 파악할 수 있는 sentByMe를 설정해줘야 한다.

협약 승인/거절

@PostMapping("/approve")
public ResponseEntity<?> approvePartnership(@RequestParam Long storeId, @RequestParam Long partnershipId) {

    return partnershipService.approvePartnership(storeId, partnershipId);
}

  승인/거절 모두 동일한 과정이므로 승인 코드만 살펴보자.

/**
 * 제휴 요청을 승인했을 때
 */
@Transactional
public ResponseEntity<?> approvePartnership(Long storeId, Long partnershipId) {
    Partnership partnership = partnershipRepository.findById(partnershipId).orElse(null);
    Partnership counterpart = partnershipRepository.findById(partnership.getCounterpartId()).orElse(null);

    partnership.changePartnershipState(PartnershipState.APPROVED);
    counterpart.changePartnershipState(PartnershipState.APPROVED);

    partnership.getArticle().setCompleted(true);

    partnershipRepository.save(partnership);
    partnershipRepository.save(counterpart);

    return response.success();
}

  앞서 말했듯이 하나의 협약에 대해 두 개의 row가 Partnership 테이블에 만들어지기 때문에 두 rows를 모두 갱신해줄 필요가 있다. 여기서는 PartnershipStateAPPROVED로 바꿔주고, 해당 협약 요청이 이뤄진 게시글의 isCompleted를 true로 바꿔준다. 그렇게 해야 협약 완료된 게시글을 다르게 표현할 수 있기 때문이다.

댓글