Skip to content

Commit ba4e49e

Browse files
authored
Merge pull request #334 from SOLPLY/feat/#308-alert-by-new-user-create
[FEAT] 신규 유저 가입시 디코 알람 기능 구현(+ 공유된 코스만 조회)
2 parents 188731d + 9b31ea4 commit ba4e49e

File tree

7 files changed

+113
-0
lines changed

7 files changed

+113
-0
lines changed

src/main/java/org/sopt/solply_server/domain/course/service/CourseService.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ public CourseAddPlaceResponse addPlaceToCourse(final Long userId, final Long pla
197197
public CourseDetailGetResponse getCourseDetailsById(final Long userId, final Long courseId) {
198198
Course course = entityLoader.getActiveCourseWithTagsAndPlaces(courseId);
199199

200+
if (!isSharedCourse(course, userId)) throw new BusinessException(ErrorCode.NOT_SHARED_COURSE);
201+
200202
Tag courseTag = course.getTag();
201203
tagValidator.validateCourseTag(courseTag);
202204

@@ -283,6 +285,7 @@ public CourseBookmarkListGetResponse getBookmarkedCourses(
283285

284286
final List<CourseInfoDto> courseInfoDtos = createSortedCourseInfoDtoList(
285287
filteredCourses,
288+
userId,
286289
courseIdCreatedAtMap,
287290
candidatePlace,
288291
candidatePlaceId != null
@@ -477,6 +480,7 @@ private List<CourseFolderDto> createSortedCourseFolderDtoList(List<Long> sortedC
477480

478481
private List<CourseInfoDto> createSortedCourseInfoDtoList(
479482
final List<Course> filteredCourses,
483+
final Long userId,
480484
final Map<Long, LocalDateTime> courseIdCreatedAtMap,
481485
final Place candidatePlace,
482486
final boolean hasCandidatePlace
@@ -486,6 +490,7 @@ private List<CourseInfoDto> createSortedCourseInfoDtoList(
486490
prepareValidationResults(filteredCourses, candidatePlace, hasCandidatePlace);
487491

488492
return filteredCourses.stream()
493+
.filter(course -> isSharedCourse(course, userId))
489494
.map(course -> {
490495
Tag courseTag = course.getTag();
491496
String courseTagName = TagViewUtils.getActiveNameOrNull(courseTag);
@@ -504,5 +509,9 @@ private List<CourseInfoDto> createSortedCourseInfoDtoList(
504509
.toList();
505510
}
506511

512+
private boolean isSharedCourse(Course course, Long userId) {
513+
return course.isShared() || course.isCreatedBy(userId);
514+
}
515+
507516

508517
}

src/main/java/org/sopt/solply_server/domain/user/repository/UserRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
2828
@Query(value = "SELECT * FROM users WHERE email = :email LIMIT 1", nativeQuery = true)
2929
Optional<User> findAnyByEmail(@Param("email") String email);
3030

31+
@Query("SELECT COUNT(u) FROM User u WHERE u.isDeleted = false ")
32+
long countByActiveTrue();
3133
}

src/main/java/org/sopt/solply_server/domain/user/service/SocialUserService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import org.sopt.solply_server.domain.user.entity.User;
88
import org.sopt.solply_server.domain.user.repository.SocialUserInfoRepository;
99
import org.sopt.solply_server.domain.user.repository.UserRepository;
10+
import org.sopt.solply_server.domain.user.service.event.UserRegistrationEvent;
1011
import org.sopt.solply_server.global.exception.BusinessException;
1112
import org.sopt.solply_server.global.exception.ErrorCode;
13+
import org.springframework.context.ApplicationEventPublisher;
1214
import org.springframework.stereotype.Service;
1315
import org.springframework.transaction.annotation.Transactional;
1416

@@ -19,6 +21,7 @@ public class SocialUserService {
1921

2022
private final SocialUserInfoRepository socialUserInfoRepository;
2123
private final UserRepository userRepository;
24+
private final ApplicationEventPublisher eventPublisher;
2225

2326
@Transactional
2427
public User createOrLoginSocialUser(
@@ -46,6 +49,7 @@ public User createOrLoginSocialUser(
4649

4750
// 2) 해당 소셜 링크가 없으면, email로 기존 유저 연동 시도
4851
User user = null;
52+
boolean isNewUser = false; // 신규 유저 여부 체크 플래그
4953

5054
if (email != null) {
5155
user = userRepository.findAnyByEmail(email)
@@ -56,12 +60,18 @@ public User createOrLoginSocialUser(
5660
// 3) 없으면 신규 생성
5761
if (user == null) {
5862
user = userRepository.save(User.create(email)); // email은 null 가능하게
63+
isNewUser = true;
5964
}
6065

6166
// 4) 소셜 링크 생성
6267
// socialCode는 유니크라서 여기서 동시성 안전하게 처리하는 게 좋음(아래 참고)
6368
linkSocialAccount(user, platform, socialId);
6469

70+
if (isNewUser) {
71+
eventPublisher.publishEvent(new UserRegistrationEvent(user, platform));
72+
}
73+
74+
6575
return user;
6676
}
6777

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.sopt.solply_server.domain.user.service.event;
2+
3+
import org.sopt.solply_server.domain.auth.entity.SocialPlatform;
4+
import org.sopt.solply_server.domain.user.entity.User;
5+
6+
public record UserRegistrationEvent(
7+
User user,
8+
SocialPlatform platform
9+
) {
10+
}

src/main/java/org/sopt/solply_server/global/exception/ErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public enum ErrorCode {
7575
NOT_SUFFICIENT_PLACE_COUNT(HttpStatus.BAD_REQUEST, "COURSE-006", "코스에 최소 1개 이상의 장소가 필요합니다."),
7676
INVALID_PLACES_ORDER(HttpStatus.BAD_REQUEST, "COURSE-007", "장소 순서가 올바르지 않습니다."),
7777
DUPLICATE_COURSE_NAME(HttpStatus.BAD_REQUEST,"COURSE-008","코스 이름이 중복됩니다."),
78+
NOT_SHARED_COURSE(HttpStatus.FORBIDDEN,"COURSE-009" ,"공유되지 않은 코스입니다." ),
7879

7980
// 동네 관련 (TOWN-xxx)
8081
NOT_FOUND_TOWN(HttpStatus.NOT_FOUND, "TOWN-001" , "존재하지 않는 동네입니다."),
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.sopt.solply_server.global.external.discord.dto;
2+
3+
import java.util.List;
4+
5+
public record DiscordMessageDto(
6+
String content,
7+
List<Embed> embeds
8+
) {
9+
public static DiscordMessageDto newUser(
10+
String nickname, String joinTime, long totalCount
11+
) {
12+
return new DiscordMessageDto(
13+
"🎉 **새로운 솔플러가 합류했습니다!**",
14+
List.of(new Embed(
15+
"신규 유저 가입 알림",
16+
0x3498DB, // 파란색 코드
17+
List.of(
18+
new Field("닉네임", nickname, true),
19+
new Field("가입시간", joinTime, true),
20+
new Field("총 가입자", totalCount + "명", true)
21+
)
22+
))
23+
);
24+
}
25+
26+
public record Embed(String title, int color, List<Field> fields) {}
27+
public record Field(String name, String value, boolean inline) {}
28+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.sopt.solply_server.global.listener;
2+
3+
import java.time.LocalDateTime;
4+
import java.time.format.DateTimeFormatter;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.sopt.solply_server.domain.user.service.event.UserRegistrationEvent;
8+
import org.sopt.solply_server.domain.user.repository.UserRepository;
9+
import org.sopt.solply_server.global.external.discord.dto.DiscordMessageDto;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.scheduling.annotation.Async;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.transaction.event.TransactionPhase;
14+
import org.springframework.transaction.event.TransactionalEventListener;
15+
import org.springframework.web.client.RestTemplate;
16+
17+
@Slf4j
18+
@Component
19+
@RequiredArgsConstructor
20+
public class UserRegistrationEventListener {
21+
22+
@Value("${discord.webhook.url}")
23+
private String webhookUrl;
24+
25+
@Value("${discord.enabled:false}")
26+
private boolean isDiscordEnabled;
27+
28+
private final UserRepository userRepository;
29+
private final RestTemplate restTemplate = new RestTemplate();
30+
31+
@Async
32+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 커밋 완료 후 실행
33+
public void onUserRegistered(UserRegistrationEvent event) {
34+
if (!isDiscordEnabled) return;
35+
36+
try {
37+
var user = event.user();
38+
39+
String joinTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
40+
long totalCount = userRepository.countByActiveTrue();
41+
42+
DiscordMessageDto message = DiscordMessageDto.newUser(
43+
user.getNickname(),
44+
joinTime,
45+
totalCount
46+
);
47+
48+
restTemplate.postForEntity(webhookUrl, message, String.class);
49+
} catch (Exception e) {
50+
log.error("유저 가입 디스코드 알림 전송 실패", e);
51+
}
52+
}
53+
}

0 commit comments

Comments
 (0)