Skip to content

Commit 8fa6b6e

Browse files
authored
#69 [Feat] S3 보안 처리 및 TTS 재사용 전략 도입, 관리자 폼 개편 및 N+1 개선 (#77)
1 parent e551577 commit 8fa6b6e

30 files changed

Lines changed: 856 additions & 302 deletions

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

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
import com.swyp.app.domain.battle.entity.BattleOption;
77
import com.swyp.app.domain.battle.entity.BattleOptionTag;
88
import com.swyp.app.domain.battle.enums.BattleCreatorType;
9-
import com.swyp.app.domain.battle.enums.BattleStatus;
109
import com.swyp.app.domain.battle.repository.BattleOptionTagRepository;
1110
import com.swyp.app.domain.tag.entity.Tag;
1211
import com.swyp.app.domain.tag.enums.TagType;
1312
import com.swyp.app.domain.user.entity.User;
13+
import com.swyp.app.global.infra.s3.service.S3UploadService;
1414
import lombok.RequiredArgsConstructor;
1515
import org.springframework.stereotype.Component;
1616

17+
import java.time.Duration;
1718
import java.util.List;
1819

1920
@Component
@@ -37,7 +38,7 @@ public Battle toEntity(AdminBattleCreateRequest request, User admin) {
3738
.thumbnailUrl(request.thumbnailUrl())
3839
.type(request.type())
3940
.targetDate(request.targetDate())
40-
.status(BattleStatus.PENDING)
41+
.status(request.status())
4142
.creatorType(BattleCreatorType.ADMIN)
4243
.creator(admin)
4344
.build();
@@ -74,15 +75,23 @@ public BattleSimpleResponse toSimpleResponse(Battle b) {
7475
);
7576
}
7677

