-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/#64 좋아요 토글 api 구현 #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
The head ref may contain hidden characters: "Feature/#64-\uC88B\uC544\uC694_\uD1A0\uAE00_API_\uAD6C\uD604"
Changes from all commits
3114aca
12ebbb7
ab45b00
a441591
44eac9f
90c910e
f2354a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| ifndef::snippets[] | ||
| :snippets: ./build/generated-snippets | ||
| endif::[] | ||
|
|
||
| = TEAM LIKE API 문서 | ||
| :doctype: book | ||
| :icons: font | ||
| :source-highlighter: highlightjs | ||
| :toc: left | ||
| :toclevels: 3 | ||
| :sectnums: | ||
|
|
||
| == API 목록 | ||
|
|
||
| link:./opus.html[API 목록으로 돌아가기] | ||
|
|
||
| == `PUT`: 팀 좋아요 토글 | ||
|
|
||
| 해당 팀에 대해 좋아요(찜) 상태를 토글합니다. | ||
|
|
||
| * `isLiked: true` → 좋아요 등록 | ||
| * `isLiked: false` → 좋아요 취소 | ||
|
|
||
|
|
||
| NOTE: 좋아요를 통해 팀을 찜할 수 있습니다. | ||
|
|
||
| NOTE: 투표 기간에는 투표만 가능하고, 투표 기간이 아닐 때만 좋아요가 가능합니다. | ||
|
|
||
| .Path Parameters | ||
| include::{snippets}/toggle-team-like/path-parameters.adoc[] | ||
|
|
||
| .HTTP Request Headers | ||
| include::{snippets}/toggle-team-like/request-headers.adoc[] | ||
|
|
||
| .Request Fields | ||
| include::{snippets}/toggle-team-like/request-fields.adoc[] | ||
|
|
||
| .Response Fields | ||
| include::{snippets}/toggle-team-like/response-fields.adoc[] | ||
|
|
||
| === 시나리오 1: TeamLike 데이터가 없는 경우 | ||
|
|
||
| 특정 멤버가 특정 팀에 대해 좋아요 API를 처음 호출하면, TeamLike 테이블에 데이터가 새로 생성됩니다. | ||
|
|
||
| [cols="1,2,1"] | ||
| |=== | ||
| |Request isLiked |응답 메시지 |HTTP 상태 코드 | ||
|
|
||
| |true | ||
| |좋아요가 등록되었습니다. | ||
| |200 OK | ||
|
|
||
| |false | ||
| |아직 좋아요하지 않은 팀입니다. | ||
| |400 Bad Request | ||
| |=== | ||
|
|
||
| isLiked: true → 좋아요 등록 | ||
|
|
||
| include::{snippets}/toggle-team-like/http-request.adoc[] | ||
|
|
||
| include::{snippets}/toggle-team-like/http-response.adoc[] | ||
|
|
||
| === 시나리오 2: TeamLike 데이터가 있는 경우 | ||
|
|
||
| 이미 해당 팀에 대한 좋아요 기록이 있는 경우, 상태에 따라 토글됩니다. | ||
|
|
||
| [cols="1,1,2,1"] | ||
| |=== | ||
| |현재 isLiked |Request isLiked |응답 메시지 |HTTP 상태 코드 | ||
|
|
||
| |true | ||
| |true | ||
| |이미 좋아요한 팀입니다. | ||
| |400 Bad Request | ||
|
|
||
| |true | ||
| |false | ||
| |좋아요가 취소되었습니다. | ||
| |200 OK | ||
|
|
||
| |false | ||
| |true | ||
| |좋아요가 등록되었습니다. | ||
| |200 OK | ||
|
|
||
| |false | ||
| |false | ||
| |이미 좋아요를 취소한 팀입니다. | ||
| |400 Bad Request | ||
| |=== | ||
|
|
||
| 좋아요 취소 (isLiked: true → false) | ||
|
|
||
| include::{snippets}/cancel-team-like/http-request.adoc[] | ||
|
|
||
| include::{snippets}/cancel-team-like/http-response.adoc[] | ||
|
|
||
| === ⚠️ 실패 케이스 | ||
|
|
||
|
Comment on lines
+98
to
+100
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| .❌ Case 1: 존재하지 않는 팀 | ||
|
|
||
| [%collapsible] | ||
|
|
||
| ==== | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-not-found/http-request.adoc[] | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-not-found/http-response.adoc[] | ||
|
|
||
| ==== | ||
|
|
||
| .❌ Case 2: 이미 좋아요한 팀 | ||
|
|
||
| [%collapsible] | ||
|
|
||
| ==== | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-already-liked/http-request.adoc[] | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-already-liked/http-response.adoc[] | ||
|
|
||
| ==== | ||
|
|
||
| .❌ Case 3: 이미 좋아요 취소한 팀 | ||
|
|
||
| [%collapsible] | ||
|
|
||
| ==== | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-already-unliked/http-request.adoc[] | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-already-unliked/http-response.adoc[] | ||
|
|
||
| ==== | ||
|
|
||
| .❌ Case 4: 좋아요한 적 없는 팀에 취소 요청 | ||
|
|
||
| [%collapsible] | ||
|
|
||
| ==== | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-not-liked-yet/http-request.adoc[] | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-not-liked-yet/http-response.adoc[] | ||
|
|
||
| ==== | ||
|
|
||
| .❌ Case 5: 투표 기간 중 좋아요 요청 | ||
|
|
||
| [%collapsible] | ||
|
|
||
| ==== | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-voting-period/http-request.adoc[] | ||
|
|
||
| include::{snippets}/toggle-team-like-fail-voting-period/http-response.adoc[] | ||
|
|
||
| ==== | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package com.opus.opus.modules.team.application; | ||
|
|
||
| import static com.opus.opus.modules.team.exception.TeamLikeExceptionType.ALREADY_LIKED; | ||
| import static com.opus.opus.modules.team.exception.TeamLikeExceptionType.ALREADY_UNLIKED; | ||
| import static com.opus.opus.modules.team.exception.TeamLikeExceptionType.DUPLICATE_LIKE_REQUEST; | ||
| import static com.opus.opus.modules.team.exception.TeamLikeExceptionType.NOT_LIKED_YET; | ||
|
|
||
| import com.opus.opus.modules.contest.application.convenience.ContestConvenience; | ||
| import com.opus.opus.modules.contest.domain.Contest; | ||
| import com.opus.opus.modules.team.application.convenience.TeamConvenience; | ||
| import com.opus.opus.modules.team.application.dto.response.TeamLikeToggleResponse; | ||
| import com.opus.opus.modules.team.domain.Team; | ||
| import com.opus.opus.modules.team.domain.TeamLike; | ||
| import com.opus.opus.modules.team.domain.dao.TeamLikeRepository; | ||
| import com.opus.opus.modules.team.exception.TeamLikeException; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.dao.DataIntegrityViolationException; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional | ||
| public class TeamLikeCommandService { | ||
|
|
||
| private final TeamConvenience teamConvenience; | ||
| private final ContestConvenience contestConvenience; | ||
|
|
||
| private final TeamLikeRepository teamLikeRepository; | ||
|
|
||
| public TeamLikeToggleResponse toggleLike(Long memberId, Long teamId, Boolean isLiked) { | ||
| Team team = teamConvenience.getValidateExistTeam(teamId); | ||
| Contest contest = contestConvenience.getValidateExistContest(team.getContestId()); | ||
|
|
||
| contestConvenience.validateNotInVotingPeriod(contest); | ||
|
|
||
| Optional<TeamLike> teamLikeOptional = teamLikeRepository.findByMemberIdAndTeam(memberId, team); | ||
| return teamLikeOptional | ||
| .map(teamLike -> handleExistingLike(teamLike, isLiked)) | ||
| .orElseGet(() -> handleFirstTimeLike(memberId, team, isLiked)); | ||
| } | ||
|
|
||
| private TeamLikeToggleResponse handleFirstTimeLike(Long memberId, Team team, Boolean isLiked) { | ||
| if (!isLiked) { | ||
| throw new TeamLikeException(NOT_LIKED_YET); | ||
| } | ||
|
|
||
| saveTeamLike(memberId, team, true); | ||
| return TeamLikeToggleResponse.of(team.getId(), true, "좋아요가 등록되었습니다."); | ||
| } | ||
|
|
||
| private TeamLikeToggleResponse handleExistingLike(final TeamLike teamLike, final Boolean isLiked) { | ||
| if (Objects.equals(teamLike.getIsLiked(), isLiked)) { | ||
| throw new TeamLikeException(isLiked ? ALREADY_LIKED : ALREADY_UNLIKED); | ||
| } | ||
|
|
||
| teamLike.updateIsLiked(isLiked); | ||
|
|
||
| return TeamLikeToggleResponse.of(teamLike.getTeam().getId(), isLiked, isLiked ? "좋아요가 등록되었습니다." : "좋아요가 취소되었습니다."); | ||
| } | ||
|
|
||
| private void saveTeamLike(Long memberId, Team team, Boolean isLiked) { | ||
| try { | ||
| teamLikeRepository.save(TeamLike.builder() | ||
| .memberId(memberId) | ||
| .team(team) | ||
| .isLiked(isLiked) | ||
| .build()); | ||
| teamLikeRepository.flush(); | ||
| } catch (DataIntegrityViolationException e) { | ||
| throw new TeamLikeException(DUPLICATE_LIKE_REQUEST); | ||
| } | ||
| } | ||
|
Comment on lines
+64
to
+75
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 투표와 비슷한 흐름으로 설계하신 것 같아요! 근데 제 생각에는 투표와 달리 반복해서 좋아요-좋아요 취소가 가능하고 (투표는 취소는 가능하나, 재투표는 불가) 한 transaction 내에서 flush가 발생하는 것도 예상하지 못한 side effect 발생 가능성 있다고 생각해서 DB hit를 강제할 필요는 없다고 생각하는데, 어떻게 생각하시나요?? |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.opus.opus.modules.team.application.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| public record TeamLikeToggleRequest( | ||
| @NotNull(message = "isLiked 값은 필수입니다.") | ||
| Boolean isLiked | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.opus.opus.modules.team.application.dto.response; | ||
|
|
||
| public record TeamLikeToggleResponse( | ||
| Long teamId, | ||
| Boolean isLiked, | ||
| String message | ||
| ) { | ||
| public static TeamLikeToggleResponse of(Long teamId, Boolean isLiked, String message) { | ||
| return new TeamLikeToggleResponse(teamId, isLiked, message); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API의 이름이 좋아요 토글인데 이미 좋아요 한 팀에게 해당 API를 다시 요청할 경우
Bad Request가 뜨는 것이 조금 어색하게 느껴집니다.만약 좋아요 한 팀에 재요청이 들어갔다면, 좋아요 취소 처리가 되는 것은 어떤가요??
같은 흐름으로 이미 좋아요를 취소한 팀경우도 같은 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 이 부분을 생각해봤는데 현재 구현에서는
isLiked=True상태인 팀에 다시isLiked=True를 하면 예외를 던지는게 맞는게 아닐까 생각합니다.토글만 하려면 Body가 없는게 낫지 않을까요?