-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/이미지 관련 기능 구현 #87
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
The head ref may contain hidden characters: "feat/\uC774\uBBF8\uC9C0-\uAD00\uB828-\uAE30\uB2A5-\uAD6C\uD604"
Conversation
개요AWS S3 통합을 통한 이미지 업로드 기능, 진단 기반 추천을 그룹화하는 V2 진단 API, 카테고리 필터링과 진단 기반 추천을 포함한 확장된 홈 검색 기능, 그리고 검색 키워드 범위(scope) 관리를 추가합니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
participant Client
participant ImageApi
participant ImageService
participant S3PresignedUrlGenerator
participant S3Client
participant AWS S3
Client->>ImageApi: POST /v1/images/presigned-url<br/>PresignedUrlRequest
ImageApi->>ImageService: generatePresignedUrl(request, userId)
ImageService->>ImageService: validateContentType(contentType)
ImageService->>ImageService: extractAndValidateExtension(fileName)
ImageService->>ImageService: generateObjectKey(userId, extension)
ImageService->>S3PresignedUrlGenerator: generatePutPresignedUrl(objectKey, contentType)
S3PresignedUrlGenerator->>S3Client: getRegion()
S3PresignedUrlGenerator->>S3PresignedUrlGenerator: buildS3Presigner()
S3PresignedUrlGenerator->>AWS S3: generatePresignedPutObjectRequest()
AWS S3-->>S3PresignedUrlGenerator: presignedUrl
S3PresignedUrlGenerator->>S3PresignedUrlGenerator: getPublicUrl(objectKey)
S3PresignedUrlGenerator-->>ImageService: PresignedUrlResponse
ImageService-->>ImageApi: PresignedUrlResponse
ImageApi-->>Client: ApiResponse<PresignedUrlResponse><br/>presignedUrl, imageUrl, expiresIn
sequenceDiagram
participant Client
participant DiagnosisApi
participant DiagnosisService
participant EvaluationContext
participant DiagnosisResponseV2
participant DiagnosisRecommendationGroup
Client->>DiagnosisApi: POST /v2/diagnosis<br/>DiagnosisRequest
DiagnosisApi->>DiagnosisService: diagnoseV2(userId, request)
DiagnosisService->>DiagnosisService: loadUser(userId)
DiagnosisService->>DiagnosisService: createAndSave(Diagnosis)
DiagnosisService->>EvaluationContext: evaluateRules()
DiagnosisService->>DiagnosisResponseV2: from(EvaluationContext)
DiagnosisResponseV2->>DiagnosisResponseV2: getCurrentCandidates()
alt No Candidates
DiagnosisResponseV2-->>DiagnosisResponseV2: eligible=false, empty list
else Has Candidates
DiagnosisResponseV2->>DiagnosisResponseV2: groupByNoticeType()
DiagnosisResponseV2->>DiagnosisResponseV2: groupBySupplyType(deduped)
DiagnosisResponseV2->>DiagnosisRecommendationGroup: create(noticeType, supplyTypes)
DiagnosisRecommendationGroup-->>DiagnosisResponseV2: DiagnosisRecommendationGroup
DiagnosisResponseV2-->>DiagnosisResponseV2: eligible=true, recommended list
end
DiagnosisResponseV2-->>DiagnosisService: DiagnosisResponseV2
DiagnosisService-->>DiagnosisApi: DiagnosisResponseV2
DiagnosisApi-->>Client: ApiResponse<DiagnosisResponseV2>
sequenceDiagram
participant Client
participant HomeApi
participant HomeService
participant NoticeDocumentRepository
participant DiagnosisService
participant SliceResponse
Client->>HomeApi: GET /v1/homes/recommended-notices<br/>SliceRequest, userId
HomeApi->>HomeService: getRecommendedNoticesByDiagnosis(sliceRequest, userId)
HomeService->>DiagnosisService: getDiagnoseSummaryV2(userId)
DiagnosisService-->>HomeService: DiagnosisResponseV2
alt No Diagnosis Found
HomeService-->>HomeApi: 404 Error
else Diagnosis Exists
HomeService->>HomeService: extractSupplyTypes()
HomeService->>NoticeDocumentRepository: findRecommendedNoticesByDiagnosis(supplyTypes, pageable)
NoticeDocumentRepository-->>HomeService: Page<NoticeDocument>
HomeService->>HomeService: buildAddress(), mapToDto()
HomeService->>HomeService: likedIds(userId)
HomeService->>SliceResponse: wrapResponse(notices, hasNext)
SliceResponse-->>HomeService: HomeNoticeListResponse
HomeService-->>HomeApi: HomeNoticeListResponse
end
HomeApi-->>Client: ApiResponse<HomeNoticeListResponse>
예상 코드 리뷰 노력🎯 4 (복잡) | ⏱️ ~45분 시
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/com/pinHouse/server/platform/search/application/service/SearchKeywordService.java (1)
44-90:scopenull 방어 추가가 필요합니다.
외부 호출에서 null이 들어오면 null 스코프로 기록/조회되어 통계 누락이 발생할 수 있습니다. 기본값 처리 또는 명시적 예외가 필요합니다.🔧 제안 수정안
public void recordSearch(String keyword, SearchKeywordScope scope) { + if (scope == null) { + scope = SearchKeywordScope.GENERAL; + } if (keyword == null || keyword.trim().isEmpty()) { return; } @@ public List<PopularKeywordResponse> getPopularKeywords(int limit, SearchKeywordScope scope) { + if (scope == null) { + scope = SearchKeywordScope.GENERAL; + } Pageable pageable = PageRequest.of(0, limit); List<SearchKeyword> keywords = repository.findAllByScopeOrderByCountDescLastSearchedAtDesc(scope, pageable); return PopularKeywordResponse.from(keywords); }src/main/java/com/pinHouse/server/platform/diagnostic/diagnosis/presentation/DiagnosisApi.java (1)
17-79: 경로 기반 버전과 쿼리 파라미터 혼용으로 인한 매핑 오류 및 모호성 문제클래스 레벨 매핑이
/v1/diagnosis와/v2/diagnosis를 모두 포함하면서 메서드 수준에서는params="v=2"조건으로만 v2를 구분하는 구조는 다음과 같은 문제를 야기합니다:
- 의도하지 않은 라우팅:
POST /v2/diagnosis요청이v=2파라미터 없이 들어오면 v1 메서드(diagnosis())로 매칭되어 v1 응답을 반환합니다.- 모호한 매핑:
POST /v2/diagnosis?v=2요청은v=2파라미터 조건이 없는 v1 메서드와params="v=2"조건의 v2 메서드 모두와 매칭되어 Spring이 시작 시IllegalStateException을 발생시킵니다.권장: 경로 기반 버전(
/v1,/v2)을 선택하면 메서드별params조건을 제거하고 별도 컨트롤러로 분리하거나, 쿼리 파라미터 기반 버전(/diagnosis?v=1vs?v=2)으로 통일하는 것이 안전합니다.
🤖 Fix all issues with AI agents
In `@build.gradle`:
- Around line 52-55: Update the AWS SDK BOM dependency version used for S3/auth
from 'software.amazon.awssdk:bom:2.21.0' to at least 2.33.x to remediate known
CVEs; in the build.gradle block that declares implementation
platform('software.amazon.awssdk:bom:2.21.0') (alongside implementation
'software.amazon.awssdk:s3' and implementation 'software.amazon.awssdk:auth'),
bump the bom coordinate to a 2.33.x+ release and then re-run your dependency
vulnerability scanners (OWASP Dependency-Check, Snyk, Dependabot/GitHub) to
verify all transitive issues are resolved and compatible with Spring Boot 3.4.3.
In `@src/main/java/com/pinHouse/server/core/exception/code/ImageErrorCode.java`:
- Line 20: Remove the unused enum constant FILE_SIZE_EXCEEDED from
ImageErrorCode by deleting the entry FILE_SIZE_EXCEEDED(400_303,
HttpStatus.BAD_REQUEST, "파일 크기가 5MB를 초과합니다.") and any trailing comma/formatting
adjustments so the enum remains syntactically correct; ensure there are no
remaining references to ImageErrorCode.FILE_SIZE_EXCEEDED elsewhere (search for
FILE_SIZE_EXCEEDED) and run tests/compile to confirm the enum compiles after
removal.
In
`@src/main/java/com/pinHouse/server/platform/home/application/dto/HomeNoticeListResponse.java`:
- Around line 15-16: The Schema for the title field in HomeNoticeListResponse
should be marked nullable because HomeService.getDeadlineApproachingNotices()
sets title to null; update the `@Schema` annotation on the title field (in class
HomeNoticeListResponse) to include nullable = true so the API contract reflects
that the field can be null.
In
`@src/main/java/com/pinHouse/server/platform/home/application/dto/HomeSearchCategoryType.java`:
- Around line 21-29: The static factory HomeSearchCategoryType.from currently
calls equalsIgnoreCase on the input and can NPE for null/blank inputs; add a
guard at the start of HomeSearchCategoryType.from(String value) that treats null
or blank (e.g., value == null || value.trim().isEmpty() or use
StringUtils.isBlank) as invalid and immediately throw new
CustomException(CommonErrorCode.BAD_PARAMETER); leave the existing loop and
error throw for non-matching values unchanged.
In
`@src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java`:
- Around line 371-405: The current logic in HomeService checks
availableRentalTypes for the sentinel "해당 없음" and returns an empty
HomeNoticeListResponse even if other valid types exist; instead, remove the
contains("해당 없음") early-return and filter out that sentinel when building
targetSupplyTypes (use availableRentalTypes.stream().filter(...).filter(t ->
!t.equals("해당 없음"))...) so valid types are preserved; then keep the existing
check that returns an empty response only if the resulting targetSupplyTypes is
empty, updating log messages if needed to reference the filtered result.
- Around line 118-124: The response currently sets
HomeNoticeListResponse.builder().region to only county, losing higher-level
region info; update the code in HomeService (the HomeNoticeListResponse builder
call) to set region to a combined value such as (region + (county != null &&
!county.isEmpty() ? " " + county : "")) or equivalent null/empty-safe logic so
that when county exists the field contains "region county" and when county is
absent it contains just region; ensure you reference the existing variables used
in this method (region and county) and replace the single county assignment with
the combined, trimmed string.
In
`@src/main/java/com/pinHouse/server/platform/housing/complex/domain/repository/CustomComplexDocumentRepositoryImpl.java`:
- Around line 114-127: The searchByName method currently inserts the raw keyword
into Criteria.where("name").regex(...) causing regex injection; escape the
keyword before building the regex (e.g. use
java.util.regex.Pattern.quote(keyword) or a shared RegexUtils.escapeLiteral) and
then call Criteria.where("name").regex(escapedKeyword, "i"); also handle
null/empty keyword (return empty Slice or match-all as intended) and keep the
rest of the logic (query, pagination, mongoTemplate.find, SliceImpl) unchanged
so ComplexDocument results are safe from special-character/DoS injection.
In
`@src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java`:
- Around line 211-233: The method findRecommendedNoticesByDiagnosis in
NoticeDocumentRepositoryImpl currently removes the supplyType filter when
supplyTypes is null/empty and ends up scanning the whole collection; change it
to defensively return an empty Page (new PageImpl<>(Collections.emptyList(),
pageable, 0)) when supplyTypes is null or empty to avoid full collection scans
and unexpected results from other callers; keep the existing behavior when
supplyTypes is non-empty (apply criteria.and("supplyType").in(supplyTypes),
build the Query, run mongoTemplate.find and count) but short-circuit early for
null/empty supplyTypes.
- Around line 235-317: The code currently injects raw user input into
Criteria.regex in methods searchTargetGroups, searchRegions, searchHouseTypes,
searchNoticesByTargetGroup, searchNoticesByRegion and searchNoticesByHouseType
which allows regex metacharacters and ReDoS; fix by escaping the keyword (e.g.,
wrap with Pattern.quote(keyword) or a small helper escapeRegex(String)) before
passing to .regex(..., "i"), centralize the escaping in a private method (e.g.,
escapeForRegex) and call it from each of the six methods so all
Criteria.where(...).regex(...) calls use the escaped value; optionally consider
switching to a text-index based search if you need full-text semantics instead
of literal matching.
In
`@src/main/java/com/pinHouse/server/platform/image/application/service/ImageService.java`:
- Around line 68-75: In validateContentType, add a null check for the
contentType parameter and treat null or blank values as invalid (log a warning
and throw CustomException(ImageErrorCode.INVALID_FILE_TYPE)), and remove the
non-standard "image/jpg" entry from the allowed list so only standard MIME types
(e.g., "image/jpeg", "image/png", "image/gif") are checked; ensure you call
contentType.toLowerCase(Locale.ROOT) (or equivalent) only after verifying
non-null to avoid NullPointerException and update the log message to include the
original (possibly null) value for debugging.
🧹 Nitpick comments (14)
src/main/java/com/pinHouse/server/platform/housing/complex/domain/repository/CustomComplexDocumentRepository.java (1)
22-30: LGTM! 메서드 선언이 적절합니다.무한 스크롤을 위한
Slice반환 타입과Pageable파라미터 사용이 Spring Data 패턴에 부합합니다. Javadoc도 명확하게 작성되어 있습니다.다만, 완전한 클래스 경로(fully qualified name) 대신 import문을 사용하면 가독성이 향상됩니다:
♻️ 선택적 개선안
+import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + public interface CustomComplexDocumentRepository { // ... - org.springframework.data.domain.Slice<ComplexDocument> searchByName(String keyword, org.springframework.data.domain.Pageable pageable); + Slice<ComplexDocument> searchByName(String keyword, Pageable pageable); }src/main/java/com/pinHouse/server/platform/housing/complex/domain/repository/CustomComplexDocumentRepositoryImpl.java (1)
115-126: import문 사용으로 가독성 개선완전한 클래스 경로 대신 import문을 사용하면 코드가 더 깔끔해집니다.
♻️ 개선안
파일 상단에 import 추가:
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl;메서드 시그니처 및 반환문 수정:
`@Override` -public org.springframework.data.domain.Slice<ComplexDocument> searchByName(String keyword, org.springframework.data.domain.Pageable pageable) { +public Slice<ComplexDocument> searchByName(String keyword, Pageable pageable) { // ... - return new org.springframework.data.domain.SliceImpl<>(content, pageable, hasNext); + return new SliceImpl<>(content, pageable, hasNext); }src/main/java/com/pinHouse/server/platform/image/application/dto/PresignedUrlRequest.java (2)
17-20: contentType 정규식에서 'jpg' MIME 타입 검토 필요
image/jpg는 비표준 MIME 타입입니다. 표준 MIME 타입은image/jpeg입니다. 대부분의 브라우저와 도구에서image/jpeg만 사용하므로,jpg를 제거하는 것을 권장합니다.♻️ 수정 제안
- `@Pattern`(regexp = "^image/(jpeg|jpg|png|gif)$", message = "지원하지 않는 이미지 형식입니다") + `@Pattern`(regexp = "^image/(jpeg|png|gif)$", message = "지원하지 않는 이미지 형식입니다")
13-15: fileName에 대한 보안 검증 추가 권장현재
fileName은 빈 값 검사만 수행합니다. Path traversal 공격(예:../../../etc/passwd) 방지와 URL-safe 문자만 허용하기 위해 패턴 검증을 추가하는 것을 권장합니다.♻️ 수정 제안
`@NotBlank`(message = "파일명은 필수 입력값입니다") + `@Pattern`(regexp = "^[a-zA-Z0-9가-힣._-]+\\.(jpg|jpeg|png|gif)$", message = "유효하지 않은 파일명입니다") `@Schema`(description = "업로드할 파일명", example = "profile.jpg", required = true) String fileName,src/main/java/com/pinHouse/server/core/config/S3Config.java (1)
20-24: 프로덕션 환경에서의 자격 증명 관리 방식 검토 필요하드코딩된 AWS 자격 증명(access-key, secret-key)을 사용하는 것은 보안상 권장되지 않습니다. 프로덕션 환경에서는 IAM 역할, EC2 인스턴스 프로필, 또는
DefaultCredentialsProvider를 사용하는 것이 좋습니다.♻️ 프로덕션 환경용 수정 제안
`@Bean` public S3Client s3Client() { - AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); - return S3Client.builder() .region(Region.of(region)) - .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .credentialsProvider(DefaultCredentialsProvider.create()) .build(); }로컬/개발 환경용이라면 현재 구현도 괜찮으나, 환경별 프로파일 분리를 고려해 주세요.
src/main/java/com/pinHouse/server/platform/image/external/S3PresignedUrlGenerator.java (1)
74-80: Object Key URL 인코딩 누락
objectKey에 특수문자나 공백이 포함될 경우 URL이 깨질 수 있습니다. 현재 구현에서는 UUID와 확장자만 사용하므로 큰 문제는 없지만, 방어적으로 인코딩을 추가하는 것이 좋습니다.♻️ 제안된 개선
+import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + `@Override` public String getPublicUrl(String objectKey) { + String encodedKey = URLEncoder.encode(objectKey, StandardCharsets.UTF_8) + .replace("%2F", "/"); // 경로 구분자는 유지 return String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, s3Client.serviceClientConfiguration().region().id(), - objectKey); + encodedKey); }src/main/java/com/pinHouse/server/platform/home/application/dto/HomeSearchSectionResponse.java (1)
19-23: content 리스트 방어적 복사 고려record는 불변이지만
List는 외부에서 수정될 수 있습니다.of(...)에서List.copyOf로 감싸면 응답 불변성이 강화됩니다. (null 허용이면 별도 처리 필요)♻️ 제안 변경
public static <T> HomeSearchSectionResponse<T> of(List<T> content, boolean hasNext) { return HomeSearchSectionResponse.<T>builder() - .content(content) + .content(List.copyOf(content)) .hasNext(hasNext) .build(); }src/main/java/com/pinHouse/server/platform/diagnostic/diagnosis/application/dto/DiagnosisRecommendationGroup.java (1)
19-23: supplyTypes 리스트 방어적 복사 고려응답 DTO의 리스트가 외부에서 변형될 수 있어
List.copyOf로 불변성 강화가 가능합니다.♻️ 제안 변경
public static DiagnosisRecommendationGroup of(String noticeType, List<String> supplyTypes) { return DiagnosisRecommendationGroup.builder() .noticeType(noticeType) - .supplyTypes(supplyTypes) + .supplyTypes(List.copyOf(supplyTypes)) .build(); }src/main/java/com/pinHouse/server/platform/diagnostic/diagnosis/application/dto/DiagnosisResponseV2.java (1)
38-51: 중복 제거를 Set으로 단순화하면 성능/가독성이 좋아집니다현재
List.contains로 중복 체크가 반복되어 후보 수가 커지면 O(n^2)로 커질 수 있습니다.LinkedHashSet으로 순서를 유지하면서 중복 제거를 단순화하는 쪽을 추천합니다.♻️ 제안 변경
-import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ - Map<String, List<String>> grouped = new LinkedHashMap<>(); + Map<String, LinkedHashSet<String>> grouped = new LinkedHashMap<>(); @@ - grouped.computeIfAbsent(noticeType, k -> new ArrayList<>()); - if (!grouped.get(noticeType).contains(supplyType)) { - grouped.get(noticeType).add(supplyType); - } + grouped.computeIfAbsent(noticeType, k -> new LinkedHashSet<>()) + .add(supplyType); } List<DiagnosisRecommendationGroup> groups = grouped.entrySet().stream() - .map(e -> DiagnosisRecommendationGroup.of(e.getKey(), e.getValue())) + .map(e -> DiagnosisRecommendationGroup.of(e.getKey(), List.copyOf(e.getValue()))) .toList();src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryCustom.java (1)
67-108: 공급유형 목록이 비어 있을 때의 동작을 계약으로 명시해주세요.
supplyTypes가 null/empty일 때 전체 공고가 반환될 여지가 있으니(impl 기준), 빈 입력이면 빈 결과를 반환한다는 계약을 Javadoc에 명시하거나 구현에서 가드 처리하는 편이 안전합니다.src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java (1)
320-330: 정렬이 없으면 Slice 페이지가 불안정할 수 있습니다.
findNoticeSlice가pageable정렬에만 의존합니다. 호출부에서 정렬이 비어 있으면 Mongo 자연 순서로 결과가 흔들릴 수 있으니 기본 정렬을 주는 편이 안전합니다.♻️ 제안 변경
private org.springframework.data.domain.Slice<NoticeDocument> findNoticeSlice(Criteria criteria, Pageable pageable) { Query query = new Query(criteria).with(pageable); + if (pageable.getSort().isUnsorted()) { + query.with(Sort.by(Sort.Order.asc("noticeId"))); + } int limit = pageable.getPageSize(); query.limit(limit + 1);src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java (3)
196-243: 빈 검색어는 인기검색어에 기록하지 않도록 가드 권장.공백/빈 문자열이 인기검색어에 누적되면 품질이 떨어질 수 있습니다. 기록 전에 간단한 가드를 두는 편이 안전합니다(카테고리 검색에도 동일 적용 권장).
♻️ 제안 변경
- searchKeywordService.recordSearch(keyword, SearchKeywordScope.HOME); + if (keyword != null && !keyword.isBlank()) { + searchKeywordService.recordSearch(keyword, SearchKeywordScope.HOME); + }
292-295: limit 파라미터 상한/하한 클램핑을 권장합니다.비정상적으로 큰 limit가 들어오면 부하가 증가할 수 있으니 적절한 상·하한을 두는 편이 안전합니다.
♻️ 제안 변경
`@Override` public List<PopularKeywordResponse> getHomePopularKeywords(int limit) { - return searchKeywordService.getPopularKeywords(limit, SearchKeywordScope.HOME); + int capped = Math.max(1, Math.min(limit, 50)); + return searchKeywordService.getPopularKeywords(capped, SearchKeywordScope.HOME); }
245-290: page 파라미터에@Min(1) 검증 추가 권장현재 코드에서 page 값이 1 미만이면
PageRequest.of(page - 1, ...)에서IllegalArgumentException이 발생합니다. 전역 예외 핸들러에서 이를 HTTP 400으로 처리하고 있지만, 컨트롤러에서@Min(value = 1) 검증 annotation을 추가하면 더 명확하고 안전합니다. 코드베이스에 이미@Min검증 패턴이 있으므로 일관성 있게 적용해주세요.
| // AWS SDK v2 for S3 | ||
| implementation platform('software.amazon.awssdk:bom:2.21.0') | ||
| implementation 'software.amazon.awssdk:s3' | ||
| implementation 'software.amazon.awssdk:auth' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
latest AWS SDK for Java v2 BOM version 2024 2025
💡 Result:
Do you mean (A) the current/latest AWS SDK for Java v2 BOM version as of today (Jan 18, 2026), or (B) the latest BOM released in 2024 and the latest released in 2025?
🌐 Web query:
AWS SDK v2 2.21.0 security CVE vulnerabilities advisories
💡 Result:
Summary — AWS SDK for Java (v2) 2.21.0: known security advisories affecting that release
-
Maven listings for multiple 2.21.0 artifacts (s3, sts, sdk-core, apache-client, auth) show known CVEs in dependencies that affect that BOM/artifact set, including (as reported on Maven Repository): CVE-2024-47554, CVE-2023-41329, CVE-2025-48924 and others. See artifact pages for details per module. [1][2][3]
-
There is an open GitHub issue noting Netty dependency CVE-2023-44487 (HTTP/2 “rapid reset”) and discussion about upgrading Netty in aws-sdk-java-v2 releases; users should check whether your 2.21.0 runtime pulls an affected Netty version. [4]
Recommendation
- Upgrade to a later aws-sdk-java-v2 release (2.33.x / 2.40.x+ as shown on Maven listings) or the latest patched version; verify updated dependency versions (Netty, etc.) in the newer release and retest. [1][2]
- Run your dependency-vulnerability scanner (OWASP Dependency-Check / Snyk / GitHub Dependabot) against your built artifact to get the exact transitive CVE list for your environment.
Sources
- software.amazon.awssdk:s3 2.21.0 (Maven Repository) — vulnerability list and linked CVEs. [https://mvnrepository.com/artifact/software.amazon.awssdk/s3/2.21.0].
- software.amazon.awssdk:sts & sdk-core 2.21.0 (Maven Repository) — vulnerability listings. [https://mvnrepository.com/artifact/software.amazon.awssdk/sts/2.21.0] [https://mvnrepository.com/artifact/software.amazon.awssdk/sdk-core/2.21.0].
- software.amazon.awssdk:apache-client & auth 2.21.0 (Maven Repository) — dependency CVE listings. [https://mvnrepository.com/artifact/software.amazon.awssdk/apache-client/2.21.0] [https://mvnrepository.com/artifact/software.amazon.awssdk/auth/2.21.0].
- GitHub issue re: Netty dependency / CVE-2023-44487 on aws-sdk-java-v2. [https://github.com/Dependency of netty has an vulnerability CVE-2023-44487 'rapid reset' for HTTP/2 aws/aws-sdk-java-v2#4619].
If you want, I can:
- produce the exact transitive CVE list for your project by scanning your pom/gradle coordinates, or
- look up the fixed versions for each CVE and suggest minimal upgrades. Which would you prefer?
🌐 Web query:
AWS SDK v2 compatibility Spring Boot 3.4.3
💡 Result:
Short answer: Yes — AWS SDK for Java v2 is compatible with Spring Boot 3.4.3. Spring Cloud AWS 3.x is built on AWS SDK v2 and targets Spring Boot 3.x, so using SDK v2 (or Spring Cloud AWS 3.x which depends on it) is the recommended approach. [1][2]
Notes / pointers:
- Spring Cloud AWS 3.x is explicitly built on AWS SDK v2. Use the 3.x awspring artifacts when integrating with Spring Boot 3.4.3. [2][3]
- If you’re migrating from SDK v1, AWS provides a migration tool (OpenRewrite-based) to help convert code to SDK v2. [4]
Sources:
- Spring blog — Announcing Spring Cloud AWS 3.0.0 (built on AWS SDK v2). [2]
- AWSpring (spring-cloud-aws) GitHub compatibility table showing Spring Cloud AWS 3.x -> Spring Boot 3.x and use of SDK v2. [1]
- Spring Cloud AWS project page / compatibility notes. [3]
- AWS Developer Blog — Migration tool for AWS SDK for Java 2.x. [4]
AWS SDK BOM 버전 2.21.0은 여러 보안 취약점이 있어 즉시 업그레이드가 필요합니다.
버전 2.21.0은 다음을 포함하여 여러 알려진 CVE의 영향을 받습니다: CVE-2024-47554, CVE-2023-41329, CVE-2025-48924, CVE-2023-44487 (Netty HTTP/2). 버전 2.33.x 이상으로 업그레이드한 후 의존성 취약점 스캐너(OWASP Dependency-Check, Snyk, GitHub Dependabot)로 재검증하세요. Spring Boot 3.4.3과의 호환성은 문제없습니다.
🤖 Prompt for AI Agents
In `@build.gradle` around lines 52 - 55, Update the AWS SDK BOM dependency version
used for S3/auth from 'software.amazon.awssdk:bom:2.21.0' to at least 2.33.x to
remediate known CVEs; in the build.gradle block that declares implementation
platform('software.amazon.awssdk:bom:2.21.0') (alongside implementation
'software.amazon.awssdk:s3' and implementation 'software.amazon.awssdk:auth'),
bump the bom coordinate to a 2.33.x+ release and then re-run your dependency
vulnerability scanners (OWASP Dependency-Check, Snyk, Dependabot/GitHub) to
verify all transitive issues are resolved and compatible with Spring Boot 3.4.3.
| // ======================== | ||
| INVALID_FILE_TYPE(400_301, HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 형식입니다. (jpg, jpeg, png, gif만 가능)"), | ||
| INVALID_FILE_EXTENSION(400_302, HttpStatus.BAD_REQUEST, "파일 확장자가 유효하지 않습니다."), | ||
| FILE_SIZE_EXCEEDED(400_303, HttpStatus.BAD_REQUEST, "파일 크기가 5MB를 초과합니다."), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "FILE_SIZE_EXCEEDED" --type javaRepository: PinHouse/PinHouse_BE
Length of output: 219
🏁 Script executed:
rg -n "400_303" --type javaRepository: PinHouse/PinHouse_BE
Length of output: 219
🏁 Script executed:
rg -n "FILE_SIZE|파일 크기" --type java -iRepository: PinHouse/PinHouse_BE
Length of output: 379
FILE_SIZE_EXCEEDED 에러 코드를 제거하세요
FILE_SIZE_EXCEEDED는 정의되었지만 코드베이스에서 사용되지 않는 dead code입니다. 스웨거 문서에도 "파일 크기 전달은 필요 없으며"라고 명시되어 있어 파일 크기 유효성 검사가 제거되었음을 확인할 수 있습니다. 이 미사용 에러 코드는 제거되어야 합니다.
🤖 Prompt for AI Agents
In `@src/main/java/com/pinHouse/server/core/exception/code/ImageErrorCode.java` at
line 20, Remove the unused enum constant FILE_SIZE_EXCEEDED from ImageErrorCode
by deleting the entry FILE_SIZE_EXCEEDED(400_303, HttpStatus.BAD_REQUEST, "파일
크기가 5MB를 초과합니다.") and any trailing comma/formatting adjustments so the enum
remains syntactically correct; ensure there are no remaining references to
ImageErrorCode.FILE_SIZE_EXCEEDED elsewhere (search for FILE_SIZE_EXCEEDED) and
run tests/compile to confirm the enum compiles after removal.
| @Schema(description = "목록 설명/출처", example = "진단 기반 추천") | ||
| String title, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, find and read the HomeNoticeListResponse.java file
fd -t f "HomeNoticeListResponse.java" | head -5Repository: PinHouse/PinHouse_BE
Length of output: 155
🏁 Script executed:
# Search for the file more broadly
find . -name "HomeNoticeListResponse.java" -type fRepository: PinHouse/PinHouse_BE
Length of output: 157
🏁 Script executed:
# List files in the home/application/dto directory
fd -t f . "src/main/java/com/pinHouse/server/platform/home/application/dto"Repository: PinHouse/PinHouse_BE
Length of output: 803
🏁 Script executed:
# Read the HomeNoticeListResponse.java file
cat -n src/main/java/com/pinHouse/server/platform/home/application/dto/HomeNoticeListResponse.javaRepository: PinHouse/PinHouse_BE
Length of output: 1035
🏁 Script executed:
# Search for where HomeNoticeListResponse is instantiated or built
rg "HomeNoticeListResponse" --type java -A 3 -B 1Repository: PinHouse/PinHouse_BE
Length of output: 13096
🏁 Script executed:
# Look at the HomeService implementation more carefully
rg -A 20 "getDeadlineApproachingNotices" src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java | head -50Repository: PinHouse/PinHouse_BE
Length of output: 964
🏁 Script executed:
# Also check the getRecommendedNoticesByDiagnosis method
rg -A 30 "getRecommendedNoticesByDiagnosis" src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java | grep -A 10 "HomeNoticeListResponse.builder"Repository: PinHouse/PinHouse_BE
Length of output: 359
🏁 Script executed:
# Get the exact line where title is set to null in HomeService
rg -n "\.title\(null\)" src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.javaRepository: PinHouse/PinHouse_BE
Length of output: 92
🏁 Script executed:
# Also check all title assignments
rg -n "\.title\(" src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.javaRepository: PinHouse/PinHouse_BE
Length of output: 205
🏁 Script executed:
# Get the full context around line 120 where title is set to null
sed -n '110,130p' src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.javaRepository: PinHouse/PinHouse_BE
Length of output: 800
🏁 Script executed:
# Check if there are any other annotations or constraints on the title field in the DTO
cat -n src/main/java/com/pinHouse/server/platform/home/application/dto/HomeNoticeListResponse.java | sed -n '14,18p'Repository: PinHouse/PinHouse_BE
Length of output: 225
title 필드를 nullable로 명시 필요
HomeService.getDeadlineApproachingNotices()에서 title을 명시적으로 null로 설정하고 있으나, 현재 @Schema 어노테이션은 nullable 여부를 표시하지 않습니다. API 계약을 명확히 하기 위해 nullable = true를 추가하세요.
수정 사항
- `@Schema`(description = "목록 설명/출처", example = "진단 기반 추천")
+ `@Schema`(description = "목록 설명/출처", example = "진단 기반 추천", nullable = true)
String title,📝 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.
| @Schema(description = "목록 설명/출처", example = "진단 기반 추천") | |
| String title, | |
| `@Schema`(description = "목록 설명/출처", example = "진단 기반 추천", nullable = true) | |
| String title, |
🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/home/application/dto/HomeNoticeListResponse.java`
around lines 15 - 16, The Schema for the title field in HomeNoticeListResponse
should be marked nullable because HomeService.getDeadlineApproachingNotices()
sets title to null; update the `@Schema` annotation on the title field (in class
HomeNoticeListResponse) to include nullable = true so the API contract reflects
that the field can be null.
| @JsonCreator | ||
| public static HomeSearchCategoryType from(String value) { | ||
| for (HomeSearchCategoryType type : values()) { | ||
| if (type.value.equalsIgnoreCase(value)) { | ||
| return type; | ||
| } | ||
| } | ||
| throw new CustomException(CommonErrorCode.BAD_PARAMETER); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
null/blank 입력 시 NPE로 500 발생 가능.
equalsIgnoreCase는 null에 취약합니다. 잘못된 입력을 BAD_PARAMETER로 처리하도록 가드가 필요합니다.
🛠️ 제안 수정안
`@JsonCreator`
public static HomeSearchCategoryType from(String value) {
+ if (value == null || value.isBlank()) {
+ throw new CustomException(CommonErrorCode.BAD_PARAMETER);
+ }
for (HomeSearchCategoryType type : values()) {
if (type.value.equalsIgnoreCase(value)) {
return type;
}
}
throw new CustomException(CommonErrorCode.BAD_PARAMETER);
}🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/home/application/dto/HomeSearchCategoryType.java`
around lines 21 - 29, The static factory HomeSearchCategoryType.from currently
calls equalsIgnoreCase on the input and can NPE for null/blank inputs; add a
guard at the start of HomeSearchCategoryType.from(String value) that treats null
or blank (e.g., value == null || value.trim().isEmpty() or use
StringUtils.isBlank) as invalid and immediately throw new
CustomException(CommonErrorCode.BAD_PARAMETER); leave the existing loop and
error throw for non-matching values unchanged.
| return HomeNoticeListResponse.builder() | ||
| .region(county) | ||
| .content(content) | ||
| .hasNext(page.hasNext()) | ||
| .totalElements(page.getTotalElements()) | ||
| .build(); | ||
| .region(county) | ||
| .title(null) | ||
| .content(content) | ||
| .hasNext(page.hasNext()) | ||
| .totalElements(page.getTotalElements()) | ||
| .build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
응답 region에 county만 넣으면 지역 정보가 누락됩니다.
현재 region 값이 county로만 채워져 광역 단위 정보가 사라질 수 있습니다. region + county 조합(또는 county가 없을 때 region)을 사용하도록 조정하는 편이 안전합니다.
🐛 제안 변경
- return HomeNoticeListResponse.builder()
- .region(county)
+ String regionLabel = buildAddress(region.getFullName(), county);
+ return HomeNoticeListResponse.builder()
+ .region(regionLabel)
.title(null)
.content(content)
.hasNext(page.hasNext())
.totalElements(page.getTotalElements())
.build();🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java`
around lines 118 - 124, The response currently sets
HomeNoticeListResponse.builder().region to only county, losing higher-level
region info; update the code in HomeService (the HomeNoticeListResponse builder
call) to set region to a combined value such as (region + (county != null &&
!county.isEmpty() ? " " + county : "")) or equivalent null/empty-safe logic so
that when county exists the field contains "region county" and when county is
absent it contains just region; ensure you reference the existing variables used
in this method (region and county) and replace the single county assignment with
the combined, trimmed string.
| // 3. 추천 임대주택 유형 추출 | ||
| List<String> availableRentalTypes = diagnosis.availableRentalTypes(); | ||
|
|
||
| // 4. 자격 없는 경우 빈 응답 반환 | ||
| if (availableRentalTypes == null || | ||
| availableRentalTypes.isEmpty() || | ||
| availableRentalTypes.contains("해당 없음")) { | ||
| log.info("추천 가능한 임대주택이 없습니다 - userId={}", userId); | ||
| return HomeNoticeListResponse.builder() | ||
| .region(null) | ||
| .title("진단 기반 추천") | ||
| .content(List.of()) | ||
| .hasNext(false) | ||
| .totalElements(0L) | ||
| .build(); | ||
| } | ||
|
|
||
| // 5. 진단 결과 → 공고 supplyType 매핑 | ||
| List<String> targetSupplyTypes = availableRentalTypes.stream() | ||
| .filter(type -> type != null && !type.isBlank()) | ||
| .distinct() | ||
| .toList(); | ||
|
|
||
| // 진단 결과에 매핑될 공고 유형이 없는 경우 빈 응답 반환 | ||
| if (targetSupplyTypes.isEmpty()) { | ||
| log.info("진단 결과에 매핑 가능한 주택 유형이 없습니다 - userId={}", userId); | ||
| return HomeNoticeListResponse.builder() | ||
| .region(null) | ||
| .title("진단 기반 추천") | ||
| .content(List.of()) | ||
| .hasNext(false) | ||
| .totalElements(0L) | ||
| .build(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'해당 없음'이 섞여 있어도 다른 유형을 버리지 않도록 필터링 권장.
현재는 availableRentalTypes에 "해당 없음"이 포함되면 다른 유효 값이 있어도 즉시 빈 결과를 반환합니다. 유효 값은 유지하고 sentinel만 제거하는 편이 안전합니다.
🐛 제안 변경
- List<String> availableRentalTypes = diagnosis.availableRentalTypes();
-
- // 4. 자격 없는 경우 빈 응답 반환
- if (availableRentalTypes == null ||
- availableRentalTypes.isEmpty() ||
- availableRentalTypes.contains("해당 없음")) {
- log.info("추천 가능한 임대주택이 없습니다 - userId={}", userId);
- return HomeNoticeListResponse.builder()
- .region(null)
- .title("진단 기반 추천")
- .content(List.of())
- .hasNext(false)
- .totalElements(0L)
- .build();
- }
-
- // 5. 진단 결과 → 공고 supplyType 매핑
- List<String> targetSupplyTypes = availableRentalTypes.stream()
- .filter(type -> type != null && !type.isBlank())
- .distinct()
- .toList();
+ List<String> availableRentalTypes = diagnosis.availableRentalTypes();
+
+ // 4. 진단 결과 → 공고 supplyType 매핑 (sentinel 제거)
+ List<String> targetSupplyTypes = availableRentalTypes == null ? List.of()
+ : availableRentalTypes.stream()
+ .filter(type -> type != null && !type.isBlank() && !"해당 없음".equals(type))
+ .distinct()
+ .toList();
// 진단 결과에 매핑될 공고 유형이 없는 경우 빈 응답 반환
if (targetSupplyTypes.isEmpty()) {
log.info("진단 결과에 매핑 가능한 주택 유형이 없습니다 - userId={}", userId);
return HomeNoticeListResponse.builder()
.region(null)
.title("진단 기반 추천")
.content(List.of())
.hasNext(false)
.totalElements(0L)
.build();
}🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java`
around lines 371 - 405, The current logic in HomeService checks
availableRentalTypes for the sentinel "해당 없음" and returns an empty
HomeNoticeListResponse even if other valid types exist; instead, remove the
contains("해당 없음") early-return and filter out that sentinel when building
targetSupplyTypes (use availableRentalTypes.stream().filter(...).filter(t ->
!t.equals("해당 없음"))...) so valid types are preserved; then keep the existing
check that returns an empty response only if the resulting targetSupplyTypes is
empty, updating log messages if needed to reference the filtered result.
| @Override | ||
| public org.springframework.data.domain.Slice<ComplexDocument> searchByName(String keyword, org.springframework.data.domain.Pageable pageable) { | ||
| Criteria criteria = Criteria.where("name").regex(keyword, "i"); | ||
| Query query = new Query(criteria).with(pageable); | ||
|
|
||
| int limit = pageable.getPageSize(); | ||
| query.limit(limit + 1); | ||
|
|
||
| List<ComplexDocument> complexes = mongoTemplate.find(query, ComplexDocument.class); | ||
| boolean hasNext = complexes.size() > limit; | ||
| List<ComplexDocument> content = hasNext ? complexes.subList(0, limit) : complexes; | ||
|
|
||
| return new org.springframework.data.domain.SliceImpl<>(content, pageable, hasNext); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regex 인젝션 취약점: 키워드 이스케이프 필요
keyword가 정규식에 직접 사용되므로, 특수 문자(예: .*, (), [] 등)가 포함된 경우 예기치 않은 동작이나 PatternSyntaxException이 발생할 수 있습니다. 악의적인 입력 시 ReDoS 공격에도 취약할 수 있습니다.
🔒 권장 수정안: 정규식 특수 문자 이스케이프
`@Override`
public org.springframework.data.domain.Slice<ComplexDocument> searchByName(String keyword, org.springframework.data.domain.Pageable pageable) {
- Criteria criteria = Criteria.where("name").regex(keyword, "i");
+ String escapedKeyword = java.util.regex.Pattern.quote(keyword);
+ Criteria criteria = Criteria.where("name").regex(escapedKeyword, "i");
Query query = new Query(criteria).with(pageable);
// ...
}📝 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.
| @Override | |
| public org.springframework.data.domain.Slice<ComplexDocument> searchByName(String keyword, org.springframework.data.domain.Pageable pageable) { | |
| Criteria criteria = Criteria.where("name").regex(keyword, "i"); | |
| Query query = new Query(criteria).with(pageable); | |
| int limit = pageable.getPageSize(); | |
| query.limit(limit + 1); | |
| List<ComplexDocument> complexes = mongoTemplate.find(query, ComplexDocument.class); | |
| boolean hasNext = complexes.size() > limit; | |
| List<ComplexDocument> content = hasNext ? complexes.subList(0, limit) : complexes; | |
| return new org.springframework.data.domain.SliceImpl<>(content, pageable, hasNext); | |
| } | |
| `@Override` | |
| public org.springframework.data.domain.Slice<ComplexDocument> searchByName(String keyword, org.springframework.data.domain.Pageable pageable) { | |
| String escapedKeyword = java.util.regex.Pattern.quote(keyword); | |
| Criteria criteria = Criteria.where("name").regex(escapedKeyword, "i"); | |
| Query query = new Query(criteria).with(pageable); | |
| int limit = pageable.getPageSize(); | |
| query.limit(limit + 1); | |
| List<ComplexDocument> complexes = mongoTemplate.find(query, ComplexDocument.class); | |
| boolean hasNext = complexes.size() > limit; | |
| List<ComplexDocument> content = hasNext ? complexes.subList(0, limit) : complexes; | |
| return new org.springframework.data.domain.SliceImpl<>(content, pageable, hasNext); | |
| } |
🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/housing/complex/domain/repository/CustomComplexDocumentRepositoryImpl.java`
around lines 114 - 127, The searchByName method currently inserts the raw
keyword into Criteria.where("name").regex(...) causing regex injection; escape
the keyword before building the regex (e.g. use
java.util.regex.Pattern.quote(keyword) or a shared RegexUtils.escapeLiteral) and
then call Criteria.where("name").regex(escapedKeyword, "i"); also handle
null/empty keyword (return empty Slice or match-all as intended) and keep the
rest of the logic (query, pagination, mongoTemplate.find, SliceImpl) unchanged
so ComplexDocument results are safe from special-character/DoS injection.
| @Override | ||
| public Page<NoticeDocument> findRecommendedNoticesByDiagnosis( | ||
| java.util.List<String> supplyTypes, | ||
| Pageable pageable | ||
| ) { | ||
| Criteria criteria = new Criteria(); | ||
|
|
||
| /// supplyType 필터링 (진단 결과에서 매핑된 주택 유형) | ||
| if (supplyTypes != null && !supplyTypes.isEmpty()) { | ||
| criteria.and("supplyType").in(supplyTypes); | ||
| } | ||
|
|
||
| /// 모집 상태 필터링 없음 (모든 공고 포함) | ||
| /// 정렬은 Pageable의 Sort로 처리 (마감임박순 권장) | ||
|
|
||
| Query query = new Query(criteria).with(pageable); | ||
|
|
||
| /// 실행 및 Page 응답 구성 | ||
| List<NoticeDocument> notices = mongoTemplate.find(query, NoticeDocument.class); | ||
| long count = mongoTemplate.count(Query.of(query).limit(-1).skip(-1), NoticeDocument.class); | ||
|
|
||
| return new PageImpl<>(notices, pageable, count); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Search for the method definition and its usage
rg -n "findRecommendedNoticesByDiagnosis" --type javaRepository: PinHouse/PinHouse_BE
Length of output: 596
🏁 Script executed:
# Look for test cases related to this method
fd -e java | xargs grep -l "findRecommendedNoticesByDiagnosis" | head -20Repository: PinHouse/PinHouse_BE
Length of output: 370
🏁 Script executed:
# Examine similar filter methods in the repository to understand pattern
rg "public Page.*find.*Criteria" src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/ -A 10 -B 2 --type javaRepository: PinHouse/PinHouse_BE
Length of output: 46
🏁 Script executed:
# Check the full NoticeDocumentRepositoryImpl file structure
wc -l src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.javaRepository: PinHouse/PinHouse_BE
Length of output: 177
🏁 Script executed:
# Get context around the method in question
sed -n '200,240p' src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.javaRepository: PinHouse/PinHouse_BE
Length of output: 1636
🏁 Script executed:
# Check for any documentation or comments about this method's purpose
sed -n '200,250p' src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java | grep -E "(@|//|/\*)"Repository: PinHouse/PinHouse_BE
Length of output: 276
🏁 Script executed:
# Check the caller in HomeService to understand context
sed -n '410,425p' src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.javaRepository: PinHouse/PinHouse_BE
Length of output: 723
🏁 Script executed:
# Check if there are test cases for this method
fd -e java test | xargs grep -l "findRecommendedNoticesByDiagnosis" 2>/dev/null | head -5Repository: PinHouse/PinHouse_BE
Length of output: 46
🏁 Script executed:
# Check the interface definition to understand the contract
cat src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryCustom.javaRepository: PinHouse/PinHouse_BE
Length of output: 3431
🏁 Script executed:
# Check other similar find methods in the same implementation file to see if there are guards
sed -n '1,200p' src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java | grep -A 15 "public Page.*find"Repository: PinHouse/PinHouse_BE
Length of output: 1331
🏁 Script executed:
# Check where targetSupplyTypes is set in HomeService
sed -n '400,420p' src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.javaRepository: PinHouse/PinHouse_BE
Length of output: 815
🏁 Script executed:
# Look for more context around the diagnosis feature and targetSupplyTypes assignment
sed -n '380,430p' src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.javaRepository: PinHouse/PinHouse_BE
Length of output: 1834
🏁 Script executed:
# Check if there are any guards before calling findRecommendedNoticesByDiagnosis
grep -B 10 "findRecommendedNoticesByDiagnosis" src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java | head -20Repository: PinHouse/PinHouse_BE
Length of output: 522
🏁 Script executed:
# Check all callers of findRecommendedNoticesByDiagnosis to ensure there's only one
grep -r "findRecommendedNoticesByDiagnosis" src/ --include="*.java" | grep -v "public.*findRecommendedNoticesByDiagnosis" | grep -v "@Override" | grep -v "interface"Repository: PinHouse/PinHouse_BE
Length of output: 408
🏁 Script executed:
# Double-check the exact implementation to see the behavior when supplyTypes is empty
sed -n '211,235p' src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.javaRepository: PinHouse/PinHouse_BE
Length of output: 887
진단 기반 추천 조회 시 공급유형이 비어 있으면 전체 공고를 스캔합니다.
supplyTypes가 null/empty일 때 필터 조건이 제거되어 전체 컬렉션을 스캔하게 됩니다. 현재 HomeService에서 사전에 검증하고 있지만, 이 메서드는 공개 인터페이스(NoticeDocumentRepositoryCustom)의 일부이므로 다른 호출처에서 사용될 수 있습니다. 저방어적 프로그래밍을 위해 빈 목록 반환으로 처리하는 것이 안전합니다.
제안 변경
public Page<NoticeDocument> findRecommendedNoticesByDiagnosis(
java.util.List<String> supplyTypes,
Pageable pageable
) {
+ if (supplyTypes == null || supplyTypes.isEmpty()) {
+ return new PageImpl<>(List.of(), pageable, 0);
+ }
Criteria criteria = new Criteria();🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java`
around lines 211 - 233, The method findRecommendedNoticesByDiagnosis in
NoticeDocumentRepositoryImpl currently removes the supplyType filter when
supplyTypes is null/empty and ends up scanning the whole collection; change it
to defensively return an empty Page (new PageImpl<>(Collections.emptyList(),
pageable, 0)) when supplyTypes is null or empty to avoid full collection scans
and unexpected results from other callers; keep the existing behavior when
supplyTypes is non-empty (apply criteria.and("supplyType").in(supplyTypes),
build the Query, run mongoTemplate.find and count) but short-circuit early for
null/empty supplyTypes.
| @Override | ||
| public org.springframework.data.domain.Slice<String> searchTargetGroups(String keyword, Pageable pageable) { | ||
| Aggregation aggregation = Aggregation.newAggregation( | ||
| Aggregation.match(Criteria.where("targetGroup").regex(keyword, "i")), | ||
| Aggregation.unwind("targetGroup"), | ||
| Aggregation.match(Criteria.where("targetGroup").regex(keyword, "i")), | ||
| Aggregation.group("targetGroup"), | ||
| Aggregation.sort(Sort.by(Sort.Order.asc("_id"))), | ||
| Aggregation.skip((long) pageable.getPageNumber() * pageable.getPageSize()), | ||
| Aggregation.limit(pageable.getPageSize() + 1) | ||
| ); | ||
|
|
||
| List<String> results = mongoTemplate.aggregate(aggregation, "notices", StringAggregationResult.class) | ||
| .getMappedResults() | ||
| .stream() | ||
| .map(StringAggregationResult::getId) | ||
| .toList(); | ||
|
|
||
| return toSlice(results, pageable); | ||
| } | ||
|
|
||
| @Override | ||
| public org.springframework.data.domain.Slice<String> searchRegions(String keyword, Pageable pageable) { | ||
| Aggregation aggregation = Aggregation.newAggregation( | ||
| Aggregation.match(new Criteria().orOperator( | ||
| Criteria.where("city").regex(keyword, "i"), | ||
| Criteria.where("county").regex(keyword, "i") | ||
| )), | ||
| Aggregation.project("city", "county") | ||
| .andExpression("concat(city, ' ', county)").as("region"), | ||
| Aggregation.group("region"), | ||
| Aggregation.sort(Sort.by(Sort.Order.asc("_id"))), | ||
| Aggregation.skip((long) pageable.getPageNumber() * pageable.getPageSize()), | ||
| Aggregation.limit(pageable.getPageSize() + 1) | ||
| ); | ||
|
|
||
| List<String> results = mongoTemplate.aggregate(aggregation, "notices", StringAggregationResult.class) | ||
| .getMappedResults() | ||
| .stream() | ||
| .map(StringAggregationResult::getId) | ||
| .toList(); | ||
|
|
||
| return toSlice(results, pageable); | ||
| } | ||
|
|
||
| @Override | ||
| public org.springframework.data.domain.Slice<String> searchHouseTypes(String keyword, Pageable pageable) { | ||
| Aggregation aggregation = Aggregation.newAggregation( | ||
| Aggregation.match(Criteria.where("houseType").regex(keyword, "i")), | ||
| Aggregation.group("houseType"), | ||
| Aggregation.sort(Sort.by(Sort.Order.asc("_id"))), | ||
| Aggregation.skip((long) pageable.getPageNumber() * pageable.getPageSize()), | ||
| Aggregation.limit(pageable.getPageSize() + 1) | ||
| ); | ||
|
|
||
| List<String> results = mongoTemplate.aggregate(aggregation, "notices", StringAggregationResult.class) | ||
| .getMappedResults() | ||
| .stream() | ||
| .map(StringAggregationResult::getId) | ||
| .toList(); | ||
|
|
||
| return toSlice(results, pageable); | ||
| } | ||
|
|
||
| @Override | ||
| public org.springframework.data.domain.Slice<NoticeDocument> searchNoticesByTargetGroup(String keyword, Pageable pageable) { | ||
| Criteria criteria = Criteria.where("targetGroup").regex(keyword, "i"); | ||
| return findNoticeSlice(criteria, pageable); | ||
| } | ||
|
|
||
| @Override | ||
| public org.springframework.data.domain.Slice<NoticeDocument> searchNoticesByRegion(String keyword, Pageable pageable) { | ||
| Criteria criteria = new Criteria().orOperator( | ||
| Criteria.where("city").regex(keyword, "i"), | ||
| Criteria.where("county").regex(keyword, "i") | ||
| ); | ||
| return findNoticeSlice(criteria, pageable); | ||
| } | ||
|
|
||
| @Override | ||
| public org.springframework.data.domain.Slice<NoticeDocument> searchNoticesByHouseType(String keyword, Pageable pageable) { | ||
| Criteria criteria = Criteria.where("houseType").regex(keyword, "i"); | ||
| return findNoticeSlice(criteria, pageable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "NoticeDocumentRepositoryImpl.java"Repository: PinHouse/PinHouse_BE
Length of output: 175
🏁 Script executed:
wc -l ./src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.javaRepository: PinHouse/PinHouse_BE
Length of output: 179
🏁 Script executed:
cat -n ./src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java | sed -n '230,320p'Repository: PinHouse/PinHouse_BE
Length of output: 4898
🏁 Script executed:
cat -n ./src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java | head -50Repository: PinHouse/PinHouse_BE
Length of output: 2399
🏁 Script executed:
rg -t java "searchTargetGroups|searchRegions|searchHouseTypes|searchNoticesByTargetGroup|searchNoticesByRegion|searchNoticesByHouseType" --max-count 20 -A 3Repository: PinHouse/PinHouse_BE
Length of output: 11620
🏁 Script executed:
cat -n ./src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java | grep -B 15 "searchNoticesByTargetGroup" | head -30Repository: PinHouse/PinHouse_BE
Length of output: 1602
🏁 Script executed:
rg -t java "public.*homeSearchOverview|public.*homeSearchCategory" -A 10 src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java | head -40Repository: PinHouse/PinHouse_BE
Length of output: 46
🏁 Script executed:
rg -t java "keyword" src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java -B 5 | head -80Repository: PinHouse/PinHouse_BE
Length of output: 3314
🏁 Script executed:
rg -t java "class HomeService|public.*searchHomeOverview|public.*searchHomeByCategory" -A 2 src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java | head -20Repository: PinHouse/PinHouse_BE
Length of output: 484
🏁 Script executed:
rg -t java "@RequestParam|@PathVariable" src/main/java/com/pinHouse/server/platform/home -B 2 -A 8 | grep -A 10 "keyword"Repository: PinHouse/PinHouse_BE
Length of output: 46
🏁 Script executed:
find ./src/main/java/com/pinHouse/server/platform/home -name "*Controller.java" -type fRepository: PinHouse/PinHouse_BE
Length of output: 46
🏁 Script executed:
rg -t java "searchHomeOverview|searchHomeByCategory" --max-count 20Repository: PinHouse/PinHouse_BE
Length of output: 1013
🏁 Script executed:
cat -n ./src/main/java/com/pinHouse/server/platform/home/presentation/HomeApi.javaRepository: PinHouse/PinHouse_BE
Length of output: 6541
🏁 Script executed:
rg -t java "Pattern.quote|safeRegex|escapeRegex|sanitize" --max-count 20Repository: PinHouse/PinHouse_BE
Length of output: 46
🏁 Script executed:
rg -t java "keyword" src/main/java/com/pinHouse/server/platform/home/application/service/HomeService.java -B 2 -A 2 | head -60Repository: PinHouse/PinHouse_BE
Length of output: 2784
🏁 Script executed:
web_search
MongoDB Spring Data regex method and Pattern.quote usageRepository: PinHouse/PinHouse_BE
Length of output: 160
사용자 입력을 정규식에 그대로 넣는 것은 보안 위험입니다.
keyword 파라미터가 HTTP 요청으로부터 직접 전달되어 검증 없이 .regex() 메서드에 사용됩니다. 정규식 메타 문자가 포함될 경우 과도한 매칭 또는 ReDoS 공격으로 이어질 수 있습니다. 6개 메서드(searchTargetGroups, searchRegions, searchHouseTypes, searchNoticesByTargetGroup, searchNoticesByRegion, searchNoticesByHouseType)에서 동일한 패턴이 반복되고 있으므로 Pattern.quote()를 이용하여 입력을 이스케이프하거나 텍스트 인덱스 기반 검색으로 전환을 권장합니다.
🔒 제안 변경
+import java.util.regex.Pattern;
...
- Aggregation.match(Criteria.where("targetGroup").regex(keyword, "i")),
+ Aggregation.match(Criteria.where("targetGroup").regex(safeRegex(keyword))),
...
- Aggregation.match(new Criteria().orOperator(
- Criteria.where("city").regex(keyword, "i"),
- Criteria.where("county").regex(keyword, "i")
- )),
+ Aggregation.match(new Criteria().orOperator(
+ Criteria.where("city").regex(safeRegex(keyword)),
+ Criteria.where("county").regex(safeRegex(keyword))
+ )),
...
- Aggregation.match(Criteria.where("houseType").regex(keyword, "i")),
+ Aggregation.match(Criteria.where("houseType").regex(safeRegex(keyword))),
...
- Criteria criteria = Criteria.where("targetGroup").regex(keyword, "i");
+ Criteria criteria = Criteria.where("targetGroup").regex(safeRegex(keyword));
...
- Criteria criteria = new Criteria().orOperator(
- Criteria.where("city").regex(keyword, "i"),
- Criteria.where("county").regex(keyword, "i")
- );
+ Criteria criteria = new Criteria().orOperator(
+ Criteria.where("city").regex(safeRegex(keyword)),
+ Criteria.where("county").regex(safeRegex(keyword))
+ );
...
- Criteria criteria = Criteria.where("houseType").regex(keyword, "i");
+ Criteria criteria = Criteria.where("houseType").regex(safeRegex(keyword));
...
+ private String safeRegex(String keyword) {
+ return Pattern.quote(keyword);
+ }🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/housing/notice/domain/repository/NoticeDocumentRepositoryImpl.java`
around lines 235 - 317, The code currently injects raw user input into
Criteria.regex in methods searchTargetGroups, searchRegions, searchHouseTypes,
searchNoticesByTargetGroup, searchNoticesByRegion and searchNoticesByHouseType
which allows regex metacharacters and ReDoS; fix by escaping the keyword (e.g.,
wrap with Pattern.quote(keyword) or a small helper escapeRegex(String)) before
passing to .regex(..., "i"), centralize the escaping in a private method (e.g.,
escapeForRegex) and call it from each of the six methods so all
Criteria.where(...).regex(...) calls use the escaped value; optionally consider
switching to a text-index based search if you need full-text semantics instead
of literal matching.
| private void validateContentType(String contentType) { | ||
| List<String> allowedContentTypes = List.of("image/jpeg", "image/jpg", "image/png", "image/gif"); | ||
|
|
||
| if (!allowedContentTypes.contains(contentType.toLowerCase())) { | ||
| log.warn("지원하지 않는 Content-Type: {}", contentType); | ||
| throw new CustomException(ImageErrorCode.INVALID_FILE_TYPE); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null 체크 누락 및 잘못된 MIME 타입
두 가지 이슈가 있습니다:
contentType이 null일 경우NullPointerException이 발생합니다."image/jpg"는 유효한 MIME 타입이 아닙니다. 표준 MIME 타입은"image/jpeg"입니다.
🐛 제안된 수정
private void validateContentType(String contentType) {
- List<String> allowedContentTypes = List.of("image/jpeg", "image/jpg", "image/png", "image/gif");
+ List<String> allowedContentTypes = List.of("image/jpeg", "image/png", "image/gif");
- if (!allowedContentTypes.contains(contentType.toLowerCase())) {
+ if (contentType == null || !allowedContentTypes.contains(contentType.toLowerCase())) {
log.warn("지원하지 않는 Content-Type: {}", contentType);
throw new CustomException(ImageErrorCode.INVALID_FILE_TYPE);
}
}🤖 Prompt for AI Agents
In
`@src/main/java/com/pinHouse/server/platform/image/application/service/ImageService.java`
around lines 68 - 75, In validateContentType, add a null check for the
contentType parameter and treat null or blank values as invalid (log a warning
and throw CustomException(ImageErrorCode.INVALID_FILE_TYPE)), and remove the
non-standard "image/jpg" entry from the allowed list so only standard MIME types
(e.g., "image/jpeg", "image/png", "image/gif") are checked; ensure you call
contentType.toLowerCase(Locale.ROOT) (or equivalent) only after verifying
non-null to avoid NullPointerException and update the log message to include the
original (possibly null) value for debugging.
📌 작업한 내용
🔍 참고 사항
🖼️ 스크린샷
🔗 관련 이슈
✅ 체크리스트
Summary by CodeRabbit
릴리즈 노트
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.