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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Tag(name = "관점 API", description = "관점 생성, 조회, 수정, 삭제")
@RestController
@RequestMapping("/api/v1")
Expand Down Expand Up @@ -63,9 +65,9 @@ public ApiResponse<PerspectiveListResponse> getPerspectives(
return ApiResponse.onSuccess(perspectiveService.getPerspectives(battleId, userId, cursor, size, optionId, sort));
}

@Operation(summary = "내 관점 조회", description = "해당 배틀에서 본인이 작성한 관점을 조회합니다.")
@Operation(summary = "내 관점 조회", description = "해당 배틀에서 본인이 작성한 관점 목록을 조회합니다.")
@GetMapping("/battles/{battleId}/perspectives/me")
public ApiResponse<MyPerspectiveResponse> getMyPerspective(
public ApiResponse<List<MyPerspectiveResponse>> getMyPerspective(
@PathVariable Long battleId,
@AuthenticationPrincipal Long userId) {
return ApiResponse.onSuccess(perspectiveService.getMyPerspective(battleId, userId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(
name = "perspectives",
uniqueConstraints = @UniqueConstraint(columnNames = {"battle_id", "user_id"})
)
@Table(name = "perspectives")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Perspective extends BaseEntity {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,10 @@

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

public interface PerspectiveRepository extends JpaRepository<Perspective, Long> {

boolean existsByBattleIdAndUserId(Long battleId, Long userId);

Optional<Perspective> findByBattleIdAndUserId(Long battleId, Long userId);
List<Perspective> findByBattleIdAndUserIdOrderByCreatedAtDesc(Long battleId, Long userId);

List<Perspective> findByBattleIdAndStatusOrderByCreatedAtDesc(Long battleId, PerspectiveStatus status, Pageable pageable);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ public CreatePerspectiveResponse createPerspective(Long battleId, Long userId, C
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

if (perspectiveRepository.existsByBattleIdAndUserId(battleId, userId)) {
throw new CustomException(ErrorCode.PERSPECTIVE_ALREADY_EXISTS);
}

Long postVoteOptionId = BattleVoteService.findPostVoteOptionId(battleId, userId);
BattleOption option = battleService.findOptionById(postVoteOptionId);

Expand Down Expand Up @@ -181,27 +177,30 @@ public UpdatePerspectiveResponse updatePerspective(Long perspectiveId, Long user
return new UpdatePerspectiveResponse(perspective.getId(), perspective.getContent(), perspective.getUpdatedAt());
}

public MyPerspectiveResponse getMyPerspective(Long battleId, Long userId) {
public List<MyPerspectiveResponse> getMyPerspective(Long battleId, Long userId) {
battleService.findById(battleId);
Perspective perspective = perspectiveRepository.findByBattleIdAndUserId(battleId, userId)
.orElseThrow(() -> new CustomException(ErrorCode.PERSPECTIVE_NOT_FOUND));
List<Perspective> perspectives = perspectiveRepository.findByBattleIdAndUserIdOrderByCreatedAtDesc(battleId, userId);

UserSummary user = userQueryService.findSummaryById(userId);
String characterImageUrl = resolveCharacterImageUrl(user.characterType());
BattleOption option = perspective.getOption();
boolean isLiked = perspectiveLikeRepository.existsByPerspectiveAndUserId(perspective, userId);

return new MyPerspectiveResponse(
perspective.getId(),
new MyPerspectiveResponse.UserSummary(user.userTag(), user.nickname(), user.characterType(), characterImageUrl),
new MyPerspectiveResponse.OptionSummary(option.getId(), BattleOptionDisplay.opinion(option), option.getTitle(), option.getStance()),
perspective.getContent(),
perspective.getLikeCount(),
perspective.getCommentCount(),
isLiked,
perspective.getStatus(),
perspective.getCreatedAt()
);
return perspectives.stream()
.map(perspective -> {
BattleOption option = perspective.getOption();
boolean isLiked = perspectiveLikeRepository.existsByPerspectiveAndUserId(perspective, userId);
return new MyPerspectiveResponse(
perspective.getId(),
new MyPerspectiveResponse.UserSummary(user.userTag(), user.nickname(), user.characterType(), characterImageUrl),
new MyPerspectiveResponse.OptionSummary(option.getId(), BattleOptionDisplay.opinion(option), option.getTitle(), option.getStance()),
perspective.getContent(),
perspective.getLikeCount(),
perspective.getCommentCount(),
isLiked,
perspective.getStatus(),
perspective.getCreatedAt()
);
})
.toList();
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public enum ErrorCode {

// Perspective
PERSPECTIVE_NOT_FOUND(HttpStatus.NOT_FOUND, "PERSPECTIVE_404", "존재하지 않는 관점입니다."),
PERSPECTIVE_ALREADY_EXISTS(HttpStatus.CONFLICT, "PERSPECTIVE_409", "이미 관점을 작성한 배틀입니다."),
PERSPECTIVE_FORBIDDEN(HttpStatus.FORBIDDEN, "PERSPECTIVE_403", "본인 관점만 수정/삭제할 수 있습니다."),
PERSPECTIVE_POST_VOTE_REQUIRED(HttpStatus.CONFLICT, "PERSPECTIVE_VOTE_409", "사후 투표가 완료되지 않았습니다."),
PERSPECTIVE_MODERATION_NOT_FAILED(HttpStatus.BAD_REQUEST, "PERSPECTIVE_400", "검수 실패 상태의 관점이 아닙니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- 1인 1관점 제한 해제: perspectives(battle_id, user_id) 유니크 제약 제거
-- Hibernate가 이름을 자동 생성해 환경마다 다를 수 있으므로 이름에 의존하지 않고 동적으로 찾아 드롭한다.
DO $$
DECLARE
found_constraint_name text;
BEGIN
SELECT tc.constraint_name INTO found_constraint_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_name = kcu.table_name
WHERE tc.table_name = 'perspectives'
AND tc.constraint_type = 'UNIQUE'
GROUP BY tc.constraint_name
HAVING array_agg(kcu.column_name ORDER BY kcu.column_name) = ARRAY['battle_id', 'user_id'];

IF found_constraint_name IS NOT NULL THEN
EXECUTE format('ALTER TABLE perspectives DROP CONSTRAINT %I', found_constraint_name);
END IF;
END $$;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import com.swyp.picke.domain.battle.enums.BattleOptionLabel;
import com.swyp.picke.domain.battle.enums.BattleStatus;
import com.swyp.picke.domain.battle.service.BattleService;
import com.swyp.picke.domain.perspective.dto.request.CreatePerspectiveRequest;
import com.swyp.picke.domain.perspective.dto.response.CreatePerspectiveResponse;
import com.swyp.picke.domain.perspective.dto.response.MyPerspectiveResponse;
import com.swyp.picke.domain.perspective.dto.response.PerspectiveDetailResponse;
import com.swyp.picke.domain.perspective.entity.Perspective;
import com.swyp.picke.domain.perspective.repository.PerspectiveCommentRepository;
Expand All @@ -19,6 +22,7 @@
import com.swyp.picke.domain.vote.service.BattleVoteService;
import com.swyp.picke.global.infra.s3.service.S3PresignedUrlService;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -90,6 +94,54 @@ void getPerspectiveDetail_returns_option_opinion_as_label() {
assertThat(response.option().title()).isEqualTo("예술이 아니다");
}

@Test
@DisplayName("이미 관점을 남긴 배틀에도 새 관점을 추가로 등록할 수 있다")
void createPerspective_allows_multiple_perspectives_for_same_user_and_battle() {
Battle battle = battle(100L);
User user = user(1L);
BattleOption option = option(10L, battle, BattleOptionLabel.A, "예술이다");

when(battleService.findById(100L)).thenReturn(battle);
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
when(battleVoteService.findPostVoteOptionId(100L, 1L)).thenReturn(10L);
when(battleService.findOptionById(10L)).thenReturn(option);
when(perspectiveRepository.save(org.mockito.ArgumentMatchers.any(Perspective.class)))
.thenAnswer(invocation -> invocation.getArgument(0));

CreatePerspectiveResponse response = perspectiveService.createPerspective(
100L, 1L, new CreatePerspectiveRequest("두 번째 관점"));

assertThat(response).isNotNull();
org.mockito.Mockito.verify(perspectiveRepository).save(org.mockito.ArgumentMatchers.any(Perspective.class));
}

@Test
@DisplayName("내 관점 조회는 한 배틀에 남긴 여러 관점을 목록으로 반환한다")
void getMyPerspective_returns_multiple_perspectives_as_list() {
Battle battle = battle(100L);
User user = user(1L);
BattleOption option = option(10L, battle, BattleOptionLabel.A, "예술이다");

Perspective first = Perspective.builder().battle(battle).user(user).option(option).content("첫 번째").build();
first.publish();
ReflectionTestUtils.setField(first, "id", 1L);

Perspective second = Perspective.builder().battle(battle).user(user).option(option).content("두 번째").build();
second.publish();
ReflectionTestUtils.setField(second, "id", 2L);

when(battleService.findById(100L)).thenReturn(battle);
when(perspectiveRepository.findByBattleIdAndUserIdOrderByCreatedAtDesc(100L, 1L))
.thenReturn(List.of(second, first));
when(userQueryService.findSummaryById(1L)).thenReturn(new UserSummary("user-1", "nick", "OWL"));
when(s3PresignedUrlService.generatePresignedUrl(anyString())).thenReturn("character-url");

List<MyPerspectiveResponse> responses = perspectiveService.getMyPerspective(100L, 1L);

assertThat(responses).hasSize(2);
assertThat(responses).extracting(MyPerspectiveResponse::perspectiveId).containsExactly(2L, 1L);
}

private Battle battle(Long id) {
Battle battle = Battle.builder()
.title("battle")
Expand Down