[refactor] MOA-228 백엔드 비즈니스 로직 리팩토링#736
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Change Summary |
|---|---|
Metrics 제거backend/src/main/java/moadong/club/controller/ClubMetricController.java, backend/src/main/java/moadong/club/service/ClubMetricService.java, backend/src/main/java/moadong/club/entity/ClubMetric.java, backend/src/main/java/moadong/club/repository/ClubMetricRepository.java, backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java, backend/src/test/java/moadong/club/fixture/MetricFixture.java |
클럽 지표 컨트롤러/서비스/엔티티/레포지토리 및 관련 테스트·픽스처 삭제 |
클럽 상세 지표 패치 제거backend/src/main/java/moadong/club/controller/ClubProfileController.java |
HttpServletRequest 기반 IP 추출 및 ClubMetricService.patch 호출 제거; 메서드 시그니처에서 request 파라미터 삭제 |
미디어(GDrive/GCS) 제거backend/src/main/java/moadong/media/service/GcsClubImageService.java, backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java, backend/src/main/java/moadong/media/util/GoogleDriveConfig.java |
GCS/Google Drive 이미지 서비스 및 Drive Bean 설정 삭제 |
빌드/보안 설정 정리backend/build.gradle, backend/src/main/java/moadong/global/config/SecurityConfig.java |
Gradle에서 Google API 클라이언트 의존성 제거; SecurityConfig의 미사용 임포트 제거 |
컨트롤러 임포트 정리backend/src/main/java/moadong/club/controller/ClubApplyController.java, backend/src/main/java/moadong/club/controller/ClubSearchController.java, backend/src/main/java/moadong/user/controller/UserController.java |
와일드카드→명시적 임포트, 미사용 임포트 제거(동작 변화 없음) |
서비스/유틸 임포트 정리backend/src/main/java/moadong/club/service/ClubApplyService.java, .../ClubProfileService.java, .../ClubSearchService.java, .../RecruitmentStateChecker.java, backend/src/main/java/moadong/global/util/AESCipher.java |
와일드카드 제거·명시적 임포트/정렬(로직 변경 없음) |
엔티티/레포지토리 임포트 정리backend/src/main/java/moadong/club/entity/Club.java, .../ClubApplication.java, .../ClubApplicationQuestion.java, .../ClubQuestion.java, backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java, .../ClubQuestionRepository.java, .../ClubRepository.java, backend/src/main/java/moadong/user/entity/UserInformation.java, .../RefreshToken.java, backend/src/main/java/moadong/user/repository/UserRepository.java |
임포트 순서/중복 정리 및 미사용 임포트 제거(시그니처/동작 불변) |
DTO/Request/Response 정리backend/src/main/java/moadong/club/payload/dto/*, backend/src/main/java/moadong/club/payload/request/*, backend/src/main/java/moadong/user/payload/request/*, backend/src/main/java/moadong/user/payload/response/* |
임포트/공백/포맷 정리; ClubCreateRequest 레코드 삭제 |
글로벌 어노테이션/밸리데이터 정리backend/src/main/java/moadong/global/annotation/*, backend/src/main/java/moadong/global/validator/*, backend/src/main/java/moadong/global/exception/ErrorCode.java |
임포트 재배치/미사용 제거(기능 동일) |
테스트 정리 및 추가backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java, backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java, backend/src/test/java/moadong/unit/*, backend/src/test/java/moadong/util/annotations/*, backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java, backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java |
동시성 시나리오 추가(ClubApplyServiceTest); 미사용 임포트/주석/공백 정리 |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor User
participant ClubProfileController as ClubProfileController (GET /api/club/{id})
participant ClubProfileService as ClubProfileService
participant ClubMetricService as ClubMetricService [removed]
rect rgb(245,245,255)
note over User,ClubProfileService: 이전(Old)
User->>ClubProfileController: 요청(클럽 상세)
ClubProfileController->>ClubMetricService: patch(clubId, clientIp)
ClubMetricService-->>ClubProfileController: 완료
ClubProfileController->>ClubProfileService: getClubDetail(clubId)
ClubProfileService-->>ClubProfileController: ClubDetailedResult
ClubProfileController-->>User: 200 OK
end
rect rgb(245,255,245)
note over User,ClubProfileService: 이후(New)
User->>ClubProfileController: 요청(클럽 상세)
ClubProfileController->>ClubProfileService: getClubDetail(clubId)
ClubProfileService-->>ClubProfileController: ClubDetailedResult
ClubProfileController-->>User: 200 OK
end
sequenceDiagram
autonumber
actor User
participant Controller as Image API
participant ClubRepository as ClubRepository
participant Storage as Google Drive/GCS [removed]
rect rgb(255,245,245)
note over User,Storage: 이전(Old) 업로드 흐름(예: 로고)
User->>Controller: uploadLogo(clubId, file)
Controller->>ClubRepository: findById(clubId)
ClubRepository-->>Controller: Club
Controller->>Storage: uploadFile(...)
Storage-->>Controller: public URL
Controller->>ClubRepository: save(updated Club)
Controller-->>User: URL
end
note over User,Controller: 이후(New): 해당 구현 삭제됨
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related issues
- [refactor] MOA-228 백엔드 비즈니스 로직 리팩토링 #735 — 클럽 지표 모듈 제거와 GDrive/GCS 미디어 서비스 삭제 및 임포트 정리가 일치함
Possibly related PRs
- [release] 모아동 BE ver 1.0.2 #329 — 동일 영역(클럽 지표/프로필) 변경으로 코드 라인 단위에서 직접 겹침
- [feature] cover image 업로드 및 삭제 기능 구현 #537 — GoogleDrive/GCS 이미지 서비스 비활성/제거와 동일 컴포넌트 수정
- [release] 모아동 BE ver 1.0.3 #503 — 클럽 지표/미디어 구성요소 추가/변경 이력과 이번 제거가 맞물림
Suggested reviewers
- Zepelown
- lepitaaar
- oesnuj
Pre-merge checks and finishing touches
❌ Failed checks (2 warnings, 1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | 검토 결과 PR에는 목표 범위(클럽 매트릭스 제거, import 정리 등)에 부합하는 변경 외에 public API 제거(예: backend/src/main/java/moadong/club/payload/request/ClubCreateRequest.java 삭제)와 미디어 업로드 구현·설정 파일(GcsClubImageService, GoogleDriveClubImageService, GoogleDriveConfig) 전면 삭제가 포함되어 있어 PR 설명에 명시된 범위를 벗어났을 가능성이 있습니다. 특히 ClubCreateRequest 삭제는 API 소비자에 즉각적인 영향이 발생할 수 있습니다. | 조치 제안: 작성자에게 해당 삭제들이 의도된지 즉시 확인하고, 의도적이라면 PR 설명과 linked issue를 업데이트하여 삭제 사유와 소비자 영향(대체 DTO, 마이그레이션 방법)을 문서화하며 통합테스트와 API 문서 갱신 결과를 첨부해 주세요; 의도치 않은 변경이면 해당 파일들을 복원하거나 변경을 분리된 PR로 이동시켜 검증을 진행하십시오. | |
| Docstring Coverage | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
|
| Linked Issues Check | ❓ Inconclusive | PR은 보고된 목표들 중 import 정리와 "club metric" 관련 코드 전면 제거(ClubMetric 엔티티·리포지토리·서비스·컨트롤러 및 관련 테스트 삭제)를 충실히 반영하고 있으며 build.gradle에서 Drive 관련 의존성 제거 등 미디어 업로드 관련 코드 정리도 이루어졌습니다. 다만 PR 설명에는 "미디어 업로드 관련 주석 제거"로 기재되어 있으나 실제 변경은 미디어 업로드 구현(GcsClubImageService, GoogleDriveClubImageService, GoogleDriveConfig 등)과 public DTO(ClubCreateRequest) 삭제까지 포함되어 있어 명시된 링크드 이슈의 범위와 정확히 일치하는지 확인이 필요합니다. 따라서 현재 주어진 정보만으로는 모든 링크드 이슈 요구사항이 의도대로 충족되었는지 확정할 수 없습니다. | 작성자에게 ClubCreateRequest 및 미디어 서비스 삭제가 의도된 작업인지 확인해 주시고, 의도된 변경이라면 linked issue와 PR 설명을 업데이트하여 삭제 사유 및 API 영향(마이그레이션 가이드, 소비자 통지 등)을 명시해 주시기 바랍니다; 의도치 않은 삭제라면 복원하거나 별도 PR로 분리하십시오. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | 제목 "[refactor] MOA-228 백엔드 비즈니스 로직 리팩토링"은 PR의 주된 의도(백엔드 리팩터링)를 간결하게 나타내며 이슈 번호와 유형([refactor])을 포함해 변경 의도를 빠르게 파악할 수 있습니다. 제목이 과도하게 모호하지 않고 변경 히스토리를 스캔하는 동료에게 충분히 의미가 있어 적절합니다. |
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
refactor/#735-be_refactoring
Tip
👮 Agentic pre-merge checks are now available in preview!
Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
- Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
- Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.
Please see the documentation for more information.
Example:
reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post.
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 @coderabbitai help to get the list of available commands and usage tips.
Test Results72 tests 69 ✅ 7s ⏱️ Results for commit 710b3f3. |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java (1)
15-16: 치명적 버그: 정규식 앵커가 대안식 전체에 적용되지 않음현재 패턴은
^(A)|(B)$형태여서 A에는$이, B에는^가 적용되지 않습니다. 예:"010-1234-5678XYZ"가 매칭될 수 있습니다. 대안을 하나의 비캡처 그룹으로 감싸고 양쪽에 앵커를 적용해 주세요.다음과 같이 수정 제안합니다.
- private static final Pattern PHONE_NUMBER_PATTERN = - Pattern.compile("^(01[016789]-?\\d{3,4}-?\\d{4})|(0\\d{1,2}-?\\d{3,4}-?\\d{4})$"); + private static final Pattern PHONE_NUMBER_PATTERN = + Pattern.compile("^(?:01[016789]-?\\d{3,4}-?\\d{4}|0\\d{1,2}-?\\d{3,4}-?\\d{4})$");또한 TODO에 따라 하이픈 유무, 지역번호(02/0XX) 케이스, 잘못된 접미 텍스트 존재 시 불일치 등을 포함한 단위 테스트를 추가해 주세요.
backend/src/main/java/moadong/global/util/AESCipher.java (3)
17-18: [치명] GCM IV(Nonce)를 고정값으로 사용 — 재사용 시 기밀성/무결성 붕괴AES‑GCM은 (key, IV) 쌍이 매 암호화마다 유일해야 합니다. 현재
@Value("${application.encryption.iv}")로 고정 IV를 주입하고 동일 IV로encrypt/decrypt를 수행합니다. 운영에서 동일 키로 여러 데이터를 암호화하면 태그 위조 및 평문 누출 위험이 즉시 발생합니다. 반드시 "매 암호화 시 난수 IV(보통 12바이트)"를 생성하여 암호문에 함께 저장/전달하도록 변경해야 합니다. 설정의application.encryption.iv는 제거하세요.다음과 같이 수정 제안드립니다(IV per-message 생성, IV+Ciphertext를 Base64로 반환/수신):
@@ -package moadong.global.util; +package moadong.global.util; @@ import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.security.SecureRandom; @@ - @Value("${application.encryption.key}") - private String key; + @Value("${application.encryption.key}") + private String key; @@ - @Value("${application.encryption.iv}") - private String iv; + // 고정 IV 제거 (per-message 난수 사용) @@ public String encrypt(String text) throws Exception { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv.getBytes(StandardCharsets.UTF_8)); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec); - - byte[] encrypted = cipher.doFinal(text.getBytes(StandardCharsets.UTF_8)); - return Base64.getEncoder().encodeToString(encrypted); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + // 12바이트 난수 IV 생성 + byte[] ivBytes = new byte[12]; + new SecureRandom().nextBytes(ivBytes); + SecretKeySpec keySpec = keySpec(); // 아래 helper 참조 + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, ivBytes); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec); + + byte[] plaintext = text.getBytes(StandardCharsets.UTF_8); + byte[] ciphertext = cipher.doFinal(plaintext); + // [IV || CIPHERTEXT]로 합쳐서 반환 + byte[] out = new byte[ivBytes.length + ciphertext.length]; + System.arraycopy(ivBytes, 0, out, 0, ivBytes.length); + System.arraycopy(ciphertext, 0, out, ivBytes.length, ciphertext.length); + return Base64.getEncoder().encodeToString(out); } @@ public String decrypt(String cipherText) throws Exception { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); - GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv.getBytes(StandardCharsets.UTF_8)); - cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec); - - byte[] decodedBytes = Base64.getDecoder().decode(cipherText); - byte[] decrypted = cipher.doFinal(decodedBytes); - return new String(decrypted, StandardCharsets.UTF_8); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + byte[] all = Base64.getDecoder().decode(cipherText); + if (all.length < 13) { // 최소 IV(12) + 1 + throw new IllegalArgumentException("Invalid ciphertext"); + } + byte[] ivBytes = new byte[12]; + System.arraycopy(all, 0, ivBytes, 0, 12); + byte[] ciphertext = new byte[all.length - 12]; + System.arraycopy(all, 12, ciphertext, 0, ciphertext.length); + + SecretKeySpec keySpec = keySpec(); // 아래 helper 참조 + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, ivBytes); + cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec); + + byte[] decrypted = cipher.doFinal(ciphertext); + return new String(decrypted, StandardCharsets.UTF_8); } + + // Helper: Base64 키 디코드 및 길이 검증(32바이트) + private SecretKeySpec keySpec() { + byte[] keyBytes = Base64.getDecoder().decode(key); + if (keyBytes.length != 32) { + throw new IllegalArgumentException("application.encryption.key must be 32 bytes (Base64-encoded)."); + } + return new SecretKeySpec(keyBytes, "AES"); + }마이그레이션 유의:
- 기존 저장된 암호문에 IV가 포함돼 있지 않다면 복호 로직 호환 계층이 필요합니다.
- 설정 파일의
application.encryption.iv제거 및 배포 전 점검이 필요합니다.Also applies to: 30-31, 47-48
14-15: [중대] "AES‑256" 표기와 달리 키 길이 미검증 — 16/24바이트일 경우 자동 다운그레이드현재
new SecretKeySpec(key.getBytes(...), "AES")로 길이 검증 없이 키를 사용합니다. 프로퍼티가 16/24바이트면 실제로는 AES‑128/192가 됩니다. 위 제안된 diff에 포함된keySpec()처럼 Base64 디코드 후 32바이트 검증을 권장합니다. 프로퍼티 키를 Base64(32바이트)로 관리해 주세요.Also applies to: 29-29, 46-46
11-18: 생성자 주입 + 불변 필드로 전환스프링
@Component에서 필드를final로 두고 생성자 주입을 사용하면 테스트 용이성과 불변성이 향상됩니다(특히 보안 키).예시:
-@Component -public class AESCipher { - @Value("${application.encryption.key}") - private String key; +@Component +public class AESCipher { + private final String key; + + public AESCipher(@Value("${application.encryption.key}") String key) { + this.key = key; + }backend/build.gradle (2)
35-35: GCS 스토리지 의존성 제거 필요rg 검색 결과 backend/build.gradle(35행)에만 의존성이 남아 있고, GCS 관련 코드는 backend/src/main/java/moadong/global/config/SecurityConfig.java(주석 — 약 33행, 56–58행)에만 존재합니다. 사용처가 없으니 의존성 라인을 제거하세요.
- implementation 'com.google.cloud:spring-cloud-gcp-storage:5.8.0'
27-27: springdoc 버전 업데이트 — Spring Boot 3.3.x와 호환되는 2.x(2.6.0)로 상향backend/build.gradle:27 — 현재:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
변경:
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'사유: springdoc 2.6.x(최신 2.6 릴리스: 2.6.0, 2024-06-30)가 Spring Boot 3.3.x와 호환됨. 빌드·테스트 후 병합.
backend/src/main/java/moadong/club/entity/Club.java (1)
84-100: NPE 방지: 입력 값 null 가드 추가 제안
validateTags,validateIntroduction가 null 입력 시 NPE 발생합니다. 컨트롤러/검증기로 항상 비null을 보장하지 않는다면 가드를 권장합니다.- private void validateTags(List<String> tags) { + private void validateTags(List<String> tags) { + if (tags == null) { + throw new RestApiException(ErrorCode.INVALID_PARAMETER); + } if (tags.size() > 3) { throw new RestApiException(ErrorCode.TOO_MANY_TAGS); } for (String tag : tags) { if (tag.length() > 5) { throw new RestApiException(ErrorCode.TOO_LONG_TAG); } } } - private void validateIntroduction(String introduction) { + private void validateIntroduction(String introduction) { + if (introduction == null) { + throw new RestApiException(ErrorCode.INVALID_PARAMETER); + } if (introduction.length() > 24) { throw new RestApiException(ErrorCode.TOO_LONG_INTRODUCTION); } }backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java (1)
24-35: 모집 시작 전 D-Day 계산 부호 오류로 항상 UPCOMING 반환 가능
between = ChronoUnit.DAYS.between(recruitmentStartDate, now)는 now가 시작일 이전이면 음수입니다. 현재 조건between <= 14로 인해 시작까지 수백 일이 남아도 UPCOMING이 됩니다. 기준을now -> start로 바꾸거나 절대값을 써야 합니다.- if (now.isBefore(recruitmentStartDate)) { - long between = ChronoUnit.DAYS.between(recruitmentStartDate, now); - if (between <= 14) { + if (now.isBefore(recruitmentStartDate)) { + long daysUntilStart = ChronoUnit.DAYS.between(now, recruitmentStartDate); + if (daysUntilStart <= 14) { club.updateRecruitmentStatus(ClubRecruitmentStatus.UPCOMING); } else { club.updateRecruitmentStatus(ClubRecruitmentStatus.CLOSED); } } else if (now.isAfter(recruitmentStartDate) && now.isBefore(recruitmentEndDate)) {backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java (1)
24-37: 예외 범위 축소 및 부분 실패 처리 고려모든 예외를 한 번에 잡아 전체 요청을 실패시키고 있습니다. 복호화 관련 구체 예외만 캐치하고, 실패 항목을 스킵하거나 마스킹하는 정책을 검토하세요. 과도한 실패 전파를 줄일 수 있습니다.
- try { - for (ClubQuestionAnswer answer : application.getAnswers()) { - String decryptedValue = cipher.decrypt(answer.getValue()); - decryptedAnswers.add(ClubQuestionAnswer.builder() - .id(answer.getId()) - .value(decryptedValue) - .build()); - } - } catch (Exception e) { + try { + for (ClubQuestionAnswer answer : application.getAnswers()) { + String decryptedValue = cipher.decrypt(answer.getValue()); + decryptedAnswers.add(ClubQuestionAnswer.builder() + .id(answer.getId()) + .value(decryptedValue) + .build()); + } + } catch (RuntimeException e) { // AESCipher의 런타임 예외 타입으로 한정 log.error("AES_CIPHER_ERROR", e); throw new RestApiException(ErrorCode.AES_CIPHER_ERROR); }backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (6)
51-52: 잘못된 필드 언와인드 가능성 (club_tags)스키마 전반에서 태그는
recruitmentInformation.tags를 사용합니다. 여기서unwind("club_tags", true)는 존재하지 않는 필드로 보이며, 이후 project에서도recruitmentInformation.tags를 사용합니다. 불필요하거나 오타로 보입니다.- operations.add(Aggregation.unwind("club_tags", true)); + // tags는 배열이지만 단순 검색/정렬만 하므로 unwind 불필요 + // 필요 시 아래와 같이 올바른 경로로 변경 + // operations.add(Aggregation.unwind("recruitmentInformation.tags", true));
54-61: projection에 id 누락 및 tags 타입 미스매치(String vs List)
ClubSearchResult는id가 필요하지만_id를 매핑하지 않아club.id()가 null입니다.ifNull(...tags).then("")는 빈 문자열을 반환하여 DTO의List<String>와 불일치합니다. 같은 클래스의 다른 메서드는Collections.emptyList()를 사용합니다.- operations.add( - Aggregation.project("name", "state", "category", "division") + operations.add( + Aggregation.project("name", "state", "category", "division") + .and("_id").as("id") .and("recruitmentInformation.introduction").as("introduction") .and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus") - .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")) - .as("logo") - .and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then("")) - .as("tags")); + .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")).as("logo") + .and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then(Collections.emptyList())) + .as("tags"));
111-116: _id 타입 불일치 가능성 (String vs ObjectId)
excludeIds는 문자열 Set인데, Criteria는_id(ObjectId)와 비교합니다. 제외가 작동하지 않을 수 있습니다.ObjectId로 변환하거나_id를 문자열로 project해서 비교하세요.- Criteria criteria = Criteria.where("category").is(category) - .and("_id").nin(excludeIds); + Criteria criteria = Criteria.where("category").is(category); + if (!excludeIds.isEmpty()) { + // ObjectId 변환 + Set<ObjectId> objectIds = excludeIds.stream() + .filter(id -> id != null && !id.isBlank()) + .map(ObjectId::new) + .collect(Collectors.toSet()); + criteria = criteria.and("_id").nin(objectIds); + }
185-197: 랜덤 조회 projection에서도id누락동일 문제로
addClubs의 중복 제거가 실패합니다.- ops.add( - Aggregation.project("name", "state", "category", "division") + ops.add( + Aggregation.project("name", "state", "category", "division") + .and("_id").as("id") .and("recruitmentInformation.introduction").as("introduction") .and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus") .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")) .as("logo") .and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then(Collections.emptyList())) .as("tags") );
186-187: 여기도 excludeIds 타입 변환 필요
findRandomClubs의_id.nin(excludeIds)도 ObjectId 변환이 필요합니다.- ops.add(Aggregation.match(Criteria.where("_id").nin(excludeIds))); + Set<ObjectId> objectIds = excludeIds.stream() + .filter(id -> id != null && !id.isBlank()) + .map(ObjectId::new) + .collect(Collectors.toSet()); + ops.add(Aggregation.match(Criteria.where("_id").nin(objectIds)));
141-147: null id 처리로 중복 제거 실패 가능
club.id()가 null이면excludeIds에 추가되지 않아 중복 가능성이 있습니다. projection 수정 후에도 방어 로직을 넣어두는 것을 권장합니다.for (ClubSearchResult club : clubs) { - if (!excludeIds.contains(club.id())) { - result.add(club); - excludeIds.add(club.id()); + String id = club.id(); + if (id != null && !excludeIds.contains(id)) { + result.add(club); + excludeIds.add(id); } }
🧹 Nitpick comments (30)
backend/src/main/java/moadong/user/payload/response/LoginResponse.java (1)
3-4: 코딩 스타일 니트: 파라미터 쉼표 뒤 공백 추가 권장가독성/일관성을 위해 공백을 추가해 주세요.
-public record LoginResponse(String accessToken,String clubId) { +public record LoginResponse(String accessToken, String clubId) {backend/src/test/java/moadong/util/annotations/UnitTest.java (2)
3-15: 메타 애너테이션에 @documented 추가 제안IDE/Javadoc 노출을 위해 @documented를 함께 두는 것을 권장합니다.
적용 예:
import java.lang.annotation.Target; +import java.lang.annotation.Documented; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Tag(TestTypeConstants.UNIT_TEST) @ExtendWith(MockitoExtension.class) public @interface UnitTest {
13-14: 애너테이션 순서 일관화(선택)IntegrationTest와 순서를 맞추거나 사내 컨벤션을 명시하면 리뷰 소음이 줄어듭니다. 기능 영향은 없습니다.
backend/src/test/java/moadong/util/annotations/IntegrationTest.java (1)
3-14: 메타 애너테이션에 @documented 추가 제안UnitTest와 동일하게 문서화 노출을 위해 @documented 추가를 권장합니다.
적용 예:
import java.lang.annotation.Target; +import java.lang.annotation.Documented; import org.junit.jupiter.api.Tag; import org.springframework.boot.test.context.SpringBootTest; +@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @SpringBootTest @Tag(TestTypeConstants.INTEGRATION_TEST) public @interface IntegrationTest {backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)
9-18: 검증 일관성 확인 요청: Password만 null/empty → invalid다른 Validator(PhoneNumber/Korean/UserId)는 null/empty 시 true를 반환하지만, 여기만 false입니다. 의도라면 해당 필드에 @notblank 등과의 조합 정책을 README/컨벤션에 명시해 주세요.
backend/src/main/java/moadong/global/annotation/Korean.java (1)
15-15: 메시지 문구 케이스 통일 제안
"Invalid korean format"→"Invalid Korean format"로 고유명사 표기를 통일하면 가독성이 좋아집니다.backend/src/main/java/moadong/global/validator/KoreanValidator.java (1)
9-11: 테스트 보강 권장요구사항 주석에 맞게(자모 단독 제외, 1~10자) 경계값(0자, 1자, 10자, 11자) 및 자모만 입력 케이스에 대한 단위 테스트를 추가해 주세요.
backend/src/main/java/moadong/global/annotation/PhoneNumber.java (1)
11-11: Validator 정규식 수정 반영 필요PhoneNumberValidator의 정규식 버그 수정이 들어가면(제안 코멘트 참고) 이 어노테이션을 사용하는 DTO 테스트도 함께 갱신해 주세요(하이픈 유무/지역번호 케이스).
backend/src/main/java/moadong/global/validator/UserIdValidator.java (1)
12-16: 검증 일관성 확인 요청: null/empty 처리여기는 null/empty → true, Password는 false입니다. 각 필드의 필수 여부가 도메인 정책과 일치하는지, 필수 필드는 @notblank와 조합되는지 확인 부탁드립니다.
backend/src/main/java/moadong/global/util/AESCipher.java (3)
27-35: 예외 타입 세분화/래핑 권장
throws Exception대신GeneralSecurityException으로 한정하거나, 내부에서RuntimeException으로 래핑해 상위 계층에 의미 있는 에러 메시지를 제공하는 편이 디버깅과 API 계약에 유리합니다.Also applies to: 44-53
21-26: Javadoc 정확성 업데이트키 길이 32바이트 고정 검증을 반영하여 “AES‑256” 전제를 문서화하거나, 일반 “AES‑GCM”으로 표기 정정하세요.
Also applies to: 38-43
11-18: 운영 가이드: IV 재사용 가능성 점검 및 키 로테이션 계획과거에 고정 IV로 암호화된 데이터가 있다면 위험 노출 가능성이 있습니다. 최소 조치:
- 기존 데이터 전수 스캔 및 재암호화(난수 IV 포함 포맷으로).
- 키 로테이션(새 키는 Base64 32바이트) 및 단계적 복호/재암호화 백필.
- 설정/런북 업데이트: “IV는 보관하지 않고 암호문에 프리픽스” 원칙 명시.
backend/build.gradle (2)
3-4: 의견: dependency-management 플러그인 중복 적용 가능성Spring Boot Gradle 플러그인은 보통 자체적으로 의존성 관리를 적용합니다.
io.spring.dependency-management플러그인을 추가로 명시할 필요가 없을 수 있습니다. 팀 규약에 따라 유지할 수도 있으나, 중복 구성을 피하려면 제거 고려 바랍니다.plugins { id 'java' id 'org.springframework.boot' version '3.3.8' - id 'io.spring.dependency-management' version '1.1.7' }
59-60: 불필요한 공백 라인 제거의미 없는 공백 2줄이 추가되었습니다. 변경 히스토리 노이즈를 줄이기 위해 제거를 권장합니다.
- -backend/src/main/java/moadong/user/entity/RefreshToken.java (1)
3-3: java.util.Date 대신 java.time.Instant 권장레거시 Date 대신
Instant/ZonedDateTime사용을 권장합니다. 직렬화/타임존 처리에서 일관성이 높습니다.-import java.util.Date; +import java.time.Instant; ... - private Date expiresAt; + private Instant expiresAt;backend/src/main/java/moadong/club/service/ClubProfileService.java (1)
29-35: 트랜잭션 누락 가능성
updateClubRecruitmentInfo는 변경/저장을 수행하지만@Transactional이 없습니다. 동일한 일관성 보장을 위해 메서드에 부여를 권장합니다.- public void updateClubRecruitmentInfo(ClubRecruitmentInfoUpdateRequest request, + @Transactional + public void updateClubRecruitmentInfo(ClubRecruitmentInfoUpdateRequest request, CustomUserDetails user) {backend/src/test/java/moadong/unit/user/UserLoginTest.java (1)
59-59: 테스트 간 상태 공유 가능성
MockHttpServletResponse를 필드로 재사용하면 테스트 순서/상태에 따른 간헐적 실패가 생길 수 있습니다. 각 테스트 메서드 내에서 새 인스턴스를 생성해 사용하세요.backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java (1)
28-30: primitive boolean에 @NotNull은 무의미합니다
boolean required는 null이 될 수 없으므로 애노테이션이 효과가 없습니다. 제거해 혼동을 줄이세요.public record Options( - @NotNull - boolean required + boolean required ) {}backend/src/main/java/moadong/club/service/ClubSearchService.java (1)
32-41: 불필요한 스트림 → 제자리 정렬로 단순화리스트를 스트림으로 다시 수집할 필요 없이 제자리 정렬이 더 간단하고 할당이 줄어듭니다.
- // 정렬 - result = result.stream() - .sorted( - // - Comparator - .comparingInt((ClubSearchResult club) -> ClubRecruitmentStatus.getPriorityFromString(club.recruitmentStatus())) - .thenComparingInt((ClubSearchResult club) -> ClubCategory.getPriorityFromString(club.category())) - .thenComparing(ClubSearchResult::name) - ) - .collect(Collectors.toList()); + // 정렬 + result.sort( + Comparator + .comparingInt((ClubSearchResult club) -> ClubRecruitmentStatus.getPriorityFromString(club.recruitmentStatus())) + .thenComparingInt((ClubSearchResult club) -> ClubCategory.getPriorityFromString(club.category())) + .thenComparing(ClubSearchResult::name) + );추가로
Collectors임포트를 제거할 수 있습니다.backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (2)
22-22: 주석과 실제 스케줄 불일치
fixedRate = 60 * 60 * 1000은 1시간인데 주석은 “5분마다”. 혼동 방지를 위해 주석을 코드에 맞게 수정하세요.- @Scheduled(fixedRate = 60 * 60 * 1000) // 5분마다 실행 + @Scheduled(fixedRate = 60 * 60 * 1000) // 1시간마다 실행
24-34: NPE 가드 추가 제안일부 클럽에서
recruitInfo또는 시작/종료일이 null일 경우 NPE 위험이 있습니다. 스킵 가드를 추가하세요.- for (Club club : clubs) { - ClubRecruitmentInformation recruitInfo = club.getClubRecruitmentInformation(); - ZonedDateTime recruitmentStartDate = recruitInfo.getRecruitmentStart(); - ZonedDateTime recruitmentEndDate = recruitInfo.getRecruitmentEnd(); + for (Club club : clubs) { + ClubRecruitmentInformation recruitInfo = club.getClubRecruitmentInformation(); + if (recruitInfo == null) { + continue; + } + ZonedDateTime recruitmentStartDate = recruitInfo.getRecruitmentStart(); + ZonedDateTime recruitmentEndDate = recruitInfo.getRecruitmentEnd(); if (recruitInfo.getClubRecruitmentStatus() == ClubRecruitmentStatus.ALWAYS) { continue; } + if (recruitmentStartDate == null && recruitmentEndDate == null) { + continue; + } RecruitmentStateCalculator.calculate(club, recruitmentStartDate, recruitmentEndDate); clubRepository.save(club); }backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1)
124-134: 불필요한 try/catch 제거 및 어서션 단순화검증 실패만 확인하면 되는 테스트입니다. 빈
catch는 제거하고 검증 결과를 직접 어서트하세요.- try { - UserRegisterRequest request = UserRequestFixture.createUserRegisterRequest(userId, password, name, phoneNumber); - Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request); - // then - if (violations.isEmpty()) { - fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId); - } - } catch (RestApiException e) { - - } + UserRegisterRequest request = UserRequestFixture.createUserRegisterRequest(userId, password, name, phoneNumber); + Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request); + assertThat(violations).isNotEmpty();- try { - UserRegisterRequest request = UserRequestFixture.createUserRegisterRequest(userId, password, name, phoneNumber); - Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request); - // then - if (violations.isEmpty()) { - fail("예외나 검증 실패가 발생하지 않았습니다. 유효하지 않은 userId: " + userId); - } - } catch (RestApiException e) { - - } + UserRegisterRequest request = UserRequestFixture.createUserRegisterRequest(userId, password, name, phoneNumber); + Set<ConstraintViolation<UserRegisterRequest>> violations = validator.validate(request); + assertThat(violations).isNotEmpty();Also applies to: 166-176
backend/src/main/java/moadong/club/entity/ClubQuestion.java (1)
40-44: 하드코딩된 타임존 상수화 또는 Clock 주입 고려
Asia/Seoul하드코딩은 테스트/운영 일관성에 영향 줄 수 있습니다. 상수화하거나Clock주입으로 테스트 가능성을 높이세요.public class ClubQuestion implements Persistable<String> { + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); @@ - private LocalDateTime createdAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(); + private LocalDateTime createdAt = ZonedDateTime.now(KST).toLocalDateTime(); @@ - private LocalDateTime editedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(); + private LocalDateTime editedAt = ZonedDateTime.now(KST).toLocalDateTime(); @@ - this.editedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(); + this.editedAt = ZonedDateTime.now(KST).toLocalDateTime();Also applies to: 61-63
backend/src/main/java/moadong/club/entity/ClubApplication.java (1)
28-31: 접근 제어자 누락(nitpick)다른 필드와 일관되게
status에도private을 명시하세요.- ApplicationStatus status = ApplicationStatus.SUBMITTED; + private ApplicationStatus status = ApplicationStatus.SUBMITTED;backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (2)
15-16: 파라미터 명 혼동 가능: clubId → questionId로 리네이밍 제안메서드 파생 쿼리는
...ByIdInAndQuestionId로 동작하므로 2번째 파라미터 명을questionId로 두는 편이 명확합니다. 코드베이스에서 clubId≡questionId(동일 개체)로 운용되는 건 알고 있으나, 외부 독자 입장에선 혼동 여지가 큽니다.다음과 같이 파라미터 명만 교체를 제안합니다(바이너리/동작 변화 없음).
- List<ClubApplication> findAllByIdInAndQuestionId(List<String> ids, String clubId); + List<ClubApplication> findAllByIdInAndQuestionId(List<String> ids, String questionId);
10-16: 조회 성능 인덱스 점검 제안다음 인덱스를 확인/추가해주세요:
ClubApplication(questionId, status)— 상태 필터링 조회ClubApplication(id, questionId)또는(questionId, id)— 배치 조회backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java (1)
26-28: Mongo 환경에서 @Enumerated 무의미 가능성해당 엔티티가 Mongo 문서로 직렬화된다면 JPA의
@Enumerated는 효과가 없습니다. 직렬화 요구가 없다면 제거를, 필요 시 전용 직렬화 설정을 고려해주세요.backend/src/main/java/moadong/club/service/ClubApplyService.java (2)
96-107: 암호화 예외 처리 범위 축소 제안
Exception포괄 캐치는 디버깅을 어렵게 합니다. 암호화 관련 예외만 포착하고, 기타 예외는 상위로 전파하세요.다음과 같이 예외 범위를 좁히는 것을 제안합니다(실제 예외 타입에 맞게 조정 필요).
- } catch (Exception e) { + } catch (RuntimeException e) { + // 예기치 않은 런타임 예외는 원인 보존 + throw e; + } catch (Exception e) { log.error("AES_CIPHER_ERROR", e); throw new RestApiException(ErrorCode.AES_CIPHER_ERROR); }
176-183: 중복 applicantId 입력 검증 추천현재
toMap(..., (prev,next)->next)로 마지막 값이 승리합니다. 요청 자체에 중복이 있으면 조용히 덮어써 디버깅이 어려울 수 있습니다. 중복을 검출해 400으로 거부하는 방식을 권장합니다.다음 검증을 추가 제안합니다.
Map<String, ClubApplicantEditRequest> requestMap = request.stream() .collect(Collectors.toMap(ClubApplicantEditRequest::applicantId, Function.identity(), (prev, next) -> next)); + if (requestMap.size() != request.size()) { + throw new RestApiException(ErrorCode.INVALID_REQUEST); + }backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
100-106: 테스트 행(hang) 방지: 대기 타임아웃 추가 제안Latch 무기한 대기는 CI를 멈출 수 있습니다. 타임아웃과 검증을 추가하세요.
다음 변경을 제안합니다(추가로
assertTrue와TimeUnit임포트 필요).- latch.await(); + boolean completed = latch.await(10, java.util.concurrent.TimeUnit.SECONDS); + assertTrue(completed, "작업이 10초 내에 완료되지 않았습니다.");그리고 상단 임포트에 다음을 추가:
+import static org.junit.jupiter.api.Assertions.assertTrue;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (65)
backend/build.gradle(1 hunks)backend/src/main/java/moadong/club/controller/ClubApplyController.java(1 hunks)backend/src/main/java/moadong/club/controller/ClubMetricController.java(0 hunks)backend/src/main/java/moadong/club/controller/ClubProfileController.java(1 hunks)backend/src/main/java/moadong/club/controller/ClubSearchController.java(0 hunks)backend/src/main/java/moadong/club/entity/Club.java(1 hunks)backend/src/main/java/moadong/club/entity/ClubApplication.java(1 hunks)backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java(1 hunks)backend/src/main/java/moadong/club/entity/ClubMetric.java(0 hunks)backend/src/main/java/moadong/club/entity/ClubQuestion.java(1 hunks)backend/src/main/java/moadong/club/payload/dto/ClubApplicantsResult.java(1 hunks)backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java(0 hunks)backend/src/main/java/moadong/club/payload/dto/ClubSearchResult.java(1 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicantDeleteRequest.java(0 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicationCreateRequest.java(0 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java(0 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java(1 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplyRequest.java(0 hunks)backend/src/main/java/moadong/club/payload/request/ClubCreateRequest.java(0 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubMetricRepository.java(0 hunks)backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubRepository.java(0 hunks)backend/src/main/java/moadong/club/repository/ClubSearchRepository.java(1 hunks)backend/src/main/java/moadong/club/service/ClubApplyService.java(1 hunks)backend/src/main/java/moadong/club/service/ClubMetricService.java(0 hunks)backend/src/main/java/moadong/club/service/ClubProfileService.java(1 hunks)backend/src/main/java/moadong/club/service/ClubSearchService.java(1 hunks)backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java(1 hunks)backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java(1 hunks)backend/src/main/java/moadong/global/annotation/Korean.java(1 hunks)backend/src/main/java/moadong/global/annotation/Password.java(1 hunks)backend/src/main/java/moadong/global/annotation/PhoneNumber.java(1 hunks)backend/src/main/java/moadong/global/annotation/UserId.java(1 hunks)backend/src/main/java/moadong/global/config/SecurityConfig.java(0 hunks)backend/src/main/java/moadong/global/exception/ErrorCode.java(0 hunks)backend/src/main/java/moadong/global/util/AESCipher.java(1 hunks)backend/src/main/java/moadong/global/validator/KoreanValidator.java(1 hunks)backend/src/main/java/moadong/global/validator/PasswordValidator.java(1 hunks)backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java(1 hunks)backend/src/main/java/moadong/global/validator/UserIdValidator.java(1 hunks)backend/src/main/java/moadong/media/service/GcsClubImageService.java(0 hunks)backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java(0 hunks)backend/src/main/java/moadong/media/util/GoogleDriveConfig.java(0 hunks)backend/src/main/java/moadong/user/controller/UserController.java(1 hunks)backend/src/main/java/moadong/user/entity/RefreshToken.java(1 hunks)backend/src/main/java/moadong/user/entity/UserInformation.java(0 hunks)backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java(0 hunks)backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java(0 hunks)backend/src/main/java/moadong/user/payload/response/FindUserClubResponse.java(1 hunks)backend/src/main/java/moadong/user/payload/response/LoginResponse.java(1 hunks)backend/src/main/java/moadong/user/payload/response/RefreshResponse.java(1 hunks)backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java(0 hunks)backend/src/main/java/moadong/user/repository/UserRepository.java(1 hunks)backend/src/test/java/moadong/club/fixture/MetricFixture.java(0 hunks)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(1 hunks)backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java(0 hunks)backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java(0 hunks)backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java(1 hunks)backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java(0 hunks)backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java(0 hunks)backend/src/test/java/moadong/unit/user/UserLoginTest.java(1 hunks)backend/src/test/java/moadong/unit/user/UserRegisterTest.java(1 hunks)backend/src/test/java/moadong/util/annotations/IntegrationTest.java(1 hunks)backend/src/test/java/moadong/util/annotations/UnitTest.java(1 hunks)
💤 Files with no reviewable changes (26)
- backend/src/main/java/moadong/global/exception/ErrorCode.java
- backend/src/main/java/moadong/club/payload/request/ClubApplyRequest.java
- backend/src/main/java/moadong/club/payload/request/ClubCreateRequest.java
- backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java
- backend/src/main/java/moadong/global/config/SecurityConfig.java
- backend/src/main/java/moadong/club/payload/request/ClubApplicantDeleteRequest.java
- backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java
- backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java
- backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java
- backend/src/test/java/moadong/club/fixture/MetricFixture.java
- backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java
- backend/src/main/java/moadong/club/service/ClubMetricService.java
- backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java
- backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java
- backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java
- backend/src/main/java/moadong/club/repository/ClubRepository.java
- backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java
- backend/src/main/java/moadong/user/entity/UserInformation.java
- backend/src/main/java/moadong/club/payload/request/ClubApplicationCreateRequest.java
- backend/src/main/java/moadong/club/controller/ClubMetricController.java
- backend/src/main/java/moadong/club/controller/ClubSearchController.java
- backend/src/main/java/moadong/club/repository/ClubMetricRepository.java
- backend/src/main/java/moadong/media/service/GcsClubImageService.java
- backend/src/main/java/moadong/media/util/GoogleDriveConfig.java
- backend/src/main/java/moadong/club/entity/ClubMetric.java
- backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.javabackend/src/main/java/moadong/club/repository/ClubQuestionRepository.javabackend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/main/java/moadong/club/repository/ClubApplicationRepository.javabackend/src/main/java/moadong/club/entity/ClubApplicationQuestion.javabackend/src/test/java/moadong/club/service/ClubApplyServiceTest.javabackend/src/test/java/moadong/fixture/ClubApplicationEditFixture.javabackend/src/main/java/moadong/club/entity/ClubQuestion.java
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/main/java/moadong/club/entity/ClubApplication.javabackend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/main/java/moadong/club/controller/ClubApplyController.javabackend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
📚 Learning: 2025-05-15T12:03:57.356Z
Learnt from: Zepelown
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java:32-33
Timestamp: 2025-05-15T12:03:57.356Z
Learning: 엔티티 클래스는 요청/응답 객체(DTO)에 의존해서는 안 됩니다. 계층 간 의존성 문제를 방지하기 위해 엔티티와 DTO는 분리되어야 합니다. 예를 들어, `ClubApplicationQuestion` 엔티티가 `ClubApplicationRequest.Options`와 같은 요청 객체를 직접 참조하는 대신, 엔티티 패키지 내에 `QuestionOptions`와 같은 별도의 클래스를 정의하고 사용해야 합니다. 이렇게 하면 요청 객체 변경이 데이터베이스 스키마나 엔티티 계층에 영향을 미치지 않습니다.
Applied to files:
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java
🧬 Code graph analysis (8)
backend/src/main/java/moadong/global/annotation/Korean.java (1)
backend/src/main/java/moadong/global/validator/KoreanValidator.java (1)
KoreanValidator(8-20)
backend/src/main/java/moadong/global/annotation/UserId.java (1)
backend/src/main/java/moadong/global/validator/UserIdValidator.java (1)
UserIdValidator(8-18)
backend/src/main/java/moadong/global/annotation/PhoneNumber.java (1)
backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java (1)
PhoneNumberValidator(8-24)
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)
backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java (1)
AllArgsConstructor(12-36)backend/src/main/java/moadong/club/entity/ClubQuestionAnswer.java (1)
AllArgsConstructor(7-16)
backend/src/test/java/moadong/util/annotations/IntegrationTest.java (1)
backend/src/test/java/moadong/MoadongApplicationTests.java (1)
SpringBootTest(6-13)
backend/src/test/java/moadong/util/annotations/UnitTest.java (1)
backend/src/test/java/moadong/media/service/CloudflareClubImageServiceLogoTest.java (1)
ExtendWith(30-127)
backend/src/main/java/moadong/global/annotation/Password.java (1)
backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)
PasswordValidator(8-20)
backend/src/main/java/moadong/club/controller/ClubProfileController.java (1)
frontend/src/apis/getClubDetail.ts (1)
getClubDetail(4-17)
🔇 Additional comments (37)
backend/src/main/java/moadong/user/payload/response/FindUserClubResponse.java (1)
3-4: LGTM — 선언부 단일화(포맷팅)만 이뤄졌고 동작/공개 API 변화 없음레코드 필드와 시그니처 동일, 직렬화 스키마 영향도 없습니다.
backend/src/main/java/moadong/user/payload/response/RefreshResponse.java (1)
3-4: LGTM — 포맷 정리만 수행기존 동작/계약과 동일합니다.
backend/src/test/java/moadong/util/annotations/UnitTest.java (1)
7-9: 임포트 정리 LGTM기능 변화 없이 가독성 개선입니다.
backend/src/test/java/moadong/util/annotations/IntegrationTest.java (2)
7-8: 임포트 정리 LGTM기능 변화 없이 정돈되었습니다.
12-13: 프로필 지정 필요 여부 확인프로젝트가 통합테스트에 공통적으로 test 프로필을 요구한다면 여기서 @activeprofiles("test")를 함께 지정하면 중복을 줄일 수 있습니다. 필요 없다면 현 상태 유지가 바람직합니다.
backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)
6-6: 임포트 정리 LGTM임포트 순서 정리만 이뤄졌고 동작 변화 없습니다.
backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java (1)
6-6: 임포트 정리 LGTM불필요 공백 제거 및 임포트 재정렬만 이뤄졌습니다.
backend/src/main/java/moadong/global/annotation/Korean.java (1)
9-9: 임포트 순서 정리 LGTM기능 변화 없습니다.
backend/src/main/java/moadong/global/validator/KoreanValidator.java (1)
6-6: 임포트 정리 LGTMbackend/src/main/java/moadong/global/annotation/Password.java (1)
9-9: 임포트 순서 정리 LGTM주석/동작 변화 없습니다.
backend/src/main/java/moadong/global/annotation/PhoneNumber.java (1)
9-9: 불필요 임포트 제거 및 정리 LGTMKoreanValidator 불필요 임포트 제거와 정렬이 적절합니다.
backend/src/main/java/moadong/global/annotation/UserId.java (1)
9-9: 임포트 순서 정리 LGTMbackend/src/main/java/moadong/global/validator/UserIdValidator.java (1)
6-6: 임포트 정리 LGTMbackend/src/main/java/moadong/global/util/AESCipher.java (1)
3-4: 임포트 정리 LGTM불필요 임포트 제거 및 정렬 문제 없습니다. 동작 영향 없음.
Also applies to: 8-9
backend/build.gradle (1)
36-38: 조치: JJWT 업그레이드 검토 — 최신 안정 0.13.0(2025-08-20)backend/build.gradle (36–38): 현재 io.jsonwebtoken:jjwt-* 0.11.5
요약: 최신 안정은 0.13.0(배포일 2025‑08‑20). 0.12.x→0.13.0에서 JWE/JWK 지원 추가(일부 브레이킹 변경), JDK17+ 관련 리플렉션/모듈 호환성 개선, org.json·BouncyCastle 등 보안 패치 및 API/동작 변경이 포함되어 있습니다.
권장 조치:
- CHANGELOG(0.12.x/0.13.0)에서 브레이킹 항목 확인.
- 사용 중인 JDK 버전과 JWE/JWK·custom ObjectMapper·BouncyCastle 사용 여부 점검.
- 통합 테스트·보안 스캔 후 0.13.0으로 마이그레이션(즉시 불가 시 0.12.x로 단계적 업그레이드).
backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java (1)
3-3: 임포트 정리 LGTM중복 임포트 제거만 이므로 동작 변화 없습니다. 리포지토리 시그니처는 그대로입니다.
backend/src/main/java/moadong/club/entity/Club.java (1)
3-4: 임포트 정리 LGTM단순 정리로 동작 영향 없습니다.
backend/src/main/java/moadong/club/payload/dto/ClubSearchResult.java (1)
4-4: 임포트 순서 정리 LGTM레코드 시그니처와 빌더 사용은 그대로입니다.
backend/src/main/java/moadong/club/service/ClubProfileService.java (1)
50-61: 추천 조회 excludeId 타입/전파 확인 필요
searchRecommendClubs(club.getCategory(), clubId)로 전달된excludeClubId는 문자열입니다. 리포지토리에서는_id필드에 대해Criteria.nin(excludeIds)를 수행합니다. 컬렉션의_id가ObjectId이면 불일치로 제외가 안 됩니다. 또한 리포지토리의 projection에id가 누락되어addClubs에서club.id()가 null일 가능성이 있습니다(아래 리포지토리 코멘트 참고). 두 지점 확인/수정이 필요합니다.backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (1)
126-134: projection 불일치 정합성 유지여기도
id미포함 시 이후 로직에서 중복 제거가 동작하지 않습니다. 위와 동일하게id를 포함하고tags는 빈 리스트로 통일하세요.- ops.add( - Aggregation.project("name", "state", "category", "division") + ops.add( + Aggregation.project("name", "state", "category", "division") + .and("_id").as("id") .and("recruitmentInformation.introduction").as("introduction") .and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus") .and(ConditionalOperators.ifNull("$recruitmentInformation.logo").then("")) .as("logo") .and(ConditionalOperators.ifNull("$recruitmentInformation.tags").then(Collections.emptyList())) .as("tags") );backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java (1)
9-9: 임포트 정리 LGTM불필요한 공백/중복 제거와 명시적 임포트로 가독성이 좋아졌습니다.
backend/src/main/java/moadong/club/service/ClubSearchService.java (1)
3-5: 임포트 정리 LGTM중복 임포트 제거와 상단 정리로 일관성이 좋아졌습니다.
backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1)
4-11: 임포트 정리 LGTM명시적 static import로 테스트 가독성과 유지보수성이 개선되었습니다.
Also applies to: 16-16
backend/src/main/java/moadong/club/entity/ClubQuestion.java (1)
5-9: 임포트/초기화 정리 LGTM시간/컬렉션 관련 임포트가 상단으로 정리되어 읽기 수월합니다.
backend/src/main/java/moadong/club/controller/ClubProfileController.java (1)
34-36: 메트릭 패치 제거에 따른 엔드포인트 시그니처 단순화 LGTM
HttpServletRequest제거로 부수효과 없이 상세 조회만 수행합니다. FE의/api/club/{clubId}호출과 호환됩니다.backend/src/main/java/moadong/club/entity/ClubApplication.java (1)
11-16: 임포트 정리 LGTM정리만 수행되어 동작 변경 없습니다.
backend/src/test/java/moadong/unit/user/UserLoginTest.java (1)
3-9: 컴파일 실패 위험: 잘못된 static import 및 테스트 assert 교체 필요
- 문제: org.mockito.BDDMockito에 mock/when/verify의 static import가 없고, com.mongodb.assertions.Assertions.assertTrue는 테스트 어서션으로 부적절해 컴파일/테스트 실패 가능.
- 대상: backend/src/test/java/moadong/unit/user/UserLoginTest.java (lines 3-9)
- 조치: 아래 diff 적용
-import static com.mongodb.assertions.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.verify; -import static org.mockito.BDDMockito.when; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when;
- 추가 검증(리포지토리 전반): 로컬에서 동일 import 검색 후 적용/교정
rg -n --hidden --no-ignore -S "import static org.mockito.BDDMockito" || true rg -n --hidden --no-ignore -S "import static com.mongodb.assertions.Assertions.assertTrue" || truebackend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (1)
3-4: 임포트 정리 LGTM불필요한 와일드카드 제거 및 순서 정돈 좋습니다.
backend/src/main/java/moadong/user/controller/UserController.java (1)
26-32: 와일드카드 제거 LGTM명시적 임포트로 가독성과 IDE 최적화에 유리합니다.
backend/src/main/java/moadong/user/repository/UserRepository.java (1)
3-3: Optional 임포트 중복 정리 LGTM동작 변화 없이 코드 정돈되었습니다.
backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java (1)
6-6: 임포트 순서 정리 LGTMbackend/src/main/java/moadong/club/controller/ClubApplyController.java (1)
8-15: 명시적 임포트 전환 LGTM컨트롤러 전반의 임포트 일관성 향상에 도움 됩니다.
Also applies to: 22-29
backend/src/main/java/moadong/club/service/ClubApplyService.java (3)
4-12: 임포트 정리 LGTM명시적 임포트로 변경되어 가독성 및 빌드 안정성이 향상되었습니다.
Also applies to: 14-21, 23-27
109-114: questionId=clubId 관례 사용 확인
ClubApplication.questionId에clubQuestion.getClubId()를 저장하는 현 관례(= clubId≡questionId)대로 구현되어 일관됩니다. 레포지토리 파생 쿼리들과의 합치성만 재확인 부탁드립니다.
125-131: APPLY 통계에서 DRAFT 제외 의도 재확인 요청
findAllByQuestionId는 DRAFT를 제외하도록 @query에 명시되어 있습니다. 리뷰 대기/합격 등 통계에서 DRAFT 미포함이 기획 의도와 합치하는지 확인해주세요.backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
4-4: 테스트 픽스처 임포트 정리 LGTM불필요한 의존 제거로 테스트 유지보수성이 좋아졌습니다.
#️⃣연관된 이슈
📝작업 내용
Summary by CodeRabbit