본문 바로가기
Spring Boot Project/Plming

[Plming] 신청 기능 추가하기

by slchoi 2022. 4. 4.
728x90
SMALL

게시글의 신청 버튼을 추가해 사용자가 참여하고 싶은 모임에 참여할 수 있도록 기능을 추가하려고 한다.

1. DB 테이블과 Entity 생성

우선 신청 정보를 담을 DB를 생성해보자.

CREATE TABLE `application` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '사용자ID',
  `post_id` bigint(20) NOT NULL COMMENT '게시글ID',
  `status` enum("승인", "거절", "대기") NOT NULL DEFAULT "대기" COMMENT '지원상태',
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  KEY `post_id` (`post_id`),
  CONSTRAINT `application_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
  CONSTRAINT `application_ibfk_2` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • user_id, post_id: 사용자의 id와 사용자가 신청하고자 하는 게시글 id가 저장된다.
  • status: 사용자가 게시글을 신청하면 '대기' 상태가 되고, 작성자가 참여를 허락하면 "승인", 참여를 거절하면 "거절" 상태가 된다.

테이블을 생성한 뒤에 application 테이블의 정보를 확인해보자.

application 테이블

 

이제 엔티티를 생성해볼 것이다. "plming.board.entity" 패키지에 Application 클래스를 생성하고, 아래 코드를 작성한다.

package plming.board.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import plming.user.entity.User;

import javax.persistence.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "application")
public class Application {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "post_id")
    private Board board;

    @Column(columnDefinition = "enum")
    private String status = "대기";

    @Builder
    public Application(User user, Board board, String status) {
        this.user = user;
        this.board = board;
        this.status = status;
    }
}

같은 패키지 내에 ApplicationRepository 클래스를 생성하고 아래 코드를 작성한다.

package plming.board.entity;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ApplicationRepository extends JpaRepository<Application, Long> {

}

 

신청 기능을 구현하면 POST /posts/{id}/application 요청이 들어오면 boardService의 apply 메서드가 실행되고, 신청이 잘 됐을 경우 "ok"를 반환하도록 구현할 것이다. 아래 코드를 BoardApiController 클래스에 추가하자.

/**
 * 게시글 신청
 */
@PostMapping("/{id}/application")
public String apply(@PathVariable final id, @RequestParam final Long userId) {

    return boardService.apply(boardId, userId);
}

BoardService 클래스에 아래 메서드를 추가한다.

/**
 * 게시글 신청 하기
 */
@Transactional
public String apply(final Long boardId, final Long userId) {
	
    // 신청 기능 구현
    
    return "ok";
}
  • 이 메서드 안에 신청 기능을 구현할 것이다.

 

2. 신청 기능 구현

2.1. 게시글 신청하기

이제 신청 기능을 구현해볼 것이다. 가장 먼저 "plming.board.model" 패키지 안에 ApplicationService 클래스를 생성한 뒤 더보기 코드를 작성한다.

더보기
package plming.board.model;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import plming.board.entity.Application;
import plming.board.entity.ApplicationRepository;
import plming.board.entity.BoardRepository;
import plming.user.entity.UserRepository;

@Service
@RequiredArgsConstructor
public class ApplicationService {

    private final BoardRepository boardRepository;
    private final UserRepository userRepository;
    private final ApplicationRepository applicationRepository;

    @Transactional
    public Long save(final Long boardId, final Long userId) {
        Application application = Application.builder()
                .board(boardRepository.getById(boardId))
                .user(userRepository.getById(userId))
                .status("대기")
                .build();
        applicationRepository.save(application);

        return application.getId();
    }
}

 

  • boardId와 userId가 매개변수로 주어지면 boardRepository와 userRepository의 getById 메서드를 사용해 게시글과 사용자 정보를 가져오고, 가져온 정보를 사용해 application 객체를 생성한다.
  • 생성한 application 객체를 applicationRepository 인터페이스의 save 메서드를 사용해 DB에 저장한다.
  • application 객체 저장이 끝나면 신청한 게시글 id를 반환한다.

 

save( ) 메서드가 잘 작동하는지 테스트해볼 것이다. "src.test.java.plming.board" 패키지에 ApplicationServiceTest 클래스를 생성하고 더보기 코드를 작성한다.

더보기
package plming.board;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Sort;
import plming.board.dto.BoardResponseDto;
import plming.board.entity.Application;
import plming.board.entity.ApplicationRepository;
import plming.board.entity.Board;
import plming.board.entity.BoardRepository;
import plming.board.model.ApplicationService;
import plming.board.model.BoardService;
import plming.user.entity.User;
import plming.user.entity.UserRepository;

import java.util.List;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.data.domain.Sort.Direction.DESC;

@SpringBootTest
public class ApplicationServiceTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ApplicationRepository applicationRepository;

    @Autowired
    private BoardRepository boardRepository;

    @Autowired
    private BoardService boardService;

    @Autowired
    private ApplicationService applicationService;

    private User user1;
    private User user2;
    private Board post1;
    private Board post2;

    @BeforeEach
    void beforeEach() {
        user1 = User.builder().email("email@email.com").github("github")
                .image("no image").introduce("introduce").nickname("nickname")
                .password("password").role("ROLE_USER").social(1)
                .build();
        user2 = User.builder().email("email2@email.com").github("github2")
                .image("no image").introduce("introduce2").nickname("nickname2")
                .password("password2").role("ROLE_ADMIN").social(1)
                .build();
        post1 = Board.builder().user(user1).content("사용자1의 첫 번째 게시글입니다.")
                .period("1개월").category("스터디").status("모집 중").title("사용자1의 게시글1")
                .build();
        post2 = Board.builder().user(user2).content("사용자2의 첫 번째 게시글입니다.")
                .period("1개월").category("프로젝트").status("모집 중").title("사용자2의 게시글 1")
                .build();

        userRepository.save(user1);
        userRepository.save(user2);
        boardRepository.save(post1);
        boardRepository.save(post2);

    }

    @AfterEach
    void afterEach() {
        applicationRepository.deleteAll();
        boardRepository.deleteAll();
        userRepository.deleteAll();
    }

    @Test
    @DisplayName("게시글 신청")
    void save() {

        // when
        Long board1Id = applicationService.save(post1.getId(), user2.getId());
        Long board2Id = applicationService.save(post2.getId(), user1.getId());

        // then
        assertEquals(post1.getId(), board1Id);
        assertEquals(post2.getId(), board2Id);
    }
}

테스트를 실행해보면 성공한 것을 확인할 수 있다.

save( ) 테스트

코드를 살펴보면

  • applicationService의 save 메서드의 인자로 post1의 id, user2의 id 값을 넣어준 뒤 저장하고, 신청한 게시글의 id가 반환되면 board1Id에 저장한다. (user2 사용자가 post1 게시글에 참여 신청하는 경우)
  • applicationService의 save 메서드의 인자로 post2의 id, user1의 id 값을 넣어준 뒤 저장하고, 신청한 게시글의 id가 반환되면 board2Id에 저장한다. (user1 사용자가 post2 게시글에 참여 신청하는 경우)
  • board1Id의 값은 post1 id의 값과 일치하고, board2Id의 값은 post2 id의 값과 일치하면 테스트에 성공한다.

 

테스트에 성공했으니 boardService 클래스에 생성해둔 apply( ) 메서드를 아래 코드로 수정한다.

/**
 * 게시글 신청 하기
 */
@Transactional
public Long apply(final Long boardId, final Long userId) {

    return applicationService.save(boardId, userId);
}

 

BoardApplication을 실행하고 Postman을 사용해 테스트해보자. 55번 게시글에 15번 사용자가 신청하기 버튼을 클릭했다고 가정하고 "/posts/55/application?userId=15" URI로 Post 요청해볼 것이다.

save( ) 메서드 테스트

응답으로 사용자가 신청한 게시글 id를 반환하는 것을 확인할 수 있다.

DB에 들어가서 application 테이블을 조회해보면

application 테이블

데이터가 잘 들어가 있는 것을 확인할 수 있다.

 

2.2. 신청한 게시글 리스트 조회하기

이 부분은 테스트 코드를 먼저 짜보자. (그냥 TDD를 적용해보고 싶었다..)

ApplicationServiceTest에 아래 메서드를 추가한다.

@Test
void findByApplicationByUserId() {

    // when
    List<BoardResponseDto> appliedBoards = boardService.findAppliedBoardByUserId(user1.getId());

    // then
    assertEquals(1, appliedBoards.size());
}
  • boardService에 findAppliedBoardByUserId( ) 메서드를 추가해 userId를 매개변수로 넣어주면 해당 사용자가 신청한 게시글 정보를 담은 리스트를 반환해주는 것이 목표이다.

이제 신청한 게시글 리스트를 반환하는 코드를 구현해보자. 먼저, BoardService 클래스에 아래 메서드를 추가한다.

/**
 * 신청 게시글 리스트 조회 - (사용자 ID 기준)
 */
public List<BoardResponseDto> findAppliedBoardByUserId(final Long userId) {

    List<Board> appliedBoards = applicationService.findByAppliedBoardByUserId(userId);

    return getTagName(appliedBoards);
}
  • ApplicationService의 findByAppliedBoardByUserId 메서드를 호출해 사용자가 신청한 게시글 객체 리스트를 받아올 것이다.
  • 받아온 객체 리스트를 getTagName( ) 메서드에 넣어 BoardResponseDTO 객체 리스트를 생성해 반환할 것이다.

 

이제 ApplicationService 클래스에서 해당 기능을 구현해보자. 이 클래스에 아래 메서드를 추가한다.

public List<Board> findByAppliedBoardByUserId(final Long userId) {

    List<Application> applications = applicationRepository.findAllByUserId(userId);
    List<Board> appliedBoards = applications.stream().map(Application::getBoard).collect(Collectors.toList());

    return appliedBoards;
}

 

이제 기능 구현이 끝났으니 가장 먼저 생성해둔 테스트 메서드를 실행시켜 볼 것이다.

findAppliedBoardByUserId 테스트 성공

 

마지막으로 API와 연동하기 위해 BoardApiController 클래스에 아래 메서드를 추가한다.

/**
 * 신청 게시글 리스트 조회 - 사용자 ID 기준
 */
@GetMapping("/application")
public List<BoardResponseDto> findAppliedBoardByUserID(@RequestParam final Long userId) {

    return boardService.findAppliedBoardByUserId(userId);
}

 

Postman을 사용해 api를 테스트해볼 것이다. 테스트를 실행하면서 DB를 초기화시켜 현재 DB에는 사용자와 게시글이 아무것도 없으니 사용자는 DB에서, 게시글은 Postman에서 추가해보자.

사용자 정보 추가
게시글 추가
게시글 추가

 

사용자와 게시글을 추가했으니 이제 신청 API를 테스트해보자. 205번 사용자가 242번, 243번 게시글을 신청하고, 207번 사용자가 243번 게시글을 신청한다고 가정하고 테스트를 진행할 것이다.

205번 사용자가 242번 게시글을 신청
205번 사용자가 243번 게시글 신청
207번 사용자가 243번 게시글을 신청
DB 확인

사용자가 신청한 게시글 id가 응답으로 반환되고, DB에도 데이터가 잘 들어가 있는 것을 확인할 수 있다.

다음으로 205번 사용자가 신청한 게시글 정보를 요청해보자. GET /posts/application?userId=205로 요청을 보내보면 242번, 243번 게시글의 정보가 응답으로 반환되는 것을 확인할 수 있다.

findAppliedBoardByUserId API 테스트

 

2.3. 게시글 신청 사용자 리스트 조회하기

사용자가 본인의 게시글 상세 보기 페이지에 들어갈 경우 해당 게시글에 참여 신청한 사용자의 리스트도 함께 보여줄 예정이므로 각 게시판의 신청 사용자 리스트를 조회하는 기능을 구현할 것이다.

먼저 ApplicationServiceTest 클래스에 아래 테스트 메서드를 추가한다.

@Test
@DisplayName("게시글 신청한 사용자 리스트 조회")
void findAppliedUserIdByBoardId() {

    // given
    boardService.apply(post1.getId(), user2.getId());
    boardService.apply(post2.getId(), user1.getId());

    // when
    List<UserResponseDto> appliedUsers = boardService.findAppliedUserByBoardId(post1.getId());

    // then
    assertEquals(user2.getNickname(), appliedUsers.get(0).getNickname());
}
  • user2는 post1에 참여 신청을 하고, user1은 post1에 참여 신청한다.
  • boardService의 findAppliedUserByBoardId의 메서드를 호출해 매개변수로 게시글 id를 전달하고 해당 게시글에 참여 신청한 사용자 리스트를 받아온다.
  • 받아온 사용자 정보와 user2의 정보가 같으면 테스트는 성공한다.

 

이제, BoardService 클래스에 findAppliedUserByBoardId( ) 메서드를 생성한다.

/**
 * 신청 사용자 리스트 조회 - (게시글 ID 기준)
 */
public List<UserResponseDto> findAppliedUserByBoardId(final Long boardId) {

    List<User> appliedUsers = applicationService.findAppliedUserByBoardId(boardId);

    return appliedUsers.stream().map(User::getId).map(userService::getUser).collect(Collectors.toList());
}
  • ApplicationService의 findAppliedUserByBoardId의 메서드를 호출해 게시글 id를 매개변수로 넘겨주고 해당 게시글의 신청자 정보 리스트를 받아온다.
  • 받아온 신청자 정보 리스트를 UserResponsdeDto 객체로 생성해 리스트 화한 뒤 반환한다.

다음으로, ApplicationRepository 클래스에 아래 메서드를 추가한다.

/**
 * 신청 사용자 리스트 조회 - (게시글 ID 기준)
 */
List<Application> findAllByBoardId(final Long boardId);

마지막으로, ApplicationService의 findAppliedUserByBoardId( ) 메서드를 생성한다.

/**
 * 게시글 신청자 리스트 조회
 */
public List<User> findAppliedUserByBoardId(final Long boardId) {

    List<Application> applications = applicationRepository.findAllByBoardId(boardId);
    List<User> appliedUsers = applications.stream().map(Application::getUser).collect(Collectors.toList());

    return appliedUsers;
}
  • ApplicationRepository의 findAllByBoardId 메서드를 호출해 모든 게시글 id와 일치하는 application 정보를 가져온다.
  • application 정보 중에서 사용자와 관련된 정보를 가져와 리스트화한 뒤 반환한다.

 

모든 코드를 구현했으므로 가장 처음 구현한 테스트 메서드를 실행해볼 것이다.

findAppliedUserByBoardId

테스트 메서드가 오류 없이 잘 실행되는 것을 확인할 수 있다.

 

이제 API와 연동해볼 것이다. BoardApiController 클래스에 아래 메서드를 추가한다.

/**
 * 신청 사용자 리스트 조회 - 게시글 ID 기준
 */
@GetMapping("/{id}/application")
public List<UserResponseDto> findAppliedUserByBoardId(@PathVariable final Long id) {

    return boardService.findAppliedUserByBoardId(id);
}

API 연동이 완료되었으면, Postman을 사용해 테스트해볼 것이다. 

먼저, user2가 user1이 작성한 두 개의 게시글에 참여 신청을 하고, user1은 user2가 작성한 한 개의 게시글에 참여 신청하도록 요청할 것이다.

user1이 user2의 게시글에 참여 신청
user2가 user1의 게시글에 참여 신청
user2가 user1의 게시글에 참여 신청

신청 요청이 들어가면 응답으로 신청한 게시글의 id를 반환해주는 것을 확인할 수 있다.

application 테이블

MySQL Workbench에서 application 테이블을 확인해보면 정보가 잘 들어가 있는 것을 확인할 수 있다. 

이제 297번 게시글을 신청한 사용자 정보 리스트를 확인해볼 것이다. GET "/297/application" 요청을 보내면

findAppliedUserByBoardId 메서드 API 테스트

297번 게시글을 신청한 사용자의 정보 리스트가 잘 응답으로 전달되는 것을 확인할 수 있다.

 

2.4. 신청 상태 업데이트 기능 구현

게시글 작성자가 게시글에 참여를 신청한 사용자를 확인해보고 수락 혹은 거절할 수 있다. 이때 application에서 참여를 수락한 사용자의 상태는 "수락"으로, 참여를 거절한 사용자의 상태는 "거절"로 값을 변경해줘야 한다.

이번에도 먼저 테스트 코드를 작성해볼 것이다. ApplicationServiceTest 클래스에 아래 테스트 메서드를 추가한다.

@Test
@DisplayName("지원 상태 업데이트")
void update() {
    // given
    boardService.apply(post1.getId(), user2.getId());
    boardService.apply(post2.getId(), user1.getId());

    // when
    String status1 = boardService.updateAppliedStatus(post1.getId(), user2.getId(), "승인");
    String status2 = boardService.updateAppliedStatus(post2.getId(), user1.getId(), "거절");

    // then
    assertEquals("승인", status1);
    assertEquals("거절", status2);
}
  • user2는 post1에, user1은 post2에 참여를 신청한다.
  • post1을 작성한 사용자가 user2의 참여 요청을 승인하고, post2을 작성한 사용자가 user1의 참여 요청을 거절한다고 가정하고 테스트해볼 것이다. 
    • BoardService 클래스의 updateAppliedStatus 메서드를 호출해 post1의 id, user2의 id, status(승인)을 매개변수로 전달한다.
    • BoardService 클래스의 updateAppliedStatus 메서드를 호출해 post1의 id, user2의 id, status(거절)을 매개변수로 전달한다.
    • BoardService 클래스의 updateAppliedStatus는 업데이트된 신청 상태를 String으로 반환한다.
  • 반환받은 신청 상태를 각각 "승인", "거절"과 비교해 일치하면 테스트에 성공한다.

 

이제 BoardService 클래스에 아래 메서드를 추가한다.

/**
 * 게시글 신청 정보 업데이트
 */
public String updateAppliedStatus(final Long boardId, final Long userId, final String status) {

    Application application = applicationService.updateStatus(boardId, userId, status);

    return application.getStatus();
}
  • ApplicationService 클래스의 updateStatus 메서드를 통해 신청 상태를 업데이트한다.
    • updateStatus는 신청 상태가 업데이트된 Application 객체를 반환한다.
  • 받아온 Application 객체의 신청 상태를 반환한다.

 

마지막으로 ApplicationService 클래스에 updateStatus 메서드를 추가한다.

/**
 * 게시글 신청 상태 업데이트
 */
@Transactional
public Application updateStatus(final Long boardId, final Long userId, final String status) {

    List<Application> boardApplications = applicationRepository.findAllByBoardId(boardId);
    List<Application> applicationList = boardApplications.stream().filter(app -> app.getUser().getId().equals(userId)).collect(Collectors.toList());
    applicationList.get(0).update(status);

    return applicationList.get(0);
}
  • ApplicationRepository의 findAllByBoardId를 사용해 주어진 게시글 id에 대한 신청 정보를 담은 Application 객체를 리스트화해 받아온다.
  • 받아온 Application 객체 중 User 정보를 빼와 사용자 id와 게시글 참여 신청자의 id 값을 비교하다 두 값이 서로 같은 경우 신청 상태를 update 한다.
  • 신청 상태를 업데이트한 Application 객체를 반환한다.

 

이제 모두 구현했으므로, 처음에 작성한 테스트 메서드를 실행시켜본다.

status update 테스트

테스트가 오류 없이 잘 실행되는 것을 확인할 수 있다. 테스트가 잘 실행되는 것을 확인했으니 API와 연동시켜볼 것이다.

BoardApiController에 아래 메서드를 추가한다.

/**
 * 게시글 신청 상태 업데이트
 */
@PatchMapping("/{id}/application") 
public String updateAppliedStatus(@PathVariable final Long id, @RequestParam final Long userId, @RequestParam final String status) {

    return boardService.updateAppliedStatus(id, userId, status);
}

 

API 연동까지 완료되었으니, 이제 Postman을 사용해서 게시글 신청 상태 업데이트 API를 테스트해볼 것이다. 

먼저 현재 게시글 신청 상태는 아래와 같다.

application

291번 사용자가 신청한 게시글은 "거절"로, 292번 사용자가 신청한 게시글 중 335번 게시글 신청 상태를 "승인"으로 변경해보자.

291번 사용자 신청 상태 변경
292번 사용자 신청 상태 변경

 

요청에 대한 응답으로 변경된 신청 상태를 반환하는 것을 확인할 수 있다.

 

application 테이블

MySQL에서 application 테이블을 조회해보면 status 값이 잘 변경되어 있는 것을 확인할 수 있다.

728x90
LIST

댓글