Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
@@ -1,14 +1,14 @@
package com.kustacks.kuring.admin.adapter.in.web;

import com.kustacks.kuring.admin.adapter.in.web.dto.AdminAlertResponse;
import com.kustacks.kuring.admin.adapter.in.web.dto.AdminAlertListResponse;
import com.kustacks.kuring.admin.adapter.in.web.dto.AdminFeedbackListResponse;
import com.kustacks.kuring.admin.adapter.in.web.dto.AdminReportListResponse;
import com.kustacks.kuring.admin.application.port.in.AdminQueryUseCase;
import com.kustacks.kuring.admin.domain.AdminRole;
import com.kustacks.kuring.auth.authorization.AuthenticationPrincipal;
import com.kustacks.kuring.auth.context.Authentication;
import com.kustacks.kuring.auth.secured.Secured;
import com.kustacks.kuring.common.dto.BaseResponse;
import com.kustacks.kuring.report.application.port.in.dto.AdminReportsResult;
import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -27,10 +27,7 @@

import java.util.List;

import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.ALERT_SEARCH_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.AUTH_AUTHENTICATION_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.FEEDBACK_SEARCH_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.REPORT_SEARCH_SUCCESS;
import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.*;

@Tag(name = "Admin-Query", description = "관리자가 주체가 되는 정보 조회")
@Validated
Expand All @@ -45,36 +42,39 @@ public class AdminQueryApiV2 {
@SecurityRequirement(name = "JWT")
@Secured(AdminRole.ROLE_ROOT)
@GetMapping("/feedbacks")
public ResponseEntity<BaseResponse<List<AdminFeedbacksResult>>> getFeedbacks(
public ResponseEntity<BaseResponse<AdminFeedbackListResponse>> getFeedbacks(
@Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page,
@Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size
) {
List<AdminFeedbacksResult> feedbacks = adminQueryUseCase.lookupFeedbacks(page, size);
return ResponseEntity.ok().body(new BaseResponse<>(FEEDBACK_SEARCH_SUCCESS, feedbacks));
var pageResult = adminQueryUseCase.lookupFeedbacks(page, size);
AdminFeedbackListResponse response = AdminFeedbackListResponse.from(pageResult);
return ResponseEntity.ok().body(new BaseResponse<>(FEEDBACK_SEARCH_SUCCESS, response));
}

@Operation(summary = "예약 알림 조회", description = "어드민이 등록한 모든 예약 알림을 조회한다")
@SecurityRequirement(name = "JWT")
@Secured(AdminRole.ROLE_ROOT)
@GetMapping("/alerts")
public ResponseEntity<BaseResponse<List<AdminAlertResponse>>> getAlerts(
public ResponseEntity<BaseResponse<AdminAlertListResponse>> getAlerts(
@Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page,
@Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size
) {
List<AdminAlertResponse> alerts = adminQueryUseCase.lookupAlerts(page, size);
return ResponseEntity.ok().body(new BaseResponse<>(ALERT_SEARCH_SUCCESS, alerts));
var pageResult = adminQueryUseCase.lookupAlerts(page, size);
AdminAlertListResponse response = AdminAlertListResponse.from(pageResult);
return ResponseEntity.ok().body(new BaseResponse<>(ALERT_SEARCH_SUCCESS, response));
}

@Operation(summary = "신고 목록 조회", description = "사용자의 모든 신고 목록을 조회합니다")
@SecurityRequirement(name = "JWT")
@Secured(AdminRole.ROLE_ROOT)
@GetMapping("/reports")
public ResponseEntity<BaseResponse<List<AdminReportsResult>>> getReports(
public ResponseEntity<BaseResponse<AdminReportListResponse>> getReports(
@Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page,
@Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size
) {
List<AdminReportsResult> result = adminQueryUseCase.lookupReports(page, size);
return ResponseEntity.ok().body(new BaseResponse<>(REPORT_SEARCH_SUCCESS, result));
var pageResult = adminQueryUseCase.lookupReports(page, size);
AdminReportListResponse response = AdminReportListResponse.from(pageResult);
return ResponseEntity.ok().body(new BaseResponse<>(REPORT_SEARCH_SUCCESS, response));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.kustacks.kuring.admin.adapter.in.web.dto;

import org.springframework.data.domain.Page;

import java.util.List;

public record AdminAlertListResponse(
List<AdminAlertResponse> alerts,
boolean hasNext,
long totalElements,
int totalPages
) {

public static AdminAlertListResponse from(Page<AdminAlertResponse> page) {
return new AdminAlertListResponse(
page.getContent(),
page.hasNext(),
page.getTotalElements(),
page.getTotalPages()
);
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.kustacks.kuring.admin.adapter.in.web.dto;

import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult;
import org.springframework.data.domain.Page;

import java.util.List;

public record AdminFeedbackListResponse(
List<AdminFeedbacksResult> feedbacks,
boolean hasNext,
long totalElements,
int totalPages
) {

public static AdminFeedbackListResponse from(Page<AdminFeedbacksResult> page) {
return new AdminFeedbackListResponse(
page.getContent(),
page.hasNext(),
page.getTotalElements(),
page.getTotalPages()
);
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.kustacks.kuring.admin.adapter.in.web.dto;

import com.kustacks.kuring.report.application.port.in.dto.AdminReportsResult;
import org.springframework.data.domain.Page;

import java.util.List;

public record AdminReportListResponse(
List<AdminReportsResult> reports,
boolean hasNext,
long totalElements,
int totalPages
) {

public static AdminReportListResponse from(Page<AdminReportsResult> page) {
return new AdminReportListResponse(
page.getContent(),
page.hasNext(),
page.getTotalElements(),
page.getTotalPages()
);
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import com.kustacks.kuring.admin.adapter.in.web.dto.AdminAlertResponse;
import com.kustacks.kuring.report.application.port.in.dto.AdminReportsResult;
import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult;
import org.springframework.data.domain.Page;

import java.util.List;

public interface AdminQueryUseCase {
List<AdminFeedbacksResult> lookupFeedbacks(int page, int size);

List<AdminAlertResponse> lookupAlerts(int page, int size);
Page<AdminFeedbacksResult> lookupFeedbacks(int page, int size);

List<AdminReportsResult> lookupReports(int page, int size);
Page<AdminAlertResponse> lookupAlerts(int page, int size);

Page<AdminReportsResult> lookupReports(int page, int size);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.kustacks.kuring.admin.application.port.out;

import com.kustacks.kuring.user.application.port.out.dto.FeedbackDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface AdminUserFeedbackPort {
List<String> findAllToken();
List<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable);

Page<FeedbackDto> findAllFeedbackByPageRequest(Pageable pageable);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Port 시그니처 변경으로 인한 하위 구현/호출부 전파 확인 필요

List→Page로 변경은 어댑터 구현체, 서비스, 컨트롤러, 응답 DTO 래핑(Page DTO)까지 연쇄 영향입니다. 전파 누락 시 런타임 에러 발생합니다.

다음 스크립트로 구현체/호출부의 시그니처 및 사용처를 점검해 주세요:


🏁 Script executed:

#!/bin/bash
# AdminUserFeedbackPort 구현 및 사용처 점검
set -euo pipefail

echo "== Port 구현 클래스 =="
rg -nP --type=java -C2 '\bimplements\s+AdminUserFeedbackPort\b'

echo -e "\n== 메소드 선언/호출부 =="
rg -nP --type=java -C2 '\bfindAllFeedbackByPageRequest\s*\('

echo -e "\n== 컨트롤러/응답 DTO에서 Page 래핑 여부(예: PageDto, PageResponse 등) =="
rg -nP --type=java -C2 '\bPage(<?)|PageDto|PageResponse|toPage|of\(.*Page<'

echo -e "\n== Admin API 문서/스웨거 스펙 반영 여부 =="
rg -nP --type=java -C2 '(@Operation|@ApiResponse|@Schema).*feedback' || true

Length of output: 17


추가 검증을 위해 다음 스크립트를 실행해 구현체, 호출부, Page 래핑 및 Swagger 스펙 반영 여부를 다시 확인해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Interface 위치 검색 =="
rg -n 'interface\s+AdminUserFeedbackPort'

echo -e "\n== AdminUserFeedbackPort 전체 참조 검색 =="
rg -n 'AdminUserFeedbackPort'

echo -e "\n== 메소드 선언 및 사용 검색 =="
rg -n 'findAllFeedbackByPageRequest'

echo -e "\n== implements 키워드로 구현체 검색 =="
rg -n 'implements.*AdminUserFeedbackPort'

echo -e "\n== 컨트롤러/서비스에서 Page<> 래핑 확인 =="
rg -n 'Page<.*>' -C1 src/main/java/com/kustacks/kuring

echo -e "\n== Swagger 어노테이션에서 feedback 반영 여부 =="
rg -n '(@Operation|@ApiResponse|@Schema).*feedback'

Length of output: 9830


확인 완료 — Port 시그니처(Page) 변경이 전파됨

AdminUserFeedbackPort, 구현체 및 호출부에서 Page 반환으로 정상 반영됨 (예: src/main/java/com/kustacks/kuring/admin/application/port/out/AdminUserFeedbackPort.java, src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserPersistenceAdapter.java, src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepository.java, src/main/java/com/kustacks/kuring/user/adapter/out/persistence/UserQueryRepositoryImpl.java, src/main/java/com/kustacks/kuring/admin/application/service/AdminQueryService.java, src/main/java/com/kustacks/kuring/admin/adapter/in/web/dto/AdminFeedbackListResponse.java).

문제: 테스트 코드에 아직 List로 할당한 사용처가 남아 있음 — src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java (List feedbackDtos = userPersistenceAdapter.findAllFeedbackByPageRequest(...))를 Page로 변경하거나 .getContent()로 추출하여 수정 필요.

🤖 Prompt for AI Agents
In
src/test/java/com/kustacks/kuring/user/adapter/out/persistence/UserRepositoryTest.java
around the line where List<FeedbackDto> feedbackDtos =
userPersistenceAdapter.findAllFeedbackByPageRequest(...); the test still assigns
the port result to a List while the port now returns a Page; update the test to
either (A) change the variable type to Page<FeedbackDto> and adjust assertions
to use page methods (e.g., getContent(), getTotalElements(), getTotalPages()) or
(B) call .getContent() on the returned Page to obtain a List for existing
assertions; also add the required import for
org.springframework.data.domain.Page if you choose option A and update any
size/contains assertions to reference the content or page metadata accordingly.

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
import com.kustacks.kuring.user.application.port.in.dto.AdminFeedbacksResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@UseCase
Expand All @@ -27,47 +27,41 @@ public class AdminQueryService implements AdminQueryUseCase {

@Transactional(readOnly = true)
@Override
public List<AdminFeedbacksResult> lookupFeedbacks(int page, int size) {
public Page<AdminFeedbacksResult> lookupFeedbacks(int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size);
return adminUserFeedbackPort.findAllFeedbackByPageRequest(pageRequest)
.stream()
.map(dto -> new AdminFeedbacksResult(
dto.getContents(),
dto.getUserId(),
dto.getCreatedAt()
))
.toList();
var pageResult = adminUserFeedbackPort.findAllFeedbackByPageRequest(pageRequest);
return pageResult.map(dto -> new AdminFeedbacksResult(
dto.getContents(),
dto.getUserId(),
dto.getCreatedAt()
));
}

@Override
public List<AdminAlertResponse> lookupAlerts(int page, int size) {
public Page<AdminAlertResponse> lookupAlerts(int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Order.desc("createdAt")));
return adminAlertQueryPort.findAllAlertByPageRequest(pageRequest)
.stream()
.map(alert -> AdminAlertResponse.of(
alert.getId(),
alert.getTitle(),
alert.getContent(),
alert.getStatus(),
alert.getAlertTime()
))
.toList();
var pageResult = adminAlertQueryPort.findAllAlertByPageRequest(pageRequest);
return pageResult.map(alert -> AdminAlertResponse.of(
alert.getId(),
alert.getTitle(),
alert.getContent(),
alert.getStatus(),
alert.getAlertTime()
));
}

@Override
public List<AdminReportsResult> lookupReports(int page, int size) {
public Page<AdminReportsResult> lookupReports(int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Order.desc("createdAt")));
return adminUserReportPort.findAllReportByPageRequest(pageRequest)
.stream()
.map(report -> AdminReportsResult.of(
report.getId(),
report.getTargetId(),
report.getUserId(),
report.getTargetType(),
report.getContent(),
report.getCreatedAt(),
report.getUpdatedAt()
))
.toList();
var pageResult = adminUserReportPort.findAllReportByPageRequest(pageRequest);
return pageResult.map(report -> AdminReportsResult.of(
report.getId(),
report.getTargetId(),
report.getReporterId(),
report.getTargetType(),
report.getContent(),
report.getCreatedAt(),
report.getUpdatedAt()
));
}
}
39 changes: 37 additions & 2 deletions src/main/java/com/kustacks/kuring/auth/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@
import com.kustacks.kuring.auth.interceptor.FirebaseTokenAuthenticationFilter;
import com.kustacks.kuring.auth.interceptor.UserRegisterNonChainingFilter;
import com.kustacks.kuring.auth.token.JwtTokenProvider;
import com.kustacks.kuring.message.application.port.in.FirebaseWithUserUseCase;
import com.kustacks.kuring.common.properties.ServerProperties;
import com.kustacks.kuring.message.application.port.in.FirebaseWithUserUseCase;
import com.kustacks.kuring.user.adapter.out.persistence.UserPersistenceAdapter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

Expand All @@ -34,6 +40,34 @@ public class AuthConfig implements WebMvcConfigurer {
private final FirebaseWithUserUseCase firebaseService;
private final UserPersistenceAdapter userPersistenceAdapter;

@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistration() {
CorsConfiguration config = new CorsConfiguration();

config.setAllowedOriginPatterns(List.of(
"https://www.ku-ring.com",
"https://ku-ring.com",
"http://localhost:[*]",
"http://127.0.0.1:[*]"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*")); // 모든 요청 헤더 허용

// [보안 권장] 모든 응답 헤더를 노출하는 대신 필요한 헤더만 명시적으로 노출합니다.
// 예를 들어, 프론트에서 Authorization 헤더에 담긴 토큰을 읽어야 할 경우 아래와 같이 설정합니다.
config.setExposedHeaders(List.of("Authorization", "Location"));

config.setAllowCredentials(true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

allowCredentials=true 재검토 (쿠키 미사용 시 보안 여지)

쿠키/세션을 사용하지 않고 Authorization 헤더만 사용한다면 setAllowCredentials(false)가 원칙적으로 더 안전합니다. 불필요한 크로스 사이트 쿠키 전송을 차단하세요.

다음 변경을 제안합니다:

-        config.setAllowCredentials(true);
+        // 쿠키 기반 인증이 필요한 경우에만 true
+        config.setAllowCredentials(false);
📝 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
config.setAllowCredentials(true);
// 쿠키 기반 인증이 필요한 경우에만 true
config.setAllowCredentials(false);
🤖 Prompt for AI Agents
In src/main/java/com/kustacks/kuring/auth/AuthConfig.java around line 60, the
CORS config currently calls config.setAllowCredentials(true) which is unsafe if
the app does not use cookies/sessions; change it to
config.setAllowCredentials(false). Update any client code/tests that rely on
sending credentials with cross-origin requests (remove credentials: 'include' or
adjust to same-origin), and ensure Access-Control-Allow-Credentials is no longer
exposed for cross-site requests.

config.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(new CorsFilter(source));
registration.setOrder(Ordered.HIGHEST_PRECEDENCE); // [수정] 필터 순서를 가장 높게 설정하여 다른 필터보다 먼저 실행되도록 합니다.
return registration;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SecurityContextPersistenceFilter());
Expand All @@ -56,7 +90,7 @@ adminDetailsService, passwordEncoder(), objectMapper,
}

@Override
public void addArgumentResolvers(List argumentResolvers) {
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}

Expand Down Expand Up @@ -84,4 +118,5 @@ AuthenticationSuccessHandler userRegisterSuccessHandler() {
AuthenticationFailureHandler userRegisterFailureHandler() {
return new UserRegisterFailureHandler(objectMapper);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public enum ResponseCodeAndMessages {

/* Academic Events */
ACADEMIC_EVENT_SEARCH_SUCCESS(HttpStatus.OK.value(), "학사일정 조회에 성공했습니다."),
ACADEMIC_EVENT_NOTIFICATION_UPDATE_SUCCESS(HttpStatus.OK.value(), "학사일정 알림 설정이 변경되었습니다."),

/**
* ErrorCodes about auth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public enum KuringFeatures {
UPDATE_KUIS_NOTICE(new Feature("update_kuis_notice")),
UPDATE_USER(new Feature("update_user")),
UPDATE_STAFF(new Feature("update_staff")),
UPDATE_ACADEMIC_EVENT(new Feature("update_academic_event"));
UPDATE_ACADEMIC_EVENT(new Feature("update_academic_event")),
NOTIFY_ACADEMIC_EVENT(new Feature("notify_academic_event"));

private final Feature feature;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ public ResponseEntity<BaseResponse<List<NoticeRangeLookupResponse>>> getNotices(
@Parameter(description = "공지 타입") @RequestParam(name = "type") String type,
@Parameter(description = "학과는 hostPrefix 로 전달") @RequestParam(name = "department", required = false) String department,
@Parameter(description = "중요도") @RequestParam(name = "important", defaultValue = "false") Boolean important,
@Parameter(description = "대학원 여부") @RequestParam(name = "graduated", required = false) Boolean graduated,
@Parameter(description = "페이지") @RequestParam(name = "page") @Min(0) int page,
@Parameter(description = "단일 페이지의 사이즈, 1 ~ 30까지 허용") @RequestParam(name = "size") @Min(1) @Max(30) int size
) {
NoticeRangeLookupCommand command = new NoticeRangeLookupCommand(type, department, important, page, size);
NoticeRangeLookupCommand command = new NoticeRangeLookupCommand(type, department, important, graduated, page, size);
List<NoticeRangeLookupResponse> searchResults = noticeQueryUseCase.getNotices(command)
Comment on lines 52 to 64
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

graduated 파라미터의 의미와 null 처리 계약을 명확히 해주세요.

nullable(Boolean)로 전달되어 하위 계층에서 null 의미(예: 전체, 필터 미적용 등)를 어떻게 해석하는지 명시/검증이 필요합니다. 스웨거 설명에도 기본 동작을 문서화하세요. 필요 시 Optional 또는 enum(ALL/UNDERGRAD/GRAD)로의 전환도 고려해볼 만합니다.

아래 스크립트로 graduated 처리 경로를 점검해 주세요(UseCase/Port/Repository에서 null 안전 여부 확인).


🏁 Script executed:

#!/bin/bash
rg -n --type=java -C2 '\bgraduated\b'

Length of output: 35847


graduated 파라미터 null 처리 — 긴급 수정 필요

Controller가 @RequestParam(required = false) Boolean graduated로 nullable을 허용하지만, 하위 계층에서 null 검사 없이 departmentNotice.graduated.eq(graduated)를 직접 사용하고 있습니다. graduated == null일 때 의도된 “필터 미적용(전체 조회)”이 아니라 쿼리 조건이 잘못 적용되어 결과가 누락될 위험이 있습니다.

수정(필수)

  • src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java
    • 메서드: findImportantArticleIdsByDepartment, findNormalArticleIdsByDepartment, findImportantNoticesByDepartment, findNormalNoticesByDepartmentWithOffset
    • 조치: QueryDSL where 절에서 departmentNotice.graduated.eq(graduated) 를 그대로 호출하지 말고 graduated != null 일 때만 predicate를 추가하거나 적절히 isNull()/isTrue()/isFalse()로 분기 처리.
  • src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java
    • 스웨거/파라미터 설명에 graduated의 의미(예: null = 전체/필터 미적용)를 명확히 문서화하거나 API 계약을 변경(기본값 설정/enum/Optional 사용)하여 ambiguity 제거.
  • src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java 및 port 계층
    • API 명세에 따라 파라미터 전달 경로 전체를 일관되게 검증/정리.

권장 대안(간단)

  • QueryDSL: BooleanExpression graduatedPredicate = graduated == null ? null : departmentNotice.graduated.eq(graduated); 를 사용해 null이면 predicate를 추가하지 않기.
  • 또는 API를 Optional 또는 enum(ALL/UNDERGRAD/GRAD)으로 변경해 명시적 계약을 만들기.
🤖 Prompt for AI Agents
In src/main/java/com/kustacks/kuring/notice/adapter/in/web/NoticeQueryApiV2.java
around lines 52-57 and in the repository/service layers, the nullable
@RequestParam Boolean graduated is passed into QueryDSL as
departmentNotice.graduated.eq(graduated) which breaks when graduated == null;
change the persistence predicates in
src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticeQueryRepositoryImpl.java
(methods: findImportantArticleIdsByDepartment, findNormalArticleIdsByDepartment,
findImportantNoticesByDepartment, findNormalNoticesByDepartmentWithOffset) to
only add a graduated predicate when graduated != null (e.g., build a
BooleanExpression that's null when graduated is null or branch to use
isTrue()/isFalse()), and update the API layer (NoticeQueryApiV2.java)
Swagger/param docs to state that null means "no filter / all", plus adjust
NoticeQueryService and port interfaces to propagate/validate this contract
consistently (or alternatively switch the API to Optional<Boolean> or an
explicit enum to remove ambiguity).

.stream()
.map(NoticeRangeLookupResponse::from)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ public record NoticeRangeLookupResponse(
String subject,
String category,
Boolean important,
Boolean graduated,
Long commentCount
){
) {
public static NoticeRangeLookupResponse from(NoticeRangeLookupResult result) {
return new NoticeRangeLookupResponse(
result.id(),
Expand All @@ -21,6 +22,7 @@ public static NoticeRangeLookupResponse from(NoticeRangeLookupResult result) {
result.subject(),
result.category(),
result.important(),
result.graduated(),
result.commentCount()
);
}
Expand Down
Loading
Loading