15. Submitting a vote

Hyeseong·2023년 3월 6일
0

실시간설문조사앱

목록 보기
5/6

🏛 Architecture


1️⃣ 들어가기 앞서

지난 시간 투표시 후보 안건을 생성 및 삭제하는 기능을 만들었습니다.

이번에는 start_poll 이벤트를 만들겠습니다.
또한 각 참가자가 순위를 제출하는 순서대로 전송할 submit_rankings 이벤트를 추가하겠습니다.


2️⃣ Poll Type 추가

우선 기능 추가에 앞서서 필요한 타입을 추가하겠습니다. poll-types모듈에 추가 명세 및 업데이트 하도록 하겠습니다.

type NominationID = string;

export type Nominations = {
  [nominationID: NominationID]: Nomination;
}

export type Rankings = {
  [userID: string]: NominationID[];
};

export type Poll = {
  id: string;
  topic: string;
  votesPerVoter: number;
  participants: Participants;
  nominations: Nominations;
  rankings: Rankings;
  // results: Results;
  adminID: string;
  hasStarted: boolean;
}

Poll type 변경에 따라 PollsRepository의 변경 되어야 할 사항이 있습니다.

  • createPoll 메서드의 intialPoll 객체에 rankings key와 value는 empty object로 할당해줍니다.
    const initialPoll = {
      id: pollID,
      topic,
      votesPerVoter,
      participants: {},
      nominations: {},
      rankings: {}, // add this
      adminID: userID,
      hasStarted: false,
    };

3️⃣ Poll Start 기능 추가

관리자만 사용 가능한 start_poll 기능을 추가하겠습니다.

🔥 PollsRepository

  /**
   *
   * Starts a poll by setting the hasStarted property to true in Redis.
   * @param {string} pollID - The ID of the poll to start.
   * @returns {Promise<Poll>} - A Promise that resolves with the updated Poll object.
   * @throws {InternalServerErrorException} - If there was an error starting the poll.
   */
  async startPoll(pollID) {
    this.logger.log(`setting hasStarted for poll: ${pollID}`);

    const key = `polls:${pollID}`;

    try {
      await this.redisClient.send_command(
        'JSON.SET',
        key,
        '.hasStarted',
        JSON.stringify(true),
      );

      return this.getPoll(pollID);
    } catch (error) {
      const errorMessage = `Failed set hasStarted for poll: ${pollID}`;
      this.logger.error(errorMessage, error);
      throw new InternalServerErrorException(errorMessage);
    }
  }

위 코드는 Redis에서 hasStarted 속성을 true로 설정하여 투표를 시작하는 비동기 메서드입니다. pollID를 매개변수로 받고, 해당 ID를 가진 투표에 대한 Redis 키를 생성합니다. 그 후 JSON.SET 명령을 사용하여 Redis에서 hasStarted 속성을 설정하고, 업데이트된 투표를 가져와 Promise로 반환합니다. 만약 에러가 발생하면, 에러 메시지를 로깅하고 InternalServerErrorException 예외를 던집니다.

⚡️ PollService

  async startPoll(pollID: string): Promise<Poll> {
    return this.pollsRepository.startPoll(pollID);
  }

매개변수로 주어진 pollID로 pollsRepository 객체의 startPoll 메서드를 호출하여 투표를 시작하는 비동기 메서드입니다. 메서드는 업데이트된 투표 객체를 반환하는 Promise를 리턴합니다.

🚪 PollsGateway - Admin Handler

해당 코드는 웹소켓 이벤트 'start_vote'를 처리하는 메서드로, GatewayAdminGuard 가드를 적용하여 관리자만 투표를 시작할 수 있도록 보호합니다. 이벤트 핸들러는 ConnectedSocket 데코레이터로 client 파라미터를 전달받으며, 연결된 클라이언트의 pollID를 기반으로 pollsService 객체의 startPoll 메서드를 호출하여 투표를 시작합니다. 투표가 성공적으로 시작되면 업데이트된 투표 객체를 반환하고, 해당 객체를 'poll_updated' 이벤트로 연결된 모든 클라이언트에게 emit하여 업데이트된 투표 정보를 전달합니다.

