Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
04247e0
feat: 미션/레벨 도메인 엔티티 설계 및 미션 시스템 구현
hajimeong Jun 25, 2026
5dc57d8
fix: 로컬 테스트용 application-local.yaml 설정 복구
hajimeong Jun 25, 2026
90f694b
feat: 유저의 현재 미션 현황 조회 API 기능 구현
hajimeong Jun 29, 2026
594645c
test: Mission 현재 미션 현황 조회 API 테스트 코드 작성
hajimeong Jun 29, 2026
d1edbe4
docs: 미션 조회 API 문서 작성
hajimeong Jun 29, 2026
106c0f4
- Badge 엔티티에 category, condition(VO), iconUrl, congratsTitle, congrat…
leejaeseok-98 Jun 29, 2026
59eb800
feat: streak 시스템 구현
leejaeseok-98 Jun 29, 2026
bed7c9a
Merge branch 'develop' into DIT-213-streak-system-dev
leejaeseok-98 Jun 29, 2026
b0233b8
feat: 팝업 확인 처리 API 구현
hajimeong Jun 29, 2026
0495341
refactor: 팝업 확인 처리 API 엔드포인트에 missionId Path variable 제거
hajimeong Jun 29, 2026
98ed53f
test: 팝업 확인 API 테스트 코드 및 UserMissionService 코드 테스트 코드 추가
hajimeong Jun 29, 2026
cd4f60b
docs: 팝업 확인 처리 API 문서 추가
hajimeong Jun 29, 2026
ecfc011
refactor: 레벨업 팝업 확인 API 엔드포인트 문서에 맞게 수정
hajimeong Jun 29, 2026
273fe80
refactor: 레벨업 팝업과 실패 팝업 분리를 위한 /missions/level-up/confirm API 기능 수정
hajimeong Jun 29, 2026
defcd9a
feat: missions/retry 실패 팝업 및 재시작 기능 API 구현
hajimeong Jun 29, 2026
8f114b6
test: 미션 실패 및 재도전 API 테스트 코드 작성
hajimeong Jun 29, 2026
8c2f682
docs: retry API 문서 추가
hajimeong Jun 29, 2026
bc9c291
feat: 미션 배치 스케줄러 구현
hajimeong Jun 29, 2026
24d874b
test: MissionScheduler 테스트 코드 작성
hajimeong Jun 29, 2026
fbd1f9d
refactor: UserMission 엔티티 json 제거 및 구조 정리
hajimeong Jun 29, 2026
7e08490
chore: develop 브랜치 업데이트 반영
hajimeong Jun 29, 2026
ef9b3e3
test: 테스트 코드 수정
hajimeong Jun 29, 2026
e3301ff
fix(db): 마이그레이션 버전 충돌 해소 (V34~V37 → V37~V40)
Develop-KIM Jun 30, 2026
9ecec7d
fix(db): 마이그레이션 버전 충돌 해소 (V34/V35 → V35/V36)
Develop-KIM Jun 30, 2026
ebfff72
fix(mission): 미션 시스템 기획 불일치 버그 수정
Develop-KIM Jun 30, 2026
edead91
Merge pull request #263 from ddiddit/feat/DIT-209-mission-level-system
Develop-KIM Jun 30, 2026
de7270a
Merge pull request #262 from ddiddit/DIT-213-streak-system-dev
Develop-KIM Jun 30, 2026
c7612d6
feat(admin): 배지 관리 CRUD 및 레벨/미션/배지 운영 통계 API
Develop-KIM Jun 30, 2026
a99eb97
fix(admin): 배지 condition param 스키마 type을 number로 표준화
Develop-KIM Jun 30, 2026
805209a
feat(home,profile): 홈 안읽은 알림 플래그 및 프로필 레벨 응답 추가
Develop-KIM Jun 30, 2026
7febab4
fix(admin): 코드리뷰 반영 - 레벨 분포 누락 보정 및 배지 param 검증
Develop-KIM Jun 30, 2026
9fd5697
perf(notification): 홈 unread 조회용 복합 인덱스 추가
Develop-KIM Jun 30, 2026
9d5c01f
Merge pull request #264 from ddiddit/feat/admin-badge-mission-management
Develop-KIM Jun 30, 2026
aa664cb
Merge pull request #265 from ddiddit/feat/home-profile-achievement
Develop-KIM Jun 30, 2026
48f1025
chore(db): 구 배지 획득 기록(orphan user_badges) 정리
Develop-KIM Jun 30, 2026
94ff1dd
Merge pull request #267 from ddiddit/chore/cleanup-orphan-user-badges
Develop-KIM Jun 30, 2026
469795b
fix(mission): 주간 미션 KST 주차 경계를 ServiceTime으로 UTC 변환
Develop-KIM Jun 30, 2026
7230ce6
Merge pull request #268 from ddiddit/fix/mission-weekly-timezone
Develop-KIM Jun 30, 2026
d36e717
fix(mission): /missions/current 레벨 미초기화 시 500 대신 Lv.1 자동 생성
Develop-KIM Jun 30, 2026
59353c5
Merge pull request #269 from ddiddit/fix/missions-current-init
Develop-KIM Jun 30, 2026
62e1d39
feat(home): /api/v2/home에 현재 미션 포함(홈 집계)
Develop-KIM Jun 30, 2026
233a188
feat(level): 레벨을 0부터 시작 (currentLevel=완료 미션 수)
Develop-KIM Jun 30, 2026
322c018
Merge pull request #271 from ddiddit/feat/level-zero-start
Develop-KIM Jun 30, 2026
a0619e8
Merge pull request #270 from ddiddit/feat/home-aggregate-mission
Develop-KIM Jun 30, 2026
b355253
feat(badge): 배지 카운트 비소급 - launch 이후 회고만 집계
Develop-KIM Jun 30, 2026
689b0b6
fix(mission): 레벨 1 도달 시 레벨업 팝업 미노출
Develop-KIM Jun 30, 2026
7a3b371
Merge pull request #272 from ddiddit/feat/badge-non-retroactive
Develop-KIM Jun 30, 2026
7606d65
Merge pull request #273 from ddiddit/fix/level1-no-popup
Develop-KIM Jun 30, 2026
adb010d
feat: 5xx 에러율 디스코드 알림 형식 개선
hyoj-dev Jun 30, 2026
7a58020
[DIT-218] 5xx 에러율 디스코드 알림 형식 개선 #274
hyoj-dev Jun 30, 2026
825aaa8
fix(achievement): 미션 진행률을 레벨업 시점 이후 회고만 카운트
Develop-KIM Jul 1, 2026
8c168f6
Merge: 미션 진행률 레벨업 시점 이후 카운트 픽스
Develop-KIM Jul 1, 2026
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
52 changes: 48 additions & 4 deletions deploy/prod/grafana/provisioning/alerting/contact-points.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,59 @@ contactPoints:
settings:
url: $DISCORD_MONITOR_WEBHOOK_URL
use_discord_username: false
title: '{{ if eq .Status "firing" }}{{ else }}[복구] {{ end }} {{ .CommonLabels.alertname }}'
title: '{{ if eq .CommonLabels.alertname "[CRITICAL] 5xx 에러율 1% 초과" }}{{ if eq .Status "firing" }}{{ .CommonLabels.alertname }}{{ else }}[RESOLVED] 5xx 에러율 정상화{{ end }}{{ else }}{{ if eq .Status "firing" }}{{ else }}[복구] {{ end }}{{ .CommonLabels.alertname }}{{ end }}'
message: |
{{ if eq .CommonLabels.alertname "[CRITICAL] 5xx 에러율 1% 초과" }}
{{ $notificationStatus := .Status }}
{{ range .Alerts }}
{{ if eq $notificationStatus "firing" }}
Time : {{ .StartsAt | tz "Asia/Seoul" | date "2006-01-02 15:04:05" }} (KST)
5xx Rate : {{ .Annotations.error_rate }}

