Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 8 additions & 8 deletions CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
| Facade Service 구현체 | `Facade{Role}{Domain}ServiceImpl` | `FacadeClubFeedServiceImpl` |
| Domain Service 인터페이스 | `{Domain}Service` | `FeedService`, `ClubService` |
| Domain Service 구현체 | `General{Domain}Service` | `GeneralFeedService`, `GeneralClubService` |
| Repository | `{Entity}Repository` | `FeedRepository`, `FeedLikeRepository` |
| Repository | `{Entity}Repository` | `FeedRepository`, `FeedCommentRepository` |
| Request DTO | `{Action}{Entity}Request` | `CreateFeedRequest`, `UpdateFixZoneRequest` |
| Response DTO | `{Context}{Entity}Response` | `MyFeedPageResponse`, `CentralMyFixZoneListResponse` |
| Command DTO | `{Action}{Entity}Command` | `CreateFeedCommand`, `UpdateFormCommand` |
| Query DTO | `{Entity}{Context}Query` | `FeedPageQuery`, `ClubFeedPageQuery` |
| Exception (부모) | `{Domain}Exception` | `FeedException`, `FormException` |
| Exception (구체) | `{Description}Exception` (static inner) | `DuplicatedFeedLikeException`, `FormPeriodException` |
| Exception (구체) | `{Description}Exception` (static inner) | `FeedNotFoundException`, `FormPeriodException` |
| Repository Projection | `{QueryPurpose}Dto` (interface) | `MonthlyFeedRankingDto`, `ClubFeedRankingDto` |

---
Expand Down Expand Up @@ -243,16 +243,16 @@ public class FeedException extends CustomException {
}

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

