11import { Injectable } from '@nestjs/common'
2- import type { ContestProblem } from '@prisma/client'
3- import { UnprocessableDataException } from '@libs/exception'
2+ import { Prisma , type ContestProblem } from '@prisma/client'
3+ import { MAX_DATE , MIN_DATE } from '@libs/constants'
4+ import {
5+ EntityNotExistException ,
6+ UnprocessableDataException
7+ } from '@libs/exception'
48import { PrismaService } from '@libs/prisma'
59import type { ProblemScoreInput } from './model/problem-score.input'
610
@@ -20,6 +24,191 @@ export class ContestProblemService {
2024 return contestProblems
2125 }
2226
27+ /**
28+ * 대회에 여러 문제를 일괄적으로 추가합니다.
29+ * 이미 추가된 문제는 건너뛰며, 'visibleLockTime'를 업데이트합니다.
30+ *
31+ * @param {number } contestId 대회 ID
32+ * @param {ProblemScoreInput[] } problemIdsWithScore 추가할 문제 ID와 배점
33+ * @returns 'ContestProblem' 정보
34+ */
35+ async importProblemsToContest (
36+ contestId : number ,
37+ problemIdsWithScore : ProblemScoreInput [ ]
38+ ) {
39+ const [ contest , maxOrderResult , existingProblems ] = await Promise . all ( [
40+ this . prisma . contest . findUniqueOrThrow ( {
41+ where : { id : contestId } ,
42+ select : { endTime : true }
43+ } ) ,
44+ this . prisma . contestProblem . aggregate ( {
45+ where : { contestId } ,
46+ // eslint-disable-next-line @typescript-eslint/naming-convention
47+ _max : { order : true }
48+ } ) ,
49+ this . prisma . contestProblem . findMany ( {
50+ where : { contestId } ,
51+ select : { problemId : true }
52+ } )
53+ ] )
54+
55+ let maxOrder = maxOrderResult . _max ?. order ?? - 1
56+ const existingProblemIds = new Set ( existingProblems . map ( ( p ) => p . problemId ) )
57+
58+ const contestProblems : ContestProblem [ ] = [ ]
59+
60+ for ( const { problemId, score } of problemIdsWithScore ) {
61+ if ( existingProblemIds . has ( problemId ) ) {
62+ continue
63+ }
64+
65+ try {
66+ const [ contestProblem ] = await this . prisma . $transaction ( [
67+ this . prisma . contestProblem . create ( {
68+ data : {
69+ order : ++ maxOrder ,
70+ contestId,
71+ problemId,
72+ score
73+ }
74+ } ) ,
75+ this . prisma . problem . updateMany ( {
76+ where : {
77+ id : problemId ,
78+ OR : [
79+ { visibleLockTime : { equals : MIN_DATE } } ,
80+ { visibleLockTime : { equals : MAX_DATE } } ,
81+ { visibleLockTime : { lte : contest . endTime } }
82+ ]
83+ } ,
84+ data : {
85+ visibleLockTime : contest . endTime
86+ }
87+ } )
88+ ] )
89+ contestProblems . push ( contestProblem )
90+ } catch ( error ) {
91+ if ( error instanceof Prisma . PrismaClientKnownRequestError ) {
92+ if ( error . code === 'P2003' ) {
93+ throw new EntityNotExistException ( `Problem ${ problemId } ` )
94+ }
95+ }
96+ throw new UnprocessableDataException ( ( error as Error ) . message )
97+ }
98+ }
99+
100+ return contestProblems
101+ }
102+
103+ /**
104+ * 대회에서 특정 문제들을 제거합니다.
105+ *
106+ * @param {number } contestId 대회 ID
107+ * @param {number[] } problemIds 제거할 문제 ID 배열
108+ * @returns 삭제된 `ContestProblem` 정보
109+ */
110+ async removeProblemsFromContest ( contestId : number , problemIds : number [ ] ) {
111+ const contest = await this . prisma . contest . findUnique ( {
112+ where : {
113+ id : contestId
114+ } ,
115+ select : { endTime : true }
116+ } )
117+ if ( ! contest ) {
118+ throw new EntityNotExistException ( 'Contest' )
119+ }
120+
121+ const contestProblems : ContestProblem [ ] = [ ]
122+
123+ for ( const problemId of problemIds ) {
124+ const [ otherContestIds , removeContestProblem ] = await Promise . all ( [
125+ this . prisma . contestProblem
126+ . findMany ( {
127+ where : {
128+ problemId,
129+ contestId : { not : contestId }
130+ } ,
131+ select : { contestId : true }
132+ } )
133+ . then ( ( cps ) => cps . map ( ( cp ) => cp . contestId ) ) ,
134+
135+ this . prisma . contestProblem . findUniqueOrThrow ( {
136+ where : {
137+ // eslint-disable-next-line @typescript-eslint/naming-convention
138+ contestId_problemId : {
139+ contestId,
140+ problemId
141+ }
142+ } ,
143+ select : { order : true }
144+ } )
145+ ] )
146+
147+ // 문제가 포함된 대회 중 가장 늦게 끝나는 대회의 종료시각으로 visibleLockTime 설정 (없을시 비공개 전환)
148+ let visibleLockTime = MAX_DATE
149+
150+ if ( otherContestIds . length ) {
151+ const latestContest = await this . prisma . contest . findFirst ( {
152+ where : {
153+ id : { in : otherContestIds }
154+ } ,
155+ orderBy : { endTime : 'desc' } ,
156+ select : { endTime : true }
157+ } )
158+ visibleLockTime = latestContest ! . endTime
159+ }
160+
161+ try {
162+ const [ , contestProblem ] = await this . prisma . $transaction ( [
163+ this . prisma . problem . updateMany ( {
164+ where : {
165+ id : problemId ,
166+ visibleLockTime : {
167+ lte : contest . endTime
168+ }
169+ } ,
170+ data : {
171+ visibleLockTime
172+ }
173+ } ) ,
174+ this . prisma . contestProblem . delete ( {
175+ where : {
176+ // eslint-disable-next-line @typescript-eslint/naming-convention
177+ contestId_problemId : {
178+ contestId,
179+ problemId
180+ }
181+ }
182+ } ) ,
183+ this . prisma . contestProblem . updateMany ( {
184+ where : {
185+ contestId,
186+ order : {
187+ gt : removeContestProblem . order
188+ }
189+ } ,
190+ data : {
191+ order : {
192+ decrement : 1
193+ }
194+ }
195+ } )
196+ ] )
197+
198+ contestProblems . push ( contestProblem )
199+ } catch ( error ) {
200+ if ( error instanceof Prisma . PrismaClientKnownRequestError ) {
201+ if ( error . code === 'P2025' ) {
202+ throw new EntityNotExistException ( 'ContestProblem' )
203+ }
204+ }
205+ throw new UnprocessableDataException ( ( error as Error ) . message )
206+ }
207+ }
208+
209+ return contestProblems
210+ }
211+
23212 async updateContestProblemsScore (
24213 contestId : number ,
25214 problemIdsWithScore : ProblemScoreInput [ ]
0 commit comments