Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 124 additions & 33 deletions apps/backend/apps/admin/src/contest/contest.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,39 +626,54 @@ export class ContestService {
* @param reqId Contest Admin / Manager ID
* @throws {EntityNotExistException} 해당 contestId를 가지는 Contest가 존재하지 않을 경우
* @throws {EntityNotExistException} 해당 Contest에 참여하고 있지 않은 userId인 경우
* @throws {EntityNotExistException} 해당 Contest에 기록이 존재하지 않는 userId인 경우
* @throws {ForbiddenAccessException} ContestAdmin 또는 ContestManager가 아닌 reqId인 경우
* @throws {ForbiddenAccessException} 진행 중이거나 종료된 Contest인 경우
* @throws {ForbiddenAccessException} 종료된 Contest인 경우
* @returns
*/
async removeUserFromContest(
contestId: number,
userId: number,
reqId: number
) {
const [contest, contestRecord, requesterRole] = await Promise.all([
this.prisma.contest.findUnique({
where: { id: contestId },
select: { startTime: true }
}),
this.prisma.contestRecord.findFirst({
where: { userId, contestId },
select: { id: true }
}),
this.prisma.userContest.findUnique({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
userId_contestId: {
userId: reqId,
contestId
}
},
select: { role: true }
})
])
const [contest, userContest, contestRecord, requesterRole] =
await Promise.all([
this.prisma.contest.findUnique({
where: { id: contestId },
select: { startTime: true, endTime: true }
}),
this.prisma.userContest.findUnique({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
userId_contestId: {
userId,
contestId
}
},
select: { id: true }
}),
this.prisma.contestRecord.findFirst({
where: { userId, contestId },
select: { id: true }
}),
this.prisma.userContest.findUnique({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
userId_contestId: {
userId: reqId,
contestId
}
},
select: { role: true }
})
])

if (!contest) {
throw new EntityNotExistException('Contest')
}
if (!userContest) {
throw new EntityNotExistException('UserContest')
}
if (!contestRecord) {
throw new EntityNotExistException('ContestRecord')
}
Expand All @@ -673,19 +688,74 @@ export class ContestService {
}

const now = new Date()
if (now >= contest.startTime) {
throw new ForbiddenAccessException(
'Cannot unregister ongoing or ended contest'
)
if (now >= contest.endTime) {
throw new ForbiddenAccessException('Cannot unregister ended contest')
}

return await this.prisma.$transaction(async (tx) => {
await tx.contestRecord.delete({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
contestId_userId: { contestId, userId }
if (now >= contest.startTime) {
// 강퇴당한 user가 firstSolver인 문제 존재 여부 확인 후 있으면 다음 사람에게 이월
const firstSolveProblems = await tx.contestProblemFirstSolver.findMany({
where: { contestRecordId: contestRecord.id },
select: { contestProblemId: true }
})

for (const { contestProblemId } of firstSolveProblems) {
await tx.contestProblemRecord.update({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
contestProblemId_contestRecordId: {
contestProblemId,
contestRecordId: contestRecord.id
}
},
data: { isFirstSolver: false }
})
const nextSolver = await tx.contestProblemRecord.findFirst({
where: {
contestProblemId,
finishTime: { not: null },
contestRecordId: { not: contestRecord.id },
contestRecord: {
user: {
userContest: {
some: { contestId }
}
}
}
},
orderBy: { finishTime: 'asc' }
})

if (nextSolver) {
await tx.contestProblemFirstSolver.update({
where: { contestProblemId },
data: { contestRecordId: nextSolver.contestRecordId }
})
await tx.contestProblemRecord.update({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
contestProblemId_contestRecordId: {
contestProblemId,
contestRecordId: nextSolver.contestRecordId
}
},
data: { isFirstSolver: true }
})
} else {
await tx.contestProblemFirstSolver.delete({
where: { contestProblemId }
})
}
}
})
} else {
await tx.contestRecord.delete({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
contestId_userId: { contestId, userId }
}
})
}

