-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/user-feedback] - 피드백 저장 API #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
60e86df
997b260
1a80d92
f6505cf
741ef7f
cf2dbea
30f67f7
b539a81
2c403fe
3169f6c
70b7ee8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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,
🤖 Prompt for AI Agents |
||||||||||||||
| @Schema(description = "피드백 내용", example = "피드백 내용") | ||||||||||||||
| @Length(max = 100) | ||||||||||||||
| String feedback | ||||||||||||||
|
Comment on lines
+16
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 @Schema(description = "피드백 내용", example = "피드백 내용")
+@NotBlank
@Length(max = 100)
String feedback📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| ) { | ||||||||||||||
|
|
||||||||||||||
| public static FeedbackCommand toCommand(Long userId, FeedbackRequest request) { | ||||||||||||||
| return new FeedbackCommand(userId, request.rate(), request.feedback()); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| } | ||||||||||||||
| 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; | ||
ekgns33 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @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 |
|---|---|---|
| @@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||
| ); |
| 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 | ||
| ); |
| 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()); | ||
|
|
||
|
|
||
|
|
||
|
|
Uh oh!
There was an error while loading. Please reload this page.