Skip to content

Commit abf8e38

Browse files
authored
#101 [Feat] 배틀 API 고도화, 성능 최적화, 홈 이미지 오류 해결 (#102)
1 parent f4b59b2 commit abf8e38

11 files changed

Lines changed: 138 additions & 46 deletions

File tree

src/main/java/com/swyp/picke/domain/battle/controller/BattleController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.swyp.picke.domain.battle.dto.response.BattleUserDetailResponse;
55
import com.swyp.picke.domain.battle.dto.response.TodayBattleListResponse;
66
import com.swyp.picke.domain.battle.service.BattleService;
7+
import com.swyp.picke.domain.user.dto.response.UserBattleStatusResponse;
78
import com.swyp.picke.global.common.response.ApiResponse;
89
import io.swagger.v3.oas.annotations.Operation;
910
import io.swagger.v3.oas.annotations.Parameter;
@@ -45,4 +46,10 @@ public ApiResponse<BattleUserDetailResponse> getBattleDetail(
4546
) {
4647
return ApiResponse.onSuccess(battleService.getBattleDetail(battleId));
4748
}
49+
50+
@Operation(summary = "사용자 배틀 진행 상태 조회 (사전투표/TTS/사후투표)")
51+
@GetMapping("/{battleId}/status")
52+
public ApiResponse<UserBattleStatusResponse> getUserBattleStatus(@PathVariable Long battleId) {
53+
return ApiResponse.onSuccess(battleService.getUserBattleStatus(battleId));
54+
}
4855
}

src/main/java/com/swyp/picke/domain/battle/converter/BattleConverter.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,24 @@
44
import com.swyp.picke.domain.battle.dto.response.*;
55
import com.swyp.picke.domain.battle.entity.Battle;
66
import com.swyp.picke.domain.battle.entity.BattleOption;
7-
import com.swyp.picke.domain.battle.entity.BattleOptionTag;
87
import com.swyp.picke.domain.battle.enums.BattleCreatorType;
98
import com.swyp.picke.domain.user.enums.UserBattleStep;
10-
import com.swyp.picke.domain.battle.repository.BattleOptionTagRepository;
119
import com.swyp.picke.domain.tag.entity.Tag;
1210
import com.swyp.picke.domain.tag.enums.TagType;
1311
import com.swyp.picke.domain.user.entity.User;
12+
import com.swyp.picke.domain.user.enums.VoteSide;
1413
import com.swyp.picke.global.infra.s3.enums.FileCategory;
1514
import com.swyp.picke.global.infra.s3.util.ResourceUrlProvider;
1615
import lombok.RequiredArgsConstructor;
1716
import org.springframework.stereotype.Component;
1817

1918
import java.util.List;
19+
import java.util.Map;
2020

2121
@Component
2222
@RequiredArgsConstructor
2323
public class BattleConverter {
2424

25-
private final BattleOptionTagRepository optionTagRepository;
2625
private final ResourceUrlProvider urlProvider;
2726
private static final String BASE_SHARE_URL = "https://pique.app/battles/";
2827

@@ -78,7 +77,7 @@ public BattleSimpleResponse toSimpleResponse(Battle battle) {
7877
);
7978
}
8079

