Skip to content

Commit

Permalink
Merge pull request #236 from ssu-student-union/feat/232-discord-webho…
Browse files Browse the repository at this point in the history
…ok-signup

[feat] #232 디스코드 웹훅, 봇 사용자수 알림
  • Loading branch information
JangInho authored Mar 3, 2025
2 parents c15087a + e64e73b commit 3df017a
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ussum.homepage.application.notification.service;

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class DiscordWebhookScheduler {
private final DiscordWebhookService discordWebhookService;

@Scheduled(cron = "0 0 0 * * ?")
public void sendUserStatisticsPeriodically() {
discordWebhookService.sendToDiscord();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ussum.homepage.application.notification.service;

import lombok.*;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import ussum.homepage.application.user.service.UserService;

@Service
@RequiredArgsConstructor
public class DiscordWebhookService {
private final UserService userService;
private final RestTemplate restTemplate = new RestTemplate();

private final String DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/1345355644236075018/ZMpO9d7Jh30jvSKf5U3gw9i9xoczFAX9f6DLN8YadBYBZDM9WxRpSr-kz1KyQbniikQA"; // 🔥 여기에 실제 웹훅 URL을 입력하세요


public void sendToDiscord() {
try {
String jsonMessage = userService.generateDiscordMessage();

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<String> requestEntity = new HttpEntity<>(jsonMessage, headers);

ResponseEntity<String> response = restTemplate.exchange(
DISCORD_WEBHOOK_URL, HttpMethod.POST, requestEntity, String.class);

} catch (Exception e) {
// 디스코드로 웹훅으로 인해 앱이 죽지 않게 따로 처리하지 않음.
System.err.println("디스코드 웹훅 전송 실패: " + e.getMessage());
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import ussum.homepage.application.user.service.dto.request.TokenRequest;
import ussum.homepage.application.user.service.dto.response.MyPageInfoResponse;
import ussum.homepage.application.user.service.dto.response.UserInfoResponse;
import ussum.homepage.domain.user.service.UserAnalyzer;
import ussum.homepage.global.ApiResponse;
import ussum.homepage.global.config.auth.UserId;

Expand Down Expand Up @@ -54,5 +55,11 @@ public ApiResponse<?> deleteUser(@Parameter(hidden = true) @UserId Long userId)
return ApiResponse.onSuccess(null);
}

// TODO(inho): 디스코드 컨트롤러로 빼야함
@GetMapping("/discord")
public ResponseEntity<String> getUserStatistics() {
String jsonMessage = userService.generateDiscordMessage();
return ResponseEntity.ok(jsonMessage);
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package ussum.homepage.application.user.service;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
Expand All @@ -21,7 +27,9 @@
import ussum.homepage.domain.postlike.service.PostReactionModifier;
import ussum.homepage.domain.reaction.service.PostCommentReactionModifier;
import ussum.homepage.domain.reaction.service.PostReplyCommentReactionModifier;
import ussum.homepage.domain.user.MonthlySignupStats;
import ussum.homepage.domain.user.User;
import ussum.homepage.domain.user.service.UserAnalyzer;
import ussum.homepage.domain.user.service.UserManager;
import ussum.homepage.domain.user.service.UserModifier;
import ussum.homepage.domain.user.service.UserReader;
Expand All @@ -42,6 +50,7 @@ public class UserService {
private final UserModifier userModifier;
private final UserMapper userMapper;
private final UserManager userManager;
private final UserAnalyzer userAnalyzer;
private final PasswordEncoder passwordEncoder;
private final PostModifier postModifier;
private final MemberManager memberManager;
Expand Down Expand Up @@ -134,4 +143,40 @@ public void deleteUser(Long userId) {
userModifier.deleteUser(userId);
}

public String generateDiscordMessage() {
Long totalUsers = userAnalyzer.getTotalUserCount();
Long yearlyUsers = userAnalyzer.getNewUserCountBetween(LocalDateTime.now().withDayOfYear(1), LocalDateTime.now());
Long monthlyUsers = userAnalyzer.getNewUserCountBetween(LocalDateTime.now().withDayOfMonth(1), LocalDateTime.now());
Long dailyUsers = userAnalyzer.getNewUserCountBetween(LocalDateTime.now().truncatedTo(ChronoUnit.DAYS), LocalDateTime.now());
Long last5MinUsers = userAnalyzer.getNewUserCountBetween(LocalDateTime.now().minusMinutes(5), LocalDateTime.now());

List<MonthlySignupStats> monthlyStats = userAnalyzer.getMonthlySignupStats(LocalDateTime.now().getYear());
StringBuilder monthlyStatsBuilder = new StringBuilder();
for (MonthlySignupStats stat : monthlyStats) {
monthlyStatsBuilder.append(String.format("%d월: %d명\n", stat.getMonth(), stat.getCount()));
}

// 🎯 JSON 변환을 위해 Map 사용
Map<String, Object> payload = new HashMap<>();
payload.put("content", "🔥사용자 통계가 업데이트되었습니다🔥");
payload.put("embeds", List.of(Map.of(
"title", "📊 사용자 통계",
"fields", List.of(
Map.of("name", "[전체 가입자 수]", "value", totalUsers, "inline", false),
Map.of("name", "[신규 가입자 수]", "value", String.format("올해: %d\n이번 달: %d\n오늘: %d", yearlyUsers, monthlyUsers, dailyUsers), "inline", false),
Map.of("name", "[지난 5분간 가입자]", "value", last5MinUsers, "inline", false),
Map.of("name", "[월별 가입자]", "value", monthlyStatsBuilder.toString(), "inline", false)
),
"footer", Map.of("text", "업데이트: " + LocalDateTime.now())
)));

try {
// 🎯 JSON 변환
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(payload);
} catch (Exception e) {
throw new RuntimeException("JSON parsing error", e);
}
}

}
13 changes: 13 additions & 0 deletions src/main/java/ussum/homepage/domain/user/MonthlySignupStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ussum.homepage.domain.user;

import lombok.*;

/// TODO(inho): DDD 구조에 맞게 다시 리팩토링 해야함
@Getter
@AllArgsConstructor
public class MonthlySignupStats {
private int year;
private int month;
private Long count;

}
5 changes: 5 additions & 0 deletions src/main/java/ussum/homepage/domain/user/UserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ussum.homepage.application.user.service.dto.request.OnBoardingRequest;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

Expand All @@ -13,4 +14,8 @@ public interface UserRepository {
User save(User user);
void updateOnBoardingUser(Long userId, OnBoardingRequest request);
void deleteUser(Long id);

Long findTotalUserCount();
Long findNewUserCountBetween(LocalDateTime start, LocalDateTime end);
List<MonthlySignupStats> findMonthlySignupStats(int year);
}
27 changes: 27 additions & 0 deletions src/main/java/ussum/homepage/domain/user/service/UserAnalyzer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ussum.homepage.domain.user.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ussum.homepage.domain.user.MonthlySignupStats;
import ussum.homepage.domain.user.UserRepository;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UserAnalyzer {
private final UserRepository userRepository;

public Long getTotalUserCount() {
return userRepository.findTotalUserCount();
}

public Long getNewUserCountBetween(LocalDateTime start, LocalDateTime end) {
return userRepository.findNewUserCountBetween(start, end);
}

public List<MonthlySignupStats> getMonthlySignupStats(int year) {
return userRepository.findMonthlySignupStats(year);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package ussum.homepage.infra.jpa.user;

import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import ussum.homepage.application.user.service.dto.request.OnBoardingRequest;
import ussum.homepage.domain.user.MonthlySignupStats;
import ussum.homepage.domain.user.User;
import ussum.homepage.domain.user.UserRepository;
import ussum.homepage.infra.jpa.user.repository.UserJpaRepository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static ussum.homepage.infra.jpa.user.entity.QUserEntity.userEntity;
Expand Down Expand Up @@ -55,4 +59,37 @@ public void updateOnBoardingUser(Long userId, OnBoardingRequest request) {
public void deleteUser(Long userId) {
userJpaRepository.deleteById(userId);
}

@Override
public Long findTotalUserCount() {
return queryFactory
.select(userEntity.count())
.from(userEntity)
.fetchOne();
}

@Override
public Long findNewUserCountBetween(LocalDateTime start, LocalDateTime end) {
return queryFactory
.select(userEntity.count())
.from(userEntity)
.where(userEntity.createdAt.between(start, end))
.fetchOne();
}

@Override
public List<MonthlySignupStats> findMonthlySignupStats(int year) {
return queryFactory
.select(Projections.constructor(
MonthlySignupStats.class,
userEntity.createdAt.year(),
userEntity.createdAt.month(),
userEntity.count()
))
.from(userEntity)
.where(userEntity.createdAt.year().eq(year))
.groupBy(userEntity.createdAt.year(), userEntity.createdAt.month())
.orderBy(userEntity.createdAt.month().asc())
.fetch();
}
}

0 comments on commit 3df017a

Please sign in to comment.