Skip to content

Commit b16492b

Browse files
KoSeonJeclaude
andauthored
refactor: 피드 좋아요를 UUID 기반에서 카운터 방식으로 전환 (#391)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c1fd293 commit b16492b

27 files changed

+133
-565
lines changed

CONVENTIONS.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
| Facade Service 구현체 | `Facade{Role}{Domain}ServiceImpl` | `FacadeClubFeedServiceImpl` |
1717
| Domain Service 인터페이스 | `{Domain}Service` | `FeedService`, `ClubService` |
1818
| Domain Service 구현체 | `General{Domain}Service` | `GeneralFeedService`, `GeneralClubService` |
19-
| Repository | `{Entity}Repository` | `FeedRepository`, `FeedLikeRepository` |
19+
| Repository | `{Entity}Repository` | `FeedRepository`, `FeedCommentRepository` |
2020
| Request DTO | `{Action}{Entity}Request` | `CreateFeedRequest`, `UpdateFixZoneRequest` |
2121
| Response DTO | `{Context}{Entity}Response` | `MyFeedPageResponse`, `CentralMyFixZoneListResponse` |
2222
| Command DTO | `{Action}{Entity}Command` | `CreateFeedCommand`, `UpdateFormCommand` |
2323
| Query DTO | `{Entity}{Context}Query` | `FeedPageQuery`, `ClubFeedPageQuery` |
2424
| Exception (부모) | `{Domain}Exception` | `FeedException`, `FormException` |
25-
| Exception (구체) | `{Description}Exception` (static inner) | `DuplicatedFeedLikeException`, `FormPeriodException` |
25+
| Exception (구체) | `{Description}Exception` (static inner) | `FeedNotFoundException`, `FormPeriodException` |
2626
| Repository Projection | `{QueryPurpose}Dto` (interface) | `MonthlyFeedRankingDto`, `ClubFeedRankingDto` |
2727

2828
---
@@ -243,16 +243,16 @@ public class FeedException extends CustomException {
243243
}
244244

245245
// 구체적 예외 — static final inner class
246-
public static final class DuplicatedFeedLikeException extends FeedException {
247-
private static final String MESSAGE = "이미 좋아요한 피드입니다.";
248-
public DuplicatedFeedLikeException() {
249-
super(MESSAGE, BAD_REQUEST.value());
246+
public static final class FeedNotFoundException extends FeedException {
247+
private static final String MESSAGE = "존재하지 않는 피드입니다.";
248+
public FeedNotFoundException() {
249+
super(MESSAGE, NOT_FOUND.value());
250250
}
251251
}
252252
}
253253

254254
// 사용
255-
throw new FeedException.DuplicatedFeedLikeException();
255+
throw new FeedException.FeedNotFoundException();
256256
```
257257

258258
| 예외 유형 | 패턴 | HTTP 코드 |
@@ -302,4 +302,4 @@ public void createFeed(CreateFeedRequest request, PrincipalDetails principalDeta
302302
| Service Impl 네이밍 | Impl 유무 혼재 | **항상 Impl suffix 붙이기** |
303303
| Exception 메시지 | 상수 vs 인라인 혼재 | **`private static final String MESSAGE`로 상수화** |
304304
| Boolean 필드 네이밍 | `is` / `has` 혼재 | **엔티티 상태 → `is`, 보유 여부 → `has`** |
305-
| FeedLike soft delete | unique constraint 충돌 | **FeedLike만 예외적으로 hard delete** |
305+
| FeedComment hard delete | 댓글 삭제 시 물리 삭제 | **FeedComment만 예외적으로 hard delete** |

src/main/java/ddingdong/ddingdongBE/common/config/SecurityConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.springframework.http.HttpMethod.DELETE;
44
import static org.springframework.http.HttpMethod.GET;
5+
import static org.springframework.http.HttpMethod.PATCH;
56
import static org.springframework.http.HttpMethod.POST;
67

78
import ddingdong.ddingdongBE.auth.service.JwtAuthService;
@@ -59,12 +60,14 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthService authSer
5960
.requestMatchers(POST,
6061
API_PREFIX + "/forms/{formId}/applications",
6162
API_PREFIX + "/pair-game/appliers",
62-
API_PREFIX + "/feeds/*/likes",
6363
API_PREFIX + "/feeds/*/comments"
6464
)
6565
.permitAll()
66+
.requestMatchers(PATCH,
67+
API_PREFIX + "/feeds/*/likes"
68+
)
69+
.permitAll()
6670
.requestMatchers(DELETE,
67-
API_PREFIX + "/feeds/*/likes",
6871
API_PREFIX + "/feeds/*/comments/*"
6972
)
7073
.permitAll()

src/main/java/ddingdong/ddingdongBE/common/exception/FeedException.java

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ddingdong.ddingdongBE.common.exception;
22

3-
import static org.springframework.http.HttpStatus.CONFLICT;
43
import static org.springframework.http.HttpStatus.FORBIDDEN;
54
import static org.springframework.http.HttpStatus.NOT_FOUND;
65

@@ -10,24 +9,6 @@ public FeedException(String message, int errorCode) {
109
super(message, errorCode);
1110
}
1211

13-
public static final class DuplicatedFeedLikeException extends FeedException {
14-
15-
private static final String MESSAGE = "이미 좋아요한 피드입니다.";
16-
17-
public DuplicatedFeedLikeException() {
18-
super(MESSAGE, CONFLICT.value());
19-
}
20-
}
21-
22-
public static final class FeedLikeNotFoundException extends FeedException {
23-
24-
private static final String MESSAGE = "좋아요 기록이 존재하지 않습니다.";
25-
26-
public FeedLikeNotFoundException() {
27-
super(MESSAGE, NOT_FOUND.value());
28-
}
29-
}
30-
3112
public static final class CommentNotFoundException extends FeedException {
3213

3314
private static final String MESSAGE = "존재하지 않는 댓글입니다.";
Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
package ddingdong.ddingdongBE.domain.feed.api;
22

3-
import static ddingdong.ddingdongBE.common.constant.ValidationConstants.UUID_V4_REGEXP;
4-
3+
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.CreateFeedLikeRequest;
54
import io.swagger.v3.oas.annotations.Operation;
65
import io.swagger.v3.oas.annotations.responses.ApiResponse;
76
import io.swagger.v3.oas.annotations.responses.ApiResponses;
87
import io.swagger.v3.oas.annotations.tags.Tag;
9-
import jakarta.validation.constraints.Pattern;
8+
import jakarta.validation.Valid;
109
import org.springframework.http.HttpStatus;
11-
import org.springframework.web.bind.annotation.DeleteMapping;
1210
import org.springframework.web.bind.annotation.PathVariable;
13-
import org.springframework.web.bind.annotation.PostMapping;
14-
import org.springframework.web.bind.annotation.RequestHeader;
11+
import org.springframework.web.bind.annotation.PatchMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
1513
import org.springframework.web.bind.annotation.RequestMapping;
1614
import org.springframework.web.bind.annotation.ResponseStatus;
1715

@@ -21,30 +19,13 @@ public interface FeedLikeApi {
2119

2220
@Operation(summary = "피드 좋아요 API")
2321
@ApiResponses({
24-
@ApiResponse(responseCode = "201", description = "피드 좋아요 성공"),
25-
@ApiResponse(responseCode = "400", description = "유효하지 않은 UUID 형식"),
26-
@ApiResponse(responseCode = "404", description = "피드 없음"),
27-
@ApiResponse(responseCode = "409", description = "이미 좋아요한 피드")
28-
})
29-
@ResponseStatus(HttpStatus.CREATED)
30-
@PostMapping("/{feedId}/likes")
31-
void createLike(
32-
@PathVariable("feedId") Long feedId,
33-
@Pattern(regexp = UUID_V4_REGEXP, message = "유효하지 않은 UUID v4 형식입니다.")
34-
@RequestHeader("X-Anonymous-UUID") String uuid
35-
);
36-
37-
@Operation(summary = "피드 좋아요 취소 API")
38-
@ApiResponses({
39-
@ApiResponse(responseCode = "204", description = "피드 좋아요 취소 성공"),
40-
@ApiResponse(responseCode = "400", description = "유효하지 않은 UUID 형식"),
41-
@ApiResponse(responseCode = "404", description = "좋아요 기록 없음")
22+
@ApiResponse(responseCode = "204", description = "피드 좋아요 성공"),
23+
@ApiResponse(responseCode = "400", description = "좋아요 횟수 초과 (최대 100)")
4224
})
4325
@ResponseStatus(HttpStatus.NO_CONTENT)
44-
@DeleteMapping("/{feedId}/likes")
45-
void deleteLike(
26+
@PatchMapping("/{feedId}/likes")
27+
void createLike(
4628
@PathVariable("feedId") Long feedId,
47-
@Pattern(regexp = UUID_V4_REGEXP, message = "유효하지 않은 UUID v4 형식입니다.")
48-
@RequestHeader("X-Anonymous-UUID") String uuid
29+
@RequestBody @Valid CreateFeedLikeRequest request
4930
);
5031
}
Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
11
package ddingdong.ddingdongBE.domain.feed.controller;
22

33
import ddingdong.ddingdongBE.domain.feed.api.FeedLikeApi;
4-
import ddingdong.ddingdongBE.domain.feed.service.FeedLikeService;
4+
import ddingdong.ddingdongBE.domain.feed.controller.dto.request.CreateFeedLikeRequest;
5+
import ddingdong.ddingdongBE.domain.feed.service.FeedService;
56
import lombok.RequiredArgsConstructor;
6-
import org.springframework.validation.annotation.Validated;
77
import org.springframework.web.bind.annotation.RestController;
88

9-
@Validated
109
@RestController
1110
@RequiredArgsConstructor
1211
public class FeedLikeController implements FeedLikeApi {
1312

14-
private final FeedLikeService feedLikeService;
13+
private final FeedService feedService;
1514

1615
@Override
17-
public void createLike(Long feedId, String uuid) {
18-
feedLikeService.create(feedId, uuid);
19-
}
20-
21-
@Override
22-
public void deleteLike(Long feedId, String uuid) {
23-
feedLikeService.delete(feedId, uuid);
16+
public void createLike(Long feedId, CreateFeedLikeRequest request) {
17+
feedService.getById(feedId);
18+
feedService.addLikeCount(feedId, request.count());
2419
}
2520
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package ddingdong.ddingdongBE.domain.feed.controller.dto.request;
2+
3+
import jakarta.validation.constraints.Max;
4+
import jakarta.validation.constraints.Min;
5+
import jakarta.validation.constraints.NotNull;
6+
7+
public record CreateFeedLikeRequest(
8+
@NotNull(message = "count는 null이 될 수 없습니다.")
9+
@Min(value = 1, message = "count는 1 이상이어야 합니다.")
10+
@Max(value = 100, message = "count는 100 이하여야 합니다.")
11+
Integer count
12+
) {
13+
14+
}

src/main/java/ddingdong/ddingdongBE/domain/feed/entity/Feed.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,20 @@ public class Feed extends BaseEntity {
4343
@Column(nullable = false)
4444
private long viewCount;
4545

46+
@Column(nullable = false)
47+
private long likeCount;
48+
4649
@Column(name = "deleted_at")
4750
private LocalDateTime deletedAt;
4851

4952
@Builder
50-
private Feed(Long id, String activityContent, Club club, FeedType feedType, long viewCount) {
53+
private Feed(Long id, String activityContent, Club club, FeedType feedType, long viewCount, long likeCount) {
5154
this.id = id;
5255
this.activityContent = activityContent;
5356
this.club = club;
5457
this.feedType = feedType;
5558
this.viewCount = viewCount;
59+
this.likeCount = likeCount;
5660
}
5761

5862
public boolean isImage() {

src/main/java/ddingdong/ddingdongBE/domain/feed/entity/FeedLike.java

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/main/java/ddingdong/ddingdongBE/domain/feed/repository/FeedLikeRepository.java

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/main/java/ddingdong/ddingdongBE/domain/feed/repository/FeedRepository.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,22 @@ Slice<Feed> getAllFeedPage(
6969
@Query(value = "UPDATE feed SET view_count = view_count + 1 WHERE id = :feedId", nativeQuery = true)
7070
void incrementViewCount(@Param("feedId") Long feedId);
7171

72+
@Modifying(clearAutomatically = true)
73+
@Query(value = "UPDATE feed SET like_count = like_count + :count WHERE id = :feedId", nativeQuery = true)
74+
void addLikeCount(@Param("feedId") Long feedId, @Param("count") int count);
75+
7276
@Query(value = """
7377
SELECT c.id AS clubId,
7478
c.name AS clubName,
7579
COUNT(f.id) AS feedCount,
7680
COALESCE(SUM(f.view_count), 0) AS viewCount,
77-
COALESCE(SUM(sub_like.like_cnt), 0) AS likeCount,
81+
COALESCE(SUM(f.like_count), 0) AS likeCount,
7882
COALESCE(SUM(sub_comment.comment_cnt), 0) AS commentCount
7983
FROM club c
8084
LEFT JOIN feed f ON f.club_id = c.id
8185
AND f.deleted_at IS NULL
8286
AND YEAR(f.created_at) = :year
8387
AND MONTH(f.created_at) = :month
84-
LEFT JOIN (
85-
SELECT fl.feed_id, COUNT(*) AS like_cnt
86-
FROM feed_like fl
87-
GROUP BY fl.feed_id
88-
) sub_like ON sub_like.feed_id = f.id
8988
LEFT JOIN (
9089
SELECT fc.feed_id, COUNT(*) AS comment_cnt
9190
FROM feed_comment fc

0 commit comments

Comments
 (0)