@UseGuards(GatewayAdminGuard)
@SubscribeMessage('start_vote')
async startVote(@ConnectedSocket() client: SocketWithAuth): Promise<void> {
this.logger.debug(`Attempting to start voting for poll: ${client.pollID}`);

const updatedPoll = await this.pollsService.startPoll(client.pollID);

this.io.to(client.pollID).emit('poll_updated', updatedPoll);
  }
  • 'start_vote' 이벤트를 처리하는 WebSocket 이벤트 핸들러입니다.
  • 해당 메서드는 @UseGuards 데코레이터를 적용하여 GatewayAdminGuard 가드를 적용하고, 관리자만이 투표를 시작할 수 있도록 보호합니다.
  • 또한 @SubscribeMessage 데코레이터로 'start_vote' 이벤트를 구독하도록 처리했습니다.
  • 연결된 클라이언트 소켓을 나타내는 SocketWithAuth 객체인 클라이언트 매개변수를 사용합니다.
  • 해당 메서드는 연결된 클라이언트 소켓과 관련된 투표의 시작을 알리는 디버그 로그를 기록합니다.
  • pollsService의 startPoll 메서드를 호출하여 클라이언트 소켓과 관련된 투표를 시작하고, 반환된 업데이트된 투표 객체를 updatedPoll 변수에 저장합니다.
  • 'poll_updated' 이벤트를 발생시켜 연결된 모든 클라이언트에게 updatedPoll 객체를 데이터 페이로드로 전달합니다. 이 이벤트는 클라이언트 소켓과 관련된 투표를 구독하는 모든 클라이언트에게 전송됩니다.

4️⃣ Submittin Rankings

이제 순위를 제출하는 기능에 대해 작업해 보겠습니다. 실제로 서버보다 클라이언트에서 조금 더 복잡합니다.

먼저, types.ts에서 리포지토리 메서드에 대한 페이로드 타입을 정의해야 합니다.

export type AddParticipantRankingsData = {
  pollID: string;
  userID: string;
  rankings: string[];
};

AddParticipantRankingsData 타입은 "pollID", "userID", "rankings"라는 세 가지 속성을 포함합니다.

  • "pollID"와 "userID"는 문자열 타입입니다.
  • "rankings"는 문자열 배열 타입입니다.
  • 투표에 참가자 랭킹을 추가하기 위한 데이터를 나타내는 객체의 형태를 정의하는 데 사용됩니다.

🔥 PollsRepository

  /**
   *
   * Starts a poll by setting the hasStarted property to true in Redis.
   * @param {string} pollID - The ID of the poll to start.
   * @returns {Promise<Poll>} - A Promise that resolves with the updated Poll object.
   * @throws {InternalServerErrorException} - If there was an error starting the poll.
   */
  async startPoll(pollID: string): Promise<Poll> {
    // logs a message to indicate that the hasStarted property is being set for the poll identified by pollID
    this.logger.log(`setting hasStarted for poll: ${pollID}`);
    // constructs the key for the poll identified by pollID in Redis
    const key = `polls:${pollID}`;

    try {
      // sets the hasStarted property for the poll identified by pollID
      await this.redisClient.send_command(
        'JSON.SET',
        key,
        '.hasStarted',
        JSON.stringify(true),
      );
      // retrieves the updated poll from Redis and returns it as a Promise
      return this.getPoll(pollID);
    } catch (error) {
      // logs an error message indicating that there was an error setting the hasStarted property for the poll identified by pollID
      const errorMessage = `Failed set hasStarted for poll: ${pollID}`;
      this.logger.error(errorMessage, error);
      // throws an `InternalServerErrorException` with a message indicating that there was an error starting the poll
      throw new InternalServerErrorException(errorMessage);
    }
  }

위 코드는 JSDoc 형태로 작성된 함수 addParticipantRankings를 구현한 코드입니다. 이 함수는 AddParticipantRankingsData 타입을 인자로 받아서 Redis에 저장된 투표 데이터에 참가자의 랭킹을 추가하는 기능을 수행합니다. 함수의 반환값은 업데이트된 투표 객체(Poll)입니다. 만약 랭킹을 추가하는 과정에서 오류가 발생하면 InternalServerErrorException이 발생합니다.

⚡️ PollService

type

