14. Nomination 이벤트 추가

Hyeseong·2023년 3월 4일
0

실시간설문조사앱

목록 보기
4/6

🏛 Architecture

지난 시간

이전 Poll에서 participant를 제거하기 위해 승인된 socket.io 이벤트 및 핸들러를 만들었습니다.

오늘은 nomination(투표항목)를 추가하고 제거하기 위한 이벤트 및 핸들러를 만드는 작업을 할 것입니다. nomination은 poll에 대한 투표항목입니다. 예를 들어 설문조사의 "주제"가 "저녁 먹으러 어디로 가야 할까요?"라면 투표 항목은 "김밥천국" 또는 "Mexican Restaurant"일 수 있습니다.

이 모든 작업을 수행하려면 몇 가지 서비스 방법과 함께 투표에 투표항목을 추가하는 몇 가지 새로운 리포지토리 방법을 추가해야 합니다.

1️⃣ Nominations Type 명세

투표항목을 추가하고 제거하는 리포지토리 메서드를 만들기 전에 이에 필요한 타입을 정의하겠습니다. 먼저 polls/types.ts 모듈에서 addNomination 메서드에 사용할 페이로드를 정의해 보겠습니다.

export type AddNominationData = {
  pollID: string;
  nominationID: string;
  nomination: Nomination;
};

pollID는 토큰에서 데이터를 추출하게 됩니다. 'nominationID'는 ids.ts에서 ID를 생성하기 위해 가지고 있는 작은 도우미를 사용하여 서비스 메서드에서 생성됩니다. 그러나 우리는 아직 지명을 정의하지 않았습니다.

shared 디렉토리 아래 poll-types.ts 모듈을 아래와 같이 수정하겠습니다.!

export type Participants = {
  [participantID: string]: string;
}
export type Nomination = {
  userID: string;
  text: string;
}
export type Nominations = {
  [nominationID: string]: Nomination;
}
export type Poll = {
  id: string;
  topic: string;
  votesPerVoter: number;
  participants: Participants;
  nominations: Nominations;
  // rankings: Rankings;
  // results: Results;
  adminID: string;
  hasStarted: boolean;
}

2️⃣ Repository 메서드

repository 모듈의 createPoll 메서드에서 initialPoll에서 nominatins 속성이 없어서 오류가 나타납니다. 아래와 같이 명세해줍니다.

    const initialPoll = {
      id: pollID,
      topic,
      votesPerVoter,
      participants: {},
      nominations: {},
      adminID: userID,
      hasStarted: false,
    };

아래 코드는 투표(poll)에 후보(nomination)를 추가하거나 삭제하는 두 개의 함수인 addNomination()과 removeNomination()을 정의합니다.

addNomination() 함수는 AddNominationData 객체를 받아서, 이 객체 안에 들어있는 pollID와 nominationID에 해당하는 후보자 객체 nomination을 추가합니다. 이 때 nomination 객체는 JSON 문자열로 변환됩니다. 추가된 후에는 업데이트된 투표(poll) 객체를 반환합니다. 만약 에러가 발생하면 InternalServerErrorException을 throw합니다.

removeNomination() 함수는 RemoveNominationFields 객체를 받아서, 이 객체 안에 들어있는 pollID와 nominationID에 해당하는 nomination을 삭제합니다. 삭제된 후에는 업데이트된 투표(poll) 객체를 반환합니다. 만약 에러가 발생하면 InternalServerErrorException을 throw합니다.