────────────────────────

{{ .Annotations.summary }}

확인 필요
{{ .Annotations.description }}


────────────────────────

Explore Logs
{{ .Annotations.explore_url }}

Alert
{{ .GeneratorURL }}

Dashboard
{{ .DashboardURL }}
{{ else }}
Detected : {{ .StartsAt | tz "Asia/Seoul" | date "2006-01-02 15:04:05" }} (KST)
Resolved : {{ .EndsAt | tz "Asia/Seoul" | date "2006-01-02 15:04:05" }} (KST)
Duration : {{ .EndsAt.Sub .StartsAt }}

최근 5분 기준 5xx 에러율이 1% 이하로 내려갔습니다.

────────────────────────

Explore Logs
{{ .Annotations.explore_url }}

Alert
{{ .GeneratorURL }}

Dashboard
{{ .DashboardURL }}
{{ end }}
{{ end }}
{{ else }}
{{ range .Alerts }}
{{ .Annotations.summary }}

확인 필요
{{ .Annotations.description }}

관련 모니터링 링크
- Grafana Alert: {{ .GeneratorURL }}
{{ end }}

{{ end }}
77 changes: 9 additions & 68 deletions deploy/prod/grafana/provisioning/alerting/rules.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
apiVersion: 1

deleteRules:
- orgId: 1
uid: didit-http-5xx-detected

