Skip to content

Commit 3e73698

Browse files
authored
release: 1.9.7 (#212)
2 parents 042e1e0 + 59e1844 commit 3e73698

16 files changed

+798
-11
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Approve not approved quiz
2+
3+
미승인 퀴즈를 승인하여 승인 퀴즈로 반영합니다.
4+
5+
> [!WARN]
6+
> 요청 시 `AdminCallDetected` 이벤트가 발행됩니다.
7+
8+
## Request
9+
### HTTP METHOD : `POST`
10+
### url : `https://api.gitanimals.org/admin/quizs/not-approved/{quizId}/approve`
11+
### RequestHeader
12+
- Admin-Secret: `{발급받은 어드민 토큰을 넘겨주세요.}`
13+
- Authorization: `{어드민 요청자의 인증토큰을 넘겨주세요.}`
14+
15+
### Path Variable
16+
- quizId: `{승인할 미승인 퀴즈 ID}`
17+
18+
### Request Body
19+
```json
20+
{
21+
"reason": "review completed"
22+
}
23+
```
24+
25+
## Response
26+
27+
200 OK
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Delete approved quiz
2+
3+
승인된 퀴즈를 삭제합니다.
4+
5+
> [!WARN]
6+
> 요청 시 `AdminCallDetected` 이벤트가 발행됩니다.
7+
8+
## Request
9+
### HTTP METHOD : `DELETE`
10+
### url : `https://api.gitanimals.org/admin/quizs/approved/{quizId}`
11+
### RequestHeader
12+
- Admin-Secret: `{발급받은 어드민 토큰을 넘겨주세요.}`
13+
- Authorization: `{어드민 요청자의 인증토큰을 넘겨주세요.}`
14+
15+
### Path Variable
16+
- quizId: `{삭제할 승인 퀴즈 ID}`
17+
18+
### Request Body
19+
```json
20+
{
21+
"reason": "policy violation"
22+
}
23+
```
24+
25+
## Response
26+
27+
200 OK
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Delete not approved quiz
2+
3+
미승인 퀴즈를 삭제합니다. 내부적으로 포인트 회수 로직이 함께 수행될 수 있습니다.
4+
5+
> [!WARN]
6+
> 요청 시 `AdminCallDetected` 이벤트가 발행됩니다.
7+
8+
## Request
9+
### HTTP METHOD : `DELETE`
10+
### url : `https://api.gitanimals.org/admin/quizs/not-approved/{quizId}`
11+
### RequestHeader
12+
- Admin-Secret: `{발급받은 어드민 토큰을 넘겨주세요.}`
13+
- Authorization: `{어드민 요청자의 인증토큰을 넘겨주세요.}`
14+
15+
### Path Variable
16+
- quizId: `{삭제할 미승인 퀴즈 ID}`
17+
18+
### Request Body
19+
```json
20+
{
21+
"reason": "duplicate quiz"
22+
}
23+
```
24+
25+
## Response
26+
27+
200 OK
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Scroll approved quizs
2+
3+
승인된 퀴즈를 `id` 기반 no-offset 방식으로 조회합니다.
4+
5+
> [!WARN]
6+
> 조회는 항상 `id` 커서를 기준으로 내려갑니다.
7+
8+
## Request
9+
### HTTP METHOD : `GET`
10+
### url : `https://api.gitanimals.org/admin/quizs/approved`
11+
### RequestHeader
12+
- Admin-Secret: `{발급받은 어드민 토큰을 넘겨주세요.}`
13+
- Authorization: `{어드민 요청자의 인증토큰을 넘겨주세요.}`
14+
15+
### Query Parameter
16+
- lastId: `{optional, 다음 페이지 조회를 위한 커서}`
17+
- level: `{optional, EASY | MEDIUM | DIFFICULT}`
18+
- category: `{optional, FRONTEND | BACKEND}`
19+
- language: `{optional, KOREA | ENGLISH}`
20+
21+
## Response
22+
23+
200 OK
24+
25+
```json
26+
{
27+
"quizs": [
28+
{
29+
"id": "912345678901234567",
30+
"userId": "1234",
31+
"level": "EASY",
32+
"category": "BACKEND",
33+
"language": "ENGLISH",
34+
"problem": "Spring Bean scope의 기본값은 singleton이다.",
35+
"expectedAnswer": "YES",
36+
"createdAt": "2026-03-21 01:00:00",
37+
"modifiedAt": "2026-03-21 01:00:00"
38+
}
39+
],
40+
"nextId": "912345678901234567"
41+
}
42+
```
43+
44+
### Response Field
45+
- quizs: 최대 20개의 승인 퀴즈 목록
46+
- nextId: 다음 페이지가 없으면 `null`
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Scroll not approved quizs
2+
3+
미승인 퀴즈를 `id` 기반 no-offset 방식으로 조회합니다.
4+
5+
> [!WARN]
6+
> 조회는 항상 `id` 커서를 기준으로 내려갑니다.
7+
8+
## Request
9+
### HTTP METHOD : `GET`
10+
### url : `https://api.gitanimals.org/admin/quizs/not-approved`
11+
### RequestHeader
12+
- Admin-Secret: `{발급받은 어드민 토큰을 넘겨주세요.}`
13+
- Authorization: `{어드민 요청자의 인증토큰을 넘겨주세요.}`
14+
15+
### Query Parameter
16+
- lastId: `{optional, 다음 페이지 조회를 위한 커서}`
17+
- level: `{optional, EASY | MEDIUM | DIFFICULT}`
18+
- category: `{optional, FRONTEND | BACKEND}`
19+
- language: `{optional, KOREA | ENGLISH}`
20+
21+
## Response
22+
23+
200 OK
24+
25+
```json
26+
{
27+
"quizs": [
28+
{
29+
"id": "712345678901234567",
30+
"userId": "1234",
31+
"level": "MEDIUM",
32+
"category": "FRONTEND",
33+
"language": "KOREA",
34+
"problem": "브라우저의 reflow는 layout 계산과 관련이 있다.",
35+
"expectedAnswer": "YES",
36+
"createdAt": "2026-03-21 01:00:00",
37+
"modifiedAt": "2026-03-21 01:00:00"
38+
}
39+
],
40+
"nextId": "712345678901234567"
41+
}
42+
```
43+
44+
### Response Field
45+
- quizs: 최대 20개의 미승인 퀴즈 목록
46+
- nextId: 다음 페이지가 없으면 `null`
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Scroll quiz solve contexts by user id
2+
3+
특정 `userId``QuizSolveContext``id` 기반 no-offset 방식으로 조회합니다.
4+
5+
> [!WARN]
6+
> `userId`는 반드시 입력해야 하며, 한 번에 최대 20개를 응답합니다.
7+
8+
## Request
9+
### HTTP METHOD : `GET`
10+
### url : `https://api.gitanimals.org/admin/quizs/contexts`
11+
### RequestHeader
12+
- Admin-Secret: `{발급받은 어드민 토큰을 넘겨주세요.}`
13+
- Authorization: `{어드민 요청자의 인증토큰을 넘겨주세요.}`
14+
15+
### Query Parameter
16+
- userId: `{required, 조회할 유저 ID}`
17+
- lastId: `{optional, 다음 페이지 조회를 위한 커서}`
18+
19+
## Response
20+
21+
200 OK
22+
23+
```json
24+
{
25+
"quizSolveContexts": [
26+
{
27+
"id": "812345678901234567",
28+
"userId": "1234",
29+
"category": "BACKEND",
30+
"round": {
31+
"total": 3,
32+
"current": 1,
33+
"timeoutAt": "2026-03-21 01:00:10"
34+
},
35+
"prize": 2000,
36+
"solvedAt": "2026-03-21",
37+
"status": "SUCCESS",
38+
"createdAt": "2026-03-21 01:00:00",
39+
"modifiedAt": "2026-03-21 01:00:05"
40+
}
41+
],
42+
"nextId": "812345678901234567"
43+
}
44+
```
45+
46+
### Response Field
47+
- quizSolveContexts: 최대 20개의 풀이 컨텍스트 목록
48+
- round.total: 전체 문제 수
49+
- round.current: 현재 라운드
50+
- round.timeoutAt: 현재 라운드 제한시간, 없으면 `null`
51+
- status: `NOT_STARTED | SOLVING | SUCCESS | FAIL | DONE`
52+
- nextId: 다음 페이지가 없으면 `null`

src/main/kotlin/org/gitanimals/core/CoroutineScope.kt

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
11
package org.gitanimals.core
22

33
import jakarta.annotation.PreDestroy
4-
import kotlinx.coroutines.*
4+
import kotlinx.coroutines.CoroutineDispatcher
5+
import kotlinx.coroutines.CoroutineScope
6+
import kotlinx.coroutines.asCoroutineDispatcher
7+
import kotlinx.coroutines.launch
58
import kotlinx.coroutines.slf4j.MDCContext
6-
import org.gitanimals.core.GracefulShutdownDispatcher.executorService
9+
import org.gitanimals.core.GracefulShutdownDispatcher.graceFulShutdownExecutorServices
710
import org.slf4j.LoggerFactory
811
import org.springframework.stereotype.Component
12+
import java.util.concurrent.ExecutorService
913
import java.util.concurrent.Executors
1014
import java.util.concurrent.TimeUnit
1115

1216
object GracefulShutdownDispatcher {
1317

14-
val executorService = Executors.newFixedThreadPool(10) { runnable ->
18+
val graceFulShutdownExecutorServices: MutableList<ExecutorService> = mutableListOf()
19+
20+
private val executorService = Executors.newFixedThreadPool(10) { runnable ->
1521
Thread(runnable, "gitanimals-gracefulshutdown").apply { isDaemon = false }
16-
}
22+
}.withGracefulShutdown()
1723

18-
val dispatcher: CoroutineDispatcher = executorService.asCoroutineDispatcher()
24+
private val defaultDispatcher: CoroutineDispatcher = executorService.asCoroutineDispatcher()
1925

20-
fun gracefulLaunch(block: suspend CoroutineScope.() -> Unit) {
26+
fun ExecutorService.withGracefulShutdown(): ExecutorService {
27+
graceFulShutdownExecutorServices.add(this)
28+
return this
29+
}
30+
31+
fun gracefulLaunch(
32+
dispatcher: CoroutineDispatcher = defaultDispatcher,
33+
block: suspend CoroutineScope.() -> Unit
34+
) {
2135
CoroutineScope(dispatcher + MDCContext()).launch(block = block)
2236
}
2337
}
@@ -30,18 +44,28 @@ class GracefulShutdownHook {
3044
@PreDestroy
3145
fun tryGracefulShutdown() {
3246
logger.info("Shutting down dispatcher...")
33-
executorService.shutdown()
47+
graceFulShutdownExecutorServices.forEach {
48+
it.shutdown()
49+
}
3450
runCatching {
35-
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
51+
if (
52+
graceFulShutdownExecutorServices.any {
53+
it.awaitTermination(60, TimeUnit.SECONDS).not()
54+
}
55+
) {
3656
logger.warn("Forcing shutdown...")
37-
executorService.shutdownNow()
57+
graceFulShutdownExecutorServices.forEach {
58+
it.shutdown()
59+
}
3860
} else {
3961
logger.info("Shutdown completed gracefully.")
4062
}
4163
}.onFailure {
4264
if (it is InterruptedException) {
4365
logger.warn("Shutdown interrupted. Forcing shutdown...")
44-
executorService.shutdownNow()
66+
graceFulShutdownExecutorServices.forEach {
67+
it.shutdownNow()
68+
}
4569
Thread.currentThread().interrupt()
4670
}
4771
}

0 commit comments

Comments
 (0)