Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
16 changes: 8 additions & 8 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ jobs:
# ์ค‘์š”: PR์ด ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜์„ ๋•Œ๋งŒ(์ฝ”๋“œ ํฌ๋งท์ด ์™„๋ฒฝํ•  ๋•Œ๋งŒ) ๋นŒ๋“œ/ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
- name: Build with Gradle
if: steps.create_pr.outputs.pull-request-number == ''
run: ./gradlew build
env:
DB_URL: ${{ secrets.DB_URL }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
# Spring Boot๋Š” ์ด ํ™˜๊ฒฝ ๋ณ€์ˆ˜(API_TOKEN)๋ฅผ ์ธ์‹ํ•˜์—ฌ
# application.properties์˜ ${API_TOKEN} ํ”Œ๋ ˆ์ด์Šคํ™€๋”๋ฅผ ์น˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
API_TOKEN: ${{ secrets.REPLICATE_API_TOKEN }}
# ๋งŒ์•ฝ ํ…Œ์ŠคํŠธ DB ๋“ฑ ๋‹ค๋ฅธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
# DB_URL: ${{ secrets.DB_URL_TEST }}
# DB_USERNAME: ${{ secrets.DB_USERNAME_TEST }}
# DB_PASSWORD: ${{ secrets.DB_PASSWORD_TEST }}
run: ./gradlew build

# 8. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ์—…๋กœ๋“œ
- name: Publish Test Results
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.properties

### STS ###
.apt_generated
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.cp_main_be.global;

import com.example.cp_main_be.global.exception.UserNotFoundException;
import com.example.cp_main_be.util.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ApiResponse<Object>> handleUserNotFoundException(UserNotFoundException e) {
ApiResponse<Object> response = ApiResponse.failure("USER_NOT_FOUND", e.getMessage());

return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.cp_main_be.global.exception;

public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
15 changes: 5 additions & 10 deletions src/main/java/com/example/cp_main_be/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;

@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {

@Id
Expand All @@ -31,11 +31,9 @@ public class User {

private String profileImageUrl;

private Long level;
@Builder.Default private Long level = 1L; // ๊ธฐ๋ณธ ๋ ˆ๋ฒจ ์„ค์ •

private Integer experiencePoints;

private Integer temperatureScore;
@Builder.Default private Integer temperatureScore = 0; // ๊ธฐ๋ณธ ์˜จ๋„ ์ ์ˆ˜ ์„ค์ •

@Column(nullable = false)
private LocalDateTime createdAt;
Expand All @@ -51,9 +49,6 @@ protected void onCreate() {
this.uuid = UUID.randomUUID();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now(); // ์ตœ์ดˆ ์ƒ์„ฑ ์‹œ updated_at๋„ ์„ค์ •
if (this.level == null) this.level = 1L; // ๊ธฐ๋ณธ ๋ ˆ๋ฒจ ์„ค์ •
if (this.experiencePoints == null) this.experiencePoints = 0; // ๊ธฐ๋ณธ ๊ฒฝํ—˜์น˜ ์„ค์ •
if (this.temperatureScore == null) this.temperatureScore = 0; // ๊ธฐ๋ณธ ์˜จ๋„ ์ ์ˆ˜ ์„ค์ •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์—ฌ๊ธฐ ๊ธฐ๋ณธ ๊ฒฝํ—˜์น˜๋ž‘ ๊ธฐ๋ณธ ์˜จ๋„ ์ ์ˆ˜๊ฐ€ ๋‹ค๋ฅด๊ฒŒ ํ•ด๋†“์œผ์‹  ๊ฑฐ ๊ฐ™์€๋ฐ, ํ•˜๋‚˜๋กœ ํ†ต์ผ ํ•ด์ฃผ์„ธ์š”
๋ณ„๊ฐœ ๊ฒฝํ—˜์น˜๊ฐ€ ์•„๋‹ˆ๊ณ  ํ•˜๋‚˜์˜ ์ฒด์ œ๋กœ๋งŒ ์šด์˜๋  ๊ฑฐ์—์š”

@lejuho lejuho Jul 15, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

experiencePoints ์‚ญ์ œํ•˜๊ณ  temperaturescore๋กœ ํ†ต์ผํ–ˆ์Šด๋‹ค

@Jeonghun01 Jeonghun01 Jul 15, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๊ทผ๋ฐ ๋ฏธ์•ˆํ•œ๋ฐ โ€˜์˜จ๋„โ€™๊ฐ€ ์•„๋‹ˆ๋ผ โ€˜๊ฝƒโ€™ ์ ์ˆ˜์ด๊ธดํ•ด
์›๋ž˜ ์˜จ๋„๋Š” ๊ฐ€์ œ์˜€๊ณ  ๊ฝƒ์œผ๋กœ ํ•˜๊ธฐ๋กœ ํ–ˆ์–ด
FlowerScore๋กœ ํ†ต์ผํ•˜์‹ค๊นŒ์š”..?

if (this.status == null) this.status = UserStatus.ACTIVE; // ๊ธฐ๋ณธ ์ƒํƒœ ์„ค์ •
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import com.example.cp_main_be.user.domain.User;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByUsername(String username);

Optional<User> findByUuid(UUID uuid);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.example.cp_main_be.user.dto.request;

import java.util.UUID;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class UserRequest {

private final Long userUuid;
private final UUID userUuid;
private final Long userId;
private final String username;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.example.cp_main_be.user.service.UserService;
import com.example.cp_main_be.util.ApiResponse;
import jakarta.validation.Valid;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand Down Expand Up @@ -37,4 +38,10 @@ public ResponseEntity<ApiResponse<UserResponse>> register(
// return ResponseEntity.ok().build();
// }

@GetMapping("/users/{uuid}")
public ResponseEntity<ApiResponse<UserResponse>> getUserInfo(@PathVariable UUID uuid) {
User user = userService.findUserByUuid(uuid);
UserResponse userResponse = new UserResponse(user.getId(), user.getUsername(), user.getUuid());
return ResponseEntity.ok(ApiResponse.success(userResponse));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.example.cp_main_be.user.service;

import com.example.cp_main_be.global.exception.UserNotFoundException;
import com.example.cp_main_be.user.domain.User;
import com.example.cp_main_be.user.domain.repository.UserRepository;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -18,7 +20,15 @@ public void saveUser(User user) {
}

public User findUserById(Long id) {
return this.userRepository.findById(id).orElse(null);
return this.userRepository
.findById(id)
.orElseThrow(() -> new UserNotFoundException("ํ•ด๋‹น ID์˜ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค : " + id));
}

public User findUserByUuid(UUID uuid) {
return this.userRepository
.findByUuid(uuid)
.orElseThrow(() -> new UserNotFoundException("ํ•ด๋‹น UUID์˜ ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค : " + uuid));
}

// ์‹คํ—˜์šฉ
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
package com.example.cp_main_be.config;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@SpringBootTest
@Testcontainers // 1. ์ด ํด๋ž˜์Šค๊ฐ€ Testcontainers๋ฅผ ์‚ฌ์šฉํ•จ์„ ๋ช…์‹œ
public abstract class AbstractContainerBaseTest {

// 2. static์œผ๋กœ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ŠคํŠธ ์ „์ฒด์— ๋‹จ ํ•˜๋‚˜๋งŒ ์ƒ์„ฑ)
private static final PostgreSQLContainer<?> postgresqlContainer =
@Container
public static final PostgreSQLContainer<?> postgresqlContainer =
new PostgreSQLContainer<>("postgres:15-alpine") // ์‚ฌ์šฉํ•  PostgreSQL ์ด๋ฏธ์ง€
.withDatabaseName("testdb") // ์‚ฌ์šฉํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด๋ฆ„
.withUsername("testuser") // ์‚ฌ์šฉ์ž ์ด๋ฆ„
.withPassword("testpass"); // ๋น„๋ฐ€๋ฒˆํ˜ธ

// 3. ์ปจํ…Œ์ด๋„ˆ๋ฅผ static ๋ธ”๋ก์—์„œ ์ˆ˜๋™์œผ๋กœ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. (JUnit 5 ํ™•์žฅ๊ธฐ๋Šฅ์˜ ์ž๋™์‹œ์ž‘๋ณด๋‹ค ์•ˆ์ •์ )
static {
postgresqlContainer.start();
}

// 4. Spring ์ปจํ…์ŠคํŠธ๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ์ „์—, ๋™์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
@DynamicPropertySource
static void setProperties(DynamicPropertyRegistry registry) {
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ •๋ณด ์„ค์ •
registry.add("spring.datasource.url", postgresqlContainer::getJdbcUrl);
registry.add("spring.datasource.username", postgresqlContainer::getUsername);
registry.add("spring.datasource.password", postgresqlContainer::getPassword);
registry.add("spring.datasource.driver-class-name", postgresqlContainer::getDriverClassName);
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop"); // ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด ์Šคํ‚ค๋งˆ๋ฅผ ์‚ญ์ œํ•˜๋„๋ก ์„ค์ •
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop");

// 3. ๋ˆ„๋ฝ๋œ ๋‹ค๋ฅธ ํ”„๋กœํผํ‹ฐ๋“ค์„ ์—ฌ๊ธฐ์— ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!
// ์ด ๊ฐ’๋“ค์ด ์—†์œผ๋ฉด Spring Context๊ฐ€ ๋กœ๋“œ๋˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.
registry.add("cloudflare.r2.endpoint", () -> "http://localhost:9000"); // ํ…Œ์ŠคํŠธ์šฉ ๋”๋ฏธ๊ฐ’
registry.add("cloudflare.r2.bucket", () -> "test-bucket");
registry.add("cloudflare.r2.access-key", () -> "test-key");
registry.add("cloudflare.r2.secret-key", () -> "test-secret");

registry.add("replicate.api.token", () -> "dummy-token");
registry.add("replicate.api.url", () -> "https://api.replicate.com/test");
}
}
Original file line number Diff line number Diff line change
@@ -1,70 +1,90 @@
package com.example.cp_main_be.user.presentation;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import com.example.cp_main_be.config.AbstractContainerBaseTest;
import com.example.cp_main_be.user.domain.User;
import com.example.cp_main_be.user.domain.UserStatus;
import com.example.cp_main_be.user.service.UserService;
import org.junit.jupiter.api.Assertions;
import com.example.cp_main_be.user.domain.repository.UserRepository;
import com.example.cp_main_be.user.dto.request.UserRequest;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.dao.DataIntegrityViolationException;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class UserControllerTest extends AbstractContainerBaseTest {

@Autowired private UserService userService;
@Autowired private MockMvc mockMvc;

@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("replicate.api.token", () -> "dummy-token-for-user-test");
}
@Autowired private ObjectMapper objectMapper;

@Autowired private UserRepository userRepository;

@DisplayName("๋‹‰๋„ค์ž„์„ ๋ฐ›์•„ ์œ ์ €๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.")
@Test
public void ํšŒ์›_์ƒ์„ฑ์‹œ_uuid๊ฐ€_์ž๋™์œผ๋กœ_์ƒ๊ธฐ๋Š”๊ฐ€() throws Exception {
void register() throws Exception {
// given
User user = new User();
UserRequest userRequest = new UserRequest(UUID.randomUUID(), 1L, "test");
String requestBody = objectMapper.writeValueAsString(userRequest);

// when
user.setUsername("test");
userService.saveUser(user);
ResultActions result =
mockMvc.perform(
post("/api/v1/register/nickname")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody));

// then
// System.out.println("user.getUuid = " + user.getUuid());
Assertions.assertNotNull(user.getUuid());
result
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.username").value("test"))
.andExpect(jsonPath("$.data.uuid").isNotEmpty());
}

@DisplayName("UUID๋กœ ์œ ์ € ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค.")
@Test
public void ์œ ์ €_์ด๋ฆ„์ด_์—†์œผ๋ฉด_์˜ค๋ฅ˜() throws Exception {
void getUserInfo_success() throws Exception {
// given
User user = new User();
// ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ฏธ๋ฆฌ ์œ ์ €๋ฅผ ํ•œ ๋ช… ์ €์žฅ
User savedUser = userRepository.save(User.builder().username("existing-user").build());
UUID userUuid = savedUser.getUuid();

// when
user.setEmail("test@example.com");
user.setPasswordHash("hashedpassword");
user.setLevel(1L); // '1L' ๋Œ€์‹  '1'๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๊ฒฝ๊ณ  ์ œ๊ฑฐ
user.setExperiencePoints(0);
user.setTemperatureScore(0);
user.setStatus(UserStatus.ACTIVE);
ResultActions result = mockMvc.perform(get("/api/v1/users/{uuid}", userUuid));

// then
Assertions.assertThrows(
DataIntegrityViolationException.class,
() -> {
userService.saveUser(user);
});
result
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.username").value("existing-user"))
.andExpect(jsonPath("$.data.uuid").value(userUuid.toString()));
}

@DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” UUID๋กœ ์œ ์ € ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋ฉด 404 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.")
@Test
public void ์œ ์ €์ƒ์„ฑ์„ฑ๊ณต() throws Exception {
void getUserInfo_fail_whenUserNotFound() throws Exception {
// given
User user = new User();
UUID nonExistentUuid = UUID.randomUUID();

// when
user.setUsername("test");
userService.saveUser(user);
User user1 = userService.findUserById(user.getId());
ResultActions result = mockMvc.perform(get("/api/v1/users/{uuid}", nonExistentUuid));

// then
Assertions.assertEquals(user.getId(), user1.getId());
result.andDo(print()).andExpect(status().isNotFound()); // UserNotFoundException์ด 404๋กœ ๋ณ€ํ™˜๋˜๋Š”์ง€ ํ™•์ธ
}
}
Loading