groups:
- orgId: 1
name: didit-prod-critical
Expand Down Expand Up @@ -69,74 +73,6 @@ groups:
- 최근 트래픽 증가 여부
- 메모리 누수 및 OOM 로그

- uid: didit-http-5xx-detected
title: '[CRITICAL] 5xx 에러 발생'
condition: C
data:
- refId: A
datasourceUid: prometheus
relativeTimeRange:
from: 120
to: 0
model:
refId: A
expr: |
sum(
increase(
http_server_requests_seconds_count{
environment="prod",
status=~"5.."
}[1m]
)
)
instant: true
intervalMs: 1000
maxDataPoints: 43200

- refId: B
datasourceUid: __expr__
model:
refId: B
type: reduce
expression: A
reducer: last
datasource:
type: __expr__
uid: __expr__

- refId: C
datasourceUid: __expr__
model:
refId: C
type: threshold
expression: B
conditions:
- evaluator:
type: gt
params: [ 0 ]
operator:
type: and
query:
params: [ C ]
reducer:
type: last
type: query
datasource:
type: __expr__
uid: __expr__

noDataState: OK
execErrState: Alerting
for: 0s
labels:
severity: critical
annotations:
summary: 5xx 에러가 발생했습니다.
description: |
- API 서버 ERROR 로그
- 발생 API 및 예외 내용
- DB 및 외부 연동 서비스 상태

- uid: didit-http-5xx-rate-critical
title: '[CRITICAL] 5xx 에러율 1% 초과'
condition: C
Expand Down Expand Up @@ -204,14 +140,19 @@ groups:
noDataState: OK
execErrState: Alerting
for: 2m
dashboardUid: dfp8jkcluca2oe

labels:
severity: critical

annotations:
summary: 5xx 에러율이 1%를 초과했습니다.
error_rate: '{{ humanizePercentage $values.B.Value }}'
description: |
- API 서버 로그
- 최근 배포 여부
- 특정 API 및 외부 서비스 장애 여부
explore_url: https://monitoring.didit.ai.kr/explore?schemaVersion=1&panes=%7B%22ttc%22:%7B%22datasource%22:%22loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%7Bcontainer%3D~%5C%22didit-api-%28blue%7Cgreen%29%5C%22%7D%20%7C%3D%20%5C%22ERROR%5C%22%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22loki%22%7D,%22editorMode%22:%22code%22%7D%5D,%22range%22:%7B%22from%22:%22now-15m%22,%22to%22:%22now%22%7D,%22panelsState%22:%7B%22logs%22:%7B%22columns%22:%7B%220%22:%22Time%22,%221%22:%22Line%22%7D,%22visualisationType%22:%22table%22,%22labelFieldName%22:%22labels%22%7D%7D%7D%7D&orgId=1

