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
Expand Up @@ -24,4 +24,7 @@ Slice<RunningRecord> findFirstRunOfWeek(
@Param("now") LocalDateTime now,
Pageable pageable
);

@Query("SELECT COUNT(r.id) FROM RunningRecord r WHERE r.userId = :id")
Long countByUserId(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ public Optional<RunningRecord> findFirstRunOfCurrentWeek(Long userId) {
PageRequest pageRequest = PageRequest.of(0, 1, Sort.by("startedAt").ascending());
return recordRepository.findFirstRunOfWeek(userId, startOfWeek, now, pageRequest).stream().findFirst();
}

@Transactional(readOnly = true)
public Long countByUserId(Long userId) {
return recordRepository.countByUserId(userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.runimo.runimo.user.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.common.response.SuccessResponse;
import org.runimo.runimo.user.service.usecases.MainViewQueryUsecase;
import org.runimo.runimo.user.service.dtos.MainViewResponse;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "MAIN VIEW")
@RestController
@RequestMapping("/api/v1/main")
@RequiredArgsConstructor
public class MainViewController {

private final MainViewQueryUsecase mainViewQueryUsecase;


@Operation(summary = "메인 화면 조회", description = "메인 화면을 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "메인 화면 조회 성공"),
@ApiResponse(responseCode = "401", description = "인증 실패")
})
@GetMapping
public ResponseEntity<SuccessResponse<MainViewResponse>> queryMainView(
@UserId Long userId) {
MainViewResponse response = mainViewQueryUsecase.execute(userId);
return ResponseEntity.ok(SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
Expand All @@ -27,4 +28,9 @@ public interface UserItemRepository extends JpaRepository<UserItem, Long> {
"join Egg e on ui.itemId = e.id " +
"where ui.userId = :userId and e.eggType = :eggType")
Optional<UserItem> findByUserIdAndEggType(Long userId, EggType eggType);

@Query("select ui from UserItem ui " +
"join Egg e on ui.itemId = e.id " +
"where ui.userId = :userId")
List<UserItem> findAllEggsByUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Component
Expand All @@ -15,6 +16,11 @@ public class UserItemFinder {

private final UserItemRepository userItemRepository;

@Transactional(readOnly = true)
public List<UserItem> findEggsByUserId(Long userId) {
return userItemRepository.findAllEggsByUserId(userId);
}

@Transactional(readOnly = true)
public Optional<UserItem> findEggByUserIdAndEggType(Long userId, EggType eggType) {
return userItemRepository.findByUserIdAndEggType(userId, eggType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class UserItemProcessor {

@Transactional
public void updateItemQuantity(Long userId, Long itemId, Long amount) {
UserItem userItem = userItemFinder.findByUserIdAndItemId(userId, itemId)
UserItem userItem = userItemFinder.findByUserIdAndItemIdWithXLock(userId, itemId)
.orElseThrow(IllegalStateException::new);
userItem.gainItem(amount);
userItemRepository.save(userItem);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.runimo.runimo.user.service.dtos;

public record MainViewResponse(
String nickname,
String profileImageUrl,
Long lovePoint,
Long totalDistanceInMeters,
Long totalRunningCount,
Long totalEggCount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.runimo.runimo.user.service.usecases;

import org.runimo.runimo.user.service.dtos.MainViewResponse;

public interface MainViewQueryUsecase {

MainViewResponse execute(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.runimo.runimo.user.service.usecases;

import lombok.RequiredArgsConstructor;
import org.runimo.runimo.records.service.RecordFinder;
import org.runimo.runimo.user.domain.User;
import org.runimo.runimo.user.domain.UserItem;
import org.runimo.runimo.user.service.UserFinder;
import org.runimo.runimo.user.service.UserItemFinder;
import org.runimo.runimo.user.service.dtos.MainViewResponse;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class MainViewQueryUsecaseImpl implements MainViewQueryUsecase {

private final UserFinder userFinder;
private final UserItemFinder userItemFinder;
private final RecordFinder recordFinder;

@Override
public MainViewResponse execute(Long userId) {
// 유저 달리기 스탯
User user = userFinder.findUserById(userId)
.orElseThrow(NoSuchElementException::new);
List<UserItem> userEggs = userItemFinder.findEggsByUserId(userId);
Long eggCount = userEggs.stream()
.map(UserItem::getQuantity)
.collect(Collectors.summarizingLong(Long::longValue))
.getSum();
Long runningCount = recordFinder.countByUserId(userId);
Long lovePoint = userFinder.findLovePointByUserId(userId)
.orElseThrow(NoSuchElementException::new)
.getAmount();
return new MainViewResponse(
user.getNickname(),
user.getImgUrl(),
lovePoint,
user.getTotalDistanceInMeters(),
runningCount,
eggCount
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.runimo.runimo.user.api;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.runimo.runimo.CleanUpUtil;
import org.runimo.runimo.auth.jwt.JwtTokenFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class MainViewAcceptanceTest {

@LocalServerPort
int port;

@Autowired
private JwtTokenFactory jwtTokenFactory;

@Autowired
private CleanUpUtil cleanUpUtil;

private static final String USER_UUID = "test-user-uuid-1";
private static final String AUTH_HEADER_PREFIX = "Bearer ";

@BeforeEach
void setUp() {
RestAssured.port = port;
}

@AfterEach
void tearDown() {
cleanUpUtil.cleanUpUserInfos();
}

@Test
@Sql(scripts = "/sql/main_view_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void 메인화면_조회_성공시_정확한_정보를_반환한다() {
// given
String token = AUTH_HEADER_PREFIX + jwtTokenFactory.generateAccessToken(USER_UUID);

// when & then
given()
.header("Authorization", token)
.contentType(ContentType.JSON)
.when()
.get("/api/v1/main")
.then()
.log().ifValidationFails()
.statusCode(200)
.body("code", equalTo("USH2001"))
.body("payload.nickname", equalTo("Daniel"))
.body("payload.profile_image_url", equalTo("https://example.com/images/user1.png"))
.body("payload.total_running_count", equalTo(2))
.body("payload.total_distance_in_meters", equalTo(3000))
.body("payload.love_point", equalTo(100))
.body("payload.total_egg_count", equalTo(3));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.runimo.runimo.user.api;

import org.junit.jupiter.api.Test;
import org.runimo.runimo.auth.jwt.JwtTokenFactory;
import org.runimo.runimo.user.service.usecases.MainViewQueryUsecase;
import org.runimo.runimo.user.service.dtos.MainViewResponse;
import org.runimo.runimo.user.UserFixtures;
import org.runimo.runimo.user.service.UserFinder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import java.util.Optional;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class MainViewControllerTest {

@Autowired
private MockMvc mockMvc;

@MockitoBean
private UserFinder userFinder;
@MockitoBean
private MainViewQueryUsecase mainViewUsecase;
@Autowired
private JwtTokenFactory jwtTokenFactory;

@Test
void 메인_화면_조회_성공() throws Exception {
// given

String accessToken = "Bearer " + jwtTokenFactory.generateAccessToken("test-user-uuid-1");

MainViewResponse response = new MainViewResponse(
"Daniel",
"https://example.com/images/user1.png",
2L,
3000L,
100L,
3L
);
given(mainViewUsecase.execute(any())).willReturn(response);
given(userFinder.findUserByPublicId(any())).willReturn(Optional.ofNullable(UserFixtures.getDefaultUser()));
// when & then
mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/main")
.header("Authorization", accessToken)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.code").value("USH2001"))
.andExpect(jsonPath("$.payload.nickname").value("Daniel"))
.andExpect(jsonPath("$.payload.profile_image_url").value("https://example.com/images/user1.png"))
.andExpect(jsonPath("$.payload.total_running_count").value(100))
.andExpect(jsonPath("$.payload.total_distance_in_meters").value(3000))
.andExpect(jsonPath("$.payload.love_point").value(2))
.andExpect(jsonPath("$.payload.total_egg_count").value(3));
}
}
32 changes: 32 additions & 0 deletions src/test/resources/sql/main_view_data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- 사용자
SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE users;
INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, total_time_in_seconds, created_at,
updated_at)
VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 3000, 3600, NOW(), NOW());
SET FOREIGN_KEY_CHECKS = 1;

TRUNCATE TABLE items;
INSERT INTO items (name, item_code, description, item_type, img_url, dtype, egg_type, hatch_require_amount, created_at,
updated_at)
VALUES ('마당알', 'A100', '마당알: 기본 알', 'USABLE', 'example.url', 'EGG', 'MADANG', 10, NOW(), NOW());

INSERT INTO items (name, item_code, description, item_type, img_url, dtype, egg_type, hatch_require_amount, created_at,
updated_at)
VALUES ('숲알', 'A101', '숲알: 기본 알', 'USABLE', 'example1.url', 'EGG', 'FOREST', 20, NOW(), NOW());

TRUNCATE TABLE running_records;
INSERT INTO running_records (id, user_id, record_public_id, title, started_at, end_at, total_distance, pace_in_milli_seconds, is_rewarded, created_at, updated_at)
VALUES (1, 1, 'record-public-id-1', 'record-title-1', NOW(), NOW(), 1000, 100, false, NOW(), NOW()),
(2, 1, 'record-public-id-2', 'record-title-2', NOW(), NOW(), 2000, 200, false, NOW(), NOW());


-- 보유 아이템
TRUNCATE TABLE user_item;
INSERT INTO user_item (id, user_id, item_id, quantity, created_at, updated_at)
VALUES (1001, 1, 1, 2, NOW(), NOW()),
(1002, 1, 2, 1, NOW(), NOW());

TRUNCATE TABLE user_love_point;
INSERT INTO user_love_point (id, user_id, amount, created_at, updated_at)
VALUES (1001, 1, 100, NOW(), NOW());