Skip to content

Commit 5dd44c5

Browse files
authored
Merge pull request #143 from ddiddit/develop
release v1.0.1
2 parents adac604 + 0abea6d commit 5dd44c5

51 files changed

Lines changed: 653 additions & 460 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/docs/개발가이드.md

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/docs/도메인모델.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/docs/용어사전.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/main/kotlin/com/didit/adapter/integration/ai/ClovaClient.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ class ClovaClient(
3030
answers: List<String>,
3131
): GeneratedDeepQuestion {
3232
val prompt = FeedbackPrompts.buildDeepQuestionPrompt(job, answers)
33+
34+
logger.debug("심화 질문 프롬프트 - job: $job, prompt:\n$prompt")
35+
3336
val result = callWithResult(prompt)
37+
3438
return parseDeepQuestion(result)
3539
}
3640

@@ -39,7 +43,11 @@ class ClovaClient(
3943
allAnswers: List<String>,
4044
): AISummaryResponse {
4145
val prompt = FeedbackPrompts.buildSummaryPrompt(job, allAnswers)
46+
47+
logger.debug("요약 프롬프트 - job: $job, prompt:\n$prompt")
48+
4249
val result = callWithResult(prompt)
50+
4351
return parseSummary(result)
4452
}
4553

@@ -57,7 +65,7 @@ class ClovaClient(
5765
ClovaMessage(role = "system", content = SYSTEM_PROMPT),
5866
ClovaMessage(role = "user", content = prompt),
5967
),
60-
maxTokens = 500,
68+
maxTokens = 3000,
6169
temperature = 0.7,
6270
topP = 0.8,
6371
repetitionPenalty = 1.1,
Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,54 @@
11
package com.didit.adapter.integration.ai
22

33
import com.didit.domain.shared.Job
4+
import org.slf4j.LoggerFactory
45
import org.springframework.core.io.ClassPathResource
56

67
object FeedbackPrompts {
8+
private val logger = LoggerFactory.getLogger(FeedbackPrompts::class.java)
9+
710
fun buildDeepQuestionPrompt(
811
job: Job?,
912
answers: List<String>,
1013
): String {
11-
val template = loadPrompt("prompts/deep-question-${job?.name?.lowercase()}.txt")
12-
return template
13-
.replace("{{q1}}", answers.getOrElse(0) { "" })
14-
.replace("{{q2}}", answers.getOrElse(1) { "" })
15-
.replace("{{q3}}", answers.getOrElse(2) { "" })
14+
val path = "prompts/deep-question-${job?.name?.lowercase()}.txt"
15+
val template = loadPrompt(path)
16+
val prompt =
17+
template
18+
.replace("{{q1}}", answers.getOrElse(0) { "" })
19+
.replace("{{q2}}", answers.getOrElse(1) { "" })
20+
.replace("{{q3}}", answers.getOrElse(2) { "" })
21+
22+
logger.debug("심화 질문 프롬프트 로드 - path: $path")
23+
logger.debug("심화 질문 프롬프트 내용:\n$prompt")
24+
25+
return prompt
1626
}
1727

1828
fun buildSummaryPrompt(
1929
job: Job?,
2030
allAnswers: List<String>,
2131
): String {
22-
val template = loadPrompt("prompts/summary-${job?.name?.lowercase()}.txt")
23-
return template
24-
.replace("{{q1}}", allAnswers.getOrElse(0) { "" })
25-
.replace("{{q2}}", allAnswers.getOrElse(1) { "" })
26-
.replace("{{q3}}", allAnswers.getOrElse(2) { "" })
27-
.replace("{{q4}}", allAnswers.getOrElse(3) { "" })
32+
val path = "prompts/summary-${job?.name?.lowercase()}.txt"
33+
val template = loadPrompt(path)
34+
val prompt =
35+
template
36+
.replace("{{q1}}", allAnswers.getOrElse(0) { "" })
37+
.replace("{{q2}}", allAnswers.getOrElse(1) { "" })
38+
.replace("{{q3}}", allAnswers.getOrElse(2) { "" })
39+
.replace("{{q4}}", allAnswers.getOrElse(3) { "" })
40+
41+
logger.debug("요약 프롬프트 로드 - path: $path")
42+
logger.debug("요약 프롬프트 내용:\n$prompt")
43+
44+
return prompt
2845
}
2946

30-
private fun loadPrompt(path: String): String = ClassPathResource(path).inputStream.bufferedReader().readText()
47+
private fun loadPrompt(path: String): String =
48+
runCatching {
49+
ClassPathResource(path).inputStream.bufferedReader().readText()
50+
}.getOrElse {
51+
logger.error("프롬프트 파일 로드 실패 - path: $path", it)
52+
throw it
53+
}
3154
}