return tx.userContest.delete({
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down Expand Up @@ -1143,10 +1213,24 @@ export class ContestService {
}),
this.prisma.submission.groupBy({
by: ['userId'],
where: { contestId }
where: {
contestId,
user: {
userContest: {
some: { contestId }
}
}
}
}),
this.prisma.contestRecord.count({
where: { contestId }
where: {
contestId,
user: {
userContest: {
some: { contestId }
}
}
}
}),
// Contest의 최고 점수 계산
this.prisma.contestProblem.aggregate({
Expand All @@ -1156,7 +1240,14 @@ export class ContestService {
}),
// 항상 finalScore, finalTotalPenalty 사용
this.prisma.contestRecord.findMany({
where: { contestId },
where: {
contestId,
user: {
userContest: {
some: { contestId }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
},
select: {
userId: true,
user: { select: { username: true } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -538,9 +538,9 @@ describe('ContestService', () => {
expect(db.$transaction.calledOnce).to.be.true
})

it('should throw ForbiddenAccessException if contest started', async () => {
const startedContest = { ...contest, startTime: faker.date.past() }
db.contest.findUnique.resolves(startedContest)
it('should throw ForbiddenAccessException if contest ended', async () => {
const endedContest = { ...contest, endTime: faker.date.past() }
db.contest.findUnique.resolves(endedContest)
db.contestRecord.findFirst.resolves(contestRecord)
db.userContest.findUnique.resolves(userContest)

Expand Down
9 changes: 8 additions & 1 deletion apps/backend/apps/client/src/contest/contest.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,14 @@ export class ContestService {
const maxScore = sum._sum?.score ?? 0

const contestRecordsPromise = this.prisma.contestRecord.findMany({
where: { contestId },
where: {
contestId,
user: {
userContest: {
some: { contestId }
}
}
},
select: {
userId: true,
user: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export class SubmissionFinalizationService {
finalScore: score,
finalTimePenalty: timePenalty,
finalSubmitCountPenalty: submitCountPenalty,
finishTime: updateTime,
Comment thread
lshtar13 marked this conversation as resolved.
...(!isFreezed ? { score, submitCountPenalty, timePenalty } : {})
}

Expand Down
27 changes: 7 additions & 20 deletions apps/backend/apps/client/src/submission/submission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class SubmissionService {
* @returns {Promise<Submission>} 생성된 제출 객체
* @throws {EntityNotExistException} 아래의 경우에 발생합니다
* - 유효한 진행 중인 대회가 없을 경우 (Contest)
* - 사용자가 대회에 등록되어 있지 않은 경우 (ContestRecord)
* - 사용자가 대회에 등록되어 있지 않은 경우 (UserContest)
* - 문제를 찾을 수 없거나 대회와 매칭되지 않는 경우 (ContestProblem)
* @throws {ConflictFoundException} 아래의 경우에 발생합니다
* - 대회가 진행중이지 않을 경우
Expand Down Expand Up @@ -166,30 +166,17 @@ export class SubmissionService {

if (!isStaff) {
// 대회에 등록되어 있는지 확인합니다.
const contestRecord = await this.prisma.contestRecord.findUnique({
const userContest = await this.prisma.userContest.findUnique({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
contestId_userId: {
contestId,
userId
}
},
select: {
contest: {
select: {
startTime: true,
endTime: true
}
}
userId_contestId: { userId, contestId }
}
})
if (!contestRecord) {
throw new EntityNotExistException('ContestRecord')

if (!userContest) {
throw new EntityNotExistException('UserContest')
}
if (
contestRecord.contest.startTime > now ||
contestRecord.contest.endTime <= now
) {
if (contest.startTime > now || contest.endTime <= now) {
throw new ConflictFoundException(
'Submission is only allowed to ongoing contests'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ const db = {
update: stub()
},
userContest: {
findFirst: stub()
findFirst: stub(),
findUnique: stub()
},
assignmentRecord: {
findUnique: stub(),
Expand Down Expand Up @@ -264,14 +265,16 @@ describe('SubmissionService', () => {
describe('submitToContest', () => {
it('should call createSubmission', async () => {
const createSpy = stub(service, 'createSubmission')
db.contest.findFirst.resolves(mockContest)
db.userContest.findFirst.resolves(null)
db.contestRecord.findUnique.resolves({
contest: {
groupId: 1,
startTime: new Date(Date.now() - 10000),
endTime: new Date(Date.now() + 10000)
}
db.contest.findFirst.resolves({
...mockContest,
startTime: new Date(Date.now() - 10000),
endTime: new Date(Date.now() + 10000)
})
db.userContest.findUnique.resolves({
id: 1,
userId: submissions[0].userId,
contestId: CONTEST_ID,
role: 'participant'
})
db.contestProblem.findUnique.resolves({ problem: problems[0] })

Expand Down
Loading
Loading