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
@@ -0,0 +1,51 @@
package com.swyp.picke.domain.battle.controller;

import com.swyp.picke.domain.battle.dto.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.battle.dto.request.AdminBattleUpdateRequest;
import com.swyp.picke.domain.battle.dto.response.AdminBattleDeleteResponse;
import com.swyp.picke.domain.battle.dto.response.AdminBattleDetailResponse;
import com.swyp.picke.domain.battle.service.BattleService;
import com.swyp.picke.global.common.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(name = "배틀 API (관리자)", description = "배틀 생성/수정/삭제 (관리자 전용)")
@RestController
@RequestMapping("/api/v1/admin/battles")
@RequiredArgsConstructor
@PreAuthorize("hasRole('ADMIN')")
public class AdminBattleController {

private final BattleService battleService;

@Operation(summary = "배틀 생성")
@PostMapping
public ApiResponse<AdminBattleDetailResponse> createBattle(
@RequestBody @Valid AdminBattleCreateRequest request,
@AuthenticationPrincipal Long adminUserId
) {
return ApiResponse.onSuccess(battleService.createBattle(request, adminUserId));
}

@Operation(summary = "배틀 수정 (변경 필드만 포함)")
@PatchMapping("/{battleId}")
public ApiResponse<AdminBattleDetailResponse> updateBattle(
@PathVariable Long battleId,
@RequestBody @Valid AdminBattleUpdateRequest request
) {
return ApiResponse.onSuccess(battleService.updateBattle(battleId, request));
}

@Operation(summary = "배틀 삭제")
@DeleteMapping("/{battleId}")
public ApiResponse<AdminBattleDeleteResponse> deleteBattle(
@PathVariable Long battleId
) {
return ApiResponse.onSuccess(battleService.deleteBattle(battleId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,46 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

@Tag(name = "배틀 API", description = "배틀 조회")
@Tag(name = "배틀 API (사용자)", description = "배틀 조회")
@RestController
@RequestMapping("/api/v1/battles")
@RequiredArgsConstructor
public class BattleController {

private final BattleService battleService;

@Operation(summary = "오늘의 배틀 목록 조회 (최대 5개)")
@Operation(summary = "오늘의 배틀 목록 조회 (스와이프 UI용, 최대 5개)")
@GetMapping("/today")
public ApiResponse<TodayBattleListResponse> getTodayBattles() {
return ApiResponse.onSuccess(battleService.getTodayBattles());
}

@Operation(summary = "배틀 목록 조회")
@Operation(summary = "배틀 전체 목록 조회", description = "페이징 및 타입별(ALL, BATTLE, QUIZ, VOTE) 필터링된 배틀 목록을 조회합니다.")
@GetMapping
public ApiResponse<BattleListResponse> getBattles(
@Parameter(description = "페이지 번호 (1부터 시작)", example = "1")
@RequestParam(value = "page", defaultValue = "1") int page,
@Parameter(description = "페이지 크기", example = "10")
@RequestParam(value = "size", defaultValue = "10") int size,
@Parameter(description = "콘텐츠 상태 (ALL, PENDING, PUBLISHED, REJECTED, ARCHIVED)", example = "ALL")
@RequestParam(value = "status", required = false, defaultValue = "ALL") String status
@Parameter(description = "콘텐츠 타입 (ALL, BATTLE, QUIZ, VOTE)", example = "ALL")
@RequestParam(value = "type", required = false, defaultValue = "ALL") String type
) {
return ApiResponse.onSuccess(battleService.getBattles(page, size, status));
return ApiResponse.onSuccess(battleService.getBattles(page, size, type));
}

@Operation(summary = "배틀 상세 조회")
@GetMapping("/{battleId}")
public ApiResponse<BattleUserDetailResponse> getBattleDetail(@PathVariable Long battleId) {
public ApiResponse<BattleUserDetailResponse> getBattleDetail(
@PathVariable Long battleId
) {
return ApiResponse.onSuccess(battleService.getBattleDetail(battleId));
}

@Operation(summary = "사용자 배틀 진행 상태 조회")
@Operation(summary = "사용자 배틀 진행 상태 조회 (사전투표/TTS/사후투표)")
@GetMapping("/{battleId}/status")
public ApiResponse<UserBattleStatusResponse> getUserBattleStatus(@PathVariable Long battleId) {
return ApiResponse.onSuccess(battleService.getUserBattleStatus(battleId));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
package com.swyp.picke.domain.battle.converter;

import com.swyp.picke.domain.admin.dto.battle.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.admin.dto.battle.response.AdminBattleDetailResponse;
import com.swyp.picke.domain.battle.dto.request.AdminBattleCreateRequest;
import com.swyp.picke.domain.battle.dto.response.*;
import com.swyp.picke.domain.battle.entity.Battle;
import com.swyp.picke.domain.battle.entity.BattleOption;
import com.swyp.picke.domain.battle.enums.BattleCreatorType;
import com.swyp.picke.domain.user.enums.PhilosopherType;
import com.swyp.picke.domain.user.enums.UserBattleStep;
import com.swyp.picke.domain.tag.entity.Tag;
import com.swyp.picke.domain.tag.enums.TagType;
import com.swyp.picke.domain.user.entity.User;
import com.swyp.picke.domain.user.enums.UserBattleStep;
import com.swyp.picke.domain.user.enums.VoteSide;
import com.swyp.picke.global.infra.s3.enums.FileCategory;
import com.swyp.picke.global.infra.s3.util.ResourceUrlProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Comparator;
import java.util.List;
import java.util.Map;

Expand All @@ -26,17 +25,21 @@ public class BattleConverter {

private final ResourceUrlProvider urlProvider;
private static final String BASE_SHARE_URL = "https://pique.app/battles/";
private static final Comparator<BattleOption> OPTION_SORTER =
Comparator.comparing((BattleOption option) -> option.getDisplayOrder() == null ? Integer.MAX_VALUE : option.getDisplayOrder())
.thenComparing(option -> option.getLabel() == null ? "" : option.getLabel().name())
.thenComparing(BattleOption::getId);

public Battle toEntity(AdminBattleCreateRequest request, User admin) {
return Battle.builder()
.title(request.title())
.titlePrefix(request.titlePrefix())
.titleSuffix(request.titleSuffix())
.itemA(request.itemA())
.itemADesc(request.itemADesc())
.itemB(request.itemB())
.itemBDesc(request.itemBDesc())
.summary(request.summary())
.description(request.description())
.thumbnailUrl(request.thumbnailUrl())
.type(request.type())
.targetDate(request.targetDate())
.status(request.status())
.creatorType(BattleCreatorType.ADMIN)
.creator(admin)
Expand All @@ -49,11 +52,18 @@ public TodayBattleResponse toTodayResponse(Battle battle, List<Tag> tags, List<B
battle.getTitle(),
battle.getSummary(),
urlProvider.getImageUrl(FileCategory.BATTLE, battle.getThumbnailUrl()),
battle.getType(),
battle.getViewCount() == null ? 0 : battle.getViewCount(),
battle.getTotalParticipantsCount() == null ? 0L : battle.getTotalParticipantsCount(),
battle.getAudioDuration() == null ? 0 : battle.getAudioDuration(),
toTagResponses(tags, null),
toTodayOptionResponses(options)
toTodayOptionResponses(options),
battle.getTitlePrefix(),
battle.getTitleSuffix(),
battle.getItemA(),
battle.getItemADesc(),
battle.getItemB(),
battle.getItemBDesc()
);
}

Expand All @@ -62,6 +72,7 @@ public BattleSimpleResponse toSimpleResponse(Battle battle) {
battle.getId(),
battle.getTitle(),
urlProvider.getImageUrl(FileCategory.BATTLE, battle.getThumbnailUrl()),
battle.getType() != null ? battle.getType().name() : "BATTLE",
battle.getStatus() != null ? battle.getStatus().name() : "PENDING",
battle.getCreatedAt()
);
Expand All @@ -71,10 +82,16 @@ public AdminBattleDetailResponse toAdminDetailResponse(Battle battle, List<Tag>
return new AdminBattleDetailResponse(
battle.getId(),
battle.getTitle(),
battle.getTitlePrefix(),
battle.getTitleSuffix(),
battle.getSummary(),
battle.getDescription(),
urlProvider.getImageUrl(FileCategory.BATTLE, battle.getThumbnailUrl()),
battle.getAudioDuration(),
battle.getType(),
battle.getItemA(),
battle.getItemADesc(),
battle.getItemB(),
battle.getItemBDesc(),
battle.getTargetDate(),
battle.getStatus(),
battle.getCreatorType(),
Expand All @@ -94,6 +111,7 @@ public BattleUserDetailResponse toUserDetailResponse(
battle.getTitle(),
battle.getSummary(),
urlProvider.getImageUrl(FileCategory.BATTLE, battle.getThumbnailUrl()),
battle.getType(),
battle.getViewCount() == null ? 0 : battle.getViewCount(),
participantsCount == null ? 0L : participantsCount,
battle.getAudioDuration() == null ? 0 : battle.getAudioDuration(),
Expand All @@ -103,6 +121,12 @@ public BattleUserDetailResponse toUserDetailResponse(

return new BattleUserDetailResponse(
summary,
battle.getTitlePrefix(),
battle.getTitleSuffix(),
battle.getItemA(),
battle.getItemADesc(),
battle.getItemB(),
battle.getItemBDesc(),
battle.getDescription(),
BASE_SHARE_URL + battle.getId(),
userVoteStatus,
Expand All @@ -119,7 +143,8 @@ public BattleScenarioResponse toScenarioResponse(Battle battle, List<BattleOptio
opt.getLabel().name(),
opt.getRepresentative(),
opt.getStance(),
urlProvider.getImageUrl(FileCategory.PHILOSOPHER, opt.getImageUrl())
opt.getQuote(),
urlProvider.getImageUrl(FileCategory.PHILOSOPHER, PhilosopherType.resolveImageKey(opt.getRepresentative()))
)).toList();

return new BattleScenarioResponse(battle.getTitle(), profiles);
Expand All @@ -128,7 +153,6 @@ public BattleScenarioResponse toScenarioResponse(Battle battle, List<BattleOptio
private List<BattleOptionResponse> toOptionResponses(List<BattleOption> options, Map<Long, List<Tag>> optionTagsMap) {
if (options == null) return List.of();
return options.stream()
.sorted(OPTION_SORTER)
.map(option -> {
List<Tag> optionTags = optionTagsMap.getOrDefault(option.getId(), List.of());
return new BattleOptionResponse(
Expand All @@ -137,24 +161,24 @@ private List<BattleOptionResponse> toOptionResponses(List<BattleOption> options,
option.getTitle(),
option.getStance(),
option.getRepresentative(),
urlProvider.getImageUrl(FileCategory.PHILOSOPHER, option.getImageUrl()),
option.getQuote(),
urlProvider.getImageUrl(FileCategory.PHILOSOPHER, PhilosopherType.resolveImageKey(option.getRepresentative())),
toTagResponses(optionTags, null)
);
}).toList();
}

private List<TodayOptionResponse> toTodayOptionResponses(List<BattleOption> options) {
if (options == null) return List.of();
return options.stream()
.sorted(OPTION_SORTER)
.map(option -> new TodayOptionResponse(
option.getId(),
option.getLabel(),
option.getTitle(),
option.getRepresentative(),
option.getStance(),
urlProvider.getImageUrl(FileCategory.PHILOSOPHER, option.getImageUrl())
)).toList();
return options.stream().map(option -> new TodayOptionResponse(
option.getId(),
option.getLabel(),
option.getTitle(),
option.getRepresentative(),
option.getStance(),
urlProvider.getImageUrl(FileCategory.PHILOSOPHER, option.getImageUrl()),
option.getIsCorrect()
)).toList();
}

private List<BattleTagResponse> toTagResponses(List<Tag> tags, TagType targetType) {
Expand All @@ -164,4 +188,4 @@ private List<BattleTagResponse> toTagResponses(List<Tag> tags, TagType targetTyp
.map(tag -> new BattleTagResponse(tag.getId(), tag.getName(), tag.getType()))
.toList();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.swyp.picke.domain.battle.dto.request;

import com.swyp.picke.domain.battle.enums.BattleStatus;
import com.swyp.picke.domain.battle.enums.BattleType;
import java.time.LocalDate;
import java.util.List;

public record AdminBattleCreateRequest(
String title,
String titlePrefix,
String titleSuffix,
String summary,
String description,
String thumbnailUrl,
BattleType type,
BattleStatus status,
String itemA,
String itemADesc,
String itemB,
String itemBDesc,
LocalDate targetDate,
List<Long> tagIds,
List<AdminBattleOptionRequest> options
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.swyp.picke.domain.battle.dto.request;

import com.swyp.picke.domain.battle.enums.BattleOptionLabel;

import java.util.List;

public record AdminBattleOptionRequest(
BattleOptionLabel label,
String title,
String stance,
String representative,
String quote,
String imageUrl,
Boolean isCorrect,
List<Long> tagIds // 옵션 전용 태그 (철학자, 가치관 - 추후 사용자 유형 분석에 사용)
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.swyp.picke.domain.battle.dto.request;

import com.swyp.picke.domain.battle.enums.BattleStatus;
import java.time.LocalDate;
import java.util.List;

public record AdminBattleUpdateRequest(
String title,
String titlePrefix,
String titleSuffix,
String summary,
String description,
String thumbnailUrl,
String itemA,
String itemADesc,
String itemB,
String itemBDesc,
LocalDate targetDate,
Integer audioDuration,
BattleStatus status,
List<Long> tagIds,
List<AdminBattleOptionRequest> options
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.swyp.picke.domain.battle.dto.response;

import java.time.LocalDateTime;

/**
* 관리자 - 배틀 삭제 응답
* 역할: 배틀이 성공적으로 소프트 딜리트 되었는지 확인하고 삭제 시점을 반환합니다.
*/

public record AdminBattleDeleteResponse(
Boolean success, // 삭제 성공 여부
LocalDateTime deletedAt // 삭제 처리된 일시 (Soft Delete)
) {}
Loading