81-
public AdminBattleDetailResponse toAdminDetailResponse(Battle battle, List<Tag> tags, List<BattleOption> options) {
80+
public AdminBattleDetailResponse toAdminDetailResponse(Battle battle, List<Tag> tags, List<BattleOption> options, Map<Long, List<Tag>> optionTagsMap) {
8281
return new AdminBattleDetailResponse(
8382
battle.getId(),
8483
battle.getTitle(),
@@ -96,15 +95,15 @@ public AdminBattleDetailResponse toAdminDetailResponse(Battle battle, List<Tag>
9695
battle.getStatus(),
9796
battle.getCreatorType(),
9897
toTagResponses(tags, null),
99-
toOptionResponses(options),
98+
toOptionResponses(options, optionTagsMap),
10099
battle.getCreatedAt(),
101100
battle.getUpdatedAt()
102101
);
103102
}
104103

105104
public BattleUserDetailResponse toUserDetailResponse(
106-
Battle battle, List<Tag> tags, List<BattleOption> options,
107-
Long participantsCount, String voteStatus, UserBattleStep currentStep) {
105+
Battle battle, List<Tag> tags, List<BattleOption> options, Map<Long, List<Tag>> optionTagsMap,
106+
Long participantsCount, VoteSide userVoteStatus, UserBattleStep currentStep) {
108107

109108
BattleSummaryResponse summary = new BattleSummaryResponse(
110109
battle.getId(),
@@ -116,7 +115,7 @@ public BattleUserDetailResponse toUserDetailResponse(
116115
participantsCount == null ? 0L : participantsCount,
117116
battle.getAudioDuration() == null ? 0 : battle.getAudioDuration(),
118117
toTagResponses(tags, null),
119-
toOptionResponses(options)
118+
toOptionResponses(options, optionTagsMap)
120119
);
121120

122121
return new BattleUserDetailResponse(
@@ -129,21 +128,32 @@ public BattleUserDetailResponse toUserDetailResponse(
129128
battle.getItemBDesc(),
130129
battle.getDescription(),
131130
BASE_SHARE_URL + battle.getId(),
132-
voteStatus,
131+
userVoteStatus,
133132
currentStep,
134133
toTagResponses(tags, TagType.CATEGORY),
135134
toTagResponses(tags, TagType.PHILOSOPHER),
136135
toTagResponses(tags, TagType.VALUE)
137136
);
138137
}
139138

140-
private List<BattleOptionResponse> toOptionResponses(List<BattleOption> options) {
139+
public BattleScenarioResponse toScenarioResponse(Battle battle, List<BattleOption> options) {
140+
List<BattleScenarioResponse.PhilosopherProfileResponse> profiles = options.stream()
141+
.map(opt -> new BattleScenarioResponse.PhilosopherProfileResponse(
142+
opt.getLabel().name(),
143+
opt.getRepresentative(),
144+
opt.getStance(),
145+
opt.getQuote(),
146+
urlProvider.getImageUrl(FileCategory.PHILOSOPHER, opt.getImageUrl())
147+
)).toList();
148+
149+
return new BattleScenarioResponse(battle.getTitle(), profiles);
150+
}
151+
152+
private List<BattleOptionResponse> toOptionResponses(List<BattleOption> options, Map<Long, List<Tag>> optionTagsMap) {
141153
if (options == null) return List.of();
142154
return options.stream()
143155
.map(option -> {
144-
List<Tag> optionTags = optionTagRepository.findByBattleOption(option).stream()
145-
.map(BattleOptionTag::getTag)
146-
.toList();
156+
List<Tag> optionTags = optionTagsMap.getOrDefault(option.getId(), List.of());
147157
return new BattleOptionResponse(
148158
option.getId(),
149159
option.getLabel(),
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.swyp.picke.domain.battle.dto.response;
2+
3+
import java.util.List;
4+
5+
public record BattleScenarioResponse(
6+
String title,
7+
List<PhilosopherProfileResponse> philosophers
8+
) {
9+
public record PhilosopherProfileResponse(
10+
String label,
11+
String name,
12+
String stance,
13+
String quote,
14+
String imageUrl
15+
) {}
16+
}

src/main/java/com/swyp/picke/domain/battle/dto/response/BattleUserDetailResponse.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.swyp.picke.domain.battle.dto.response;
22

33
import com.swyp.picke.domain.user.enums.UserBattleStep;
4+
import com.swyp.picke.domain.user.enums.VoteSide;
45

56
import java.util.List;
67

@@ -18,7 +19,7 @@ public record BattleUserDetailResponse(
1819
String itemBDesc,
1920
String description, // 상세 본문 설명
2021
String shareUrl, // 공유하기 버튼용 링크
21-
String userVoteStatus, // 현재 유저의 투표 상태
22+
VoteSide userVoteStatus, // 현재 유저의 투표 상태
2223
UserBattleStep currentStep,
2324
List<BattleTagResponse> categoryTags, // UI 상단용 카테고리 태그
2425
List<BattleTagResponse> philosopherTags, // UI 하단용 철학자 태그
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package com.swyp.picke.domain.battle.repository;
22

3+
import com.swyp.picke.domain.battle.entity.Battle;
34
import com.swyp.picke.domain.battle.entity.BattleOption;
45
import com.swyp.picke.domain.battle.entity.BattleOptionTag;
56
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
9+
610
import java.util.List;
711

812
public interface BattleOptionTagRepository extends JpaRepository<BattleOptionTag, Long> {
913
List<BattleOptionTag> findByBattleOption(BattleOption battleOption);
14+
15+
@Query("SELECT bot FROM BattleOptionTag bot JOIN FETCH bot.tag WHERE bot.battleOption.battle = :battle")
16+
List<BattleOptionTag> findByBattleWithTags(@Param("battle") Battle battle);
1017
}

src/main/java/com/swyp/picke/domain/battle/service/BattleService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.swyp.picke.domain.battle.entity.BattleOption;
88
import com.swyp.picke.domain.battle.enums.BattleOptionLabel;
99
import com.swyp.picke.domain.battle.enums.BattleType;
10+
import com.swyp.picke.domain.user.dto.response.UserBattleStatusResponse;
1011

1112
import java.util.List;
1213

@@ -49,6 +50,9 @@ public interface BattleService {
4950
// 투표 실행 및 실시간 통계 결과 반환
5051
BattleVoteResponse vote(Long battleId, Long optionId);
5152

53+
BattleScenarioResponse getBattleScenario(Long battleId);
54+
55+
UserBattleStatusResponse getUserBattleStatus(Long battleId);
5256

5357
// === [관리자용 API] ===
5458

src/main/java/com/swyp/picke/domain/battle/service/BattleServiceImpl.java

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.swyp.picke.domain.tag.entity.Tag;
2121
import com.swyp.picke.domain.tag.repository.TagRepository;
2222
import com.swyp.picke.domain.user.entity.User;
23+
import com.swyp.picke.domain.user.enums.VoteSide;
2324
import com.swyp.picke.domain.user.repository.UserRepository;
2425
import com.swyp.picke.domain.user.service.UserBattleService;
2526
import com.swyp.picke.domain.vote.entity.Vote;
@@ -138,28 +139,54 @@ public BattleUserDetailResponse getBattleDetail(Long battleId) {
138139
Battle battle = findById(battleId);
139140
List<Tag> tags = getTagsByBattle(battle);
140141
List<BattleOption> options = battleOptionRepository.findByBattle(battle);
141-
142+
Map<Long, List<Tag>> optionTagsMap = battleOptionTagRepository.findByBattleWithTags(battle)
143+
.stream()
144+
.collect(Collectors.groupingBy(
145+
bot -> bot.getBattleOption().getId(),
146+
Collectors.mapping(BattleOptionTag::getTag, Collectors.toList())
147+
));
142148
Long currentUserId = SecurityUtil.getCurrentUserId();
143149
User user = userRepository.findById(currentUserId)
144150
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
145151

146152
UserBattleStatusResponse statusResponse = userBattleService.getUserBattleStatus(user, battle);
147153
UserBattleStep currentStep = statusResponse.step();
148154

149-
Optional<Vote> optionalVote = voteRepository.findByBattleIdAndUserId(battleId, currentUserId);
150-
String voteStatus = optionalVote
151-
.map(vote -> vote.getPostVoteOption() != null
152-
? vote.getPostVoteOption().getLabel().name() : "NONE")
153-
.orElse("NONE");
155+
Optional<Vote> optionalVote = voteRepository.findByBattleIdAndUserIdWithOption(battleId, currentUserId);
156+
VoteSide voteStatus = optionalVote
157+
.map(vote -> {
158+
if (vote.getPostVoteOption() != null) {
159+
return vote.getPostVoteOption().getLabel() == BattleOptionLabel.A ? VoteSide.PRO : VoteSide.CON;
160+
}
161+
return null;
162+
})
163+
.orElse(null);
154164

155165
return battleConverter.toUserDetailResponse(
156-
battle, tags, options,
166+
battle, tags, options, optionTagsMap,
157167
battle.getTotalParticipantsCount(),
158168
voteStatus,
159169
currentStep
160170
);
161171
}
162172

173+
@Override
174+
public BattleScenarioResponse getBattleScenario(Long battleId) {
175+
Battle battle = findById(battleId);
176+
List<BattleOption> options = battleOptionRepository.findByBattle(battle);
177+
return battleConverter.toScenarioResponse(battle, options);
178+
}
179+
180+
@Override
181+
public UserBattleStatusResponse getUserBattleStatus(Long battleId) {
182+
Battle battle = findById(battleId);
183+
Long currentUserId = SecurityUtil.getCurrentUserId();
184+
User user = userRepository.findById(currentUserId)
185+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
186+
187+
return userBattleService.getUserBattleStatus(user, battle);
188+
}
189+
163190
@Override
164191
@Transactional
165192
public BattleVoteResponse vote(Long battleId, Long optionId) {
@@ -225,7 +252,14 @@ public AdminBattleDetailResponse createBattle(AdminBattleCreateRequest request,
225252
savedOptions.add(option);
226253
}
227254

228-
return battleConverter.toAdminDetailResponse(battle, getTagsByBattle(battle), savedOptions);
255+
Map<Long, List<Tag>> optionTagsMap = battleOptionTagRepository.findByBattleWithTags(battle)
256+
.stream()
257+
.collect(Collectors.groupingBy(
258+
bot -> bot.getBattleOption().getId(),
259+
Collectors.mapping(BattleOptionTag::getTag, Collectors.toList())
260+
));
261+
262+
return battleConverter.toAdminDetailResponse(battle, getTagsByBattle(battle), savedOptions, optionTagsMap);
229263
}
230264

231265
@Override
@@ -268,7 +302,14 @@ public AdminBattleDetailResponse updateBattle(Long battleId, AdminBattleUpdateRe
268302
}
269303

270304
List<BattleOption> updatedOptions = battleOptionRepository.findByBattle(battle);
271-
return battleConverter.toAdminDetailResponse(battle, getTagsByBattle(battle), updatedOptions);
305+
Map<Long, List<Tag>> optionTagsMap = battleOptionTagRepository.findByBattleWithTags(battle)
306+
.stream()
307+
.collect(Collectors.groupingBy(
308+
bot -> bot.getBattleOption().getId(),
309+
Collectors.mapping(BattleOptionTag::getTag, Collectors.toList())
310+
));
311+
312+
return battleConverter.toAdminDetailResponse(battle, getTagsByBattle(battle), updatedOptions, optionTagsMap);
272313
}
273314

274315
@Override
@@ -280,7 +321,6 @@ public AdminBattleDeleteResponse deleteBattle(Long battleId) {
280321
return new AdminBattleDeleteResponse(true, LocalDateTime.now());
281322
}
282323

283-
// ✅ convertToTodayResponses — secureThumbnail 계산 제거
284324
private List<TodayBattleResponse> convertToTodayResponses(List<Battle> battles) {
285325
if (battles == null || battles.isEmpty()) return Collections.emptyList();
286326

src/main/java/com/swyp/picke/domain/home/service/HomeService.java

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import com.swyp.picke.domain.home.dto.response.*;
1111
import com.swyp.picke.domain.notification.enums.NotificationCategory;
1212
import com.swyp.picke.domain.notification.service.NotificationService;
13-
import com.swyp.picke.domain.user.enums.PhilosopherType;
1413
import com.swyp.picke.global.infra.s3.service.S3PresignedUrlService;
1514
import lombok.RequiredArgsConstructor;
1615
import org.springframework.stereotype.Service;
@@ -58,8 +57,7 @@ private HomeEditorPickResponse toEditorPick(TodayBattleResponse b) {
5857
String optionA = findOptionTitle(b.options(), BattleOptionLabel.A);
5958
String optionB = findOptionTitle(b.options(), BattleOptionLabel.B);
6059

61-
String secureThumb = (b.thumbnailUrl() != null && !b.thumbnailUrl().isBlank())
62-
? s3PresignedUrlService.generatePresignedUrl(b.thumbnailUrl()) : null;
60+
String secureThumb = b.thumbnailUrl();
6361

6462
return new HomeEditorPickResponse(
6563
b.battleId(), secureThumb,
@@ -69,13 +67,9 @@ private HomeEditorPickResponse toEditorPick(TodayBattleResponse b) {
6967
);
7068
}
7169

72-
// 트렌딩 썸네일 Presigned URL 적용
7370
private HomeTrendingResponse toTrending(TodayBattleResponse b) {
74-
String secureThumb = (b.thumbnailUrl() != null && !b.thumbnailUrl().isBlank())
75-
? s3PresignedUrlService.generatePresignedUrl(b.thumbnailUrl()) : null;
76-
7771
return new HomeTrendingResponse(
78-
b.battleId(), secureThumb,
72+
b.battleId(), b.thumbnailUrl(),
7973
b.title(), b.tags(),
8074
b.audioDuration(), b.viewCount()
8175
);
@@ -122,11 +116,8 @@ private HomeNewBattleResponse toNewBattle(TodayBattleResponse b) {
122116
String imageA = findRepresentativeImageUrl(b.options(), BattleOptionLabel.A);
123117
String imageB = findRepresentativeImageUrl(b.options(), BattleOptionLabel.B);
124118

125-
String secureThumb = (b.thumbnailUrl() != null && !b.thumbnailUrl().isBlank())
126-
? s3PresignedUrlService.generatePresignedUrl(b.thumbnailUrl()) : null;
127-
128119
return new HomeNewBattleResponse(
129-
b.battleId(), secureThumb,
120+
b.battleId(), b.thumbnailUrl(),
130121
b.title(), b.summary(),
131122
philoA, imageA,
132123
philoB, imageB,
@@ -161,12 +152,9 @@ private List<String> findPhilosopherNames(List<BattleTagResponse> tags) {
161152
private String findRepresentativeImageUrl(List<TodayOptionResponse> options, BattleOptionLabel label) {
162153
return Optional.ofNullable(options).orElse(List.of()).stream()
163154
.filter(o -> o.label() == label)
164-
.map(TodayOptionResponse::representative)
155+
.map(TodayOptionResponse::imageUrl)
165156
.filter(Objects::nonNull)
166157
.findFirst()
167-
.map(PhilosopherType::fromLabel)
168-
.map(PhilosopherType::getImageKey)
169-
.map(s3PresignedUrlService::generatePresignedUrl)
170158
.orElse(null);
171159
}
172160

0 commit comments

Comments
 (0)