Skip to content

Commit 11c4f99

Browse files
authored
#84 [Feat] UserBattle 도메인 구현 및 TTS 청취 기반 배틀 진행 단계 추적 시스템 도입 (#85)
1 parent 43214e3 commit 11c4f99

55 files changed

Lines changed: 523 additions & 123 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ public ApiResponse<BattleListResponse> getBattles(
3333
@Parameter(description = "페이지 크기", example = "10")
3434
@RequestParam(value = "size", defaultValue = "10") int size,
3535
@Parameter(description = "콘텐츠 타입 (ALL, BATTLE, QUIZ, VOTE)", example = "ALL")
36-
@RequestParam(value = "type", required = false, defaultValue = "ALL") String type // 🚀 프론트에서 보낸 type 받기
36+
@RequestParam(value = "type", required = false, defaultValue = "ALL") String type
3737
) {
38-
return ApiResponse.onSuccess(battleService.getBattles(page, size, type)); // 🚀 서비스로 type 넘겨주기
38+
return ApiResponse.onSuccess(battleService.getBattles(page, size, type));
3939
}
4040

4141
@Operation(summary = "배틀 상세 조회")

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.swyp.picke.domain.battle.entity.BattleOption;
77
import com.swyp.picke.domain.battle.entity.BattleOptionTag;
88
import com.swyp.picke.domain.battle.enums.BattleCreatorType;
9+
import com.swyp.picke.domain.user.enums.UserBattleStep;
910
import com.swyp.picke.domain.battle.repository.BattleOptionTagRepository;
1011
import com.swyp.picke.domain.tag.entity.Tag;
1112
import com.swyp.picke.domain.tag.enums.TagType;
@@ -70,7 +71,7 @@ public BattleSimpleResponse toSimpleResponse(Battle b) {
7071
b.getId(),
7172
b.getTitle(),
7273
b.getType() != null ? b.getType().name() : "BATTLE",
73-
b.getStatus() != null ? b.getStatus().name() : "DRAFT",
74+
b.getStatus() != null ? b.getStatus().name() : "PENDING",
7475
b.getCreatedAt()
7576
);
7677
}
@@ -110,7 +111,7 @@ public AdminBattleDetailResponse toAdminDetailResponse(
110111
// 유저 상세 응답 변환
111112
public BattleUserDetailResponse toUserDetailResponse(
112113
Battle b, List<Tag> tags, List<BattleOption> opts,
113-
Long partCount, String voteStatus, String secureThumbnail,
114+
Long partCount, String voteStatus, UserBattleStep currentStep, String secureThumbnail,
114115
S3UploadService s3Service) {
115116

116117
BattleSummaryResponse summary = new BattleSummaryResponse(
@@ -137,6 +138,7 @@ public BattleUserDetailResponse toUserDetailResponse(
137138
b.getDescription(),
138139
BASE_SHARE_URL + b.getId(),
139140
voteStatus,
141+
currentStep,
140142
toTagResponses(tags, TagType.CATEGORY),
141143
toTagResponses(tags, TagType.PHILOSOPHER),
142144
toTagResponses(tags, TagType.VALUE)

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

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

3+
import com.swyp.picke.domain.user.enums.UserBattleStep;
4+
35
import java.util.List;
46

57
/**
@@ -17,6 +19,7 @@ public record BattleUserDetailResponse(
1719
String description, // 상세 본문 설명
1820
String shareUrl, // 공유하기 버튼용 링크
1921
String userVoteStatus, // 현재 유저의 투표 상태
22+
UserBattleStep currentStep,
2023
List<BattleTagResponse> categoryTags, // UI 상단용 카테고리 태그
2124
List<BattleTagResponse> philosopherTags, // UI 하단용 철학자 태그
2225
List<BattleTagResponse> valueTags // 성향 분석용 가치관 태그

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

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import com.swyp.picke.domain.battle.enums.BattleOptionLabel;
1212
import com.swyp.picke.domain.battle.enums.BattleStatus;
1313
import com.swyp.picke.domain.battle.enums.BattleType;
14+
import com.swyp.picke.domain.user.dto.response.UserBattleStatusResponse;
15+
import com.swyp.picke.domain.user.enums.UserBattleStep;
1416
import com.swyp.picke.domain.battle.repository.BattleOptionRepository;
1517
import com.swyp.picke.domain.battle.repository.BattleOptionTagRepository;
1618
import com.swyp.picke.domain.battle.repository.BattleRepository;
@@ -19,10 +21,13 @@
1921
import com.swyp.picke.domain.tag.repository.TagRepository;
2022
import com.swyp.picke.domain.user.entity.User;
2123
import com.swyp.picke.domain.user.repository.UserRepository;
24+
import com.swyp.picke.domain.user.service.UserBattleService;
25+
import com.swyp.picke.domain.vote.entity.Vote;
2226
import com.swyp.picke.domain.vote.repository.VoteRepository;
2327
import com.swyp.picke.global.common.exception.CustomException;
2428
import com.swyp.picke.global.common.exception.ErrorCode;
2529
import com.swyp.picke.global.infra.s3.service.S3UploadService;
30+
import com.swyp.picke.global.util.SecurityUtil;
2631
import lombok.RequiredArgsConstructor;
2732
import org.springframework.data.domain.Page;
2833
import org.springframework.data.domain.PageRequest;
@@ -33,10 +38,7 @@
3338
import java.time.Duration;
3439
import java.time.LocalDate;
3540
import java.time.LocalDateTime;
36-
import java.util.ArrayList;
37-
import java.util.Collections;
38-
import java.util.List;
39-
import java.util.Map;
41+
import java.util.*;
4042
import java.util.stream.Collectors;
4143

4244
@Service
@@ -53,6 +55,7 @@ public class BattleServiceImpl implements BattleService {
5355
private final VoteRepository voteRepository;
5456
private final BattleConverter battleConverter;
5557
private final S3UploadService s3UploadService;
58+
private final UserBattleService userBattleService;
5659

5760
@Override
5861
public Battle findById(Long battleId) {
@@ -144,21 +147,29 @@ public BattleUserDetailResponse getBattleDetail(Long battleId) {
144147
List<Tag> tags = getTagsByBattle(battle);
145148
List<BattleOption> options = battleOptionRepository.findByBattle(battle);
146149

147-
// 1. 썸네일 보안 URL 생성
148150
String secureThumbnail = s3UploadService.getPresignedUrl(battle.getThumbnailUrl(), Duration.ofMinutes(10));
149-
String voteStatus = voteRepository.findByBattleIdAndUserId(battleId, 1L)
151+
152+
// 💡 [수정] SecurityUtils를 통한 실제 로그인 유저 조회
153+
Long currentUserId = SecurityUtil.getCurrentUserId();
154+
User user = userRepository.findById(currentUserId)
155+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
156+
157+
// 💡 [수정] UserBattleService를 사용하여 현재 진행 단계 조회
158+
UserBattleStatusResponse statusResponse = userBattleService.getUserBattleStatus(user, battle);
159+
UserBattleStep currentStep = statusResponse.step();
160+
161+
// 투표 여부 라벨 확인 (기존 VoteRepository 활용 유지)
162+
Optional<Vote> optionalVote = voteRepository.findByBattleIdAndUserId(battleId, currentUserId);
163+
String voteStatus = optionalVote
150164
.map(v -> v.getPostVoteOption() != null ? v.getPostVoteOption().getLabel().name() : "NONE")
151165
.orElse("NONE");
152166

153-
// 2. 컨버터를 통해 전체 조립 (철학자 이미지는 컨버터 내부에서 s3UploadService로 처리)
154167
return battleConverter.toUserDetailResponse(
155-
battle,
156-
tags,
157-
options,
168+
battle, tags, options,
158169
battle.getTotalParticipantsCount(),
159-
"NONE",
160-
secureThumbnail,
161-
s3UploadService // 철학자 이미지 변환을 위해 전달
170+
voteStatus,
171+
currentStep,
172+
secureThumbnail, s3UploadService
162173
);
163174
}
164175

@@ -169,17 +180,39 @@ public BattleVoteResponse vote(Long battleId, Long optionId) {
169180
BattleOption option = battleOptionRepository.findById(optionId)
170181
.orElseThrow(() -> new CustomException(ErrorCode.BATTLE_OPTION_NOT_FOUND));
171182

183+
Long currentUserId = SecurityUtil.getCurrentUserId();
184+
User user = userRepository.findById(currentUserId)
185+
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
186+
187+
// 1. [기록] 투표 기록 저장 (VoteStatus.COMPLETED 제거)
188+
voteRepository.save(Vote.builder()
189+
.user(user)
190+
.battle(battle)
191+
.postVoteOption(option)
192+
// .status(...) 필드 설정 삭제
193+
.build());
194+
195+
// 2. [단계] UserBattleStep 업데이트
196+
userBattleService.upsertStep(user, battle, UserBattleStep.PRE_VOTE);
197+
198+
// 3. [통계] 카운트 증가
172199
battle.addParticipant();
173200
option.increaseVoteCount();
174201

175-
List<OptionStatResponse> results = battleOptionRepository.findByBattle(battle).stream().map(opt -> {
202+
// 4. [응답] 통계 결과 계산
203+
List<OptionStatResponse> results = calculateOptionStats(battle);
204+
205+
return new BattleVoteResponse(battle.getId(), option.getId(), battle.getTotalParticipantsCount(), results);
206+
}
207+
208+
// 통계 계산 로직 중복 제거를 위한 헬퍼 메서드
209+
private List<OptionStatResponse> calculateOptionStats(Battle battle) {
210+
return battleOptionRepository.findByBattle(battle).stream().map(opt -> {
176211
Long v = opt.getVoteCount() == null ? 0L : opt.getVoteCount();
177212
Long t = battle.getTotalParticipantsCount() == null ? 0L : battle.getTotalParticipantsCount();
178213
Double r = (t == 0L) ? 0.0 : Math.round((double) v / t * 1000) / 10.0;
179214
return new OptionStatResponse(opt.getId(), opt.getLabel(), opt.getTitle(), v, r);
180215
}).toList();
181-
182-
return new BattleVoteResponse(battle.getId(), option.getId(), battle.getTotalParticipantsCount(), results);
183216
}
184217

185218
// [관리자용 API]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
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.entity.PhilosopherType;
13+
import com.swyp.picke.domain.user.enums.PhilosopherType;
1414
import com.swyp.picke.global.infra.s3.service.S3PresignedUrlService;
1515
import lombok.RequiredArgsConstructor;
1616
import org.springframework.stereotype.Service;

src/main/java/com/swyp/picke/domain/oauth/service/AuthService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
import com.swyp.picke.domain.oauth.jwt.JwtProvider;
1111
import com.swyp.picke.domain.oauth.repository.AuthRefreshTokenRepository;
1212
import com.swyp.picke.domain.oauth.repository.UserSocialAccountRepository;
13-
import com.swyp.picke.domain.user.entity.UserRole;
13+
import com.swyp.picke.domain.user.enums.UserRole;
1414
import com.swyp.picke.domain.user.entity.User;
1515
import com.swyp.picke.domain.user.entity.UserProfile;
1616
import com.swyp.picke.domain.user.entity.UserSettings;
17-
import com.swyp.picke.domain.user.entity.UserStatus;
17+
import com.swyp.picke.domain.user.enums.UserStatus;
1818
import com.swyp.picke.domain.user.entity.UserTendencyScore;
1919
import com.swyp.picke.domain.user.repository.UserProfileRepository;
2020
import com.swyp.picke.domain.user.repository.UserRepository;

src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveCommentService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import com.swyp.picke.domain.user.dto.response.UserSummary;
1616
import com.swyp.picke.domain.user.entity.User;
1717
import com.swyp.picke.domain.user.repository.UserRepository;
18-
import com.swyp.picke.domain.user.entity.CharacterType;
18+
import com.swyp.picke.domain.user.enums.CharacterType;
1919
import com.swyp.picke.domain.user.service.UserService;
2020
import com.swyp.picke.domain.vote.service.VoteService;
2121
import com.swyp.picke.global.common.exception.CustomException;

src/main/java/com/swyp/picke/domain/perspective/service/PerspectiveService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import com.swyp.picke.domain.perspective.repository.PerspectiveLikeRepository;
1919
import com.swyp.picke.domain.perspective.repository.PerspectiveRepository;
2020
import com.swyp.picke.domain.user.dto.response.UserSummary;
21-
import com.swyp.picke.domain.user.entity.CharacterType;
21+
import com.swyp.picke.domain.user.enums.CharacterType;
2222
import com.swyp.picke.domain.user.service.UserService;
2323
import com.swyp.picke.domain.vote.service.VoteService;
2424
import com.swyp.picke.global.common.exception.CustomException;

src/main/java/com/swyp/picke/domain/recommendation/service/RecommendationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import com.swyp.picke.domain.battle.service.BattleService;
1010
import com.swyp.picke.domain.recommendation.dto.response.RecommendationListResponse;
1111
import com.swyp.picke.domain.tag.enums.TagType;
12-
import com.swyp.picke.domain.user.entity.PhilosopherType;
12+
import com.swyp.picke.domain.user.enums.PhilosopherType;
1313
import com.swyp.picke.domain.user.service.UserService;
1414
import com.swyp.picke.domain.vote.repository.VoteRepository;
1515
import lombok.RequiredArgsConstructor;

src/main/java/com/swyp/picke/domain/scenario/converter/ScenarioConverter.java

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@
66
import com.swyp.picke.domain.scenario.entity.ScenarioNode;
77
import com.swyp.picke.domain.scenario.entity.Script;
88
import com.swyp.picke.domain.scenario.enums.AudioPathType;
9+
import org.springframework.beans.factory.annotation.Value;
910
import org.springframework.stereotype.Component;
1011

12+
import java.util.HashMap;
1113
import java.util.List;
14+
import java.util.Map;
1215
import java.util.stream.Collectors;
1316

1417
@Component
1518
public class ScenarioConverter {
1619

20+
@Value("${picke_base_url}")
21+
private String baseUrl;
22+
1723
/**
18-
* [유저용] Scenario 엔티티를 프론트엔드 전달용 DTO로 변환합니다. (태그 제거됨)
24+
* [유저용] Scenario 엔티티를 프론트엔드 전달용 DTO로 변환합니다.
1925
*/
2026
public UserScenarioResponse toUserResponse(Scenario scenario, AudioPathType recommendedPathKey) {
2127
Long startNodeId = scenario.getNodes().stream()
@@ -28,18 +34,27 @@ public UserScenarioResponse toUserResponse(Scenario scenario, AudioPathType reco
2834
.map(this::toUserNodeResponse)
2935
.collect(Collectors.toList());
3036

37+
// 💡 에러 완벽 수정: Key 타입을 AudioPathType으로 맞추고 그대로 put
38+
Map<AudioPathType, String> fullUrlAudios = new HashMap<>();
39+
if (scenario.getAudios() != null) {
40+
scenario.getAudios().forEach((key, path) -> {
41+
String fullPath = (path != null && !path.startsWith("http")) ? baseUrl + path : path;
42+
fullUrlAudios.put(key, fullPath);
43+
});
44+
}
45+
3146
return UserScenarioResponse.builder()
3247
.battleId(scenario.getBattle().getId())
3348
.isInteractive(scenario.getIsInteractive())
3449
.startNodeId(startNodeId)
3550
.recommendedPathKey(recommendedPathKey)
36-
.audios(scenario.getAudios())
51+
.audios(fullUrlAudios) // 병합된 오디오에만 Base URL 적용!
3752
.nodes(nodeResponses)
3853
.build();
3954
}
4055

4156
/**
42-
* [관리자용] 시나리오 상세 변환 메서드 (태그 보존)
57+
* [관리자용] 시나리오 상세 변환 메서드
4358
*/
4459
public AdminScenarioDetailResponse toAdminDetailResponse(Scenario scenario) {
4560
return AdminScenarioDetailResponse.builder()
@@ -53,7 +68,7 @@ public AdminScenarioDetailResponse toAdminDetailResponse(Scenario scenario) {
5368
}
5469

5570
// ==========================================
56-
// 🟢 유저용 변환 로직 (태그 정규식으로 지움)
71+
// 🟢 유저용 변환 로직 (오디오 URL 제거, 순수 데이터만)
5772
// ==========================================
5873
private NodeResponse toUserNodeResponse(ScenarioNode node) {
5974
return NodeResponse.builder()
@@ -72,8 +87,8 @@ private NodeResponse toUserNodeResponse(ScenarioNode node) {
7287

7388
private ScriptResponse toUserScriptResponse(Script script) {
7489
String cleanText = script.getText()
75-
.replaceAll("\\[.*?\\]", "") // 태그 제거
76-
.replaceAll("\\s+", " ") // 중복 공백 합치기
90+
.replaceAll("\\[.*?\\]", "")
91+
.replaceAll("\\s+", " ")
7792
.trim();
7893

7994
return ScriptResponse.builder()
@@ -85,7 +100,9 @@ private ScriptResponse toUserScriptResponse(Script script) {
85100
.build();
86101
}
87102

88-
// 관리자용 변환 로직
103+
// ==========================================
104+
// 🔵 관리자용 변환 로직 (오디오 URL 제거)
105+
// ==========================================
89106
private NodeResponse toAdminNodeResponse(ScenarioNode node) {
90107
return NodeResponse.builder()
91108
.nodeId(node.getId())

0 commit comments

Comments
 (0)