addNomination & removeNomination 메서드

  /**
   * Adds a nomination to a poll.
   * @async
   * @param {AddNominationData} data - An object containing the data needed to add a nomination to a poll.
   * @param {string} data.pollID - The ID of the poll to add the nomination to.
   * @param {string} data.nominationID - The ID of the nomination to add.
   * @param {object} data.nomination - The nomination object to add.
   * @returns {Promise<Poll>} - The updated poll object.
   * @throws {InternalServerErrorException} - If there was an error adding the nomination to the poll.
   */
  async addNomination({
    pollID,
    nominationID,
    nomination,
  }: AddNominationData): Promise<Poll> {
    this.logger.log(
      `Attempting to add a nomination with nominationID/nomination: ${nominationID}/${nomination.text} to pollID: ${pollID}`,
    );
    const key = `polls:${pollID}`;
    const nominationPath = `.nominations.${nominationID}`;

    try {
      await this.redisClient.send_command(
        'JSON.SET',
        key,
        nominationPath,
        JSON.stringify(nomination),
      );
      return this.getPoll(pollID);
    } catch (error) {
      const errorMessage = `Failed to add a nomination with nominationID/text: ${nominationID}/${nomination.text} to pollID: ${pollID}`;
      this.logger.error(errorMessage, error);
      throw new InternalServerErrorException(errorMessage);
    }
  }

  /**
   * Removes a nomination from a poll.
   * @async
   * @param {RemoveNominationFields} fields - An object containing the data needed to remove a nomination from a poll.
   * @param {string} fields.pollID - The ID of the poll to remove the nomination from.
   * @param {string} fields.nominationID - The ID of the nomination to remove.
   * @returns {Promise<Poll>} - The updated poll object.
   * @throws {InternalServerErrorException} - If there was an error removing the nomination from the poll.
   */
  async removeNomination({
    pollID,
    nominationID,
  }: RemoveNominationFields): Promise<Poll> {
    this.logger.log(
      `removing nominationID; ${nominationID} from poll: ${pollID}`,
    );

    const key = `polls:${pollID}`;
    const nominationPath = `.nominations.${nominationID}`;

    try {
      await this.redisClient.send_command('JSON.DEL', key, nominationPath);
      return this.getPoll(pollID);
    } catch (error) {
      const errorMessage = `Failed to remove nominationID: ${nominationID} from poll: ${pollID}`;
      throw new InternalServerErrorException(errorMessage);
    }
  }

3️⃣ nomination 로직 처리를 위한 서비스 메서드 추가

서비스 메서드의 매개변수 타입에 쓰기 위해 AddNominationFields 타입을 추가 하도록 하겠습니다.

export type AddNominationFields = {
  pollID: string;
  userID: string;
  text: string;
};
  • 앞서 레포지토리에 명세한 add, remove에 관한 메서드를 여기서 각각 매칭되는 addNomination, removeNomination메서드에서 사용하면 됩니다.
  /**
   * Adds a nomination to a poll.
   * @param {AddNominationFields} param - An object containing the poll ID, user ID, and nomination text.
   * @returns {Promise<Poll>} - The updated poll object.
   */
  async addNomination({
    pollID,
    userID,
    text,
  }: AddNominationFields): Promise<Poll> {
    // Call the addNomination method on the pollsRepository, passing in the poll ID, a new nomination ID, and the nomination object.
    return this.pollsRepository.addNomination({
      pollID,
      nominationID: createNominationID(),
      nomination: {
        userID,
        text,
      },
    });
  }

  /**
   * Removes a nomination from a poll.
   * @param {RemoveNominationFields} removeNomination - An object containing the poll ID and nomination ID.
   * @returns {Promise<Poll>} - The updated poll object.
   */
  async removeNomination(
    removeNomination: RemoveNominationFields,
  ): Promise<Poll> {
    // Call the removeNomination method on the pollsRepository, passing in the removeNomination object.
    return this.pollsRepository.removeNomination(removeNomination);
  }

4️⃣ Dto명세

소켓 연결시 subscribe 할 이벤트(nominate)에 던져줄 내용을 아래와 같이 명세할게요.

  export class NominationDto {
    @IsString()
    @Length(1, 100)
    text: string;
  }

5️⃣ Gateway Socket.io 이벤트 핸들러 추가

아래 코드는 Socket.IO 서버의 nominate 이벤트와 remove_nomination 이벤트를 처리하는 메소드를 구현한 TypeScript 코드입니다. 각 메소드는 @SubscribeMessage 데코레이터를 사용하여 이벤트를 구독하며, 이벤트가 발생하면 메소드가 호출됩니다.