- uid: didit-hikaricp-usage-critical
title: '[CRITICAL] HikariCP 커넥션 풀 사용률 90% 초과'
Expand Down
1 change: 1 addition & 0 deletions src/docs/asciidoc/app/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ include::auth.adoc[]
include::user.adoc[]
include::retrospect.adoc[]
include::badge.adoc[]
include::mission.adoc[]
include::notice.adoc[]
include::inquiry.adoc[]
include::project.adoc[]
Expand Down
32 changes: 32 additions & 0 deletions src/docs/asciidoc/app/mission.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
= 미션 API

== 현재 미션 조회

=== Request

include::{snippets}/mission/current/http-request.adoc[]

=== Response

include::{snippets}/mission/current/http-response.adoc[]
include::{snippets}/mission/current/response-fields.adoc[]

== 레벨업 팝업 확인

=== Request

include::{snippets}/mission/level-up-confirm/http-request.adoc[]

=== Response

include::{snippets}/mission/level-up-confirm/http-response.adoc[]

== 실패 팝업 확인 및 재도전

=== Request

include::{snippets}/mission/retry/http-request.adoc[]

=== Response

include::{snippets}/mission/retry/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.didit.adapter.integration.scheduler

import com.didit.application.achievement.required.MissionRepository
import com.didit.application.achievement.required.UserMissionRepository
import com.didit.application.retrospect.required.RetrospectiveRepository
import com.didit.domain.shared.ServiceTime
import jakarta.transaction.Transactional
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.temporal.TemporalAdjusters

@Component
class MissionScheduler(
private val userMissionRepository: UserMissionRepository,
private val missionRepository: MissionRepository,
private val retrospectiveRepository: RetrospectiveRepository,
) {
companion object {
private val logger = LoggerFactory.getLogger(MissionScheduler::class.java)
}

@Scheduled(cron = "0 0 * * * *")
@Transactional
fun checkExpiredLv2Missions() {
val expiredMissions = userMissionRepository.findExpiredLv2Missions()

expiredMissions.forEach { userMission ->
userMission.setFailureWaitingConfirm()
userMissionRepository.save(userMission)
}

logger.info("Lv2 만료 미션 검사 완료 - count: ${expiredMissions.size}")
}

@Scheduled(cron = "0 0 0 ? * MON")
@Transactional
fun checkConsecutiveWeekFailure() {
val consecutiveWeekMissions = userMissionRepository.findConsecutiveWeekMissionsInProgress()

consecutiveWeekMissions.forEach { userMission ->
val lastWeekStart =
ServiceTime.today().with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY)).minusWeeks(1)

val lastWeekRetroCount =
retrospectiveRepository.countCompletedByUserIdAndPeriod(
userId = userMission.userId,
from = ServiceTime.startOfDayUtc(lastWeekStart),
to = ServiceTime.startOfDayUtc(lastWeekStart.plusWeeks(1)),
)

if (lastWeekRetroCount == 0) {
userMission.setFailureWaitingConfirm()
userMissionRepository.save(userMission)
logger.info(
"연속주차 실패 - userId: ${userMission.userId}, missionId: ${userMission.missionId}",
)
}
}

