투표를 종료할 이벤트를 만들 차례입니다. 투표가 종료되면 서버는 결과를 계산한 다음 이 결과를 다시 클라이언트로 보냅니다! 또한 관리자가 투표를 취소할 수 있는 핸들러를 추가 하겠습니다.
설문 조사가 종료되면 설문 조사의 최종 결과를 저장해야 합니다. 이를 위해 shared/poll-types 타입을 업데이트하겠습니다.
export type Results = Array<{
nominationID: NominationID,
nominationText: string,
score: number,
}>;
export type Poll = {
id: string;
topic: string;
votesPerVoter: number;
participants: Participants;
nominations: Nominations;
rankings: Rankings;
results: Results;
adminID: string;
hasStarted: boolean;
}
createPoll 메서드의 지역변수 initialPoll의 변수를 재정의해 해줄게요.
const initialPoll = {
id: pollID,
topic,
votesPerVoter,
participants: {},
nominations: {},
rankings: {},
results: [],
adminID: userID,
hasStarted: false,
};
addResults 메서드
addResults 함수는 Redis에 있는 특정 투표(pollID)에 새로운 결과(results)를 추가하는 함수입니다. JSON.SET 명령어를 사용하여 Redis의 키(key)에 결과 데이터(results)를 추가하고, 그 결과를 Promise로 반환합니다. 만약 작업에 실패하면 InternalServerErrorException을 throw합니다.
deletePoll 메서드
deletePoll 함수는 Redis에 있는 특정 투표(pollID)를 삭제하는 함수이다. JSON.DEL 명령어를 사용하여 Redis의 키(key)를 삭제하고,Promise를 반환한다. 만약 작업에 실패하면 InternalServerErrorException을 throw합니다.
/**
* Adds the provided results to a poll in Redis.
*
* @async
* @function
* @param {string} pollID - The ID of the poll to add results to.
* @param {Results} results - The results to add to the poll.
* @returns {Promise<Poll>} A Promise that resolves to the full poll object, including the newly added results.
* @throws {InternalServerErrorException} If the operation fails.
*/
async addResults(pollID: string, results: Results): Promise<Poll> {
this.logger.log(
`Attempting to add results to pollID: ${pollID}`,
JSON.stringify(results),
);
const key = `polls:${pollID}`;
const resultsPath = `.results`;
try {
await this.redisClient.send_command(
'JSON.SET',
key,
resultsPath,
JSON.stringify(results),
);
return this.getPoll(pollID);
} catch (e) {
this.logger.error(
`Failed to add add results for pollID: ${pollID}`,
results,
e,
);
throw new InternalServerErrorException(
`Failed to add add results for pollID: ${pollID}`,
);
}
}
/**
* Deletes a poll from Redis.
*
* @async
* @function
* @param {string} pollID - The ID of the poll to delete.
* @returns {Promise<void>} A Promise that resolves when the operation is complete.
* @throws {InternalServerErrorException} If the operation fails.
*/
async deletePoll(pollID: string): Promise<void> {
const key = `polls:${pollID}`;
this.logger.log(`deleting poll: ${pollID}`);
try {
await this.redisClient.send_command('JSON.DEL', key);
} catch (e) {
this.logger.error(`Failed to delete poll: ${pollID}`, e);
throw new InternalServerErrorException(
`Failed to delete poll: ${pollID}`,
);
}
}
computeResults 함수
computeResults 함수는 특정 투표(pollID)의 결과를 계산하는 함수입니다. 먼저, pollID를 이용하여 해당 투표(poll)를 가져오고, getResults 함수를 통해 투표 결과(results)를 계산합니다. 이후, 계산된 결과를 Redis에 추가(addResults)하고, Promise를 반환합니다.
cancelPoll 함수
cancelPoll 함수는 특정 투표(pollID)를 취소하는 메서드입니다. pollID를 이용하여 Redis에서 해당 투표를 삭제(deletePoll)하고, Promise를 반환합니다.
두 함수 모두 Redis에 접근하는 과정에서 예외가 발생할 수 있으므로, 예외 처리가 필요하다.
async computeResults(pollID: string): Promise<Poll> {
const poll = await this.pollsRepository.getPoll(pollID);
const results = getResults(poll.rankings, poll.nominations);
return this.pollsRepository.addResults(pollID, results);
}
async cancelPoll(pollID: string): Promise<void> {
await this.pollsRepository.deletePoll(pollID);
}
import { Nominations, Rankings, Results } from 'shared';
export default (
rankings: Rankings,
nominations: Nominations,
votesPerVoter: number,
): Results => {
return [];
};
/**
* Closes a poll and computes the results.
*
* @async
* @function
* @param {SocketWithAuth} client - The client that initiated the close_poll request.
* @returns {Promise<void>} A Promise that resolves when the operation is complete.
* @throws {UnauthorizedException} If the client is not authorized to perform this operation.
*/
@UseGuards(GatewayAdminGuard)
@SubscribeMessage('close_poll')
async closePoll(@ConnectedSocket() client: SocketWithAuth): Promise<void> {
this.logger.debug(`Closing poll: ${client.pollID} and computing results`);
const updatedPoll = await this.pollsService.computeResults(client.pollID);
this.io.to(client.pollID).emit('poll_updated', updatedPoll);
}
/**
* Cancels a poll.
*
* @async
* @function
* @param {SocketWithAuth} client - The client that initiated the cancel_poll request.
* @returns {Promise<void>} A Promise that resolves when the operation is complete.
* @throws {UnauthorizedException} If the client is not authorized to perform this operation.
*/
@UseGuards(GatewayAdminGuard)
@SubscribeMessage('cancel_poll')
async cancelPoll(@ConnectedSocket() client: SocketWithAuth): Promise<void> {
this.logger.debug(`Cancelling poll with id: "${client.pollID}"`);
await this.pollsService.cancelPoll(client.pollID);
this.io.to(client.pollID).emit('poll_cancelled');
}
closePoll 함수
cancelPoll 함수
이제 결과 계산 방법을 살펴보겠습니다. 투표를 하고 꼴찌 후보를 제거하는 방식으로 진행하려 합니다.
다음 이미지와 같이 Voter 당 총 투표 수에 따라 각 투표에 대해 일종의 가중 공식을 적용합니다
N : 참가자당 총 투표 수
n : 0에서 N-1까지 사용자의 n번째 순위
R_n: n번째 투표의 값
위 공식을 getResults.ts 모듈에 구현하겠습니다.
import { Nominations, Rankings, Results } from 'shared';
export default (
rankings: Rankings,
nominations: Nominations,
votesPerVoter: number,
): Results => {
// 1. Each value of `rankings` key values is an array of a participants'
// vote. Points for each array element corresponds to following formula:
// r_n = ((votesPerVoter - 0.5*n) / votesPerVoter)^(n+1), where n corresponds
// to array index of rankings.
// Accumulate score per nominationID
const scores: { [nominationID: string]: number } = {};
Object.values(rankings).forEach((userRankings) => {
userRankings.forEach((nominationID, n) => {
const voteValue = Math.pow(
(votesPerVoter - 0.5 * n) / votesPerVoter,
n + 1,
);
scores[nominationID] = (scores[nominationID] ?? 0) + voteValue;
});
});
// 2. Take nominationID to score mapping, and merge in nominationText
// and nominationID into value
const results = Object.entries(scores).map(([nominationID, score]) => ({
nominationID,
nominationText: nominations[nominationID].text,
score,
}));
// 3. Sort values by score in descending order
results.sort((res1, res2) => res2.score - res1.score);
return results;
};