77-
public AdminBattleDetailResponse toAdminDetailResponse(Battle b, List<Tag> tags, List<BattleOption> opts) {
78+
// 관리자용 상세 응답 변환 (보안 URL 적용)
79+
public AdminBattleDetailResponse toAdminDetailResponse(
80+
Battle b, List<Tag> tags, List<BattleOption> opts, S3UploadService s3Service) {
81+
82+
// 썸네일 보안 URL
83+
String secureThumbnail = (b.getThumbnailUrl() != null && !b.getThumbnailUrl().isBlank())
84+
? s3Service.getPresignedUrl(b.getThumbnailUrl(), Duration.ofMinutes(10))
85+
: null;
86+
7887
return new AdminBattleDetailResponse(
7988
b.getId(),
8089
b.getTitle(),
8190
b.getTitlePrefix(),
8291
b.getTitleSuffix(),
8392
b.getSummary(),
8493
b.getDescription(),
85-
b.getThumbnailUrl(),
94+
secureThumbnail,
8695
b.getType(),
8796
b.getItemA(),
8897
b.getItemADesc(),
@@ -92,20 +101,29 @@ public AdminBattleDetailResponse toAdminDetailResponse(Battle b, List<Tag> tags,
92101
b.getStatus(),
93102
b.getCreatorType(),
94103
toTagResponses(tags, null),
95-
toOptionResponses(opts),
104+
toOptionResponses(opts, s3Service),
96105
b.getCreatedAt(),
97106
b.getUpdatedAt()
98107
);
99108
}
100109

101-
public BattleUserDetailResponse toUserDetailResponse(Battle b, List<Tag> tags, List<BattleOption> opts, Long partCount, String voteStatus) {
110+
// 유저 상세 응답 변환
111+
public BattleUserDetailResponse toUserDetailResponse(
112+
Battle b, List<Tag> tags, List<BattleOption> opts,
113+
Long partCount, String voteStatus, String secureThumbnail,
114+
S3UploadService s3Service) {
115+
102116
BattleSummaryResponse summary = new BattleSummaryResponse(
103-
b.getId(), b.getTitle(), b.getSummary(), b.getThumbnailUrl(), b.getType(),
117+
b.getId(),
118+
b.getTitle(),
119+
b.getSummary(),
120+
secureThumbnail,
121+
b.getType(),
104122
b.getViewCount() == null ? 0 : b.getViewCount(),
105123
partCount == null ? 0L : partCount,
106124
b.getAudioDuration() == null ? 0 : b.getAudioDuration(),
107125
toTagResponses(tags, null),
108-
toOptionResponses(opts)
126+
toOptionResponses(opts, s3Service)
109127
);
110128

111129
return new BattleUserDetailResponse(
@@ -125,34 +143,48 @@ public BattleUserDetailResponse toUserDetailResponse(Battle b, List<Tag> tags, L
125143
);
126144
}
127145

128-
private List<BattleOptionResponse> toOptionResponses(List<BattleOption> options) {
146+
// 철학자 이미지 보안 처리를 포함한 옵션 응답 변환
147+
private List<BattleOptionResponse> toOptionResponses(List<BattleOption> options, S3UploadService s3Service) {
129148
if (options == null) return List.of();
149+
130150
return options.stream()
131151
.map(o -> {
132152
List<Tag> optionTags = optionTagRepository.findByBattleOption(o).stream()
133153
.map(BattleOptionTag::getTag)
134154
.toList();
135155

156+
// 철학자 이미지 방어 로직 (null/공백일 경우 s3Service 호출 안 함)
157+
String securePhilosopherImg = (o.getImageUrl() != null && !o.getImageUrl().isBlank())
158+
? s3Service.getPresignedUrl(o.getImageUrl(), Duration.ofMinutes(10))
159+
: null;
160+
136161
return new BattleOptionResponse(
137-
o.getId(), o.getLabel(), o.getTitle(), o.getStance(),
138-
o.getRepresentative(), o.getQuote(), o.getImageUrl(),
162+
o.getId(),
163+
o.getLabel(),
164+
o.getTitle(),
165+
o.getStance(),
166+
o.getRepresentative(),
167+
o.getQuote(),
168+
securePhilosopherImg,
139169
toTagResponses(optionTags, null)
140170
);
141171
}).toList();
142172
}
143173

174+
// 투데이 옵션 응답 변환
144175
private List<TodayOptionResponse> toTodayOptionResponses(List<BattleOption> options) {
145176
if (options == null) return List.of();
146177
return options.stream().map(o -> new TodayOptionResponse(
147178
o.getId(), o.getLabel(), o.getTitle(), o.getRepresentative(), o.getStance(), o.getImageUrl()
148179
)).toList();
149180
}
150181

182+
// 태그 응답 변환
151183
private List<BattleTagResponse> toTagResponses(List<Tag> tags, TagType targetType) {
152184
if (tags == null) return List.of();
153185
return tags.stream()
154186
.filter(t -> targetType == null || t.getType() == targetType)
155187
.map(t -> new BattleTagResponse(t.getId(), t.getName(), t.getType()))
156188
.toList();
157189
}
158-
}
190+
}

src/main/java/com/swyp/app/domain/battle/dto/request/AdminBattleCreateRequest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.swyp.app.domain.battle.dto.request;
22

3+
import com.swyp.app.domain.battle.enums.BattleStatus;
34
import com.swyp.app.domain.battle.enums.BattleType;
45
import java.time.LocalDate;
56
import java.util.List;
@@ -12,6 +13,7 @@ public record AdminBattleCreateRequest(
1213
String description,
1314
String thumbnailUrl,
1415
BattleType type,
16+
BattleStatus status,
1517
String itemA,
1618
String itemADesc,
1719
String itemB,

src/main/java/com/swyp/app/domain/battle/repository/BattleOptionRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ public interface BattleOptionRepository extends JpaRepository<BattleOption, Long
1212

1313
List<BattleOption> findByBattle(Battle battle);
1414
Optional<BattleOption> findByBattleAndLabel(Battle battle, BattleOptionLabel label);
15-
15+
List<BattleOption> findByBattleIn(List<Battle> battles);
1616
}

src/main/java/com/swyp/app/domain/battle/repository/BattleRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ public interface BattleRepository extends JpaRepository<Battle, Long> {
3535
"ORDER BY (battle.totalParticipantsCount + (battle.commentCount * 5)) DESC")
3636
List<Battle> findBestBattles(Pageable pageable);
3737

38-
// 4. 오늘의 Pické (단일 타입)
38+
// 4. 오늘의 Pické
3939
@Query("SELECT battle FROM Battle battle " +
4040
"WHERE battle.type = :type AND battle.targetDate = :today " +
4141
"AND battle.status = 'PUBLISHED' AND battle.deletedAt IS NULL")
42-
List<Battle> findTodayPicks(@Param("type") BattleType type, @Param("today") LocalDate today);
42+
List<Battle> findTodayPicks(@Param("type") BattleType type, @Param("today") LocalDate today, Pageable pageable);
4343

4444
// 5. 새로운 배틀
4545
@Query("SELECT battle FROM Battle battle " +

src/main/java/com/swyp/app/domain/battle/repository/BattleTagRepository.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public interface BattleTagRepository extends JpaRepository<BattleTag, Long> {
1313
List<BattleTag> findByBattle(Battle battle);
1414
void deleteByBattle(Battle battle);
1515
boolean existsByTag(Tag tag);
16-
16+
// N+1 방지를 위해 Tag까지 한 번에 가져오는 쿼리
17+
@Query("SELECT bt FROM BattleTag bt JOIN FETCH bt.tag WHERE bt.battle IN :battles")
18+
List<BattleTag> findByBattleIn(@Param("battles") List<Battle> battles);
1719
// MypageService (recap): 여러 배틀의 태그를 한번에 조회
1820
@Query("SELECT bt FROM BattleTag bt JOIN FETCH bt.tag WHERE bt.battle.id IN :battleIds")
1921
List<BattleTag> findByBattleIdIn(@Param("battleIds") List<Long> battleIds);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ public interface BattleService {
2020
// === [사용자용 - 홈 화면 5단 로직 지원 API] ===
2121

2222
// 1. 에디터 픽 조회 (isEditorPick = true)
23-
List<TodayBattleResponse> getEditorPicks();
23+
List<TodayBattleResponse> getEditorPicks(int limit);
2424

2525
// 2. 지금 뜨는 배틀 조회 (최근 24시간 투표 급증순)
26-
List<TodayBattleResponse> getTrendingBattles();
26+
List<TodayBattleResponse> getTrendingBattles(int limit);
2727

2828
// 3. Best 배틀 조회 (누적 지표 랭킹)
29-
List<TodayBattleResponse> getBestBattles();
29+
List<TodayBattleResponse> getBestBattles(int limit);
3030

3131
// 4. 오늘의 Pické 조회 (단일 타입 매칭)
32-
List<TodayBattleResponse> getTodayPicks(BattleType type);
32+
List<TodayBattleResponse> getTodayPicks(BattleType type, int limit);
3333

3434
// 5. 새로운 배틀 조회 (중복 제외 리스트)
35-
List<TodayBattleResponse> getNewBattles(List<Long> excludeIds);
35+
List<TodayBattleResponse> getNewBattles(List<Long> excludeIds, int limit);
3636

3737

3838
// === [사용자용 - 기본 API] ===

0 commit comments

Comments
 (0)