// 사용
throw new FeedException.DuplicatedFeedLikeException();
throw new FeedException.FeedNotFoundException();
```

| 예외 유형 | 패턴 | HTTP 코드 |
Expand Down Expand Up @@ -302,4 +302,4 @@ public void createFeed(CreateFeedRequest request, PrincipalDetails principalDeta
| Service Impl 네이밍 | Impl 유무 혼재 | **항상 Impl suffix 붙이기** |
| Exception 메시지 | 상수 vs 인라인 혼재 | **`private static final String MESSAGE`로 상수화** |
| Boolean 필드 네이밍 | `is` / `has` 혼재 | **엔티티 상태 → `is`, 보유 여부 → `has`** |
| FeedLike soft delete | unique constraint 충돌 | **FeedLike만 예외적으로 hard delete** |
| FeedComment hard delete | 댓글 삭제 시 물리 삭제 | **FeedComment만 예외적으로 hard delete** |
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthService authSer
)
.permitAll()
.requestMatchers(DELETE,
API_PREFIX + "/feeds/*/likes",
API_PREFIX + "/feeds/*/comments/*"
)
.permitAll()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package ddingdong.ddingdongBE.common.exception;

import static org.springframework.http.HttpStatus.CONFLICT;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;

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

public static final class DuplicatedFeedLikeException extends FeedException {

private static final String MESSAGE = "이미 좋아요한 피드입니다.";

public DuplicatedFeedLikeException() {
super(MESSAGE, CONFLICT.value());
}
}

public static final class FeedLikeNotFoundException extends FeedException {

private static final String MESSAGE = "좋아요 기록이 존재하지 않습니다.";

public FeedLikeNotFoundException() {
super(MESSAGE, NOT_FOUND.value());
}
}

public static final class CommentNotFoundException extends FeedException {

private static final String MESSAGE = "존재하지 않는 댓글입니다.";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
package ddingdong.ddingdongBE.domain.feed.api;

import static ddingdong.ddingdongBE.common.constant.ValidationConstants.UUID_V4_REGEXP;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Pattern;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;

Expand All @@ -21,30 +16,10 @@ public interface FeedLikeApi {

@Operation(summary = "피드 좋아요 API")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "피드 좋아요 성공"),
@ApiResponse(responseCode = "400", description = "유효하지 않은 UUID 형식"),
@ApiResponse(responseCode = "404", description = "피드 없음"),
@ApiResponse(responseCode = "409", description = "이미 좋아요한 피드")
})
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/{feedId}/likes")
void createLike(
@PathVariable("feedId") Long feedId,
@Pattern(regexp = UUID_V4_REGEXP, message = "유효하지 않은 UUID v4 형식입니다.")
@RequestHeader("X-Anonymous-UUID") String uuid
);

@Operation(summary = "피드 좋아요 취소 API")
@ApiResponses({
@ApiResponse(responseCode = "204", description = "피드 좋아요 취소 성공"),
@ApiResponse(responseCode = "400", description = "유효하지 않은 UUID 형식"),
@ApiResponse(responseCode = "404", description = "좋아요 기록 없음")
@ApiResponse(responseCode = "204", description = "피드 좋아요 성공"),
@ApiResponse(responseCode = "404", description = "피드 없음")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

@ApiResponse(responseCode = "404")가 실제 동작과 불일치합니다.

FeedLikeControllerE2ETestcreateLike_nonExistentFeed(feedId=999999L)가 204를 기대하고 있어, 실제 구현은 존재하지 않는 피드에 대해 404를 반환하지 않음을 확인할 수 있습니다. FeedLikeControllerfeedService.incrementLikeCount(feedId)를 호출할 뿐, 피드 존재 여부를 검증하지 않기 때문입니다.

아래 두 방법 중 하나를 선택해야 합니다:

  • 옵션 A: FeedService.incrementLikeCount에서 피드 존재 여부를 검증하고 없으면 FeedException.FeedNotFoundException을 throw → 404 반환
  • 옵션 B: Swagger 문서에서 404 @ApiResponse를 제거하고, 존재하지 않는 피드 요청을 조용히 무시하는 현 동작을 명확히 문서화

현재 상태는 API 계약과 실제 동작이 다르며, 클라이언트 오류 처리를 어렵게 만듭니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/ddingdong/ddingdongBE/domain/feed/api/FeedLikeApi.java` around
lines 19 - 20, The API docs claim a 404 but the code ignores missing feeds —
implement Option A: update FeedService.incrementLikeCount to verify the feed
exists (e.g., by calling the repository/read method used elsewhere) and throw
FeedException.FeedNotFoundException when not found, so
FeedLikeController/FeedLikeApi will correctly return 404 as documented; ensure
the exception type matches the global exception handler so the
`@ApiResponse`(responseCode = "404") remains accurate (no Swagger change needed).

})
Comment on lines 20 to 24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

@ApiResponses404 응답이 누락되어 있습니다

FeedLikeController.createLikefeedService.getById(feedId)를 호출하고, 피드가 없으면 NotFoundException → 404를 반환합니다(FeedLikeControllerE2ETest.createLike_nonExistentFeed에서 404 검증 통과). 그러나 @ApiResponses에 404 케이스가 없어 OpenAPI 스펙이 실제 동작과 불일치합니다.

