Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
11 changes: 11 additions & 0 deletions src/main/java/org/sopt/app/application/stamp/ClapService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Objects;

import java.util.Optional;
import org.sopt.app.common.exception.BadRequestException;
import org.sopt.app.common.exception.ForbiddenException;
import org.sopt.app.common.exception.NotFoundException;
Expand Down Expand Up @@ -55,6 +56,16 @@ public int addClap(Long userId, Long stampId, int increment) {
return applied;
}

public Optional<Clap> getClap(Long userId, Long stampId) {
return clapRepository.findByUserIdAndStampId(userId, stampId);
}

public int getUserClapCount(Long userId, Long stampId) {
Optional<Clap> clap = getClap(userId, stampId);

return clap.map(Clap::getClapCount).orElse(0);
}

/**
* Clap(유저별 1행) 업데이트.
* - 없으면 생성하고, 있으면 도메인 메서드로 상한(50) 컷팅
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/org/sopt/app/application/stamp/StampInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.time.LocalDateTime;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -14,6 +15,7 @@ public class StampInfo {
@Getter
@Builder
@ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Stamp {

private Long id;
Expand All @@ -24,5 +26,43 @@ public static class Stamp {
private String activityDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private int clapCount;
private int viewCount;
}

@Getter
@Builder
@ToString
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class StampView {

private Long id;
private String contents;
private List<String> images;
private Long userId;
private Long missionId;
private String activityDate;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private int clapCount;
private int viewCount;
private boolean isMine;
private int myClapCount;

public static StampView of(Stamp stamp, int myClapCount, boolean isMine) {
return StampView.builder()
.id(stamp.getId())
.contents(stamp.getContents())
.images(stamp.getImages())
.activityDate(stamp.getActivityDate())
.createdAt(stamp.getCreatedAt())
.updatedAt(stamp.getUpdatedAt())
.missionId(stamp.getMissionId())
.clapCount(stamp.getClapCount())
.viewCount(stamp.getViewCount())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SoptampFacade에 단 리뷰처럼 여기서 +1 한 값 내려주도록 하면 좋을 듯합니다!

.isMine(isMine)
.myClapCount(myClapCount)
.build();
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/org/sopt/app/application/stamp/StampService.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ public StampInfo.Stamp findStamp(Long missionId, Long userId) {
entity.validate();
return StampInfo.Stamp.builder()
.id(entity.getId())
.userId(entity.getUserId())
.contents(entity.getContents())
.images(entity.getImages())
.activityDate(entity.getActivityDate())
.createdAt(entity.getCreatedAt())
.updatedAt(entity.getUpdatedAt())
.missionId(entity.getMissionId())
.clapCount(entity.getClapCount())
.viewCount(entity.getViewCount())
.build();
}

Expand Down Expand Up @@ -87,6 +90,8 @@ public StampInfo.Stamp uploadStamp(
.createdAt(newStamp.getCreatedAt())
.updatedAt(newStamp.getUpdatedAt())
.missionId(newStamp.getMissionId())
.clapCount(newStamp.getClapCount())
.viewCount(newStamp.getViewCount())
.build();
}

Expand Down Expand Up @@ -202,4 +207,9 @@ public int getStampClapCount(Long stampId) {
.orElseThrow(() -> new NotFoundException(ErrorCode.STAMP_NOT_FOUND))
.getClapCount();
}

@Transactional
public void increaseViewCountById(Long stampId) {
stampRepository.increaseViewCount(stampId);
}
}
15 changes: 4 additions & 11 deletions src/main/java/org/sopt/app/domain/entity/soptamp/Stamp.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
import org.sopt.app.domain.entity.BaseEntity;
import org.springframework.util.StringUtils;

@Builder
@Entity
@Getter
@Builder
@NoArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Stamp extends BaseEntity {

Expand All @@ -34,8 +34,10 @@ public class Stamp extends BaseEntity {
@Column(length = 10)
private String activityDate;

@Builder.Default
private int clapCount = 0;

@Builder.Default
private int viewCount = 0;

@Version
Expand Down Expand Up @@ -68,13 +70,4 @@ public void validate() {
}
}

public void incrementClapCount(int increment) {
if (increment <= 0) return;
this.clapCount += increment;
}

public void incrementViewCount() {
this.viewCount += 1;
}

}
26 changes: 19 additions & 7 deletions src/main/java/org/sopt/app/facade/SoptampFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,53 @@

import java.util.List;

import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.sopt.app.application.mission.MissionInfo.Level;
import org.sopt.app.application.mission.MissionService;
import org.sopt.app.application.soptamp.*;
import org.sopt.app.application.stamp.ClapService;
import org.sopt.app.application.stamp.StampInfo;
import org.sopt.app.application.stamp.StampInfo.Stamp;
import org.sopt.app.application.stamp.StampInfo.StampView;
import org.sopt.app.application.stamp.StampService;
import org.sopt.app.domain.entity.soptamp.Mission;
import org.sopt.app.presentation.rank.*;
import org.sopt.app.presentation.stamp.StampRequest;
import org.sopt.app.presentation.stamp.StampRequest.RegisterStampRequest;
import org.sopt.app.presentation.stamp.StampResponse;
import org.sopt.app.presentation.stamp.StampResponse.SoptampReportResponse;
import org.sopt.app.presentation.stamp.StampResponseMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class SoptampFacade {

private final StampService stampService;
private final MissionService missionService;
private final SoptampUserService soptampUserService;
private final RankResponseMapper rankResponseMapper;
private final SoptampUserFinder soptampUserFinder;
private final ClapService clapService;

private final RankResponseMapper rankResponseMapper;

@Value("${makers.app.soptamp.report.url}")
private String formUrl;

@Transactional
public Stamp uploadStamp(Long userId, RegisterStampRequest registerStampRequest){
public StampInfo.StampView uploadStamp(Long userId, RegisterStampRequest registerStampRequest){
stampService.checkDuplicateStamp(userId, registerStampRequest.getMissionId());
Stamp result = stampService.uploadStamp(registerStampRequest, userId);
Level mission = missionService.getMissionById(registerStampRequest.getMissionId());
soptampUserService.addPointByLevel(userId, mission.getLevel());
return result;

return StampInfo.StampView.of(result, 0, true);
}

@Transactional
Expand Down Expand Up @@ -70,9 +76,15 @@ public SoptampUserInfo editSoptampUserProfileMessage(Long userId, String newProf
return soptampUserService.editProfileMessage(userId, newProfileMessage);
}

public Stamp getStampInfo(Long missionId, String nickname){
val userId = soptampUserFinder.findByNickname(nickname).getUserId();
return stampService.findStamp(missionId, userId);
public StampInfo.StampView getStampInfo(Long requestUserId, Long missionId, String nickname){
val soptampUserId = soptampUserFinder.findByNickname(nickname).getUserId();
val stamp = stampService.findStamp(missionId, soptampUserId);
val requestUserClapCount = clapService.getUserClapCount(requestUserId, stamp.getId());

stampService.increaseViewCountById(stamp.getId());

return StampInfo.StampView.of(
stamp, requestUserClapCount, Objects.equals(requestUserId, soptampUserId));
Comment on lines +81 to +87
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 응답 매핑에 들어가는 stamp가 viewCount ++ 되기 전이라서 응답의 viewCount에 내 조회가 포함된 viewCount가 들어가지 않네요! 정합성이 엄청 중요한 필드는 아니니 증가 후 재조회할 필요까진 없을 것 같지만, 응답에서 viewCount + 1해서 내려주면 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 응답 Dto가 중복으로 사용되는 부분들이 있어서 이렇게 + 1 을 해주기 좀 애매하다고 생각했는데, 아예 view 라는 네이밍으로 조회 시 사용하는 Dto를 분리해버리고 viewCount 가 + 1 된 값으로 응답하도록 반영하겠습니다!

}

public RankResponse.Detail findSoptampUserAndCompletedMissionByNickname(String nickname) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import java.util.Optional;
import org.sopt.app.domain.entity.soptamp.Stamp;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface StampRepository extends JpaRepository<Stamp, Long>, StampRepositoryCustom {

Expand All @@ -15,4 +18,11 @@ public interface StampRepository extends JpaRepository<Stamp, Long>, StampReposi

Optional<Stamp> findByIdAndUserId(Long id, Long userId);

@Modifying
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Modifying(clearAutomatically = true, flushAutomatically = true)
이렇게 자동 clear/flush 되도록 수정하면 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 조금 더 안전한가요?? 조회 수 증가 쿼리로 레코드가 수정되었다고 1차 캐시를 날릴 필요가 있을지, 현재는 트랜잭션을 수정하는 레벨에서만 가져가서 상관없겠지만, 추후 트랜잭션 범위가 커졌을 때 flush를 자동으로 하면 다른 수정 쿼리로 발생한 락의 시간이 길어지진 않을지 걱정이네요..
우선 현재는 시간 이슈 + 이 방식도 좋을 것 같아서 반영하겠습니다!

Copy link
Collaborator

@hyerinhwang-sailin hyerinhwang-sailin Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JPQL update는 벌크 업데이트라 1차 캐시를 우회해서 같은 트랜잭션/영속성 컨텍스트에 Stamp 엔티티가 이미 로드돼 있으면 stale 상태가 되고, 이후 저장 시 증가분을 덮어쓸 위험이 있기 때문에 flush/clear하는 게 안전하다고 생각해요!
퍼포먼스가 걱정되면 이 메서드를 호출하는 범위를 짧게(작은 트랜잭션) 유지하거나, 조회 트랜잭션과 분리하는 식으로 운영할 수도 있을 것 같아요~

@Query("""
update Stamp s set s.viewCount = s.viewCount + 1
where s.id = :stampId
""")
void increaseViewCount(@Param("stampId") Long stampId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ public class StampController {
})
@GetMapping("")
public ResponseEntity<StampResponse.StampMain> findStampByMissionAndUserId(
@AuthenticationPrincipal Long userId,
@Valid @ModelAttribute StampRequest.FindStampRequest findStampRequest
) {
val result = soptampFacade.getStampInfo(findStampRequest.getMissionId(), findStampRequest.getNickname());
val response = stampResponseMapper.of(result);
val result = soptampFacade.getStampInfo(userId, findStampRequest.getMissionId(), findStampRequest.getNickname());
val response = stampResponseMapper.from(result);
return ResponseEntity.ok(response);
}

Expand All @@ -53,7 +54,7 @@ public ResponseEntity<StampResponse.StampMain> registerStamp(
@Valid @RequestBody StampRequest.RegisterStampRequest registerStampRequest
) {
val result = soptampFacade.uploadStamp(userId, registerStampRequest);
val response = stampResponseMapper.of(result);
val response = stampResponseMapper.from(result);
return ResponseEntity.ok(response);
}

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/sopt/app/presentation/stamp/StampResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -14,6 +15,7 @@ public class StampResponse {
@Getter
@AllArgsConstructor(access = AccessLevel.PUBLIC)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public static class StampMain {

@Schema(description = "스탬프 아이디", example = "1")
Expand All @@ -30,6 +32,14 @@ public static class StampMain {
private LocalDateTime updatedAt;
@Schema(description = "미션 아이디", example = "3")
private Long missionId;
@Schema(description = "총 박수 횟수", example = "124")
private int clapCount;
@Schema(description = "조회수", example = "58")
private int viewCount;
@Schema(description = "내 스탬프인지 여부", example = "false")
private boolean isMine;
@Schema(description = "해당 스탬프에 대한 내 박수 횟수", example = "33")
private int myClapCount;
}

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,21 @@
)
public interface StampResponseMapper {

StampResponse.StampMain of(StampInfo.Stamp stamp);
default StampResponse.StampMain from(StampInfo.StampView stampView) {
return StampResponse.StampMain.builder()
.id(stampView.getId())
.contents(stampView.getContents())
.images(stampView.getImages())
.activityDate(stampView.getActivityDate())
.createdAt(stampView.getCreatedAt())
.updatedAt(stampView.getUpdatedAt())
.missionId(stampView.getMissionId())
.clapCount(stampView.getClapCount())
.viewCount(stampView.getViewCount())
.myClapCount(stampView.getMyClapCount())
.isMine(stampView.isMine())
.build();
}

StampResponse.StampId of(Long stampId);

Expand Down