src/main/kotlin/com/didit/adapter/integration/fcm/FcmClient.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
package com.didit.adapter.integration.fcm
22

33
import com.google.firebase.messaging.FirebaseMessaging
4+
import com.google.firebase.messaging.FirebaseMessagingException
45
import com.google.firebase.messaging.Message
6+
import com.google.firebase.messaging.MessagingErrorCode
57
import com.google.firebase.messaging.Notification
8+
import org.slf4j.LoggerFactory
69
import org.springframework.stereotype.Component
710

811
@Component
912
class FcmClient {
13+
companion object {
14+
private val logger = LoggerFactory.getLogger(FcmClient::class.java)
15+
}
16+
1017
fun sendMessage(
1118
token: String,
1219
title: String,
1320
body: String,
14-
) {
21+
): Boolean {
1522
val message =
1623
Message
1724
.builder()
@@ -24,6 +31,20 @@ class FcmClient {
2431
.build(),
2532
).build()
2633

27-
FirebaseMessaging.getInstance().send(message)
34+
return try {
35+
FirebaseMessaging.getInstance().send(message)
36+
false
37+
} catch (e: FirebaseMessagingException) {
38+
when (e.messagingErrorCode) {
39+
MessagingErrorCode.UNREGISTERED -> {
40+
logger.warn("만료된 FCM 토큰 - token: ${token.take(20)}...")
41+
true
42+
}
43+
else -> {
44+
logger.error("FCM 전송 실패 - errorCode: ${e.messagingErrorCode}, message: ${e.message}", e)
45+
throw e
46+
}
47+
}
48+
}
2849
}
2950
}