📄 수정 제안
 `@ApiResponses`({
         `@ApiResponse`(responseCode = "204", description = "피드 좋아요 성공"),
-        `@ApiResponse`(responseCode = "400", description = "좋아요 횟수 초과 (최대 100)")
+        `@ApiResponse`(responseCode = "400", description = "좋아요 횟수 초과 (최대 100)"),
+        `@ApiResponse`(responseCode = "404", description = "피드 없음")
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/ddingdong/ddingdongBE/domain/feed/api/FeedLikeApi.java` around
lines 20 - 24, The OpenAPI annotations in FeedLikeApi are missing the 404
response that the controller actually returns; update the `@ApiResponses` on the
FeedLikeApi (where `@Operation`("피드 좋아요 API") is declared) to include an
`@ApiResponse`(responseCode = "404", description = "피드 없음") so the spec matches
runtime behavior (FeedLikeController.createLike -> feedService.getById(feedId)
throws NotFoundException). Ensure the new 404 entry uses the same annotation
style as the existing 204/400 entries.

@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/{feedId}/likes")
void deleteLike(
@PathVariable("feedId") Long feedId,
@Pattern(regexp = UUID_V4_REGEXP, message = "유효하지 않은 UUID v4 형식입니다.")
@RequestHeader("X-Anonymous-UUID") String uuid
);
@PostMapping("/{feedId}/likes")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

POST 엔드포인트의 HTTP 응답 코드가 컨벤션에 위배됩니다.

프로젝트 컨벤션(CONVENTIONS.md §5, HTTP 상태 코드 기준)은 POST → 201 CREATED를 명시하고 있습니다. 현재 @ResponseStatus(HttpStatus.NO_CONTENT)(204)를 반환하고 있어 위반입니다.

🔧 제안 수정
-    `@ApiResponse`(responseCode = "204", description = "피드 좋아요 성공"),
+    `@ApiResponse`(responseCode = "201", description = "피드 좋아요 성공"),
...
-    `@ResponseStatus`(HttpStatus.NO_CONTENT)
+    `@ResponseStatus`(HttpStatus.CREATED)

As per coding guidelines: "HTTP status codes: POST returns 201 Created"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ApiResponse(responseCode = "204", description = "피드 좋아요 성공"),
@ApiResponse(responseCode = "404", description = "피드 없음")
})
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/{feedId}/likes")
void deleteLike(
@PathVariable("feedId") Long feedId,
@Pattern(regexp = UUID_V4_REGEXP, message = "유효하지 않은 UUID v4 형식입니다.")
@RequestHeader("X-Anonymous-UUID") String uuid
);
@PostMapping("/{feedId}/likes")
`@ApiResponse`(responseCode = "201", description = "피드 좋아요 성공"),
`@ApiResponse`(responseCode = "404", description = "피드 없음")
})
`@ResponseStatus`(HttpStatus.CREATED)
`@PostMapping`("/{feedId}/likes")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/ddingdong/ddingdongBE/domain/feed/api/FeedLikeApi.java` around
lines 19 - 23, The POST endpoint annotated with `@PostMapping`("/{feedId}/likes")
currently uses `@ResponseStatus`(HttpStatus.NO_CONTENT) and an `@ApiResponse` with
responseCode "204"; update these to follow project conventions by replacing
HttpStatus.NO_CONTENT with HttpStatus.CREATED and changing the ApiResponse
responseCode/description to "201" (피드 좋아요 성공) so the method (FeedLikeApi POST
/{feedId}/likes) returns 201 Created instead of 204 No Content.

void createLike(@PathVariable("feedId") Long feedId);
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
package ddingdong.ddingdongBE.domain.feed.controller;

import ddingdong.ddingdongBE.domain.feed.api.FeedLikeApi;
import ddingdong.ddingdongBE.domain.feed.service.FeedLikeService;
import ddingdong.ddingdongBE.domain.feed.service.FeedService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController;

@Validated
@RestController
@RequiredArgsConstructor
public class FeedLikeController implements FeedLikeApi {

private final FeedLikeService feedLikeService;
private final FeedService feedService;

@Override
public void createLike(Long feedId, String uuid) {
feedLikeService.create(feedId, uuid);
}

@Override
public void deleteLike(Long feedId, String uuid) {
feedLikeService.delete(feedId, uuid);
public void createLike(Long feedId) {
feedService.incrementLikeCount(feedId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,20 @@ public class Feed extends BaseEntity {
@Column(nullable = false)
private long viewCount;

@Column(nullable = false)
private long likeCount;

@Column(name = "deleted_at")
private LocalDateTime deletedAt;

@Builder
private Feed(Long id, String activityContent, Club club, FeedType feedType, long viewCount) {
private Feed(Long id, String activityContent, Club club, FeedType feedType, long viewCount, long likeCount) {
this.id = id;
this.activityContent = activityContent;
this.club = club;
this.feedType = feedType;
this.viewCount = viewCount;
this.likeCount = likeCount;
}

public boolean isImage() {
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,22 @@ Slice<Feed> getAllFeedPage(
@Query(value = "UPDATE feed SET view_count = view_count + 1 WHERE id = :feedId", nativeQuery = true)
void incrementViewCount(@Param("feedId") Long feedId);

@Modifying(clearAutomatically = true)
@Query(value = "UPDATE feed SET like_count = like_count + 1 WHERE id = :feedId", nativeQuery = true)
void incrementLikeCount(@Param("feedId") Long feedId);

@Query(value = """
SELECT c.id AS clubId,
c.name AS clubName,
COUNT(f.id) AS feedCount,
COALESCE(SUM(f.view_count), 0) AS viewCount,
COALESCE(SUM(sub_like.like_cnt), 0) AS likeCount,
COALESCE(SUM(f.like_count), 0) AS likeCount,
COALESCE(SUM(sub_comment.comment_cnt), 0) AS commentCount
FROM club c
LEFT JOIN feed f ON f.club_id = c.id
AND f.deleted_at IS NULL
AND YEAR(f.created_at) = :year
AND MONTH(f.created_at) = :month
LEFT JOIN (
SELECT fl.feed_id, COUNT(*) AS like_cnt
FROM feed_like fl
GROUP BY fl.feed_id
) sub_like ON sub_like.feed_id = f.id
LEFT JOIN (
SELECT fc.feed_id, COUNT(*) AS comment_cnt
FROM feed_comment fc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import ddingdong.ddingdongBE.domain.feed.entity.Feed;
import ddingdong.ddingdongBE.domain.feed.repository.FeedCommentRepository;
import ddingdong.ddingdongBE.domain.feed.repository.FeedLikeRepository;
import ddingdong.ddingdongBE.domain.feed.repository.dto.FeedCountDto;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubFeedPageQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.ClubProfileQuery;
Expand All @@ -12,7 +11,6 @@
import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.FeedPageQuery;
import ddingdong.ddingdongBE.domain.feed.service.dto.query.PagingQuery;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand All @@ -28,9 +26,7 @@ public class FacadeFeedService {

private final FeedService feedService;
private final FeedFileService feedFileService;
private final FeedLikeService feedLikeService;
private final FeedCommentService feedCommentService;
private final FeedLikeRepository feedLikeRepository;
private final FeedCommentRepository feedCommentRepository;

public ClubFeedPageQuery getFeedPageByClub(Long clubId, int size, Long currentCursorId) {
Expand Down Expand Up @@ -63,7 +59,7 @@ public FeedQuery getById(Long feedId) {
Feed feed = feedService.getById(feedId);
ClubProfileQuery clubProfileQuery = feedFileService.extractClubInfo(feed.getClub());
FeedFileInfoQuery feedFileInfoQuery = feedFileService.extractFeedFileInfo(feed);
long likeCount = feedLikeService.countByFeedId(feedId);
long likeCount = feed.getLikeCount();
long commentCount = feedCommentService.countByFeedId(feedId);
List<FeedCommentQuery> comments = feedCommentService.getAllByFeedId(feedId);
return FeedQuery.of(feed, clubProfileQuery, feedFileInfoQuery, likeCount, commentCount, comments);
Expand All @@ -79,8 +75,8 @@ private List<FeedListQuery> buildFeedListQueriesWithCounts(List<Feed> feeds) {
return feedListQueries;
}

Map<Long, Long> likeCountMap = feedLikeRepository.countsByFeedIds(feedIds).stream()
.collect(Collectors.toMap(FeedCountDto::getFeedId, FeedCountDto::getCnt));
Map<Long, Long> likeCountMap = feeds.stream()
.collect(Collectors.toMap(Feed::getId, Feed::getLikeCount));
Map<Long, Long> commentCountMap = feedCommentRepository.countsByFeedIds(feedIds).stream()
.collect(Collectors.toMap(FeedCountDto::getFeedId, FeedCountDto::getCnt));

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public interface FeedService {
void delete(Feed feed);

void incrementViewCount(Long feedId);

void incrementLikeCount(Long feedId);
}
Loading