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
11 changes: 0 additions & 11 deletions src/main/java/com/kkinikong/be/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,6 @@ public ResponseEntity<ApiResponse<Object>> login(
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(loginResponse));
}

@Operation(
summary = "기본 회원가입",
description = "(관리자) 이메일과 비밀번호로 회원가입합니다. '@'를 포함한 이메일과 비밀번호를 입력해주세요.")
@PostMapping("/signup")
public ResponseEntity<ApiResponse<Object>> signUp(
@Valid @RequestBody BasicLoginRequest basicLoginRequest) {

authService.signup(basicLoginRequest);
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}

@Operation(
summary = "기본 로그인",
description = "(관리자) 이메일과 비밀번호로 로그인합니다. '@'를 포함한 이메일과 비밀번호를 입력해주세요.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.kkinikong.be.auth.controller;

import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

import com.kkinikong.be.auth.dto.request.BasicLoginRequest;
import com.kkinikong.be.auth.service.AuthService;
import com.kkinikong.be.global.response.ApiResponse;

@Profile({"local"})
@RequestMapping("/api/v1/auth")
@Tag(name = "Auth 로컬", description = "인증 및 회원 관련 API")
@RestController
@RequiredArgsConstructor
public class SignupController {

private final AuthService authService;

@Operation(
summary = "기본 회원가입",
description = "(관리자) 이메일과 비밀번호로 회원가입합니다. '@'를 포함한 이메일과 비밀번호를 입력해주세요.")
@PostMapping("/signup")
public ResponseEntity<ApiResponse<Object>> signUp(
@Valid @RequestBody BasicLoginRequest basicLoginRequest) {

authService.signup(basicLoginRequest);
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

@Service
@Slf4j
public class RedisTempleCacheService {
public class RedisTemplateCacheService {

@Autowired private RedisTemplate<String, Object> redisTemplate;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
@RequiredArgsConstructor
public class ScheduledService {

private final RedisTempleCacheService redisTempleCacheService;
private final RedisTemplateCacheService redisTemplateCacheService;
private final StoreRepository storeRepository;
private final CommunityPostRepository communityPostRepository;

@Scheduled(cron = "0 0 * * * *") // 매시간 0분에 실행
@Transactional
public void syncStoreViewCount() {
Map<Object, Object> storesViewCounts =
redisTempleCacheService.getViewCounts(RedisKey.STORE_VIEWS_KEY);
redisTemplateCacheService.getViewCounts(RedisKey.STORE_VIEWS_KEY);
if (storesViewCounts == null || storesViewCounts.isEmpty()) {
return;
}
Expand All @@ -38,14 +38,14 @@ public void syncStoreViewCount() {
storeRepository.incrementViews(storeId, viewCount);
});

redisTempleCacheService.clearViewCounts(RedisKey.STORE_VIEWS_KEY);
redisTemplateCacheService.clearViewCounts(RedisKey.STORE_VIEWS_KEY);
}

@Scheduled(cron = "0 10 * * * *") // 매시간 10분에 실행
@Transactional
public void syncCommunityPostViewCount() {
Map<Object, Object> communityPostsViewCounts =
redisTempleCacheService.getViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY);
redisTemplateCacheService.getViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY);
if (communityPostsViewCounts == null || communityPostsViewCounts.isEmpty()) {
return;
}
Expand All @@ -57,6 +57,6 @@ public void syncCommunityPostViewCount() {
communityPostRepository.incrementViews(storeId, viewCount);
});

redisTempleCacheService.clearViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY);
redisTemplateCacheService.clearViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY);
}
}
3 changes: 3 additions & 0 deletions src/main/java/com/kkinikong/be/community/domain/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -68,6 +69,8 @@ public class Comment extends BaseEntity {
@OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> childComments = new ArrayList<>();

@Version private Long version;

@Builder
public Comment(
String content,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -73,6 +74,8 @@ public class CommunityPost extends BaseEntity {
@OneToMany(mappedBy = "communityPost", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>();

@Version private Long version;

@Builder
public CommunityPost(String title, String content, User user, Category category) {
this.title = title;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import lombok.Builder;

import com.kkinikong.be.community.domain.Comment;
import com.kkinikong.be.community.util.TimeUtil;
import com.kkinikong.be.global.util.TimeUtil;
import com.kkinikong.be.user.utils.UserNicknameUtil;

@Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import lombok.Builder;

import com.kkinikong.be.community.domain.CommunityPost;
import com.kkinikong.be.community.util.TimeUtil;
import com.kkinikong.be.global.util.TimeUtil;
import com.kkinikong.be.user.utils.UserNicknameUtil;

@Builder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.kkinikong.be.community.dto.response;

import com.kkinikong.be.community.domain.CommunityPost;
import com.kkinikong.be.community.util.TimeUtil;
import com.kkinikong.be.global.util.TimeUtil;

public record CommunityPostListResponse(
long communityPostId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public enum CommunityErrorCode implements ErrorCode {
NOT_TOP_COMMENT(HttpStatus.BAD_REQUEST, "최상위 댓글이 아닙니다."),
COMMENT_NOT_OWNER(HttpStatus.FORBIDDEN, "댓글의 작성자가 아닙니다."),
COMMENT_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 댓글입니다."),
;

RETRY_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "재시도 횟수를 초과했습니다. 잠시 후 다시 시도해주세요.");
private final HttpStatus httpStatus;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
package com.kkinikong.be.community.repository.comment;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import io.lettuce.core.dynamic.annotation.Param;
import jakarta.persistence.LockModeType;

import com.kkinikong.be.community.domain.Comment;

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long>, CommentRepositoryCustom {

@EntityGraph(attributePaths = {"commentLikeList", "commentLikeList.user"})
List<Comment> findAllByCommunityPostId(Long postId);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT c FROM Comment c WHERE c.id = :commentId")
Optional<Comment> findByIdForUpdate(@Param("commentId") Long commentId);

void deleteAllByCommunityPostId(Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@

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

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import io.lettuce.core.dynamic.annotation.Param;
import jakarta.persistence.LockModeType;

import com.kkinikong.be.community.domain.CommunityPost;
import com.kkinikong.be.community.domain.type.Category;
Expand Down Expand Up @@ -42,10 +39,6 @@ ORDER BY COUNT(cpl.id) DESC, cp.viewCount DESC

Page<CommunityPost> findAllByUserIdOrderByCreatedDateDesc(Long userId, Pageable pageable);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM CommunityPost p WHERE p.id = :postId")
Optional<CommunityPost> findByIdForUpdate(@Param("postId") Long postId);

@Modifying(clearAutomatically = true)
@Query("UPDATE CommunityPost p SET p.viewCount = p.viewCount + :count WHERE p.id = :postId")
void incrementViews(@Param("postId") Long postId, @Param("count") Long count);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.kkinikong.be.community.service;

import java.util.Optional;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

import com.kkinikong.be.community.domain.Comment;
import com.kkinikong.be.community.domain.CommentLike;
import com.kkinikong.be.community.domain.CommunityPost;
import com.kkinikong.be.community.domain.CommunityPostLike;
import com.kkinikong.be.community.dto.response.LikeToggleResponse;
import com.kkinikong.be.community.exception.CommunityException;
import com.kkinikong.be.community.exception.errorcode.CommunityErrorCode;
import com.kkinikong.be.community.repository.CommentLikeRepository;
import com.kkinikong.be.community.repository.CommunityPostLikeRepository;
import com.kkinikong.be.community.repository.comment.CommentRepository;
import com.kkinikong.be.community.repository.communityPost.CommunityPostRepository;
import com.kkinikong.be.notification.event.payload.CommentLikeEvent;
import com.kkinikong.be.notification.event.payload.CommunityLikeEvent;
import com.kkinikong.be.user.domain.User;
import com.kkinikong.be.user.exception.UserException;
import com.kkinikong.be.user.exception.errorcode.UserErrorCode;
import com.kkinikong.be.user.repository.UserRepository;

@Service
@RequiredArgsConstructor
public class CommunityLikeToggleExecutor {
private final CommunityPostRepository communityPostRepository;
private final CommunityPostLikeRepository communityPostLikeRepository;
private final UserRepository userRepository;
private final CommentRepository commentRepository;
private final CommentLikeRepository commentLikeRepository;

private final ApplicationEventPublisher eventPublisher;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public LikeToggleResponse togglePostLikeOnce(Long postId, Long userId) {

CommunityPost post =
communityPostRepository
.findById(postId)
.orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND));
User user = getUserOrThrow(userId);

Optional<CommunityPostLike> existing =
communityPostLikeRepository.findByCommunityPostIdAndUserId(postId, userId);

boolean isLiked;
if (existing.isPresent()) {
communityPostLikeRepository.delete(existing.get());
post.decrementLikeCount();
isLiked = false;
} else {
communityPostLikeRepository.save(
CommunityPostLike.builder().communityPost(post).user(user).build());
post.incrementLikeCount();
isLiked = true;
if (!post.getUser().getId().equals(userId)) {
eventPublisher.publishEvent(new CommunityLikeEvent(post.getUser(), user, post));
}
}
communityPostRepository.saveAndFlush(post); // 버전 충돌 감지
return LikeToggleResponse.from(isLiked, post.getLikeCount());
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public LikeToggleResponse toggleCommentLikeOnce(Long commentId, Long userId) {
Comment comment =
commentRepository
.findById(commentId)
.orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMENT_NOT_FOUND));

User user = getUserOrThrow(userId);

Optional<CommentLike> commentLike =
commentLikeRepository.findByCommentIdAndUserId(commentId, userId);

boolean isLiked;
if (commentLike.isPresent()) {
commentLikeRepository.delete(commentLike.get());
comment.decrementLikeCount();
isLiked = false;
} else {
commentLikeRepository.save(CommentLike.builder().comment(comment).user(user).build());
comment.incrementLikeCount();
isLiked = true;
if (!comment.getUser().getId().equals(userId)) {
eventPublisher.publishEvent(new CommentLikeEvent(comment.getUser(), user, comment));
}
}
commentRepository.saveAndFlush(comment);
return LikeToggleResponse.from(isLiked, comment.getLikeCount());
}

private User getUserOrThrow(Long userId) {
return userRepository
.findById(userId)
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
}
}
Loading