src/main/kotlin/com/didit/adapter/integration/fcm/FirebaseConfig.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ class FirebaseConfig(
2121

2222
val serviceAccount = FileInputStream(serviceAccountPath)
2323

24+
val credentials =
25+
GoogleCredentials
26+
.fromStream(serviceAccount)
27+
.createScoped(listOf("https://www.googleapis.com/auth/firebase.messaging"))
28+
2429
val options =
2530
FirebaseOptions
2631
.builder()
27-
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
32+
.setCredentials(credentials)
2833
.build()
2934

3035
return FirebaseApp.initializeApp(options)

src/main/kotlin/com/didit/adapter/integration/scheduler/NotificationScheduler.kt

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,78 @@ import com.didit.adapter.integration.fcm.FcmClient
44
import com.didit.application.notification.provided.DeviceTokenFinder
55
import com.didit.application.notification.provided.NotificationHistoryRegister
66
import com.didit.application.notification.provided.NotificationSettingFinder
7+
import com.didit.application.notification.required.DeviceTokenRepository
78
import com.didit.domain.notification.NotificationHistoryCreateRequest
89
import com.didit.domain.notification.NotificationType
10+
import org.slf4j.LoggerFactory
911
import org.springframework.scheduling.annotation.Scheduled
1012
import org.springframework.stereotype.Component
1113
import java.time.LocalTime
14+
import java.util.UUID
1215

1316
@Component
1417
class NotificationScheduler(
1518
private val notificationSettingFinder: NotificationSettingFinder,
1619
private val deviceTokenFinder: DeviceTokenFinder,
20+
private val deviceTokenRepository: DeviceTokenRepository,
1721
private val fcmClient: FcmClient,
1822
private val notificationHistoryRegister: NotificationHistoryRegister,
1923
) {
2024
companion object {
25+
private val logger = LoggerFactory.getLogger(NotificationScheduler::class.java)
26+
2127
const val DAILY_REMINDER_TITLE = "회고 작성 알림"
2228
const val DAILY_REMINDER_BODY = "하루를 마무리하며 오늘의 회고를 기록해 보세요."
2329
}
2430

25-
@Scheduled(cron = "0 * * * * *")
31+
@Scheduled(cron = "0 */10 * * * *")
2632
fun sendReminderNotifications() {
2733
val now = LocalTime.now().withSecond(0).withNano(0)
2834
val settings = notificationSettingFinder.findAllByReminderTime(now)
2935

36+
logger.info("회고 알림 스케줄러 실행 - time: $now, 대상 유저 수: ${settings.size}")
37+
3038
settings.forEach { setting ->
31-
val tokens = deviceTokenFinder.findAllByUserId(setting.userId)
39+
runCatching {
40+
sendToUser(setting.userId)
41+
}.onFailure { e ->
42+
logger.error("회고 알림 전송 실패 - userId: ${setting.userId}", e)
43+
}
44+
}
45+
}
3246

33-
if (tokens.isEmpty()) return@forEach
47+
private fun sendToUser(userId: UUID) {
48+
val tokens = deviceTokenFinder.findAllByUserId(userId)
49+
if (tokens.isEmpty()) return
3450

35-
tokens.forEach { token ->
36-
fcmClient.sendMessage(
37-
token = token.token,
38-
title = DAILY_REMINDER_TITLE,
39-
body = DAILY_REMINDER_BODY,
40-
)
51+
val successCount =
52+
tokens.count { deviceToken ->
53+
val isExpired =
54+
fcmClient.sendMessage(
55+
token = deviceToken.token,
56+
title = DAILY_REMINDER_TITLE,
57+
body = DAILY_REMINDER_BODY,
58+
)
59+
60+
if (isExpired) {
61+
deviceTokenRepository.deleteByToken(deviceToken.token)
62+
63+
logger.warn("만료된 FCM 토큰 삭제 - userId: $userId, token: ${deviceToken.token.take(20)}...")
64+
}
65+
66+
!isExpired
4167
}
4268

69+
if (successCount > 0) {
4370
notificationHistoryRegister.save(
4471
NotificationHistoryCreateRequest(
45-
userId = setting.userId,
72+
userId = userId,
4673
type = NotificationType.DAILY_REMINDER,
4774
title = DAILY_REMINDER_TITLE,
4875
body = DAILY_REMINDER_BODY,
4976
),
5077
)
78+
logger.info("회고 알림 전송 성공 - userId: $userId, 성공 토큰 수: $successCount")
5179
}
5280
}
5381
}

src/main/kotlin/com/didit/adapter/persistence/achievement/RetrospectAchievementReaderImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class RetrospectAchievementReaderImpl(
1616

1717
override fun countDeepQuestionAnswers(userId: UUID): Int =
1818
retrospectiveRepository
19-
.findAllByUserIdAndDeletedAtIsNullOrderByCreatedAtDesc(userId)
19+
.findAllCompletedByUserId(userId)
2020
.sumOf { it.countDeepQuestionAnswers() }
2121

2222
override fun countWeeklyGoalAchievedWeeks(userId: UUID): Int =

src/main/kotlin/com/didit/adapter/webapi/home/HomeApi.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,6 @@ class HomeApi(
2626
val user = userFinder.findByIdOrThrow(userId)
2727
val recentRetros = retrospectiveFinder.findRecentByUserId(userId, limit = 5)
2828
val todayCount = retrospectiveFinder.countByUserIdAndDate(userId, LocalDate.now())
29-
val latestFeedback =
30-
retrospectiveFinder
31-
.findLatestCompletedByUserId(userId)
32-
?.summary
33-
?.feedback
3429

3530
return SuccessResponse.of(
3631
HomeResponse(
@@ -40,7 +35,6 @@ class HomeApi(
4035
recentRetros.map {
4136
HomeResponse.RecentRetrospectiveResponse.from(it)
4237
},
43-
latestFeedback = latestFeedback,
4438
),
4539
)
4640
}

0 commit comments

Comments
 (0)