export type SubmitRankingsFields = {
  pollID: string;
  userID: string;
  rankings: string[];
};

순위를 제출할 수 있도록 투표가 시작되었는지 확인하기를 원하기 때문에 서비스 방법에는 실제로 약간의 애플리케이션 로직이 있습니다. 아마도 이것은 클라이언트 응용 프로그램이 이 작업을 수행하지 않기 때문에 과도한 작업일 수 있습니다. 하지만 어쨌든 할거야!

types.ts의 리포지토리에 대해 수행한 것처럼 메서드에 대한 페이로드 유형을 추가해야 합니다.

  async submitRankings(rankingsData: SubmitRankingsFields): Promise<Poll> {
    const hasPollStarted = this.pollsRepository.getPoll(rankingsData.pollID);

    if (!hasPollStarted) {
      throw new BadRequestException(
        'Participants cannot rank until the poll has started.',
      );
    }

    return this.pollsRepository.addParticipantRankings(rankingsData);
  }

이 코드는 참가자들이 투표의 순위를 제출하는 기능을 수행합니다. 먼저, 제출하려는 투표가 이미 시작되었는지 확인하고, 시작되지 않았다면 예외를 발생시킵니다. 그리고 참가자가 제출한 순위를 추가하여 업데이트된 투표 객체를 반환합니다.

🚪 PollsGateway

  /**
   * Submits participant rankings for a poll.
   * @async
   * @function submitRankings
   * @param {SocketWithAuth} client - The connected client socket with authorization.
   * @param {string[]} rankings - The rankings being submitted by the participant.
   * @returns {Promise<void>} - A Promise that resolves once the rankings have been submitted.
   * @throws {BadRequestException} - If the poll has not started yet and participant rankings cannot be submitted.
   */
  @SubscribeMessage('submit_rankings')
  async submitRankings(
    @ConnectedSocket() client: SocketWithAuth,
    @MessageBody('rankings') rankings: string[],
  ): Promise<void> {
    // Logs the submission of rankings by the participant.
    this.logger.debug(
      `Submitting votes for user: ${client.userID} belonging to pollID: "${client.pollID}"`,
    );

    // Submits the participant's rankings to the poll.
    const updatedPoll = await this.pollsService.submitRankings({
      pollID: client.pollID,
      userID: client.userID,
      rankings,
    });

    // Sends the updated poll object to all clients in the poll.
    this.io.to(client.pollID).emit('poll_updated', updatedPoll);
  }
  • @SubscribeMessage('submit_rankings'): 클라이언트로부터 submit_rankings 메시지를 구독합니다.
  • @ConnectedSocket() client: SocketWithAuth: 인증된 클라이언트 소켓입니다.
  • @MessageBody('rankings') rankings: string[]: 클라이언트에서 전송된 순위 배열입니다.
  • @async: async 함수입니다.
  • @function submitRankings: submitRankings 함수입니다.
  • @param {SocketWithAuth} client: 연결된 인증된 클라이언트 소켓입니다.
  • @param {string[]} rankings: 참가자가 제출한 순위입니다.
  • @returns {Promise}: 순위가 제출되면 해결되는 Promise입니다.
  • @throws {BadRequestException}: 참가자 순위를 제출할 수 없는 경우 BadRequestException을 던집니다.
  • this.logger.debug(...): 참가자의 투표를 기록합니다.
  • const updatedPoll = await this.pollsService.submitRankings({ ... }): 참가자의 순위를 폴에 제출합니다.
  • this.io.to(client.pollID).emit('poll_updated', updatedPoll): 모든 클라이언트에게 업데이트된 폴 객체를 보냅니다.

🧪 POSTMAN으로 테스트하기

  1. POLL 생성 -> 유저 참가(1~4) -> 소켓 연결(1~4) -> 투표 안건 제출(1~4)
  2. submit_rankings 이벤트 날릴 경우, poll 객체의 hasStarted 키가 false일때
  3. start_vote 이벤트에 빈 객체를 메시지로 날릴 경우 상태가 변경됨.
  4. 이후 각 클라이언트마다 submit_rankings로 이벤트를 날릴 경우 정상적으로 처리 되는 것을 확인 할 수 있습니다.
profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글