-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 어드민 인증 필요 시 알림을 보내도록 #828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7c304f
cfb0b50
711fee1
bd5ef55
282496c
8729a79
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.example.solidconnection.common.discord; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public enum DiscordNotificationType { | ||
|
|
||
| GPA_SCORE("학점 성적"), | ||
| LANGUAGE_TEST_SCORE("어학 성적"), | ||
| MENTOR_APPLICATION("멘토 신청"), | ||
| REPORT("신고"), | ||
| ; | ||
|
|
||
| private final String displayName; | ||
|
|
||
| DiscordNotificationType(String displayName) { | ||
| this.displayName = displayName; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.example.solidconnection.common.discord; | ||
|
|
||
| import java.util.Map; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.HttpEntity; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.scheduling.annotation.EnableAsync; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.client.RestTemplate; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @EnableAsync | ||
| @Slf4j | ||
| public class DiscordNotifier { | ||
|
|
||
| private static final String ADMIN_PAGE_URL = "https://admins.solid-connection.com"; | ||
|
|
||
| private final RestTemplate restTemplate; | ||
|
|
||
| @Value("${discord.webhook-url:}") | ||
| private String webhookUrl; | ||
|
|
||
| @Value("${spring.profiles.active:}") | ||
| private String environment; | ||
|
|
||
| @Async | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the shared async executor is full (it is bounded in Useful? React with 👍 / 👎. |
||
| public void notify(DiscordNotificationType type, String applicantInfo) { | ||
| if (webhookUrl.isBlank()) { | ||
| return; | ||
| } | ||
| try { | ||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setContentType(MediaType.APPLICATION_JSON); | ||
| HttpEntity<Map<String, String>> request = new HttpEntity<>(Map.of("content", buildMessage(type, applicantInfo)), headers); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this notifier is called from score and mentor submissions, Useful? React with 👍 / 👎. |
||
| restTemplate.postForEntity(webhookUrl, request, Void.class); | ||
| } catch (Exception e) { | ||
| log.error("Discord 검수 알림 전송 실패. type={}, applicantInfo={}", type, applicantInfo, e); | ||
| } | ||
|
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Files matching DiscordNotifier:"
fd -i 'DiscordNotifier' . || true
echo
echo "Git status/stat:"
git diff --stat || true
echo
echo "DiscordNotifier outline:"
file="$(fd -i '^DiscordNotifier\.java$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
echo "FILE=$file"
wc -l "$file"
ast-grep outline "$file" || true
echo
echo "DiscordNotifier relevant lines:"
cat -n "$file" | sed -n '1,180p'
fi
echo
echo "Search Discord notifier usages and related outbox/queue terms:"
rg -n "DiscordNotifier|sendExam|exam|outbox|queue|retries?|retry|DeadLetter|DLQ|실패|실패한|성공" src/main/java src/test/java 2>/dev/null || trueRepository: solid-connection/solid-connect-server Length of output: 50394 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Project configuration files:"
git ls-files | rg '(^|/)(build\.gradle|build\.gradle\.kts|pom\.xml|gradle\.properties|settings\.gradle|docker-compose\.ya?ml|application.*\.ya?ml|application.*\.properties)$' || true
echo
echo "Search notification infrastructure:"
rg -n "AsyncEventListener|ApplicationEvent.|`@Transactional`|TransactionalEventListener|`@EventListener`|Outbox|Outbox|Queue|Queue|CloudEvent|Notification|Notifier\\.|sendExam|sendExam" src/main/java 2>/dev/null || trueRepository: solid-connection/solid-connect-server Length of output: 17180 1. 실패한 Discord 알림을 저장하고 재시도하세요.
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| private String buildMessage(DiscordNotificationType type, String applicantInfo) { | ||
| return "[%s] %s 검수 요청이 등록되었습니다.\n신청자: %s\n관리자 페이지: %s" | ||
| .formatted(environment.toUpperCase(), type.getDisplayName(), applicantInfo, ADMIN_PAGE_URL); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
|
|
||
| import com.example.solidconnection.application.domain.Gpa; | ||
| import com.example.solidconnection.application.domain.LanguageTest; | ||
| import com.example.solidconnection.common.discord.DiscordNotificationType; | ||
| import com.example.solidconnection.common.discord.DiscordNotifier; | ||
| import com.example.solidconnection.common.exception.CustomException; | ||
| import com.example.solidconnection.s3.domain.UploadPath; | ||
| import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; | ||
|
|
@@ -37,6 +39,7 @@ public class ScoreService { | |
| private final LanguageTestScoreRepository languageTestScoreRepository; | ||
| private final SiteUserRepository siteUserRepository; | ||
| private final HomeUniversityQueryService homeUniversityQueryService; | ||
| private final DiscordNotifier discordNotifier; | ||
|
|
||
| @Transactional | ||
| public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, MultipartFile file) { | ||
|
|
@@ -46,6 +49,7 @@ public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, Mul | |
| Gpa gpa = new Gpa(gpaScoreRequest.gpa(), gpaScoreRequest.gpaCriteria(), uploadedFile.fileUrl()); | ||
| GpaScore newGpaScore = new GpaScore(gpa, siteUser); | ||
| GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); | ||
| discordNotifier.notify(DiscordNotificationType.GPA_SCORE, siteUser.getNickname()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Find candidate service files:\n'
fd -a 'ScoreService\.java|MentorApplicationService\.java|ReportService\.java|.*Notifier.*\.java|.*Notification.*\.java|.*Listener.*\.java|.*Async.*\.java' src/main/java 2>/dev/null | sed 's#^\./##' || true
printf '\nScoreService outline/contents:\n'
ast-grep outline src/main/java/com/example/solidconnection/score/service/ScoreService.java 2>/dev/null || true
cat -n src/main/java/com/example/solidconnection/score/service/ScoreService.java
printf '\nMentorApplicationService outline/contents:\n'
ast-grep outline src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java 2>/dev/null || true
cat -n src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java
printf '\nReportService outline/contents:\n'
ast-grep outline src/main/java/com/example/solidconnection/report/service/ReportService.java 2>/dev/null || true
cat -n src/main/java/com/example/solidconnection/report/service/ReportService.java
printf '\nSearch for DiscordNotifier implementation and async/disk notification handling:\n'
rg -n "interface DiscordNotifier|class .*Discord|notify\\(|`@Async`|ApplicationEvent|AFTER_COMMIT|TransactionSynchronizationManager|TransactionManager|EventPublisher|save\\(" src/main/java src/test/java 2>/dev/null | head -n 200Repository: solid-connection/solid-connect-server Length of output: 38630 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'DiscordNotifier implementation:\n'
cat -n src/main/java/com/example/solidconnection/common/discord/DiscordNotifier.java
printf '\nAsync transactional configuration:\n'
cat -n src/main/java/com/example/solidconnection/common/config/sync/AsyncConfig.java
printf '\nStatic verifier: direct Discord notifier calls from `@Transactional` save paths:\n'
python3 - <<'PY'
from pathlib import Path
import re
targets = [
('src/main/java/com/example/solidconnection/score/service/ScoreService.java', [52, 65]),
('src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java', [66]),
('src/main/java/com/example/solidconnection/report/service/ReportService.java', [45]),
]
notifier = Path('src/main/java/com/example/solidconnection/common/discord/DiscordNotifier.java').read_text()
for file, lines in targets:
src = Path(file).read_text().splitlines()
in_transactional = False
for i, line in enumerate(src, 1):
if '`@Transactional`' in line:
in_transactional = True
if i in lines:
call = line.strip()
print(f'{file}:{i}: transactional_context={in_transactional} call={call}')
if 'public ' in line and 'void' in line or (i in lines and in_transactional):
in_transactional = 'return' not in line or 'void' not in line
has_async_notify = bool(re.search(r'@\s*Async[\s\n]*public\s+void\s+notify\b', notifier))
has_spring_async_annotation = bool(re.search(r'org\.springframework\..scheduling\.\w+\.annotation\.\s*`@Async`', Path('pom.xml').read_text() if Path('pom.xml').exists() else ''))
print(f'DiscordNotifier.notify is `@Async`={has_async_notify}')
print(f'Service paths dispatch outside transaction commit boundary; Spring `@Async` tasks queued while current request path returns before notification completion.')
PYRepository: solid-connection/solid-connect-server Length of output: 4433 1. Discord 알림 트랜잭션 커밋과 분리하세요. 네 위치 모두 [low Effort_and_high_reward] 📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| return savedNewGpaScore.getId(); | ||
| } | ||
|
|
||
|
|
@@ -58,6 +62,7 @@ public Long submitLanguageTestScore(long siteUserId, LanguageTestScoreRequest la | |
| languageTestScoreRequest.languageTestScore(), uploadedFile.fileUrl()); | ||
| LanguageTestScore newScore = new LanguageTestScore(languageTest, siteUser); | ||
| LanguageTestScore savedNewScore = languageTestScoreRepository.save(newScore); | ||
| discordNotifier.notify(DiscordNotificationType.LANGUAGE_TEST_SCORE, siteUser.getNickname()); | ||
| return savedNewScore.getId(); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
환경 값이 비어 있으면 접두사를 생략하세요.
spring.profiles.active의 기본값이 빈 문자열이므로 환경이 지정되지 않으면 메시지가[] 학점 성적 검수 요청...형식으로 시작합니다. 빈 환경을 유지하려면 환경 접두사 전체를 생략하세요.Also applies to: 46-48
🤖 Prompt for AI Agents