Skip to content

Commit 3c99880

Browse files
authored
Merge pull request #49 from Nexters/feat/9-routemission-recommendation
feat: 세션 생성 시 루트미션 추천 만들어지는 부분 구현
2 parents c638dfe + 2d71739 commit 3c99880

File tree

11 files changed

+197
-5
lines changed

11 files changed

+197
-5
lines changed

src/main/java/com/climbup/climbup/attempt/controller/AttemptController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.climbup.climbup.attempt.controller;
22

3+
34
import com.climbup.climbup.attempt.dto.request.CreateAttemptRequest;
45
import com.climbup.climbup.attempt.dto.response.CreateAttemptResponse;
56
import com.climbup.climbup.attempt.service.AttemptService;
@@ -37,6 +38,7 @@ public class AttemptController {
3738

3839
private final AttemptService attemptService;
3940

41+
4042
@Operation(summary = "도전한 루트미션과 비슷한 난이도의 루트미션 리스트 불러오기", description = "도전한 루트미션과 비슷한 난이도의 루트미션 리스트를 받아보기", security = @SecurityRequirement(name = "bearerAuth"))
4143
@ApiResponses({
4244
@ApiResponse(

src/main/java/com/climbup/climbup/recommendation/controller/RecommendationController.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package com.climbup.climbup.recommendation.controller;
22

3+
import com.climbup.climbup.auth.util.SecurityUtil;
34
import com.climbup.climbup.common.dto.ApiResult;
45
import com.climbup.climbup.recommendation.dto.response.RouteMissionRecommendationResponse;
6+
import com.climbup.climbup.recommendation.service.RecommendationService;
7+
import com.climbup.climbup.session.repository.UserSessionRepository;
8+
import com.climbup.climbup.session.service.UserSessionService;
59
import io.swagger.v3.oas.annotations.Operation;
610
import io.swagger.v3.oas.annotations.media.Content;
711
import io.swagger.v3.oas.annotations.media.ExampleObject;
@@ -25,6 +29,8 @@
2529
@RequiredArgsConstructor
2630
public class RecommendationController {
2731

32+
private final RecommendationService recommendationService;
33+
2834
@Operation(summary = "루트미션 리스트 불러오기", description = "유저의 난이도에 맞는 추천 루트미션 리스트", security = @SecurityRequirement(name = "bearerAuth"))
2935
@ApiResponses({
3036
@ApiResponse(
@@ -90,6 +96,8 @@ public class RecommendationController {
9096
@GetMapping
9197
@PreAuthorize("isAuthenticated()")
9298
public ResponseEntity<ApiResult<List<RouteMissionRecommendationResponse>>> getRouteMissionRecommendations() {
93-
return ResponseEntity.ok(ApiResult.success(List.of(RouteMissionRecommendationResponse.builder().build())));
99+
Long userId = SecurityUtil.getCurrentUserId();
100+
List<RouteMissionRecommendationResponse> routeMissionRecommendationResponses = recommendationService.getRecommendationsByUserActiveSession(userId);
101+
return ResponseEntity.ok(ApiResult.success(routeMissionRecommendationResponses));
94102
}
95103
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
package com.climbup.climbup.recommendation.repository;
22

33
import com.climbup.climbup.recommendation.entity.ChallengeRecommendation;
4+
import com.climbup.climbup.session.entity.UserSession;
45
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
8+
9+
import java.util.List;
510

611
public interface RecommendationRepository extends JpaRepository<ChallengeRecommendation, Long> {
12+
@Query("SELECT DISTINCT cr FROM ChallengeRecommendation cr " +
13+
"JOIN FETCH cr.mission rm " +
14+
"LEFT JOIN FETCH rm.attempts " +
15+
"JOIN FETCH rm.sector " +
16+
"WHERE cr.session = :session " +
17+
"ORDER BY cr.recommendedOrder")
18+
List<ChallengeRecommendation> findBySession(@Param("session") UserSession session);
719
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package com.climbup.climbup.recommendation.service;
22

3+
import com.climbup.climbup.recommendation.dto.response.RouteMissionRecommendationResponse;
4+
import com.climbup.climbup.session.entity.UserSession;
35
import org.springframework.stereotype.Service;
46

7+
import java.util.List;
8+
59
public interface RecommendationService {
10+
void generateRecommendationsForSession(UserSession session);
11+
List<RouteMissionRecommendationResponse> getRecommendationsByUserActiveSession(Long userId);
612
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,75 @@
11
package com.climbup.climbup.recommendation.service;
22

3+
import com.climbup.climbup.attempt.entity.UserMissionAttempt;
4+
import com.climbup.climbup.gym.entity.ClimbingGym;
5+
import com.climbup.climbup.recommendation.dto.response.RouteMissionRecommendationResponse;
6+
import com.climbup.climbup.recommendation.entity.ChallengeRecommendation;
7+
import com.climbup.climbup.recommendation.repository.RecommendationRepository;
8+
import com.climbup.climbup.route.entity.RouteMission;
9+
import com.climbup.climbup.route.repository.RouteMissionRepository;
10+
import com.climbup.climbup.sector.entity.Sector;
11+
import com.climbup.climbup.session.entity.UserSession;
12+
import com.climbup.climbup.session.exception.UserSessionNotFoundException;
13+
import com.climbup.climbup.session.repository.UserSessionRepository;
14+
import com.climbup.climbup.user.entity.User;
15+
import com.climbup.climbup.user.exception.UserNotFoundException;
16+
import com.climbup.climbup.user.repository.UserRepository;
17+
import lombok.RequiredArgsConstructor;
318
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
import java.util.List;
22+
import java.util.concurrent.atomic.AtomicInteger;
423

524
@Service
25+
@RequiredArgsConstructor
626
public class RecommendationServiceImpl implements RecommendationService{
27+
private static final byte MAX_NUM_OF_RECS = 30;
28+
29+
private final UserSessionRepository userSessionRepository;
30+
private final UserRepository userRepository;
31+
private final RouteMissionRepository routeMissionRepository;
32+
private final RecommendationRepository recommendationRepository;
33+
34+
@Override
35+
@Transactional
36+
public void generateRecommendationsForSession(UserSession session){
37+
User user = session.getUser();
38+
39+
List<RouteMission> routeMissions = routeMissionRepository.findUnattemptedRouteMissionsByUser(user.getId());
40+
41+
AtomicInteger index = new AtomicInteger(0);
42+
43+
List<ChallengeRecommendation> recommendations = routeMissions.stream()
44+
.map(routeMission -> {
45+
ChallengeRecommendation recommendation = ChallengeRecommendation.builder()
46+
.session(session)
47+
.mission(routeMission)
48+
.recommendedOrder(index.getAndIncrement())
49+
.build();
50+
51+
return recommendation;
52+
})
53+
.toList();
54+
55+
recommendations = recommendations.subList(0, (int) Math.min(MAX_NUM_OF_RECS, (long) recommendations.size()));
56+
recommendationRepository.saveAll(recommendations);
57+
}
58+
59+
@Override
60+
@Transactional(readOnly = true)
61+
public List<RouteMissionRecommendationResponse> getRecommendationsByUserActiveSession(Long userId) {
62+
UserSession session = userSessionRepository.findByUserIdAndEndedAtIsNull(userId).orElseThrow(UserSessionNotFoundException::new);
63+
64+
List<ChallengeRecommendation> recommendations = recommendationRepository.findBySession(session);
65+
66+
return recommendations.stream().map(recommendation -> {
67+
RouteMission routeMission = recommendation.getMission();
68+
ClimbingGym gym = routeMission.getGym();
69+
List<UserMissionAttempt> attempts = routeMission.getAttempts().stream().toList();
70+
Sector sector = routeMission.getSector();
71+
72+
return RouteMissionRecommendationResponse.toDto(recommendation, routeMission, gym, attempts, sector, recommendation.getRecommendedOrder());
73+
}).toList();
74+
}
775
}

src/main/java/com/climbup/climbup/route/repository/RouteMissionRepository.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
import com.climbup.climbup.route.entity.RouteMission;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
7+
import org.springframework.stereotype.Repository;
58

9+
import java.util.List;
10+
11+
@Repository
612
public interface RouteMissionRepository extends JpaRepository<RouteMission, Long> {
7-
}
13+
@Query("SELECT rm from RouteMission rm LEFT JOIN rm.attempts uma on uma.user.id = :userId WHERE uma.id IS NULL")
14+
List<RouteMission> findUnattemptedRouteMissionsByUser(@Param("userId") Long userId);
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.climbup.climbup.sector.repository;
2+
3+
import com.climbup.climbup.sector.entity.Sector;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
@Repository
8+
public interface SectorRepository extends JpaRepository<Sector, Long> {
9+
}

src/main/java/com/climbup/climbup/session/repository/UserSessionRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.climbup.climbup.session.repository;
22

33
import com.climbup.climbup.session.entity.UserSession;
4+
import com.climbup.climbup.user.entity.User;
45
import org.springframework.data.jpa.repository.JpaRepository;
56

67
import java.util.Optional;

src/main/java/com/climbup/climbup/session/service/UserSessionServiceImpl.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.climbup.climbup.session.service;
22

3+
import com.climbup.climbup.recommendation.service.RecommendationService;
34
import com.climbup.climbup.session.entity.UserSession;
45
import com.climbup.climbup.session.exception.UserSessionAlreadyFinishedException;
56
import com.climbup.climbup.session.exception.UserSessionNotFoundException;
@@ -22,6 +23,7 @@
2223
public class UserSessionServiceImpl implements UserSessionService {
2324
private final UserSessionRepository userSessionRepository;
2425
private final UserRepository userRepository;
26+
private final RecommendationService recommendationService;
2527

2628
@Override
2729
@Transactional
@@ -47,8 +49,11 @@ public UserSession startSession(Long userId) {
4749
.completedCount(0)
4850
.attemptedCount(0)
4951
.build();
52+
53+
session = userSessionRepository.save(session);
54+
recommendationService.generateRecommendationsForSession(session);
5055

51-
return userSessionRepository.save(session);
56+
return session;
5257
}
5358

5459
@Override

src/main/java/com/climbup/climbup/test/controller/TestController.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import com.climbup.climbup.gym.repository.GymLevelRepository;
1010
import com.climbup.climbup.level.entity.Level;
1111
import com.climbup.climbup.level.repository.LevelRepository;
12+
import com.climbup.climbup.route.entity.RouteMission;
13+
import com.climbup.climbup.route.repository.RouteMissionRepository;
14+
import com.climbup.climbup.sector.entity.Sector;
15+
import com.climbup.climbup.sector.repository.SectorRepository;
1216
import io.swagger.v3.oas.annotations.Operation;
1317
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1418
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -21,6 +25,7 @@
2125
import org.springframework.web.bind.annotation.RestController;
2226

2327
import java.time.LocalDateTime;
28+
import java.util.ArrayList;
2429
import java.util.HashMap;
2530
import java.util.List;
2631
import java.util.Map;
@@ -35,6 +40,8 @@ public class TestController {
3540
private final ClimbingGymRepository climbingGymRepository;
3641
private final BrandRepository brandRepository;
3742
private final GymLevelRepository gymLevelRepository;
43+
private final RouteMissionRepository routeMissionRepository;
44+
private final SectorRepository sectorRepository;
3845

3946
@Operation(summary = "랜덤 숫자 생성", description = "0-99 사이의 랜덤한 정수를 반환합니다")
4047
@ApiResponse(responseCode = "200", description = "성공적으로 랜덤 숫자를 생성함")
@@ -46,7 +53,7 @@ public ResponseEntity<ApiResult<Map<String, Object>>> getRandomNumber() {
4653
return ResponseEntity.ok(ApiResult.success(response));
4754
}
4855

49-
@Operation(summary = "테스트 데이터 초기화", description = "개발/테스트용 브랜드, 레벨, 암장, 브랜드별 레벨 데이터를 생성합니다")
56+
@Operation(summary = "테스트 데이터 초기화", description = "개발/테스트용 브랜드, 레벨, 암장, 브랜드별 레벨, 섹터, 루트 미션 데이터를 생성합니다")
5057
@ApiResponse(responseCode = "200", description = "테스트 데이터 생성 완료")
5158
@PostMapping("/init-data")
5259
@Transactional
@@ -55,6 +62,8 @@ public ResponseEntity<ApiResult<Map<String, Object>>> initTestData() {
5562
int levelsCreated = 0;
5663
int gymsCreated = 0;
5764
int gymLevelsCreated = 0;
65+
int sectorsCreated = 0;
66+
int routeMissionsCreated = 0;
5867

5968
// 1. 브랜드 데이터 생성
6069
if (brandRepository.count() == 0) {
@@ -237,13 +246,69 @@ public ResponseEntity<ApiResult<Map<String, Object>>> initTestData() {
237246
gymLevelsCreated = theClimbLevels.size();
238247
}
239248

249+
// 5. 섹터 데이터 생성
250+
if (sectorRepository.count() == 0) {
251+
List<Sector> sectors = sectorRepository.saveAll(List.of(
252+
Sector.builder()
253+
.name("A구역")
254+
.imageUrl("https://example.com/sector-a.jpg")
255+
.build(),
256+
Sector.builder()
257+
.name("B구역")
258+
.imageUrl("https://example.com/sector-b.jpg")
259+
.build(),
260+
Sector.builder()
261+
.name("C구역")
262+
.imageUrl("https://example.com/sector-c.jpg")
263+
.build(),
264+
Sector.builder()
265+
.name("D구역")
266+
.imageUrl("https://example.com/sector-d.jpg")
267+
.build()
268+
));
269+
sectorsCreated = sectors.size();
270+
}
271+
272+
// 6. 루트 미션 30개 생성
273+
if (routeMissionRepository.count() == 0) {
274+
List<ClimbingGym> gyms = climbingGymRepository.findAll();
275+
List<Sector> sectors = sectorRepository.findAll();
276+
277+
if (!gyms.isEmpty() && !sectors.isEmpty()) {
278+
String[] difficulties = {"V0", "V1", "V2", "V3", "V4", "V5", "V6"};
279+
280+
List<RouteMission> routeMissions = new ArrayList<>();
281+
for (int i = 1; i <= 30; i++) {
282+
ClimbingGym gym = gyms.get((i - 1) % gyms.size());
283+
Sector sector = sectors.get((i - 1) % sectors.size());
284+
String difficulty = difficulties[(i - 1) % difficulties.length];
285+
286+
routeMissions.add(RouteMission.builder()
287+
.gym(gym)
288+
.sector(sector)
289+
.difficulty(difficulty)
290+
.score(600 + (i - 1) * 50)
291+
.imageUrl("https://example.com/route-mission-" + i + ".jpg")
292+
.thumbnailUrl("https://example.com/route-mission-" + i + "-thumb.jpg")
293+
.videoUrl("https://example.com/route-mission-" + i + ".mp4")
294+
.postedAt(LocalDateTime.now().minusDays(30 - i))
295+
.build());
296+
}
297+
298+
routeMissionRepository.saveAll(routeMissions);
299+
routeMissionsCreated = routeMissions.size();
300+
}
301+
}
302+
240303
// 결과 응답
241304
Map<String, Object> response = new HashMap<>();
242305
response.put("message", "테스트 데이터가 생성되었습니다.");
243306
response.put("brandsCreated", brandsCreated);
244307
response.put("levelsCreated", levelsCreated);
245308
response.put("gymsCreated", gymsCreated);
246309
response.put("gymLevelsCreated", gymLevelsCreated);
310+
response.put("sectorsCreated", sectorsCreated);
311+
response.put("routeMissionsCreated", routeMissionsCreated);
247312
response.put("timestamp", LocalDateTime.now());
248313

249314
return ResponseEntity.ok(ApiResult.success(response));
@@ -258,6 +323,8 @@ public ResponseEntity<ApiResult<Map<String, Object>>> getDataStatus() {
258323
response.put("levelCount", levelRepository.count());
259324
response.put("gymCount", climbingGymRepository.count());
260325
response.put("gymLevelCount", gymLevelRepository.count());
326+
response.put("sectorCount", sectorRepository.count());
327+
response.put("routeMissionCount", routeMissionRepository.count());
261328
response.put("timestamp", LocalDateTime.now());
262329

263330
return ResponseEntity.ok(ApiResult.success(response));
@@ -272,10 +339,14 @@ public ResponseEntity<ApiResult<Map<String, Object>>> clearTestData() {
272339
long levelCount = levelRepository.count();
273340
long gymCount = climbingGymRepository.count();
274341
long gymLevelCount = gymLevelRepository.count();
342+
long sectorCount = sectorRepository.count();
343+
long routeMissionCount = routeMissionRepository.count();
275344

276345
// 외래키 제약조건 때문에 순서가 중요함
346+
routeMissionRepository.deleteAll();
277347
gymLevelRepository.deleteAll();
278348
climbingGymRepository.deleteAll();
349+
sectorRepository.deleteAll();
279350
levelRepository.deleteAll();
280351
brandRepository.deleteAll();
281352

@@ -285,6 +356,8 @@ public ResponseEntity<ApiResult<Map<String, Object>>> clearTestData() {
285356
response.put("deletedLevels", levelCount);
286357
response.put("deletedGyms", gymCount);
287358
response.put("deletedGymLevels", gymLevelCount);
359+
response.put("deletedSectors", sectorCount);
360+
response.put("deletedRouteMissions", routeMissionCount);
288361
response.put("timestamp", LocalDateTime.now());
289362

290363
return ResponseEntity.ok(ApiResult.success(response));

0 commit comments

Comments
 (0)