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
@@ -0,0 +1,45 @@
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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.runimo.runimo.common.response.SuccessResponse;
import org.runimo.runimo.user.controller.request.FeedbackRequest;
import org.runimo.runimo.user.enums.UserHttpResponseCode;
import org.runimo.runimo.user.service.usecases.FeedbackUsecase;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "피드백 API")
@RestController
@RequestMapping("/api/v1/feedback")
@RequiredArgsConstructor
public class FeedbackController {

private final FeedbackUsecase feedbackUsecase;

@Operation(summary = "피드백 생성", description = "사용자가 피드백을 작성합니다.")
@ApiResponses(
value = {
@ApiResponse(responseCode = "201", description = "평가 생성 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터")
}
)
@PostMapping
public ResponseEntity<SuccessResponse<Void>> createFeedback(
@UserId Long userId,
@Valid @RequestBody FeedbackRequest request
) {
feedbackUsecase.createFeedback(FeedbackRequest.toCommand(userId, request));
return ResponseEntity.status(HttpStatus.CREATED.value()).body(
SuccessResponse.messageOnly(UserHttpResponseCode.FEEDBACK_CREATED));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.runimo.runimo.user.controller.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.hibernate.validator.constraints.Length;
import org.runimo.runimo.user.service.dto.command.FeedbackCommand;

@Schema(description = "피드백 요청 DTO")
public record FeedbackRequest(

@Schema(description = "평가지표 (1: 매우 불만족, 6: 매우 만족)", example = "3")
@Min(1) @Max(6)
Integer rate,
Comment on lines +13 to +14
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding validation context and constraints.

The rate field allows values 1-6 but lacks documentation about what these values represent. Consider adding more descriptive documentation or constants for the rating scale.

+public static final int MIN_RATE = 1;
+public static final int MAX_RATE = 6;
+
-@Schema(description = "평가지표", example = "1")
+@Schema(description = "평가지표 (1: 매우 불만족, 6: 매우 만족)", example = "3")
-@Min(1) @Max(6)
+@Min(MIN_RATE) @Max(MAX_RATE)
Integer rate,

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/org/runimo/runimo/user/controller/request/FeedbackRequest.java
around lines 13 to 14, the rate field has @Min(1) and @Max(6) annotations but
lacks descriptive documentation or constants explaining what the values 1 to 6
represent. Add JavaDoc comments to the rate field describing the meaning of each
rating value or define constants/enums representing the rating scale to improve
code clarity and maintainability.

@Schema(description = "피드백 내용", example = "피드백 내용")
@Length(max = 100)
String feedback
Comment on lines +16 to +17
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add null validation for feedback field.

The feedback field can currently be null, which might not be intended. Consider adding @NotNull or @NotBlank validation if feedback is required.

@Schema(description = "피드백 내용", example = "피드백 내용")
+@NotBlank
@Length(max = 100)
String feedback
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Length(max = 100)
String feedback
@Schema(description = "피드백 내용", example = "피드백 내용")
@NotBlank
@Length(max = 100)
String feedback
🤖 Prompt for AI Agents
In src/main/java/org/runimo/runimo/user/controller/request/FeedbackRequest.java
around lines 16 to 17, the feedback field lacks null validation, allowing it to
be null which may be unintended. Add the @NotNull or @NotBlank annotation above
the feedback field declaration to enforce that feedback must not be null or
blank, ensuring proper validation of this input.

) {

public static FeedbackCommand toCommand(Long userId, FeedbackRequest request) {
return new FeedbackCommand(userId, request.rate(), request.feedback());
}

}
39 changes: 39 additions & 0 deletions src/main/java/org/runimo/runimo/user/domain/Feedback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.runimo.runimo.user.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.runimo.runimo.common.CreateUpdateAuditEntity;

@Table(name = "user_feedback")
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Feedback extends CreateUpdateAuditEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "rate", nullable = false)
private Integer rate;
@Column(name = "content", nullable = false)
private String content;

@Builder
public Feedback(Long userId, Integer rate, String content) {
this.userId = userId;
this.rate = rate;
this.content = content.trim();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public enum UserHttpResponseCode implements CustomResponseCode {
MY_INCUBATING_EGG_FETCHED(HttpStatus.OK, "부화기중인 알 조회 성공", "부화중인 알 조회 성공"),
NOTIFICATION_ALLOW_UPDATED(HttpStatus.OK, "알림 허용 업데이트 성공", "알림 허용 업데이트 성공"),
NOTIFICATION_ALLOW_FETCHED(HttpStatus.OK, "알림 허용 조회 성공", "알림 허용 조회 성공"),
FEEDBACK_CREATED(HttpStatus.CREATED, "피드백 생성 성공", "피드백 생성 성공"),

LOGIN_FAIL_NOT_SIGN_IN(HttpStatus.NOT_FOUND
, "로그인 실패 - 회원가입하지 않은 사용자", "로그인 실패 - 회원가입하지 않은 사용자"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.runimo.runimo.user.repository;

import org.runimo.runimo.user.domain.Feedback;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FeedbackRepository extends JpaRepository<Feedback, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.runimo.runimo.user.service.dto.command;

public record FeedbackCommand(
Long userId,
Integer rate,
String content
) {
Comment on lines +3 to +7
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation annotations to ensure data integrity.

The record lacks validation constraints which could allow invalid data to propagate through the system. Consider adding validation annotations:

+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;

public record FeedbackCommand(
-    Long userId,
-    Integer rate,
-    String content
+    @NotNull Long userId,
+    @NotNull @Min(1) @Max(5) Integer rate,
+    @NotBlank @Size(max = 128) String content
) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public record FeedbackCommand(
Long userId,
Integer rate,
String content
) {
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public record FeedbackCommand(
@NotNull Long userId,
@NotNull @Min(1) @Max(5) Integer rate,
@NotBlank @Size(max = 128) String content
) {
// other methods or logic, if any
}
🤖 Prompt for AI Agents
In src/main/java/org/runimo/runimo/user/service/dto/command/FeedbackCommand.java
around lines 3 to 7, the FeedbackCommand record lacks validation annotations.
Add appropriate validation annotations such as @NotNull for userId and rate,
@Min and @Max for rate to restrict its range, and @NotBlank for content to
ensure it is not empty. This will enforce data integrity and prevent invalid
data from being processed.


}
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.dto.command.FeedbackCommand;

public interface FeedbackUsecase {

void createFeedback(FeedbackCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.runimo.runimo.user.service.usecases;

import lombok.RequiredArgsConstructor;
import org.runimo.runimo.user.domain.Feedback;
import org.runimo.runimo.user.repository.FeedbackRepository;
import org.runimo.runimo.user.service.dto.command.FeedbackCommand;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class FeedbackUsecaseImpl implements FeedbackUsecase {

private final FeedbackRepository feedbackRepository;

@Override
@Transactional
public void createFeedback(FeedbackCommand command) {
Feedback feedback = Feedback.builder()
.userId(command.userId())
.rate(command.rate())
.content(command.content())
.build();
feedbackRepository.save(feedback);
}
Comment on lines +16 to +25
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation and error handling.

The method lacks validation and error handling which could lead to persistence of invalid data or unclear error messages:

@Override
@Transactional
public void createFeedback(FeedbackCommand command) {
+    // Validate command (or use @Valid parameter)
+    if (command.userId() == null || command.rate() == null || command.content() == null) {
+        throw new IllegalArgumentException("All feedback fields are required");
+    }
+    
+    if (command.rate() < 1 || command.rate() > 5) {
+        throw new IllegalArgumentException("Rate must be between 1 and 5");
+    }
+    
+    if (command.content().trim().isEmpty() || command.content().length() > 128) {
+        throw new IllegalArgumentException("Content must not be empty and max 128 characters");
+    }
+
    Feedback feedback = Feedback.builder()
        .userId(command.userId())
        .rate(command.rate())
-        .content(command.content())
+        .content(command.content().trim())
        .build();
        
+    try {
        feedbackRepository.save(feedback);
+    } catch (Exception e) {
+        throw new RuntimeException("Failed to save feedback", e);
+    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
@Transactional
public void createFeedback(FeedbackCommand command) {
Feedback feedback = Feedback.builder()
.userId(command.userId())
.rate(command.rate())
.content(command.content())
.build();
feedbackRepository.save(feedback);
}
@Override
@Transactional
public void createFeedback(FeedbackCommand command) {
// Validate command (or use @Valid parameter)
if (command.userId() == null || command.rate() == null || command.content() == null) {
throw new IllegalArgumentException("All feedback fields are required");
}
if (command.rate() < 1 || command.rate() > 5) {
throw new IllegalArgumentException("Rate must be between 1 and 5");
}
if (command.content().trim().isEmpty() || command.content().length() > 128) {
throw new IllegalArgumentException("Content must not be empty and max 128 characters");
}
Feedback feedback = Feedback.builder()
.userId(command.userId())
.rate(command.rate())
.content(command.content().trim())
.build();
try {
feedbackRepository.save(feedback);
} catch (Exception e) {
throw new RuntimeException("Failed to save feedback", e);
}
}
🤖 Prompt for AI Agents
In
src/main/java/org/runimo/runimo/user/service/usecases/FeedbackUsecaseImpl.java
around lines 16 to 25, the createFeedback method lacks input validation and
error handling. Add checks to validate the command's userId, rate, and content
before building the Feedback object, throwing appropriate exceptions if
validation fails. Wrap the save operation in a try-catch block to handle
persistence exceptions and provide clear error messages or rethrow as needed.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE `user_feedback`
(
`id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`rate` INT,
`content` VARCHAR(128),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
3 changes: 2 additions & 1 deletion src/test/java/org/runimo/runimo/CleanUpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public class CleanUpUtil {
"user_love_point",
"incubating_egg",
"runimo",
"user_token"
"user_token",
"user_feedback"
};

@Autowired
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.runimo.runimo.user.api;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.runimo.runimo.TestConsts.TEST_USER_UUID;

import io.restassured.RestAssured;
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.TokenUtils;
import org.runimo.runimo.user.repository.FeedbackRepository;
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.http.HttpStatus;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

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

@LocalServerPort
int port;

@Autowired
private CleanUpUtil cleanUpUtil;

@Autowired
private TokenUtils tokenUtils;
private String token;
@Autowired
private FeedbackRepository feedbackRepository;

@BeforeEach
void setUp() {
RestAssured.port = port;
token = tokenUtils.createTokenByUserPublicId(TEST_USER_UUID);
}

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

@Test
@Sql(scripts = "/sql/user_default_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void 피드백_저장시_성공() {
// given
String feedbackBody = """
{
"rate": 3,
"feedback": "이것은 피드백입니다."
}
""";

// when & then
RestAssured.given()
.header("Authorization", token)
.contentType("application/json")
.body(feedbackBody)
.when()
.post("/api/v1/feedback")
.then()
.log().ifError()
.statusCode(HttpStatus.CREATED.value());

var savedFeedback = feedbackRepository.findById(1L).get();
assertEquals("이것은 피드백입니다.", savedFeedback.getContent());
assertEquals(3, savedFeedback.getRate());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE `user_feedback`
(
`id` BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`rate` INT,
`content` VARCHAR(128),
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
31 changes: 31 additions & 0 deletions src/test/resources/sql/user_default_data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- 테스트용 기본 유저 (기존에 있다고 가정)
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', 10000, 3600, NOW(),
NOW());
SET FOREIGN_KEY_CHECKS = 1;

TRUNCATE TABLE oauth_account;
INSERT INTO oauth_account (id, created_at, deleted_at, updated_at, provider, provider_id, user_id)
VALUES (1, NOW(), null, NOW(), 'KAKAO', 1234, 1);

-- 기존 디바이스 토큰 데이터
INSERT INTO user_token (id, user_id, device_token, platform, notification_allowed, created_at,
updated_at)
VALUES (1, 1, 'existing_device_token_12345', 'FCM', true, NOW(), NOW());

-- 보유 애정
INSERT INTO user_love_point (id, user_id, amount, created_at, updated_at)
VALUES (1, 1, 0, NOW(), NOW());

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