diff --git a/src/competition/competition.controller.interface.ts b/src/competition/competition.controller.interface.ts index 3c8f0fc..0de363d 100644 --- a/src/competition/competition.controller.interface.ts +++ b/src/competition/competition.controller.interface.ts @@ -8,7 +8,7 @@ import { GetCompetitionResponseDto } from "./dto/response/getCompetition.respons import { GetCompetitionListResponseDto } from "./dto/response/getCompetitionList.response.dto"; import { GetNonVoterListResponseDto } from "./dto/response/getNonVoterList.response.dto"; import { GetRecentCompetitionsResponseDto } from "./dto/response/getRecentCompetitions.response.dto"; -import { GetVotingPrefectureResponseDto } from "./dto/response/getVotingPrefecture.response.dto"; +import { GetVotePerResponseDto } from "./dto/response/getVotePer.response.dto"; import { PatchCompetitionResponseDto } from "./dto/response/patchCompetition.response.dto"; import { PostAwardsResponseDto } from "./dto/response/postAwards.response.dto"; import { PostCompetitionResponseDto } from "./dto/response/postCompetition.response.dto"; @@ -24,10 +24,12 @@ export interface ICompetitionController { getCompetitionList(page: string): Promise>; getRecentCompetitions(): Promise>; getCompetition(id: string): Promise>; - getVotingPrefecture(id: string): Promise>; getNonVoterList( request: GetNonVoterListRequestDto, ): Promise>; + getVotePer( + id: string + ): Promise>; patchCompetition( id: string, request: PatchCompetitionRequestDto, diff --git a/src/competition/competition.controller.ts b/src/competition/competition.controller.ts index 1446052..f3fb624 100644 --- a/src/competition/competition.controller.ts +++ b/src/competition/competition.controller.ts @@ -6,6 +6,7 @@ import { Get, Inject, Logger, + NotFoundException, Param, ParseUUIDPipe, Patch, @@ -22,12 +23,12 @@ import { GetCompetitionResponseDto } from "./dto/response/getCompetition.respons import { GetCompetitionListResponseDto } from "./dto/response/getCompetitionList.response.dto"; import { GetNonVoterListResponseDto } from "./dto/response/getNonVoterList.response.dto"; import { GetRecentCompetitionsResponseDto } from "./dto/response/getRecentCompetitions.response.dto"; -import { GetVotingPrefectureResponseDto } from "./dto/response/getVotingPrefecture.response.dto"; import { PatchCompetitionResponseDto } from "./dto/response/patchCompetition.response.dto"; import { PostAwardsResponseDto } from "./dto/response/postAwards.response.dto"; import { PostCompetitionResponseDto } from "./dto/response/postCompetition.response.dto"; import { CompetitionService } from "./competition.service"; import { DeleteCompetitionResponseDto } from "./dto/response/deleteCompetition.response.dto"; +import { GetVotePerResponseDto } from "./dto/response/getVotePer.response.dto"; @Controller("competition") export class CompetitionController implements ICompetitionController { @@ -91,13 +92,6 @@ export class CompetitionController implements ICompetitionController { }; } - @Get("per/:id") - async getVotingPrefecture( - @Param("id") id: string, - ): Promise> { - throw new Error("Method not implemented."); - } - @Get("list") async getNonVoterList( @Query() request: GetNonVoterListRequestDto, @@ -111,6 +105,21 @@ export class CompetitionController implements ICompetitionController { }; } + @Get("per/:id") + async getVotePer( + @Param("id") id: string, + ): Promise> { + if (!id) throw new BadRequestException(); + + const data = await this.service.getVotePer(id); + + return { + data, + statusCode: 200, + statusMsg: "", + }; + } + @Get(":page") async getCompetitionList( @Param("page") page: string, diff --git a/src/competition/competition.service.interface.ts b/src/competition/competition.service.interface.ts index a4a9b74..8e3d5ec 100644 --- a/src/competition/competition.service.interface.ts +++ b/src/competition/competition.service.interface.ts @@ -7,7 +7,7 @@ import { GetCompetitionResponseDto } from "./dto/response/getCompetition.respons import { GetCompetitionListResponseDto } from "./dto/response/getCompetitionList.response.dto"; import { GetNonVoterListResponseDto } from "./dto/response/getNonVoterList.response.dto"; import { GetRecentCompetitionsResponseDto } from "./dto/response/getRecentCompetitions.response.dto"; -import { GetVotingPrefectureResponseDto } from "./dto/response/getVotingPrefecture.response.dto"; +import { GetVotePerResponseDto } from "./dto/response/getVotePer.response.dto"; import { PatchCompetitionResponseDto } from "./dto/response/patchCompetition.response.dto"; import { PostAwardsResponseDto } from "./dto/response/postAwards.response.dto"; import { PostCompetitionResponseDto } from "./dto/response/postCompetition.response.dto"; @@ -23,15 +23,13 @@ export interface ICompetitionService { getCompetitionList(page: string): Promise; getRecentCompetitions(): Promise; getCompetition(id: string): Promise; - getVotingPrefecture(id: string): Promise; getNonVoterList( request: GetNonVoterListRequestDto, ): Promise; + getVotePer(id: string): Promise; patchCompetition( id: string, request: PatchCompetitionRequestDto, ): Promise; - deleteCompetition( - id: string, - ): Promise; + deleteCompetition(id: string): Promise; } diff --git a/src/competition/competition.service.ts b/src/competition/competition.service.ts index 482f260..4cdac96 100644 --- a/src/competition/competition.service.ts +++ b/src/competition/competition.service.ts @@ -17,12 +17,12 @@ import { List, } from "./dto/response/getNonVoterList.response.dto"; import { GetRecentCompetitionsResponseDto } from "./dto/response/getRecentCompetitions.response.dto"; -import { GetVotingPrefectureResponseDto } from "./dto/response/getVotingPrefecture.response.dto"; import { PatchCompetitionResponseDto } from "./dto/response/patchCompetition.response.dto"; import { PostAwardsResponseDto } from "./dto/response/postAwards.response.dto"; import { PostCompetitionResponseDto } from "./dto/response/postCompetition.response.dto"; import { PrismaService } from "../prisma/prisma.service"; import { DeleteCompetitionResponseDto } from "./dto/response/deleteCompetition.response.dto"; +import { GetVotePerResponseDto } from "./dto/response/getVotePer.response.dto"; @Injectable() export class CompetitionService implements ICompetitionService { @@ -128,10 +128,33 @@ export class CompetitionService implements ICompetitionService { }; } - async getVotingPrefecture( - id: string, - ): Promise { - throw new Error("Method not implemented."); + async getVotePer(id: string): Promise { + const competition = await this.prisma.findCompetitionById(id); + if (!competition) { + throw new NotFoundException(); + } + + const voteStats = await this.prisma.findCountVoterPer(id); + const studentStats = voteStats?.filter( + (stat) => stat.user_role === "Student", + )[0] ?? { user_role: "Student", total_count: 1, voted_count: 0 }; + const teacherStats = voteStats?.filter( + (stat) => stat.user_role === "Teacher", + )[0] ?? { user_role: "Teacher", total_count: 1, voted_count: 0 }; + + const student = + studentStats.total_count === 0 + ? 0 + : studentStats.voted_count / studentStats.total_count; + const teacher = + teacherStats.total_count === 0 + ? 0 + : teacherStats.voted_count / teacherStats.total_count; + + return { + student, + teacher, + }; } async getNonVoterList( diff --git a/src/competition/dto/response/getVotingPrefecture.response.dto.ts b/src/competition/dto/response/getVotePer.response.dto.ts similarity index 70% rename from src/competition/dto/response/getVotingPrefecture.response.dto.ts rename to src/competition/dto/response/getVotePer.response.dto.ts index 0740d07..18a0bc2 100644 --- a/src/competition/dto/response/getVotingPrefecture.response.dto.ts +++ b/src/competition/dto/response/getVotePer.response.dto.ts @@ -1,9 +1,9 @@ import { IsNumber } from "class-validator"; -export class GetVotingPrefectureResponseDto { +export class GetVotePerResponseDto { @IsNumber() student: number; @IsNumber() teacher: number; -} +} \ No newline at end of file diff --git a/src/competition/test/competition.controller.spec.ts b/src/competition/test/competition.controller.spec.ts index 09dfe54..3392974 100644 --- a/src/competition/test/competition.controller.spec.ts +++ b/src/competition/test/competition.controller.spec.ts @@ -3,7 +3,7 @@ import { CompetitionController } from "../competition.controller"; import { JwtService } from "@nestjs/jwt"; import { PrismaService } from "../../prisma/prisma.service"; import { CompetitionService } from "../competition.service"; -import { BadRequestException, Logger } from "@nestjs/common"; +import { BadRequestException, Logger, NotFoundException } from "@nestjs/common"; import { PostCompetitionRequestDto } from "../dto/request/postCompetition.request.dto"; import { PostAwardsRequestDto } from "../dto/request/postAwards.request.dto"; import { COMPETITION_STATUS } from "../../prisma/client"; @@ -87,6 +87,12 @@ describe("CompetitionController", () => { ], }; }), + getVotePer: jest.fn(() => { + return { + student: 26, + teacher: 3, + }; + }), patchCompetition: jest.fn(() => { return { id: "1", @@ -110,11 +116,7 @@ describe("CompetitionController", () => { controller = module.get(CompetitionController); - serviceMock.postCompetition.mockClear(); - serviceMock.postAwards.mockClear(); - serviceMock.getCompetitionList.mockClear(); - serviceMock.getCompetition.mockClear(); - serviceMock.getNonVoterList.mockClear(); + jest.clearAllMocks(); }); describe("PostCompetition", () => { @@ -317,6 +319,35 @@ describe("CompetitionController", () => { }); }); + describe("GetVotePer", () => { + const id = "b6ee2cd2-4596-47ee-b332-de87e1p39e80"; + + it("[200]", async () => { + const res = await controller.getVotePer(id); + + expect(serviceMock.getVotePer).toHaveBeenCalledTimes(1); + expect(serviceMock.getVotePer).toHaveBeenCalledWith(id); + expect(res).toEqual({ + data: { + student: 26, + teacher: 3, + }, + statusCode: 200, + statusMsg: "", + }); + }); + + it("[400] empty id", async () => { + const id = undefined; + + await expect(async () => await controller.getVotePer(id)).rejects.toThrow( + new BadRequestException(), + ); + expect(serviceMock.getVotePer).toHaveBeenCalledTimes(0); + expect(serviceMock.getCompetition).toHaveBeenCalledTimes(0); + }); + }); + describe("PatchCompetition", () => { const id = "1"; const request: PatchCompetitionRequestDto = { diff --git a/src/competition/test/competition.service.spec.ts b/src/competition/test/competition.service.spec.ts index 6f1608f..26f24e9 100644 --- a/src/competition/test/competition.service.spec.ts +++ b/src/competition/test/competition.service.spec.ts @@ -128,6 +128,36 @@ describe("CompetitionService", () => { return nonVoter; }), + findCountVoterPer: jest.fn(async (id: string) => { + const allStudents = Object.values(foreignUserDatabase).filter( + (user) => user.userRole == "Student", + ); + const voteStudents = allStudents.map((user) => + Object.values(voteDatabase).filter((e) => e.userId == user.userId), + ); + const allTeachers = Object.values(foreignUserDatabase).filter( + (user) => user.userRole == "Teacher", + ); + const voteTeachers = allTeachers.map((user) => + Object.values(voteDatabase).filter((e) => (e.userId = user.userId)), + ); + + console.log(allStudents, voteStudents) + console.log(allTeachers, voteTeachers) + + return [ + { + user_role: "Student", + total_count: allStudents.length, + voted_count: voteStudents.length, + }, + { + user_role: "Teacher", + total_count: allTeachers.length, + voted_count: voteTeachers.length, + }, + ]; + }), patchCompetition: jest.fn( async ( id: string, @@ -503,7 +533,9 @@ describe("CompetitionService", () => { const res = await service.getRecentCompetitions(); - expect(prismaMock.findManyCompetitionPendingAward).toHaveBeenCalledTimes(1); + expect(prismaMock.findManyCompetitionPendingAward).toHaveBeenCalledTimes( + 1, + ); expect(prismaMock.findManyCompetitionPendingAward).toHaveBeenCalledWith(); expect(res).toEqual({ list: [ @@ -610,6 +642,80 @@ describe("CompetitionService", () => { }); }); + // describe("GetVotePer", () => { + // const id = "1"; + + // it("[200]", async () => { + // foreignUserDatabase = { + // "95d1e209-0337-40f4-a852-3c16f3a9a2df": { + // userId: "95d1e209-0337-40f4-a852-3c16f3a9a2df", + // userName: "청도서", + // userRole: "Teacher", + // userStringId: "r0adwest", + // userStudentNumber: null, + // }, + // "95d1e209-0337-40f4-a842-3c16f3a9a2df": { + // userId: "95d1e209-0337-40f4-a842-3c16f3a9a2df", + // userName: "청희도", + // userRole: "Teacher", + // userStringId: "ziio", + // userStudentNumber: null, + // }, + // "5fe187b2-cf86-4bda-b1f8-d638f2c53324": { + // userId: "5fe187b2-cf86-4bda-b1f8-d638f2c53324", + // userName: "홍길동", + // userRole: "Student", + // userStringId: "hongi1d0ng", + // userStudentNumber: 2345, + // }, + // "5fe187b2-ctfx-4bda-b1f8-d638f2c53324": { + // userId: "5fe187b2-ctfx-4bda-b1f8-d638f2c53324", + // userName: "홍이삭", + // userRole: "Student", + // userStringId: "binding0f1ssac", + // userStudentNumber: 2468, + // }, + // }; + + // voteDatabase = { + // "1": { + // userId: "95d1e209-0337-40f4-a852-3c16f3a9a2df", + // contestId: "1", + // projectId: "1", + // }, + // "2": { + // userId: "5fe187b2-cf86-4bda-b1f8-d638f2c53324", + // contestId: "1", + // projectId: "4", + // }, + // }; + + // competitionDatabase = { + // "1": { + // id: "1", + // name: "competition-test", + // startDate: new Date("2024-10-10"), + // endDate: new Date("2024-10-20"), + // purpose: "TESTING", + // audience: "members", + // place: "Github", + // status: COMPETITION_STATUS.IN_PROGRESS, + // }, + // }; + + // const res = await service.getVotePer(id); + + // expect(prismaMock.findCompetitionById).toHaveBeenCalledTimes(1); + // expect(prismaMock.findCompetitionById).toHaveBeenCalledWith(id); + // expect(prismaMock.findCountVoterPer).toHaveBeenCalledTimes(1); + // expect(prismaMock.findCountVoterPer).toHaveBeenCalledWith(id); + // expect(res).toEqual({ + // student: 0.5, + // teacher: 0.5, + // }); + // }); + // }); + describe("PatchCompetition", () => { const id = "0"; const request: PatchCompetitionRequestDto = {}; diff --git a/src/guard/adminValidator/adminValidator.guard.spec.ts b/src/guard/adminValidator/adminValidator.guard.spec.ts index f9cdaee..4f4dac6 100644 --- a/src/guard/adminValidator/adminValidator.guard.spec.ts +++ b/src/guard/adminValidator/adminValidator.guard.spec.ts @@ -26,15 +26,12 @@ describe("AdminValidatorGuard", () => { }; const userPrismaMock = { findUserByStrId: (userId: string) => { - console.log(userId, "findUserByStrId"); return true; }, findUserById: (id: string) => { - console.log(id, "findUserById"); return true; }, findUserByNumber: (number?: number) => { - console.log(number, "findUserByNumber"); return true; }, }; diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 74521ea..cdf6859 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -242,6 +242,33 @@ export class PrismaService } } + async findCountVoterPer( + id: string, + ): Promise< + { user_role: string; voted_count: number; total_count: number }[] + > { + try { + const cnt = await this.$queryRaw< + { user_role: string; voted_count: number; total_count: number }[] + >` + SELECT + user_role::TEXT, + COUNT(CASE WHEN v.user_id IS NOT NULL THEN 1 END)::INTEGER as voted_count, + COUNT(*)::INTEGER as total_count + FROM foreign_user fu + LEFT JOIN "Vote" v + ON fu.user_id = v.user_id AND v.contest_id = ${id} + WHERE user_role::TEXT IN ('Student', 'Teacher') + GROUP BY user_role + `; + + return cnt; + } catch (e) { + this.logger.error(e); + throw new InternalServerErrorException(e); + } + } + async patchClubStatus(clubId: string) { try { const thisClub = await this.findClub(clubId);