nominate 메소드는 NominationDto 타입의 nomination 객체와 SocketWithAuth 타입의 client 객체를 매개변수로 받습니다. nomination 객체는 추가할 후보자의 정보가 담겨있고, client 객체는 요청한 클라이언트의 정보가 담겨있습니다. 이 메소드는 addNomination 메소드를 호출하여 후보자를 추가하고, poll_updated 이벤트를 발생시켜 모든 클라이언트에게 새로운 투표 정보를 전달합니다.

removeNomination 메소드는 nominationID와 SocketWithAuth 타입의 client 객체를 매개변수로 받습니다. nominationID는 삭제할 후보자의 ID를 나타내고, client 객체는 요청한 클라이언트의 정보가 담겨있습니다. 이 메소드는 GatewayAdminGuard를 사용하여 요청한 클라이언트가 관리자인지 확인하고, removeNomination 메소드를 호출하여 후보자를 삭제합니다. 그리고 poll_updated 이벤트를 발생시켜 모든 클라이언트에게 새로운 투표 정보를 전달합니다.

이 코드는 투표 애플리케이션에서 투표 후보자를 추가하거나 삭제할 때 사용됩니다. nominate 메소드는 투표 후보자를 추가하고 removeNomination 메소드는 관리자 권한이 있는 클라이언트만 후보자를 삭제할 수 있도록 제한합니다.

  /**
   * Adds a nomination to a poll.
   * @param {NominationDto} nomination - An object containing the nomination text.
   * @param {SocketWithAuth} client - The socket client associated with the user making the request.
   * @returns {Promise<void>} - Resolves with void when the operation is complete.
   */
  @SubscribeMessage('nominate')
  async nominate(
    @MessageBody() nomination: NominationDto,
    @ConnectedSocket() client: SocketWithAuth,
  ): Promise<void> {
    // Log that we are attempting to add a nomination to the poll.
    this.logger.debug(`
      Attempting to add nomination for user ${client.userID} to poll ${client.pollID}\n ${nomination.text}`);

    // Call the addNomination method on the pollsService to add the nomination.
    const updatedPoll = await this.pollsService.addNomination({
      pollID: client.pollID,
      userID: client.userID,
      text: nomination.text,
    });

    // Emit a 'poll_updated' event to all clients in the poll's room with the updated poll data.
    this.io.to(client.pollID).emit('poll_updated', updatedPoll);
  }

  /**
   * Removes a nomination from a poll if the user is an admin.
   * @param {string} nominationID - The ID of the nomination to remove.
   * @param {SocketWithAuth} client - The socket client associated with the user making the request.
   * @returns {Promise<void>} - Resolves with void when the operation is complete.
   */
  @UseGuards(GatewayAdminGuard)
  @SubscribeMessage('remove_nomination')
  async removeNomination(
    @MessageBody('nominationID') nominationID: string,
    @ConnectedSocket() client: SocketWithAuth,
  ): Promise<void> {
    // Log that we are attempting to remove a nomination from the poll.
    this.logger.debug(
      `Attempting to remove nomination ${nominationID} from poll ${client.pollID}`,
    );

    // Call the removeNomination method on the pollsService to remove the nomination.
    const updatedPoll = await this.pollsService.removeNomination({
      pollID: client.pollID,
      nominationID,
    });

    // Emit a 'poll_updated' event to all clients in the poll's room with the updated poll data.
    this.io.to(client.pollID).emit('pol_updated', updatedPoll);
  }

nomination을 클라이언트로부터 생성하는 이벤트를 수신 받을때 이에 대한 validation 처리를 할 것입니다.
DTO로 명세하겠습니다. 만약 문제가 발생하면 예외처리를 던질 수 있도록 할 것입니다.

🧪 TEST with POSTMAN

  1. REST API를 이용하여 POLL을 생성
  2. player를 생성한 poll에 조인시킵니다.
  3. socket 연결을 맺도록합니다. (Client 1~3)
  4. nominate 에 JSON 데이터 포멧의 메시지를 보냅니다.
  • Validation 처리
  • 다른 유저를 통해서 추가 nominate를 처리해봅니다.
  1. remove_nomination에 삭제하려는 nominationID를 실어 보냅니다.
  • 관리자가 아닌 유저가 nomination 삭제시 Unauthorized exception을 받습니다.
profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글