Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.sopt.app.application.playground.dto;

import static org.sopt.app.domain.enums.SoptPart.findPlaygroundPartByPartName;
import static org.sopt.app.domain.enums.SoptPart.findSoptPartByPartName;

import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
Expand Down Expand Up @@ -132,7 +132,7 @@ public SoptPart getPlaygroundPart() {
log.warn("Invalid cardinalInfo format: {}", cardinalInfo);
return SoptPart.NONE;
}
return findPlaygroundPartByPartName(parts[1]);
return findSoptPartByPartName(parts[1]);
} catch (Exception e) {
log.warn("Error parsing PlaygroundPart from cardinalInfo: {}", cardinalInfo, e);
return SoptPart.NONE;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.sopt.app.application.soptamp;

import static org.sopt.app.domain.entity.soptamp.SoptampUser.createNewSoptampUser;
import static org.sopt.app.domain.enums.SoptPart.findPlaygroundPartByPartName;
import static org.sopt.app.domain.enums.SoptPart.findSoptPartByPartName;

import java.util.*;
import lombok.*;
Expand Down Expand Up @@ -66,7 +66,7 @@ private void updateSoptampUser(SoptampUser registeredUser, PlatformUserInfoRespo
registeredUser.initTotalPoints();
registeredUser.updateChangedGenerationInfo(
(long)profile.lastGeneration(),
findPlaygroundPartByPartName(part),
findSoptPartByPartName(part),
newNickname
);
rankCacheService.removeRank(userId);
Expand All @@ -76,7 +76,7 @@ private void updateSoptampUser(SoptampUser registeredUser, PlatformUserInfoRespo
private void createSoptampUser(PlatformUserInfoResponse profile, Long userId, PlatformUserInfoResponse.SoptActivities latest) {
String part = latest.part() == null ? "미상" : latest.part();
String uniqueNickname = generateUniqueNickname(profile.name(), part);
SoptampUser newSoptampUser = createNewSoptampUser(userId, uniqueNickname, (long)profile.lastGeneration(), findPlaygroundPartByPartName(part));
SoptampUser newSoptampUser = createNewSoptampUser(userId, uniqueNickname, (long)profile.lastGeneration(), findSoptPartByPartName(part));
soptampUserRepository.save(newSoptampUser);
rankCacheService.createNewRank(userId);
}
Expand All @@ -86,7 +86,7 @@ private boolean isGenerationChanged(SoptampUser registeredUser, Long profileGene
}

private String generateUniqueNickname(String nickname, String part) {
String prefixPartName = SoptPart.findPlaygroundPartByPartName(part).getShortedPartName();
String prefixPartName = SoptPart.findSoptPartByPartName(part).getShortedPartName();
StringBuilder uniqueNickname = new StringBuilder().append(prefixPartName).append(nickname);
if (soptampUserRepository.existsByNickname(uniqueNickname.toString())) {
return addSuffixToNickname(uniqueNickname);
Expand Down
141 changes: 125 additions & 16 deletions src/main/java/org/sopt/app/application/stamp/ClapEventListener.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.sopt.app.application.stamp;

import org.sopt.app.domain.enums.SoptPart;
import org.sopt.app.interfaces.postgres.ClapMilestoneGuard;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -50,39 +51,64 @@ public void onClap(ClapEvent event) {
final int oldClapTotal = event.getOldClapTotal();
final int newClapTotal = event.getNewClapTotal();

Long missionId = stampService.getMissionIdByStampId(event.getStampId());
String missionTitle = missionService.getMissionTitleById(missionId);

val ownerProfile = platformService.getPlatformUserInfoResponse(event.getOwnerUserId());
String ownerName = ownerProfile.name();
String ownerPart = Optional.ofNullable(ownerProfile.getLatestActivity())
.map(PlatformUserInfoResponse.SoptActivities::part)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_PART_NOT_FOUND));
String nickname = soptampUserFinder.findById(event.getOwnerUserId()).getNickname();
AlarmData alarmData = new AlarmData(event);

if (crossed(oldClapTotal, newClapTotal, 1) && clapMilestoneGuard.tryMarkFirstHit(event.getStampId(), 1)) {
send(ClapRequest.ClapAlarmRequest.ofOwnerClapFirst(event.getOwnerUserId(), event.getStampId(), missionTitle, ownerPart, nickname));
sendWhenClapFirst(alarmData);
}

if (crossed(oldClapTotal, newClapTotal, 100)
&& clapMilestoneGuard.tryMarkFirstHit(event.getStampId(), 100)) {
send(ClapRequest.ClapAlarmRequest.ofOwnerClap100Or500(event.getOwnerUserId(), event.getStampId(), 100, missionTitle, ownerName, ownerPart, nickname));
&& clapMilestoneGuard.tryMarkFirstHit(event.getStampId(), 100)) {
sendWhenClap100Or500(alarmData, 100);
} else if (crossed(oldClapTotal, newClapTotal, 500)
&& clapMilestoneGuard.tryMarkFirstHit(event.getStampId(), 500)) {
send(ClapRequest.ClapAlarmRequest.ofOwnerClap100Or500(event.getOwnerUserId(), event.getStampId(), 500, missionTitle, ownerName, ownerPart, nickname));
&& clapMilestoneGuard.tryMarkFirstHit(event.getStampId(), 500)) {
sendWhenClap100Or500(alarmData, 500);
}

// 한 번에 여러 구간(2000, 3000)을 넘어도 낮은 것만 처리
// 정책상 상한 10000까지 발송 (필요 시 조정)
for (int k = 1000; k <= 10000; k += 1000) {
if (crossed(oldClapTotal, newClapTotal, k)
&& clapMilestoneGuard.tryMarkFirstHit(event.getStampId(), k)) {
send(ClapRequest.ClapAlarmRequest.ofOwnerClapKilo(event.getOwnerUserId(), event.getStampId(), k, missionTitle, ownerPart, nickname));
&& clapMilestoneGuard.tryMarkFirstHit(event.getStampId(), k)) {
sendWhenClapKilo(alarmData, k);
break;
}
}
}

private void sendWhenClapFirst(AlarmData alarmData){
send(ClapRequest.ClapAlarmRequest.ofOwnerClapFirst(
alarmData.getOwnerUserId(),
alarmData.getStampId(),
alarmData.getMissionTitle(),
alarmData.getOwnerPart(),
alarmData.getOwnerNickname(),
alarmData.getMissionId()));
}

private void sendWhenClap100Or500(AlarmData alarmData, int mileStone){
send(ClapRequest.ClapAlarmRequest.ofOwnerClap100Or500(
alarmData.getOwnerUserId(),
alarmData.getStampId(),
mileStone,
alarmData.getMissionTitle(),
alarmData.getOwnerName(),
alarmData.getOwnerPart(),
alarmData.getOwnerNickname(),
alarmData.getMissionId()));
}

private void sendWhenClapKilo(AlarmData alarmData, int mileStone){
send(ClapRequest.ClapAlarmRequest.ofOwnerClapKilo(
alarmData.getOwnerUserId(),
alarmData.getStampId(),
mileStone,
alarmData.getMissionTitle(),
alarmData.getOwnerPart(),
alarmData.getOwnerNickname(),
alarmData.getMissionId()));
}

private boolean crossed(int oldTotal, int newTotal, int threshold) {
return oldTotal < threshold && newTotal >= threshold;
}
Expand All @@ -101,4 +127,87 @@ private void send(ClapRequest.ClapAlarmRequest body) {
throw e; // 재시도 위해 그대로 throw
}
}

private class AlarmData {

private final ClapEvent event;

private OwnerInfo ownerInfo;
private MissionInfo missionInfo;

public AlarmData(ClapEvent clapEvent) {
this.event = clapEvent;
}

public ClapEvent getEvent() {
return event;
}

public OwnerInfo getOwnerInfo() {
if(this.ownerInfo == null){
return fetchOwnerInfo(getEvent());
}
return this.ownerInfo;
}

public MissionInfo getMissionInfo() {
if(this.missionInfo == null){
return fetchMissionInfo(getEvent());
}
return this.missionInfo;
}

public Long getOwnerUserId() {
return getEvent().getOwnerUserId();
}

public Long getStampId(){
return getEvent().getStampId();
}

public String getOwnerName() {
return getOwnerInfo().name();
}

public SoptPart getOwnerPart() {
return getOwnerInfo().part();
}

public String getOwnerNickname() {
return getOwnerInfo().nickname();
}

public Long getMissionId() {
return getMissionInfo().id();
}

public String getMissionTitle() {
return getMissionInfo().title();
}


private OwnerInfo fetchOwnerInfo(ClapEvent clapEvent) {
val ownerProfile = platformService.getPlatformUserInfoResponse(clapEvent.getOwnerUserId());
String ownerName = ownerProfile.name();
String ownerPartName = Optional.ofNullable(ownerProfile.getLatestActivity())
.map(PlatformUserInfoResponse.SoptActivities::part)
.orElseThrow(() -> new NotFoundException(ErrorCode.USER_PART_NOT_FOUND));
SoptPart ownerPart = SoptPart.findSoptPartByPartName(ownerPartName);
String nickname = soptampUserFinder.findById(clapEvent.getOwnerUserId()).getNickname();

return new OwnerInfo(ownerName, ownerPart, nickname);
}

private MissionInfo fetchMissionInfo(ClapEvent clapEvent){
Long missionId = stampService.getMissionIdByStampId(clapEvent.getStampId());
String missionTitle = missionService.getMissionTitleById(missionId);

return new MissionInfo(missionId, missionTitle);
}

}

private record OwnerInfo(String name, SoptPart part, String nickname) {}
private record MissionInfo(Long id, String title) {}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ public final class SoptampDeepLinkBuilder {

private SoptampDeepLinkBuilder() {}

public static String buildStampDetailLink(long stampId, boolean isMine, String nickname, String part) {
String encodedNickname = URLEncoder.encode(nickname, StandardCharsets.UTF_8);
return String.format("%s?id=%d&isMine=%s&nickname=%s&part=%s",
BASE, stampId, Boolean.toString(isMine), encodedNickname, part);
public static String buildStampDetailLink(long stampId, boolean isMine, String nickname, String part, long missionId) {
return String.format("%s?id=%d&isMine=%s&nickname=%s&part=%s&missionId=%d",
BASE, stampId, Boolean.toString(isMine), nickname, part, missionId);
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/sopt/app/domain/enums/SoptPart.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public enum SoptPart {
final String partName;
final String shortedPartName;

public static SoptPart findPlaygroundPartByPartName(String partName) {
public static SoptPart findSoptPartByPartName(String partName) {
return Arrays.stream(SoptPart.values())
.filter(playgroundPart -> playgroundPart.partName.equalsIgnoreCase(partName))
.filter(soptPart -> soptPart.partName.equalsIgnoreCase(partName))
.findAny()
.orElse(SoptPart.NONE);
}
Expand Down
21 changes: 11 additions & 10 deletions src/main/java/org/sopt/app/presentation/stamp/ClapRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.sopt.app.common.utils.SoptampDeepLinkBuilder;
import org.sopt.app.domain.enums.NotificationCategory;
import org.sopt.app.domain.enums.SoptPart;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ClapRequest {
Expand Down Expand Up @@ -54,7 +55,7 @@ public static class ClapAlarmRequest {

/** 첫 박수 (소유자에게만, isMine=true) */
public static ClapAlarmRequest ofOwnerClapFirst(Long ownerUserId, Long stampId, String missionTitle,
String ownerPart, String ownerNickname) {
SoptPart ownerPart, String ownerNickname, Long missionId) {
return ClapAlarmRequest.builder()
.userIds(List.of(String.valueOf(ownerUserId)))
.title(String.format("첫 박수 도착! 💌 ‘%s’ 에 누군가가 박수를 쳤어요 👀", missionTitle))
Expand All @@ -66,36 +67,36 @@ public static ClapAlarmRequest ofOwnerClapFirst(Long ownerUserId, Long stampId,
서로에게 응원의 박수를 보내며 소통해 보세요!
""")
.category(NotificationCategory.NEWS.name())
.deepLink(SoptampDeepLinkBuilder.buildStampDetailLink(stampId, true, ownerNickname, ownerPart))
.deepLink(SoptampDeepLinkBuilder.buildStampDetailLink(stampId, true, ownerNickname, ownerPart.getShortedPartName(), missionId))
.build();
}

/** 100/500번째 박수 (소유자에게만, isMine=true) */
public static ClapAlarmRequest ofOwnerClap100Or500(Long ownerUserId, Long stampId, int targetClapCount,
String missionTitle, String ownerName, String ownerPart, String ownerNickname) {
String missionTitle, String ownerName, SoptPart ownerPart, String ownerNickname, Long missionId) {
return ClapAlarmRequest.builder()
.userIds(List.of(String.valueOf(ownerUserId)))
.title(String.format("축하해요! [%d]번째 박수를 받았어요 🎉", targetClapCount))
.title(String.format("축하해요! %d번째 박수를 받았어요 🎉", targetClapCount))
.content(String.format("""
[%s] [%s]님의 ‘%s’ 미션 사진이 %d번째 박수를 받았습니다. 짝짝짝짝! 👏
%s파트 %s님의 ‘%s’ 미션 사진이 %d번째 박수를 받았습니다. 짝짝짝짝! 👏

정말 대단해요! 앞으로도 계속해서 멋진 미션을 인증하고 파트/개인 랭킹을 올려보세요.

어떤 솝트인이 박수쳤는 지 확인할 수 있어요!

서로에게 응원의 박수를 보내며 소통해 보세요!
""", ownerPart, ownerName, missionTitle, targetClapCount))
""", ownerPart.getPartName(), ownerName, missionTitle, targetClapCount))
.category(NotificationCategory.NEWS.name())
.deepLink(SoptampDeepLinkBuilder.buildStampDetailLink(stampId, true, ownerNickname, ownerPart))
.deepLink(SoptampDeepLinkBuilder.buildStampDetailLink(stampId, true, ownerNickname, ownerPart.getShortedPartName(), missionId))
.build();
}

/** 1000 단위 박수 (소유자에게만, isMine=true) */
public static ClapAlarmRequest ofOwnerClapKilo(Long ownerUserId, Long stampId, int targetClapCount,
String missionTitle, String ownerPart, String ownerNickname) {
String missionTitle, SoptPart ownerPart, String ownerNickname, Long missionId) {
return ClapAlarmRequest.builder()
.userIds(List.of(String.valueOf(ownerUserId)))
.title(String.format("박수 누적 [%d]개 🎉 ‘%s’에 박수 갈채를 받고 있어요.", targetClapCount, missionTitle))
.title(String.format("박수 누적 %d개 🎉 ‘%s’에 박수 갈채를 받고 있어요.", targetClapCount, missionTitle))
.content(String.format("""
미션 ‘%s’ 사진이 %d번째 박수를 받았습니다. 짝짝짝짝! 👏

Expand All @@ -106,7 +107,7 @@ public static ClapAlarmRequest ofOwnerClapKilo(Long ownerUserId, Long stampId, i
서로에게 응원의 박수를 보내며 소통해 보세요!
""", missionTitle, targetClapCount))
.category(NotificationCategory.NEWS.name())
.deepLink(SoptampDeepLinkBuilder.buildStampDetailLink(stampId, true, ownerNickname, ownerPart))
.deepLink(SoptampDeepLinkBuilder.buildStampDetailLink(stampId, true, ownerNickname, ownerPart.getShortedPartName(), missionId))
.build();
}
}
Expand Down