logger.info("연속주차 실패 검사 완료 - total: ${consecutiveWeekMissions.size}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.didit.adapter.persistence.achievement

import com.didit.application.achievement.required.OrganizationAchievementReader
import com.didit.application.organization.required.ProjectRepository
import com.didit.application.retrospect.required.RetrospectiveRepository
import org.springframework.stereotype.Component
import java.util.UUID

@Component
class OrganizationAchievementReaderImpl(
private val projectRepository: ProjectRepository,
private val retrospectiveRepository: RetrospectiveRepository,
) : OrganizationAchievementReader {
override fun countProjects(userId: UUID): Int = projectRepository.findAllByUserIdAndDeletedAtIsNull(userId).size

override fun countProjectAssignedRetros(userId: UUID): Int =
retrospectiveRepository
.findAllCompletedByUserId(userId)
.count { it.projectId != null }

override fun maxRetroCountInOneProject(userId: UUID): Int =
retrospectiveRepository
.findAllCompletedByUserId(userId)
.mapNotNull { it.projectId }
.groupingBy { it }
.eachCount()
.values
.maxOrNull()
?: 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,53 @@ import com.didit.application.achievement.required.RetrospectAchievementReader
import com.didit.application.retrospect.required.RetrospectiveRepository
import com.didit.domain.retrospect.RetroStatus
import com.didit.domain.shared.ServiceTime
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.UUID

@Component
class RetrospectAchievementReaderImpl(
private val retrospectiveRepository: RetrospectiveRepository,
@Value("\${achievement.feature-launched-at:2026-06-30T00:00:00}") private val featureLaunchedAt: LocalDateTime,
) : RetrospectAchievementReader {
override fun countCompletedRetros(userId: UUID): Int =
retrospectiveRepository.countByUserIdAndStatusAndDeletedAtIsNull(userId, RetroStatus.COMPLETED)
companion object {
private const val MAX_CONSECUTIVE_WEEKS_SCAN = 60
}

override fun countDeepQuestionAnswers(userId: UUID): Int =
retrospectiveRepository
.findAllCompletedByUserId(userId)
.sumOf { it.countDeepQuestionAnswers() }
override fun countCompletedRetros(userId: UUID): Int = completedAtsSinceLaunch(userId).size

override fun countRetrosInWeek(
userId: UUID,
weekStartKst: LocalDate,
): Int = weekCountsByMondayKst(userId)[weekStartKst] ?: 0

override fun countConsecutiveWeeksWithMinRetros(
userId: UUID,
weeklyMinCount: Int,
): Int {
val weekCounts = weekCountsByMondayKst(userId)
var weekStart = ServiceTime.today().with(DayOfWeek.MONDAY)
var consecutive = 0
repeat(MAX_CONSECUTIVE_WEEKS_SCAN) {
val count = weekCounts[weekStart] ?: 0
if (count < weeklyMinCount) return consecutive
consecutive += 1
weekStart = weekStart.minusWeeks(1)
}
return consecutive
}

override fun countWeeklyGoalAchievedWeeks(userId: UUID): Int =
private fun weekCountsByMondayKst(userId: UUID): Map<LocalDate, Int> =
completedAtsSinceLaunch(userId)
.map { ServiceTime.toServiceDate(it) }
.groupingBy { it.with(DayOfWeek.MONDAY) }
.eachCount()

private fun completedAtsSinceLaunch(userId: UUID): List<LocalDateTime> =
retrospectiveRepository
.findCompletedAtByUserIdAndStatusAndDeletedAtIsNull(userId, RetroStatus.COMPLETED)
.map { ServiceTime.toServiceDate(it) }
.distinct()
.groupBy { it.with(DayOfWeek.MONDAY) }
.count { (_, dates) -> dates.size >= 3 }
.filter { !it.isBefore(featureLaunchedAt) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class BadgeApi(
@CurrentUserId userId: UUID,
): SuccessResponse<List<BadgeResponse>> = SuccessResponse.of(badgeFinder.findAll(userId))

@RequireAuth
@GetMapping("/recent")
fun findRecent(
@CurrentUserId userId: UUID,
): SuccessResponse<List<BadgeResponse>> = SuccessResponse.of(badgeFinder.findRecent(userId))

@RequireAuth
@GetMapping("/popup")
fun findUnnotified(
Expand Down
Loading
Loading