1010import com .grepp .teamnotfound .app .model .board .code .ProfileBoardType ;
1111import com .grepp .teamnotfound .app .model .board .code .SortType ;
1212import com .grepp .teamnotfound .app .model .board .dto .ArticleListDto ;
13+ import com .grepp .teamnotfound .app .model .board .dto .LikeCheckDto ;
1314import com .grepp .teamnotfound .app .model .board .dto .UserArticleListDto ;
1415import com .grepp .teamnotfound .app .model .board .entity .Article ;
1516import com .grepp .teamnotfound .app .model .board .entity .ArticleImg ;
3738import java .util .List ;
3839import java .util .Optional ;
3940import lombok .RequiredArgsConstructor ;
41+ import lombok .extern .slf4j .Slf4j ;
4042import org .springframework .data .domain .Page ;
43+ import org .springframework .data .redis .RedisConnectionFailureException ;
44+ import org .springframework .data .redis .RedisSystemException ;
4145import org .springframework .stereotype .Service ;
4246import org .springframework .transaction .annotation .Transactional ;
4347import org .springframework .web .multipart .MultipartFile ;
4448
4549@ Service
4650@ RequiredArgsConstructor
51+ @ Slf4j
4752public class ArticleService {
4853
4954 private final ArticleRepository articleRepository ;
@@ -125,8 +130,8 @@ public ArticleDetailResponse findByArticleIdAndUserId(Long articleId, Long userI
125130 ArticleDetailResponse response = articleRepository .findDetailById (articleId , userId );
126131
127132 // Redis 카운터 캐시 값으로 덮어씀
128- Integer redisLikeCount = getArticleLikeCount (articleId );
129- response .setLikes ( redisLikeCount );
133+ response . setLikes ( getArticleLikeCount (articleId ) );
134+ response .setIsLiked ( getUserLiked ( articleId , userId ) );
130135
131136 articleRepository .plusViewById (articleId );
132137 response .setViews (response .getViews () + 1 );
@@ -162,7 +167,6 @@ public void updateArticle(Long articleId, ArticleRequest request, List<Multipart
162167 beforeArticle .setUpdatedAt (OffsetDateTime .now ());
163168 Article savedArticle = articleRepository .save (beforeArticle );
164169
165- // TODO 스토리지에 저장된 불필요한 사진 파일들에 대한 배치 스케줄러 도입 필요
166170 articleImgRepository .softDeleteByArticleId (articleId , OffsetDateTime .now ());
167171 uploadAndSaveImgs (images , savedArticle );
168172 }
@@ -203,7 +207,6 @@ private void uploadAndSaveImgs(List<MultipartFile> images, Article targetArticle
203207 .stream ()
204208 .map (fileDto -> {
205209 ImgType type = ImgType .DESC ;
206- // NOTE 게시글을 작성할 때 썸네일을 설정할 수 있게? 아니면 첫번째 이미지?
207210 if (imgList .getFirst ().originName ().equals (fileDto .originName ())) {
208211 type = ImgType .THUMBNAIL ;
209212 }
@@ -274,22 +277,37 @@ public LikeResponse likeWithRedis(Long articleId, Long userId) {
274277 Article article = articleRepository .findById (articleId )
275278 .orElseThrow (() -> new BoardException (BoardErrorCode .ARTICLE_NOT_FOUND ));
276279
277- // Redis 캐시를 우선적으로 확인(빠른 응답)
278- // 중복 요청이면 무시
279- boolean isLikedInRedis = redisLikeService .isUserLikedInRedis (articleId , userId );
280- if (isLikedInRedis ) {
280+ try {
281+ boolean isLikedInRedis = getUserLiked (articleId , userId );
282+ if (isLikedInRedis ) { // 이미 좋아요 상태
283+ Integer totalCount = getArticleLikeCount (articleId );
284+ return new LikeResponse (articleId , totalCount , true );
285+ }
286+
287+ redisLikeService .addLikeRequest (articleId , userId );
288+ redisLikeService .setUserLikedStatus (articleId , userId , true );
289+ redisLikeService .incrementArticleLikesCount (articleId ); // 게시글 총 좋아요 카운터 증가
290+
281291 Integer totalCount = getArticleLikeCount (articleId );
292+ boolean isLiked = getUserLiked (articleId , userId );
293+
294+ return new LikeResponse (articleId , totalCount , isLiked );
295+ } catch (RedisConnectionFailureException | RedisSystemException e ) {
296+ log .warn ("[Redis fallback] Like Request - articleId: {}, userId: {}" , articleId , userId , e );
297+
298+ boolean alreadyLiked = articleLikeRepository .existsByArticle_ArticleIdAndUser_UserId (articleId , userId );
299+ if (!alreadyLiked ) { // 중복 확인
300+ ArticleLike like = ArticleLike .builder ()
301+ .article (article )
302+ .user (user )
303+ .createdAt (OffsetDateTime .now ())
304+ .build ();
305+ articleLikeRepository .save (like );
306+ }
307+
308+ Integer totalCount = articleLikeRepository .countByArticle_ArticleId (articleId );
282309 return new LikeResponse (articleId , totalCount , true );
283310 }
284-
285- redisLikeService .addLikeRequest (articleId , userId );
286- redisLikeService .setUserLikedStatus (articleId , userId , true );
287- redisLikeService .incrementArticleLikesCount (articleId ); // 게시글 총 좋아요 카운터 증가
288-
289- Integer totalCount = getArticleLikeCount (articleId );
290- boolean isLiked = redisLikeService .isUserLikedInRedis (articleId , userId );
291-
292- return new LikeResponse (articleId , totalCount , isLiked );
293311 }
294312
295313 @ Transactional
@@ -299,27 +317,34 @@ public LikeResponse unlikeWithRedis(Long articleId, Long userId) {
299317 Article article = articleRepository .findById (articleId )
300318 .orElseThrow (() -> new BoardException (BoardErrorCode .ARTICLE_NOT_FOUND ));
301319
302- // Redis 캐시를 우선적으로 확인(빠른 응답)
303- // 중복 요청이면 무시
304- boolean isLikedInRedis = redisLikeService .isUserLikedInRedis (articleId , userId );
305- if (!isLikedInRedis ) {
320+ try {
321+ boolean isLikedInRedis = getUserLiked (articleId , userId );
322+ if (!isLikedInRedis ) { // 이미 좋아요 취소
323+ Integer totalCount = getArticleLikeCount (articleId );
324+ return new LikeResponse (articleId , totalCount , false );
325+ }
326+
327+ redisLikeService .addUnlikeRequest (articleId , userId );
328+ redisLikeService .setUserLikedStatus (articleId , userId , false );
329+ redisLikeService .decrementArticleLikesCount (articleId );
330+
306331 Integer totalCount = getArticleLikeCount (articleId );
307- return new LikeResponse (articleId , totalCount , false );
308- }
332+ boolean isLiked = getUserLiked (articleId , userId );
309333
310- redisLikeService . addUnlikeRequest (articleId , userId );
311- redisLikeService . setUserLikedStatus ( articleId , userId , false );
312- redisLikeService . decrementArticleLikesCount ( articleId );
334+ return new LikeResponse (articleId , totalCount , isLiked );
335+ } catch ( RedisConnectionFailureException | RedisSystemException e ) {
336+ log . warn ( "[Redis fallback] Unlike Request - articleId: {}, userId: {}" , articleId , userId );
313337
314- Integer totalCount = getArticleLikeCount (articleId );
315- boolean isLiked = redisLikeService . isUserLikedInRedis ( articleId , userId );
338+ Optional < ArticleLike > like = articleLikeRepository . findByArticle_ArticleIdAndUser_UserId (articleId , userId );
339+ like . ifPresent ( articleLikeRepository :: delete );
316340
317- return new LikeResponse (articleId , totalCount , isLiked );
341+ Integer totalCount = articleLikeRepository .countByArticle_ArticleId (articleId );
342+ return new LikeResponse (articleId , totalCount , false );
343+ }
318344 }
319345
320346 // 캐시에서 좋아요 수 먼저 조회
321347 private Integer getArticleLikeCount (Long articleId ) {
322- // NOTE 게시글 리스트 조회에서도 실시간 좋아요 수가 필요할까?
323348 Long count = redisLikeService .getArticleLikesCount (articleId );
324349
325350 if (count == null ) {
@@ -333,6 +358,16 @@ private Integer getArticleLikeCount(Long articleId) {
333358 return count .intValue ();
334359 }
335360
361+ // 캐시에서 좋아요 상태 먼저 조회
362+ private boolean getUserLiked (Long articleId , Long userId ) {
363+ if (userId == null ) return false ;
364+ LikeCheckDto checkDto = redisLikeService .isUserLikedInRedis (articleId , userId );
365+ if (!checkDto .isRedisAvailable ()) {
366+ return articleLikeRepository .existsByArticle_ArticleIdAndUser_UserId (articleId , userId );
367+ }
368+ return checkDto .isLiked ();
369+ }
370+
336371 // DB 에서 댓글 수 조회
337372 @ Transactional (readOnly = true )
338373 public Integer getReplyCount (Long articleId ) {
@@ -365,7 +400,7 @@ public LikeResponse getNewestLikeCount(Long articleId, Long userId) {
365400 likesCount = redisLikeService .getArticleLikesCount (articleId );
366401
367402 if (userId != null ) {
368- isLiked = redisLikeService . isUserLikedInRedis (articleId , userId );
403+ isLiked = getUserLiked (articleId , userId );
369404 }
370405
371406 // DB 에서 조회가 필요한 경우
0 commit comments