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
25 changes: 24 additions & 1 deletion back/src/docs/asciidoc/feedback.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,27 @@ include::{snippets}/create-feedback-invalid-user-mood-400/http-request.adoc[]

===== HTTP 응답 (400 Bad Request)
include::{snippets}/create-feedback-invalid-user-mood-400/http-response.adoc[]
---
---

[[Get-Daily-Feedbacks]]
== 일간 피드백 조회 API
=== 성공: 일간 피드백 조회

==== HTTP 요청
include::{snippets}/get-daily-feedbacks-200/http-request.adoc[]

==== 요청 필드
include::{snippets}/get-daily-feedbacks-200/path-parameters.adoc[]

==== HTTP 응답 (200 OK)
include::{snippets}/get-daily-feedbacks-200/http-response.adoc[]

=== 실패: 유효성 검증 오류

==== 유효하지 않은 미래 일자 요청값 오류

===== HTTP 요청
include::{snippets}/get-daily-feedbacks-invalid-date-400/http-request.adoc[]

===== HTTP 응답 (400 Bad Request)
include::{snippets}/get-daily-feedbacks-invalid-date-400/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.rouby.assistant.feedback.application.dto;

import com.rouby.assistant.feedback.domain.entity.Feedback;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import lombok.Builder;

public record GetDailyFeedbacksInfo(
List<GetFeedbackInfo> feedbacks
) {

public static GetDailyFeedbacksInfo from(List<Feedback> feedbacks) {

if (feedbacks == null || feedbacks.isEmpty()) {
return new GetDailyFeedbacksInfo(Collections.emptyList());
}

return new GetDailyFeedbacksInfo(feedbacks.stream().map(GetFeedbackInfo::from).toList());
}

@Builder
public record GetFeedbackInfo(
int slot,
String userInput,
String userMood,
String feedbackContent,
String status,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {

public static GetFeedbackInfo from(Feedback feedback) {

return GetFeedbackInfo.builder()
.slot(feedback.getSlot())
.userInput(feedback.getUserInput())
.userMood(feedback.getMood().toString())
.feedbackContent(feedback.getFeedbackContent().getContent())
.status(feedback.getStatus().toString())
.createdAt(feedback.getCreatedAt())
.updatedAt(feedback.getUpdatedAt())
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.rouby.assistant.feedback.application.service;

import com.rouby.assistant.feedback.application.dto.FeedbackInfoForNewFeedback;
import com.rouby.assistant.feedback.application.dto.GetDailyFeedbacksInfo;
import com.rouby.assistant.feedback.domain.entity.Feedback;
import com.rouby.assistant.feedback.domain.entity.enums.Status;
import com.rouby.assistant.feedback.domain.repository.FeedbackRepository;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
Expand All @@ -21,4 +23,11 @@ public FeedbackInfoForNewFeedback getRecentFeedbackInfoWithin1W(Long userId) {
.orElse(null);
return FeedbackInfoForNewFeedback.from(recentFeedback);
}

public GetDailyFeedbacksInfo getDailyFeedbacks(Long userId, LocalDate date) {

return GetDailyFeedbacksInfo.from(
feedbackRepository.findByUserIdAndFeedbackDateAndStatusAndDeletedAtIsNull(
userId, date, Status.COMPLETED));
}
Comment thread
HanaHww2 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@ public void requestFeedback(CreateFeedbackCommand command) {
throw e;
}


TEMPERATURE = 0.5;

assistantGateway.requestDailyFeedbackAsync(
PROMPT_VERSION,
TEMPERATURE,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.rouby.assistant.feedback.domain.repository;

import com.rouby.assistant.feedback.domain.entity.Feedback;
import com.rouby.assistant.feedback.domain.entity.enums.Status;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

public interface FeedbackRepository {
Expand All @@ -17,4 +19,6 @@ Optional<Feedback> findTop1ByUserIdAndCreatedAtBetweenOrderByCreatedAtDesc(
void markFailure(Long feedbackId, Long userId);

Optional<Feedback> findByIdAndDeletedAtIsNull(Long feedbackId);

List<Feedback> findByUserIdAndFeedbackDateAndStatusAndDeletedAtIsNull(Long userId, LocalDate date, Status status);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.rouby.assistant.feedback.infrastructure.persistence.jpa;

import com.rouby.assistant.feedback.domain.entity.Feedback;
import com.rouby.assistant.feedback.domain.entity.enums.Status;
import com.rouby.assistant.feedback.domain.repository.FeedbackRepository;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -17,6 +19,8 @@ public interface FeedbackJpaRepository extends
Optional<Feedback> findTop1ByUserIdAndCreatedAtBetweenOrderByCreatedAtDesc(
Long userId, LocalDateTime fromAt, LocalDateTime toAt);

List<Feedback> findByUserIdAndFeedbackDateAndStatusAndDeletedAtIsNull(Long userId, LocalDate date, Status status);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

쿼리 메서드가 올바르게 추가되었습니다. 성능 최적화를 고려하세요.

Spring Data JPA 메서드 네이밍 컨벤션을 따른 명확한 쿼리 메서드입니다. userId, feedbackDate, status, deletedAt 조건으로 필터링합니다.

성능 최적화를 위해 데이터베이스 인덱스 존재 여부를 확인하세요:

복합 인덱스 (user_id, feedback_date, status, deleted_at)가 없다면 추가를 고려하세요. 특히 사용자당 피드백이 많아질 경우 조회 성능에 영향을 줄 수 있습니다.


🏁 Script executed:

#!/bin/bash
# Description: Check for database index on feedback table for the new query

# Search for index definitions or migration files related to feedback table
fd -e sql -e xml | xargs rg -l "feedback" | xargs rg -i "index.*feedback.*(user_id|feedback_date|status|deleted_at)"

# Also check JPA entity for index annotations
rg -A5 "@Table|@Index" back/src/main/java/com/rouby/assistant/feedback/domain/entity/Feedback.java

Length of output: 0


더 정확한 검증을 위해 추가 스크립트를 실행하겠습니다.

더 정확한 검증을 위해 추가 스크립트를 실행하겠습니다.


🏁 Script executed:

#!/bin/bash
# Find and examine the Feedback entity file
echo "=== Searching for Feedback entity ==="
find . -name "Feedback.java" -type f 2>/dev/null | head -5

# Check the entity file for index annotations
echo -e "\n=== Checking Feedback entity for @Index annotations ==="
rg -A20 "@Entity|@Table" back/src/main/java/com/rouby/assistant/feedback/domain/entity/Feedback.java 2>/dev/null || echo "Entity file not found or no matches"

# Look for database migration files
echo -e "\n=== Searching for database migration files ==="
fd -e sql -e yml -e yaml | grep -iE "(migration|flyway|liquibase|db)" | head -10

# Search for any index definitions in the entire codebase
echo -e "\n=== Searching for @Index annotations anywhere ==="
rg -n "@Index" --type java 2>/dev/null | head -10 || echo "No @Index annotations found"

Length of output: 1274


🏁 Script executed:

#!/bin/bash
# Find and examine the Feedback entity file
echo "=== Searching for Feedback entity ==="
find . -name "Feedback.java" -type f 2>/dev/null | grep -v target

# Check the entity file for index annotations
echo -e "\n=== Checking Feedback entity for @Index annotations ==="
rg -A20 "@Entity|@Table" --type java 2>/dev/null | grep -A20 "Feedback"

# Look for database migration files
echo -e "\n=== Searching for database migration files ==="
fd -e sql -e yml -e yaml 2>/dev/null | grep -iE "(migration|flyway|liquibase|db|ddl|schema)" | head -20

# Search for any index definitions in the entire codebase
echo -e "\n=== Searching for @Index annotations ==="
rg -B2 -A2 "@Index" --type java 2>/dev/null

Length of output: 5783


스키마 파일을 확인하여 데이터베이스 레벨에서 인덱스가 정의되어 있는지 검증하겠습니다.


🏁 Script executed:

#!/bin/bash
# Check schema.sql for feedback table and indexes
echo "=== Checking schema.sql for feedback table ==="
rg -A20 -i "create.*table.*feedback|feedback" back/src/main/resources/db/schema.sql

# Check if there are any index definitions for feedback table
echo -e "\n=== Searching for indexes on feedback table ==="
rg -i "create.*index.*feedback|feedback.*index" back/src/main/resources/db/schema.sql

# Display entire schema.sql if it's not too large
echo -e "\n=== Full schema.sql content ==="
cat back/src/main/resources/db/schema.sql | head -100

Length of output: 1290


복합 인덱스 추가 권장 - 현재 인덱스가 새 쿼리를 커버하지 않습니다

Spring Data JPA 쿼리 메서드 이름 규칙은 올바르게 작성되었습니다. 그러나 검증 결과, 데이터베이스 스키마에는 (user_id, feedback_date, slot) 필터링만 지원하는 유니크 인덱스만 존재하며, 새로운 쿼리 메서드가 필요로 하는 (user_id, feedbackDate, status, deletedAt) 필터 조합을 커버하지 않습니다.

코드베이스의 다른 엔티티들(RoutineTask, Schedule, EmailLog)처럼 Feedback 엔티티에 @Table 어노테이션으로 복합 인덱스를 추가하세요:

@Entity
@Table(name = "feedback", indexes = {
    @Index(name = "idx_feedback_user_date_status_deleted", 
            columnList = "user_id, feedback_date, status, deleted_at")
})
🤖 Prompt for AI Agents
In
back/src/main/java/com/rouby/assistant/feedback/infrastructure/persistence/jpa/FeedbackJpaRepository.java
around line 22: the new query method
findByUserIdAndFeedbackDateAndStatusAndDeletedAtIsNull is not covered by the
existing DB index (which only covers user_id, feedback_date, slot); add a
composite index on (user_id, feedback_date, status, deleted_at) to the Feedback
entity so the query can use an index. Open the Feedback entity class and add a
@Table annotation (or update its existing @Table) to declare an index named like
idx_feedback_user_date_status_deleted with columnList "user_id, feedback_date,
status, deleted_at", then regenerate/migrate the schema (or add a migration) so
the index is created in the database.


@Query("""
SELECT MAX(fb.slot) FROM Feedback fb
WHERE fb.userId = :userId AND fb.feedbackDate = :today AND fb.status != 'FAILED'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.rouby.assistant.feedback.presentation;

import com.rouby.assistant.feedback.application.service.FeedbackReadService;
import com.rouby.assistant.feedback.application.usecase.CreateFeedbackUsecase;
import com.rouby.assistant.feedback.presentation.dto.CreateFeedbackRequest;
import com.rouby.assistant.feedback.presentation.dto.GetDailyFeedbacksResponse;
import com.rouby.user.user.infrastructure.security.dto.SecurityUser;
import jakarta.validation.constraints.PastOrPresent;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -19,6 +25,7 @@
public class FeedbackController {

private final CreateFeedbackUsecase createFeedbackUsecase;
private final FeedbackReadService feedbackReadService;

@PreAuthorize("hasAnyRole('USER')")
@PostMapping
Expand All @@ -29,4 +36,15 @@ public ResponseEntity<Void> createDailyFeedback(
createFeedbackUsecase.requestFeedback(request.toCommand(user.getId()));
return ResponseEntity.accepted().build();
}

@PreAuthorize("hasAnyRole('USER')")
@GetMapping("/daily/{date}")
public ResponseEntity<GetDailyFeedbacksResponse> getDailyFeedback(
@AuthenticationPrincipal SecurityUser user,
@PathVariable @PastOrPresent LocalDate date) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

수고하셨습니다! @PastOrPresent는 잘 동작하는지 궁금합니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

잘 동작합니다. 관련 테스트 코드가 있습니다.


GetDailyFeedbacksResponse res = GetDailyFeedbacksResponse.from(
feedbackReadService.getDailyFeedbacks(user.getId(), date));
return ResponseEntity.ok(res);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.rouby.assistant.feedback.presentation.dto;

import com.rouby.assistant.feedback.application.dto.GetDailyFeedbacksInfo;
import com.rouby.assistant.feedback.application.dto.GetDailyFeedbacksInfo.GetFeedbackInfo;
import java.time.LocalDateTime;
import java.util.List;
import lombok.Builder;

public record GetDailyFeedbacksResponse(
List<GetDailyFeedbackResponse> feedbacks
) {

public static GetDailyFeedbacksResponse from(GetDailyFeedbacksInfo feedbacksInfo) {
return new GetDailyFeedbacksResponse(
feedbacksInfo.feedbacks().stream().map(GetDailyFeedbackResponse::from).toList());
}
Comment thread
HanaHww2 marked this conversation as resolved.

@Builder
record GetDailyFeedbackResponse(
int slot,
String userMood,
String userInput,
String feedbackContent,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {

public static GetDailyFeedbackResponse from(GetFeedbackInfo info) {

return GetDailyFeedbackResponse.builder()
.slot(info.slot())
.userMood(info.userMood())
.userInput(info.userInput())
.feedbackContent(info.feedbackContent())
.createdAt(info.createdAt())
.updatedAt(info.updatedAt())
.build();
}
}
}
6 changes: 5 additions & 1 deletion back/src/test/http/feedback.http
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
### 피드백 생성 요청
POST {{url}}/api/v1/assistants/feedbacks
content-type: application/json

{
"userInput": "오늘은 집중이 잘 안 되는 하루였던 거 같아. 빨리 집중해서 취업해야 되는데...!",
"userMood": "SOSO"
}
}

### 일간 피드백 리스트 요청
GET {{url}}/api/v1/assistants/feedbacks/daily/2025-10-22
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
package com.rouby.assistant.feedback.presentation;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.rouby.assistant.feedback.application.dto.GetDailyFeedbacksInfo;
import com.rouby.assistant.feedback.application.dto.GetDailyFeedbacksInfo.GetFeedbackInfo;
import com.rouby.assistant.feedback.fixture.CreateDailyFeedbackRequestFixture;
import com.rouby.common.security.WithMockCustomUser;
import com.rouby.common.support.ControllerTestSupport;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.stream.IntStream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.ResultActions;

Expand Down Expand Up @@ -79,4 +91,72 @@ void failCreateDailyFeedbackWithInvalidRequest() throws Exception {
getValidationErrorResponseFieldSnippet()
));
}

@WithMockCustomUser
@DisplayName("일간 피드백 리스트 요청 API: 성공 200")
@Test
void getDailyFeedbacks() throws Exception {

GetDailyFeedbacksInfo feedbacksInfo = new GetDailyFeedbacksInfo(
IntStream.range(0, 3)
.mapToObj(i -> GetFeedbackInfo.builder()
.slot(i + 1)
.userMood("SOSO")
.userInput("사용자 요청 입력값 " + (i + 1))
.feedbackContent("피드백 응답 데이터 " + (i + 1))
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build())
.toList());

when(feedbackReadService.getDailyFeedbacks(eq(1L), any(LocalDate.class)))
.thenReturn(feedbacksInfo);

// when
ResultActions resultActions = mockMvc.perform(
get("/api/v1/assistants/feedbacks/daily/{date}", LocalDate.now())
.header("Authorization", "Bearer {ACCESS_TOKEN}")
);

// then
resultActions.andExpect(status().isOk())
.andDo(print())
.andDo(document("get-daily-feedbacks-200",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
pathParameters(
parameterWithName("date").description("조회할 날짜 (YYYY-MM-DD)")
),
responseFields(
fieldWithPath("feedbacks[].slot").description("요청 횟수 (최대 3)"),
fieldWithPath("feedbacks[].userMood").description("사용자 기분 (TERRIBLE, BAD, SOSO, GOOD, EXCELLENT)"),
fieldWithPath("feedbacks[].userInput").description("사용자 입력값"),
fieldWithPath("feedbacks[].feedbackContent").description("피드백 내용"),
fieldWithPath("feedbacks[].createdAt").description("피드백 생성 요청 일시 (예: 2025-09-15T10:00:00)"),
fieldWithPath("feedbacks[].updatedAt").description("피드백 생성 일시 (예: 2025-09-15T10:00:00)")
)
));
}
Comment thread
HanaHww2 marked this conversation as resolved.

@WithMockCustomUser
@DisplayName("일간 피드백 리스트 요청 API: 잘못된 일자 요청으로 인한 실패 400")
@Test
void getDailyFeedbacksWithFutureDate_return400() throws Exception {

// when
ResultActions resultActions = mockMvc.perform(
get("/api/v1/assistants/feedbacks/daily/{date}", LocalDate.now().plusDays(1))
.header("Authorization", "Bearer {ACCESS_TOKEN}")
.header(HttpHeaders.ACCEPT_LANGUAGE, "ko-KR")
);

// then
resultActions.andExpect(status().isBadRequest())
.andDo(print())
.andDo(document("get-daily-feedbacks-invalid-date-400",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
getValidationErrorResponseFieldSnippet()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rouby.assistant.briefing.application.facade.BriefingFacade;
import com.rouby.assistant.briefing.presentation.BriefingController;
import com.rouby.assistant.feedback.application.service.FeedbackReadService;
import com.rouby.assistant.feedback.application.usecase.CreateFeedbackUsecase;
import com.rouby.assistant.feedback.presentation.FeedbackController;
import com.rouby.assistant.prompt.application.service.PromptWriteService;
Expand Down Expand Up @@ -92,6 +93,9 @@ public abstract class ControllerTestSupport {
@MockitoBean
protected BriefingFacade briefingFacade;

@MockitoBean
protected FeedbackReadService feedbackReadService;

@MockitoBean
protected CreateFeedbackUsecase createFeedbackUsecase;

Expand Down