Skip to content

Comments

[release] 모아동 BE ver 1.0.3#503

Merged
Due-IT merged 159 commits intomainfrom
develop/be
Jun 6, 2025
Merged

[release] 모아동 BE ver 1.0.3#503
Due-IT merged 159 commits intomainfrom
develop/be

Conversation

@Zepelown
Copy link
Member

@Zepelown Zepelown commented Jun 6, 2025

#️⃣연관된 이슈

📝작업 내용

  • 지원서 제작 및 지원 기능 추가
  • 이미지 처리 방식을 cloud flare 이용
  • 테스트 코드 일부분 작성
  • 스케줄러가 올바르게 기간을 처리하지 못하던 문제 해결

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 신규 기능

    • 동아리 지원서 생성, 수정, 조회, 지원 기능이 추가되었습니다.
    • 동아리 지원서 양식 및 질문 관리 기능이 도입되었습니다.
    • 동아리 지원서 제출 시 필수 응답, 답변 길이 등 유효성 검사가 적용됩니다.
    • Google Drive, AWS S3, Cloudflare 등 다양한 이미지 저장소 연동 기능이 추가되었습니다.
    • 동아리 소셜 링크 관리 기능이 추가되었습니다.
    • 동아리 모집 상태 자동 갱신을 위한 스케줄링 기능이 도입되었습니다.
  • 버그 수정

    • 비밀번호에 공백 사용을 금지하고, 유효성 검증이 강화되었습니다.
    • 지원서 제출 시 필수값 누락, 답변 길이 초과 등 다양한 예외 코드가 추가되었습니다.
  • 리팩터

    • 이미지 업로드 및 저장소 관련 서비스 구조가 개선되었습니다.
    • 기존 스케줄러 기반 모집 상태 관리 기능이 제거되고, 새로운 자동 갱신 방식으로 대체되었습니다.
    • 사용자 등록 및 토큰 갱신 로직이 개선되었습니다.
  • 테스트

    • 동아리, 사용자, 이미지, 지원서 등 주요 서비스에 대한 단위 및 통합 테스트가 추가되었습니다.
  • 문서화

    • 각종 유효성 검사 및 규칙에 대한 주석과 TODO가 추가되었습니다.

Due-IT and others added 30 commits May 1, 2025 00:40
- 메트릭이 이미 존재한다면 metric.update()를 호출하는지 검증합니다.
- 일일 활성 사용자수를 정확히 계산하는지 검증합니다.
- MetricFixture를 통해 ClubMetric을 mock객체로 정의하여 수정불가능한 값의 반환값을 직접 정의합니다.
- 일부 Club 정보가 누락되어도 null 포함하여 정상 동작해야 합니다.
- 반드시 1개 이상의 동아리를 조회해야 합니다.
- 하루 미만의 사용자수 요청시 빈배열을 반환합니다.
- ip 중복을 제거하여 카운트 합니다
- 하루 미만의 사용자수 요청시 빈배열을 반환합니다.
- ip 중복을 제거하여 카운트 합니다
- 같은 동아리에 대해 기존 스케줄이 존재하면 취소합니다.
- 모집 상태 업데이트를 성공하여 repository를 통해 저장합니다.
- 존재하지 않는 클럽 id에 대해 업데이트를 요청할시 예외를 발생합니다.
[feature] 구글 드라이브 업로드 기능 구현
@Zepelown Zepelown added ✨ Feature 기능 개발 📬 API 서버 API 통신 작업 💾 BE Backend 📈 release 릴리즈 배포 labels Jun 6, 2025
@netlify
Copy link

netlify bot commented Jun 6, 2025

Deploy Preview for moadong canceled.

Name Link
🔨 Latest commit 4ea79f5
🔍 Latest deploy log https://app.netlify.com/projects/moadong/deploys/6842765d60fc12000870e7a1

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 6, 2025

"""

Walkthrough

이 변경사항은 동아리 지원서(Club Application) 기능의 도메인, 서비스, 컨트롤러, DTO, 저장소 레이어를 대규모로 도입 및 확장합니다. 또한, 이미지 업로드를 위한 Google Drive 및 Cloudflare, S3 연동을 도입하고, 유저 정보 구조를 통합하며, 시간대 일관성을 위해 Asia/Seoul 타임존을 명시적으로 적용합니다. 테스트 인프라와 유닛/통합 테스트, 커스텀 테스트 어노테이션도 추가되었습니다.

Changes

파일/그룹 변경 요약
backend/.gitignore moadong.json 라인 끝에 개행 추가
backend/build.gradle Google Drive, AWS S3, Thumbnailator, 테스트 분리 등 의존성 및 테스트 태스크 구조 변경
backend/src/main/java/moadong/MoadongApplication.java @EnableScheduling 추가로 스케줄링 활성화
backend/src/main/java/moadong/club/controller/ClubApplyController.java 동아리 지원서 REST 컨트롤러 신설 (생성, 수정, 조회, 지원 엔드포인트)
backend/src/main/java/moadong/club/entity/Club.java
.../ClubRecruitmentInformation.java
.../ClubMetric.java
socialLinks 필드 추가, 모집상태 업데이트 메서드 추가, 타임존 명시, 불필요 필드 및 스케줄링 로직 제거, null 체크 보강
backend/src/main/java/moadong/club/entity/ClubApplication.java
.../ClubApplicationQuestion.java
.../ClubQuestion.java
.../ClubQuestionAnswer.java
.../ClubQuestionItem.java
.../ClubQuestionOption.java
지원서, 질문, 답변 등 지원서 도메인 엔티티 신설 및 빌더/게터 등 롬복 적용
backend/src/main/java/moadong/club/enums/ApplicationStatus.java
.../ClubApplicationQuestionType.java
지원서 상태, 질문 타입 enum 신설
backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java
.../payload/request/ClubInfoRequest.java
recruitmentForm → socialLinks 필드로 변경
backend/src/main/java/moadong/club/payload/request/ClubApplicationCreateRequest.java
.../ClubApplicationEditRequest.java
.../ClubApplyQuestion.java
.../ClubApplyRequest.java
지원서 생성/수정/질문/답변 요청 DTO 신설 및 검증 어노테이션 적용
backend/src/main/java/moadong/club/payload/response/ClubApplicationResponse.java 지원서 조회 응답 DTO 신설
backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java
.../ClubQuestionRepository.java
MongoRepository 기반 지원서 및 질문 저장소 신설
backend/src/main/java/moadong/club/service/ClubApplyService.java 지원서 생성/수정/조회/지원 서비스 신설, 검증 및 권한 체크 포함
backend/src/main/java/moadong/club/service/ClubMetricService.java 타임존 명시, 입력값 검증 강화, 랭킹/활성유저 조회시 예외처리 보강
backend/src/main/java/moadong/club/service/ClubProfileService.java 스케줄러 연동 제거, 포맷팅 및 불필요 의존성 제거
backend/src/main/java/moadong/club/service/RecruitmentScheduler.java
.../global/config/SchedulerConfig.java
모집 스케줄러 및 관련 스케줄러 설정 클래스 삭제
backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java 모집상태를 주기적으로 업데이트하는 스케줄러 컴포넌트 신설
backend/src/main/java/moadong/global/exception/ErrorCode.java 파일/지원서/질문 관련 신규 에러코드 추가
backend/src/main/java/moadong/global/validator/*.java 패턴 설명 주석, 비밀번호 검증 로직 강화(공백 불가, null/empty false 반환 등), 기타 주석 추가
backend/src/main/java/moadong/media/controller/ClubImageController.java 패키지 이동, cloudflare 서비스 명시적 주입, Swagger 설명 수정
backend/src/main/java/moadong/media/domain/FileType.java
.../dto/FeedUpdateRequest.java
패키지 이동
backend/src/main/java/moadong/media/service/CloudflareImageService.java
.../GcsClubImageService.java
.../GoogleDriveClubImageService.java
.../ClubImageService.java
Cloudflare, GCS, Google Drive 기반 이미지 서비스 구현 및 인터페이스 신설, 파일 업로드/삭제/피드 관리 로직 구현
backend/src/main/java/moadong/media/util/ClubImageUtil.java
.../GoogleDriveConfig.java
.../S3Config.java
이미지 파일 유틸리티, Google Drive/S3 설정 클래스 신설
backend/src/main/java/moadong/user/entity/User.java
.../UserInformation.java
유저 정보 필드 통합 및 도큐먼트/필드 구조 변경, 생성자 추가
backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java 검증 어노테이션 강화, 비밀번호/아이디 동일시 예외, 패스워드 인코딩 방식 변경
backend/src/main/java/moadong/user/repository/UserInformationRepository.java 유저 정보 저장소 삭제
backend/src/main/java/moadong/user/service/UserCommandService.java 유저 등록 반환타입 변경, 리프레시 토큰 처리, UserInformation 저장 로직 제거
backend/src/test/java/moadong/club/fixture/ClubFixture.java
.../MetricFixture.java
.../service/ClubMetricServiceTest.java
.../service/ClubSearchServiceTest.java
.../service/RecruitmentStateCheckerTest.java
동아리/메트릭 테스트 픽스처, 서비스 테스트, 모집상태 스케줄러 테스트 등 신설
backend/src/test/java/moadong/fixture/*.java 동아리/유저 관련 테스트 픽스처 신설
backend/src/test/java/moadong/media/service/GoogleDriveClubImageServiceFeedTest.java
.../GoogleDriveClubImageServiceLogoTest.java
Google Drive 이미지 서비스 피드/로고 테스트 신설
backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java 동아리 프로필 서비스 유닛 테스트 신설
backend/src/test/java/moadong/unit/user/PasswordValidatorTest.java
.../UserLoginTest.java
.../UserRegisterTest.java
비밀번호, 로그인, 회원가입 유닛 테스트 신설
backend/src/test/java/moadong/util/annotations/*.java @UnitTest, @IntegrationTEST 커스텀 테스트 어노테이션 및 상수 신설

Sequence Diagram(s)

sequenceDiagram
actor User
participant ClubApplyController
participant ClubApplyService
participant ClubQuestionRepository
participant ClubApplicationRepository
participant ClubRepository

User->>ClubApplyController: POST /api/club/{clubId}/application (생성)
ClubApplyController->>ClubApplyService: createClubApplication(clubId, user, request)
ClubApplyService->>ClubRepository: findById(clubId)
alt 권한 체크
  ClubApplyService->>ClubQuestionRepository: findByClubId(clubId)
  alt 기존 폼 존재
    ClubApplyService->>ClubQuestionRepository: save(업데이트)
  else 신규 폼
    ClubApplyService->>ClubQuestionRepository: save(신규)
  end
end
ClubApplyService-->>ClubApplyController: 성공 응답

User->>ClubApplyController: POST /api/club/{clubId}/apply (지원)
ClubApplyController->>ClubApplyService: applyToClub(clubId, request)
ClubApplyService->>ClubQuestionRepository: findByClubId(clubId)
ClubApplyService->>ClubApplicationRepository: save(지원서)
ClubApplyService-->>ClubApplyController: 성공 응답
Loading

Possibly related PRs

  • Moadong/moadong#406: 동아리 지원서 도메인, 서비스, 컨트롤러, DTO, 저장소 등 본 PR과 동일한 지원서 기능 및 파일 구조를 도입
  • Moadong/moadong#369: Google Drive 기반 이미지 업로드 서비스 및 관련 설정, 유틸리티, 인터페이스 도입
  • Moadong/moadong#401: 모집상태 스케줄링 로직 삭제 및 RecruitmentStateChecker 도입, Club 엔티티 모집상태 업데이트 메서드 추가 등 스케줄링 구조 관련

Suggested labels

✅ Test

Suggested reviewers

  • Due-IT
  • PororoAndFriends
    """
✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

수고하셨습니다

Copy link
Collaborator

@PororoAndFriends PororoAndFriends left a comment

Choose a reason for hiding this comment

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

통합테스트, 지원하기 기능, cloudflare로 이미지서버 이동 등 성공적인 1.0.3버전이 된 것 같습니다 ㅎㅎ

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 37

🔭 Outside diff range comments (2)
backend/src/main/java/moadong/user/entity/UserInformation.java (1)

15-20: 💡 Verification agent

🧩 Analysis chain

엔티티 구조 변경을 검증해주세요.

AI 요약에 따르면 @Document 어노테이션이 제거되고 userId 필드가 삭제되었습니다. UserInformation이 이제 User 엔티티에 임베디드되는 구조로 변경된 것 같습니다. 이런 구조 변경이 기존 데이터와 호환되는지, 그리고 모든 관련 코드가 업데이트되었는지 확인이 필요합니다.

다음 스크립트로 관련 변경사항을 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# UserInformation 사용처 및 관련 변경사항 확인

# UserInformation을 사용하는 모든 파일 검색
rg -A 5 -B 5 "UserInformation" --type java

# User 엔티티에서 UserInformation 필드 확인
rg -A 10 "class User" --type java

# UserInformationRepository 관련 코드 확인
rg "UserInformationRepository" --type java

Length of output: 19148


UserInformation 임베디드 구조 변경 확인 및 DB 마이그레이션 준비 필요

  • UserInformation이 독립 컬렉션이 아닌 User 엔티티에 임베디드로 포함되면서
    UserInformationRepository 파일과 @Document 어노테이션이 제거됨
    • User 클래스의 @Field("userInformation") private UserInformation userInformation; 으로 매핑
  • UserRegisterRequest#toUserEntity, UserCommandService.registerUser, 관련 테스트(UserRegisterTest, UserFixture) 등에서 새로운 구조를 모두 참조하도록 변경 완료
  • 운영 중인 MongoDB에 기존 UserInformation 도큐먼트가 남아 있다면, 호환성을 위해 데이터 마이그레이션 스크립트 또는 전환 계획을 별도로 수립·검증해주세요
backend/src/main/java/moadong/media/service/GcsClubImageService.java (1)

131-131: 🛠️ Refactor suggestion

⚠️ Potential issue

하드코딩된 배열 인덱스 사용은 위험합니다.

경로 형식이 변경되면 ArrayIndexOutOfBoundsException이 발생할 수 있습니다. 더 안전한 경로 파싱 방법을 사용하세요.

-String fileType = filePath.split("/")[5];
+String[] parts = filePath.split("/");
+if (parts.length < 6) {
+    throw new RestApiException(ErrorCode.INVALID_FILE_PATH);
+}
+String fileType = parts[5];

splitPath 메서드도 동일하게 개선이 필요합니다:

 private String splitPath(String path) {
-    // https://storage.googleapis.com/{bucketName}/{clubId}/{fileType}/{filePath} -> {filePath}
-    return path.split("/",5)[4];
+    String[] parts = path.split("/", 5);
+    if (parts.length < 5) {
+        throw new RestApiException(ErrorCode.INVALID_FILE_PATH);
+    }
+    return parts[4];
 }

Also applies to: 154-157

🧹 Nitpick comments (45)
backend/src/main/java/moadong/global/validator/KoreanValidator.java (1)

10-11: TODO 테스트 코드 작성
주석에 명시된 검증 로직(1~10자 한글, 자음/모음만 있는 경우 제외)을 검증하는 단위 테스트가 필요합니다. 가능하다면 이슈 번호나 작성 기한을 함께 주석에 명시해주세요.

backend/src/main/java/moadong/global/validator/UserIdValidator.java (1)

10-10: 정규식 설명 주석 추가
설명이 명확하게 작성되어 가독성이 향상되었습니다. 향후 테스트 커버리지 확보를 위해 TODO 주석 추가도 고려해 보세요.

backend/src/main/java/moadong/user/entity/User.java (1)

54-56: userInformation 필드 매핑 확인
User 엔티티에 userInformation을 임베딩으로 추가했습니다.

  • 해당 필드가 필수인지 검증이 필요하다면 @NotNull 또는 @Builder.Default 적용을 고려하세요.
  • 서비스 계층에서 생성·업데이트 시 userInformation을 누락 없이 설정하는지 확인해주세요.
backend/src/main/java/moadong/global/validator/PhoneNumberValidator.java (1)

10-15: 문서화 개선이 좋습니다!

정규식 패턴에 대한 명확한 설명이 추가되어 코드 이해도가 향상되었습니다. 휴대폰 번호와 지역번호 검증 로직이 잘 설명되어 있습니다.

TODO 항목에 대해 테스트 코드 작성을 도와드릴까요? 다양한 전화번호 형식에 대한 테스트 케이스를 생성할 수 있습니다.

backend/src/main/java/moadong/club/entity/ClubQuestionOption.java (2)

12-12: Boolean 대신 boolean 원시 타입 사용 고려.

required 필드가 null을 허용할 필요가 없다면 Boolean 대신 boolean 원시 타입을 사용하는 것을 고려해보세요. 이는 null pointer exception을 방지하고 메모리 효율성을 높입니다.

-    private Boolean required;
+    private boolean required;

10-14: 향후 확장 가능성을 위한 설계 고려사항.

현재는 required 필드만 있지만, 향후 질문 옵션이 확장될 가능성을 고려하여 다른 옵션들(예: maxLength, minLength, allowMultiple 등)을 추가할 수 있는 구조로 설계되어 있습니다.

backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (1)

6-6: 향후 커스텀 쿼리 메서드 추가 고려.

현재는 기본 CRUD 작업만 제공하지만, 비즈니스 요구사항에 따라 다음과 같은 커스텀 쿼리 메서드가 필요할 수 있습니다:

  • findByQuestionId(String questionId)
  • findByStatusAndQuestionId(ApplicationStatus status, String questionId)
  • countByQuestionIdAndStatus(String questionId, ApplicationStatus status)
backend/src/main/java/moadong/club/enums/ClubApplicationQuestionType.java (1)

7-13: JavaDoc 문서화를 고려해보세요.

각 enum 값에 대한 간단한 설명을 추가하면 코드의 가독성과 유지보수성이 향상될 수 있습니다.

public enum ClubApplicationQuestionType {
+    /** 단일 선택 질문 */
    CHOICE,
+    /** 다중 선택 질문 */
    MULTI_CHOICE,
+    /** 짧은 텍스트 입력 */
    SHORT_TEXT,
+    /** 긴 텍스트 입력 */
    LONG_TEXT,
+    /** 전화번호 입력 */
    PHONE_NUMBER,
+    /** 이메일 입력 */
    EMAIL,
+    /** 이름 입력 */
    NAME;
}
backend/src/test/java/moadong/club/fixture/ClubFixture.java (1)

10-15: Builder 패턴 적용을 고려해보세요.

더 많은 필드를 설정해야 하는 경우를 위해 Builder 패턴을 적용하면 확장성이 좋아집니다.

public class ClubFixture {
    
    public static Club createClub(String clubId, String name) {
        Club club = mock(Club.class);
        when(club.getId()).thenReturn(clubId);
        when(club.getName()).thenReturn(name);
        return club;
    }
    
+    public static ClubBuilder builder() {
+        return new ClubBuilder();
+    }
+    
+    public static class ClubBuilder {
+        private String clubId = "default-id";
+        private String name = "default-name";
+        
+        public ClubBuilder clubId(String clubId) {
+            this.clubId = clubId;
+            return this;
+        }
+        
+        public ClubBuilder name(String name) {
+            this.name = name;
+            return this;
+        }
+        
+        public Club build() {
+            return createClub(clubId, name);
+        }
+    }
}
backend/src/main/java/moadong/global/validator/PasswordValidator.java (1)

10-11: 정규식 패턴에 대한 문서화를 추가해보세요.

복잡한 정규식에 대한 주석이나 JavaDoc을 추가하면 다른 개발자들이 이해하기 쉬워집니다.

public class PasswordValidator implements ConstraintValidator<Password, String> {
+    /**
+     * 비밀번호 정규식 패턴
+     * - 영문자 포함 (대소문자)
+     * - 숫자 포함 (0-9)
+     * - 특수문자 포함 (!@#$%^)
+     * - 공백 문자 금지
+     * - 8-20자 길이
+     */
    private static final String REGEX = "^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^])(?!.*\\s).{8,20}$";
    private static final Pattern PASSWORD_PATTERN = Pattern.compile(REGEX);
backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java (1)

10-18: 검증 로직은 적절하나 questions 리스트 크기 제한 추가 고려

record 사용과 기본적인 검증 어노테이션들이 잘 적용되어 있습니다. 다만 questions 리스트에 크기 제한이 없어 잠재적 성능 이슈나 DoS 공격에 취약할 수 있습니다.

다음과 같은 수정을 고려해보세요:

 @NotNull
 @Valid
+@Size(max = 50)
 List<ClubApplyQuestion> questions
backend/src/main/java/moadong/club/service/ClubMetricService.java (1)

33-33: 타임존 상수화 권장

동일한 타임존 문자열이 여러 메소드에서 반복 사용되고 있습니다. 유지보수성 향상을 위해 클래스 상수로 추출하는 것을 권장합니다.

+private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul");

 public void patch(String clubId, String remoteAddr) {
-    LocalDate nowDate = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDate();
+    LocalDate nowDate = ZonedDateTime.now(SEOUL_ZONE).toLocalDate();
backend/src/main/java/moadong/club/entity/ClubQuestionAnswer.java (1)

12-14: 필드명 가독성 개선 제안
id가 질문 식별자인 경우 questionId 등으로 명시하면 의도가 더 명확해집니다. 변경이 부담스러우시면 현 상태도 무방하지만, 향후 유지보수를 고려하신다면 리네이밍을 검토해주세요.

backend/src/main/java/moadong/club/payload/request/ClubApplyRequest.java (2)

8-12: questions 리스트 최소 사이즈 검증 추가 제안
비어 있는 리스트로 요청할 경우 의미 없는 요청이 발생할 수 있습니다.

    @NotNull
+   @Size(min = 1)
    @Valid
    List<Answer> questions

13-18: id 유효성 강화 – 양수 제약 추가 제안
id가 음수일 경우 잘못된 요청이 처리될 수 있으므로 @Positive를 추가해 주세요.

     @NotNull
+    @Positive
     Long id,
     @NotNull
     String value
backend/src/main/java/moadong/club/payload/request/ClubApplicationCreateRequest.java (1)

15-18: questions 리스트 최소 개수 검증 추가 제안
질문이 없는 신청서는 무의미하므로 최소 1개 이상 존재함을 보장하도록 @Size(min = 1) 또는 @NotEmpty를 추가하세요.

     @NotNull
+    @Size(min = 1)
     @Valid
     List<ClubApplyQuestion> questions
backend/src/main/java/moadong/media/util/S3Config.java (3)

3-4: 사용되지 않는 import 제거
com.google.storage.v2.ListBucketsResponsejakarta.annotation.PostConstruct가 사용되지 않으므로 제거하여 코드 클린업을 권장합니다.


18-26: 필드 주입 → 생성자 주입 전환 제안
@Configuration 클래스에서는 생성자 주입이 더 안전하며 테스트 편의성이 높아집니다. 다음과 같이 변경을 고려해주세요.

-public class S3Config {
-    @Value("${cloud.aws.credentials.accessKey}")
-    private String accessKey;
-    @Value("${cloud.aws.credentials.secretKey}")
-    private String secretKey;
-    @Value("${cloud.aws.s3.endpoint}")
-    private String endpoint;
+public class S3Config {
+    private final String accessKey;
+    private final String secretKey;
+    private final String endpoint;
+
+    public S3Config(
+        @Value("${cloud.aws.credentials.accessKey}") String accessKey,
+        @Value("${cloud.aws.credentials.secretKey}") String secretKey,
+        @Value("${cloud.aws.s3.endpoint}") String endpoint
+    ) {
+        this.accessKey = accessKey;
+        this.secretKey = secretKey;
+        this.endpoint = endpoint;
+    }

29-33: Region 하드코딩 방지 – 설정 외부화 권장
현재 Region.US_EAST_1이 코드에 고정되어 있어, 다른 리전을 사용해야 할 경우 코드를 수정해야 합니다.
프로퍼티로 분리하는 것을 제안합니다.

-    .region(Region.US_EAST_1)
+    .region(Region.of(region))

그리고 클래스에

@Value("${cloud.aws.region}") 
private String region;

등을 추가해주세요.

backend/src/main/java/moadong/club/entity/ClubQuestion.java (2)

44-47: 리스트 업데이트 메서드의 효율성을 개선하세요.

현재 clear()addAll() 방식은 불필요한 메모리 할당을 발생시킬 수 있습니다. 새로운 리스트로 직접 교체하는 것이 더 효율적입니다.

public void updateQuestions(List<ClubApplicationQuestion> newQuestions) {
-    this.questions.clear();
-    this.questions.addAll(newQuestions);
+    this.questions = new ArrayList<>(newQuestions);
}

49-51: 타임존 로직을 중앙화하세요.

반복되는 타임존 로직을 유틸리티 메서드로 분리하여 코드 중복을 줄이고 일관성을 개선할 수 있습니다.

+private LocalDateTime getCurrentSeoulTime() {
+    return ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
+}

public void updateEditedAt() {
-   this.editedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
+   this.editedAt = getCurrentSeoulTime();
}
backend/src/test/java/moadong/fixture/UserRequestFixture.java (1)

7-12: 코드 포맷팅을 개선하세요.

메서드 사이에 일관된 공백을 추가하여 가독성을 향상시킬 수 있습니다.

public static UserRegisterRequest createUserRegisterRequest(String userId, String password, String name, String phoneNumber) {
    return new UserRegisterRequest(userId, password, name, phoneNumber);
}
+
public static UserLoginRequest createUserLoginRequest(String userId, String password){
    return new UserLoginRequest(userId,password);
}
backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java (1)

38-66: null 안전성 처리가 잘 되어 있지만 코드 중복을 개선할 수 있습니다.

모든 필드에 대해 null 체크와 기본값 설정이 잘 되어 있습니다. 하지만 반복적인 null 체크 패턴이 많아 가독성이 떨어집니다.

다음과 같이 헬퍼 메서드를 추가하여 중복을 줄일 수 있습니다:

public static ClubDetailedResult of(Club club) {
    String period = "미정";
    ClubRecruitmentInformation clubRecruitmentInformation = club.getClubRecruitmentInformation();
    if (clubRecruitmentInformation.hasRecruitmentPeriod()) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm");
        period = clubRecruitmentInformation.getRecruitmentStart().format(formatter) + " ~ "
            + clubRecruitmentInformation.getRecruitmentEnd().format(formatter);
    }
    return ClubDetailedResult.builder()
-            .id(club.getId() == null ? "" : club.getId())
-            .name(club.getName() == null ? "" : club.getName())
+            .id(orDefault(club.getId(), ""))
+            .name(orDefault(club.getName(), ""))
            // ... 다른 필드들도 동일하게 적용
            .build();
}

+private static String orDefault(String value, String defaultValue) {
+    return value == null ? defaultValue : value;
+}
+
+private static <T> List<T> orDefault(List<T> value, List<T> defaultValue) {
+    return value == null ? defaultValue : value;
+}
+
+private static <K, V> Map<K, V> orDefault(Map<K, V> value, Map<K, V> defaultValue) {
+    return value == null ? defaultValue : value;
+}
backend/src/main/java/moadong/club/enums/ApplicationStatus.java (1)

3-17: 동아리 지원 프로세스를 잘 표현한 enum입니다.

ApplicationStatus enum은 지원서 작성부터 최종 결과까지의 전체 라이프사이클을 포괄적으로 다루고 있습니다. 각 상태에 대한 한국어 주석이 비즈니스 로직을 이해하는 데 도움이 됩니다.

상태 전환 로직이 복잡할 수 있으니, 서비스 레이어에서 유효한 상태 전환만 허용하도록 검증 로직을 구현하는 것을 권장합니다.

backend/src/main/java/moadong/club/entity/ClubRecruitmentInformation.java (1)

82-82: 타임존 설정을 외부화하는 것을 고려해보세요.

ZoneId.of("Asia/Seoul")이 하드코딩되어 있습니다. 타임존 설정을 application.properties나 상수로 외부화하면 유지보수성과 테스트 용이성이 향상됩니다.

다음과 같이 상수로 분리하는 것을 권장합니다:

+    private static final ZoneId SEOUL_ZONE = ZoneId.of("Asia/Seoul");
+
     public ZonedDateTime getRecruitmentStart() {
-        ZoneId seoulZone = ZoneId.of("Asia/Seoul");
         if (recruitmentStart == null) {
             return null;
         }
-        return recruitmentStart.atZone(seoulZone);
+        return recruitmentStart.atZone(SEOUL_ZONE);
     }
 
     public ZonedDateTime getRecruitmentEnd() {
-        ZoneId seoulZone = ZoneId.of("Asia/Seoul");
         if (recruitmentEnd == null) {
             return null;
         }
-        return recruitmentEnd.atZone(seoulZone);
+        return recruitmentEnd.atZone(SEOUL_ZONE);
     }

Also applies to: 90-90

backend/src/test/java/moadong/fixture/UserFixture.java (1)

10-13: 변수명 오타 수정이 필요합니다.

상수 필드명에 "collect" 대신 "correct"를 사용하는 것이 적절해 보입니다. 현재 명명이 의도한 의미와 일치하는지 확인해주세요.

-    public static final String collectUserId = "test12345";
-    public static final String collectPassword = "test12345@";
-    public static final String collectName = "테스터";
-    public static final String collectPhoneNumber = "010-1234-5678";
+    public static final String correctUserId = "test12345";
+    public static final String correctPassword = "test12345@";
+    public static final String correctName = "테스터";
+    public static final String correctPhoneNumber = "010-1234-5678";
backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java (2)

17-19: description 필드의 검증 로직을 명확히 해주세요.

description 필드에 "빈칸 허용" 주석이 있지만 @NotNull 어노테이션이 적용되어 있습니다. 빈 문자열을 허용하려면 @NotBlank 대신 @NotNull을 사용하는 것이 맞지만, 의도를 더 명확히 하기 위해 검증 로직을 재검토해주세요.

만약 빈 문자열을 허용한다면:

@NotNull //빈칸 허용
String description,

또는 검증을 더 엄격하게 하려면:

-@NotNull //빈칸 허용
+@NotBlank
String description,

33-37: QuestionItem의 value 필드명을 더 구체적으로 개선할 수 있습니다.

value라는 필드명이 너무 일반적입니다. optionText, itemContent 등 더 구체적인 이름을 고려해보세요.

public record QuestionItem(
        @NotNull
        @Size(max = 20)
-       String value
+       String optionText
) {}
backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java (2)

18-25: MockitoExtension과 @UnitTest 어노테이션의 중복 사용을 확인하세요.

다른 테스트 클래스에서는 @UnitTest 어노테이션만 사용하고 있는데, 이 클래스는 @ExtendWith(MockitoExtension.class)도 함께 사용하고 있습니다. 프로젝트의 테스트 어노테이션 전략을 일관성 있게 적용하는 것이 좋겠습니다.

-@ExtendWith(MockitoExtension.class)
-class ClubSearchServiceTest {
+@UnitTest
+class ClubSearchServiceTest {

28-55: 정렬 검증 로직을 더 명확하게 개선할 수 있습니다.

현재 assertIterableEquals로 전체 순서를 검증하고 있지만, 정렬 우선순위(ALWAYS > OPEN > CLOSED > UPCOMING)를 더 명시적으로 테스트하는 것이 좋겠습니다.

// then
List<ClubSearchResult> sorted = response.clubs();
-assertIterableEquals(sorted, List.of(club4, club1, club3, club2));
+// 모집상태 우선순위 검증: ALWAYS > OPEN > CLOSED > UPCOMING
+assertEquals("ALWAYS", sorted.get(0).recruitmentStatus());
+assertEquals("OPEN", sorted.get(1).recruitmentStatus());
+assertEquals("CLOSED", sorted.get(2).recruitmentStatus());
+assertEquals("UPCOMING", sorted.get(3).recruitmentStatus());
backend/src/test/java/moadong/media/service/GoogleDriveClubImageServiceFeedTest.java (3)

38-40: @SPY@Injectmocks 동시 사용을 검토하세요.

@SPY@Injectmocks를 함께 사용하는 경우 예상치 못한 동작이 발생할 수 있습니다. 특히 doNothing().when(clubImageService).deleteFile()에서 실제 메서드 호출을 막고 있는데, 이것이 테스트의 의도인지 확인이 필요합니다.

만약 실제 메서드 호출을 막아야 한다면 @mock을 사용하는 것이 더 명확합니다:

-@Spy
-@InjectMocks
-private GoogleDriveClubImageService clubImageService;
+@Mock
+private GoogleDriveClubImageService clubImageService;

57-65: @beforeeach에서 ReflectionTestUtils 사용을 개선하세요.

매 테스트마다 ReflectionTestUtils로 필드를 설정하는 것보다, @value나 @TestPropertySource를 사용하는 것이 더 깔끔합니다.

-@BeforeEach
-void setUp() {
-    // ... existing setup code
-    ReflectionTestUtils.setField(clubImageService, "MAX_FEED_COUNT", 5);
-}

+@TestPropertySource(properties = {"club.feed.max-count=5"})
+// 또는 application-test.yml 에서 설정

69-82: 테스트 메서드간 독립성을 개선하세요.

이 테스트에서 club 객체를 다시 생성하고 있는데, @beforeeach에서 이미 생성된 객체와 중복됩니다. 각 테스트가 독립적으로 실행되도록 개선하는 것이 좋겠습니다.

@Test
void MAX_FEED_COUNT_이상의_피드를_업로드하면_TOO_MANY_FILES를_반환한다() {
    // given
    List<String> feedImages = Arrays.asList(new String[MAX_FEED_COUNT]);
    ClubRecruitmentInformation info = ClubRecruitmentInformation.builder()
            .feedImages(feedImages)
            .build();
-    club = Club.builder().clubRecruitmentInformation(info).build();
+    Club clubWithMaxFeeds = Club.builder().clubRecruitmentInformation(info).build();
-    when(clubRepository.findClubById(any())).thenReturn(Optional.of(club));
+    when(clubRepository.findClubById(any())).thenReturn(Optional.of(clubWithMaxFeeds));
backend/src/main/java/moadong/media/controller/ClubImageController.java (2)

29-33: 필드와 생성자의 순서를 Java 컨벤션에 맞게 개선하세요.

일반적으로 Java에서는 필드를 생성자보다 먼저 선언하는 것이 컨벤션입니다.

public class ClubImageController {

+    private final ClubImageService clubImageService;
+
    public ClubImageController(@Qualifier("cloudflare") ClubImageService clubImageService) {
        this.clubImageService = clubImageService;
    }

-    private final ClubImageService clubImageService;

29-31: @qualifier 사용이 적절합니다.

여러 ClubImageService 구현체 중에서 "cloudflare" 구현체를 명시적으로 선택하는 것이 좋은 접근입니다. 하지만 향후 구현체 변경시를 대비해 설정 파일로 externalize하는 것도 고려해볼 수 있습니다.

설정 기반 구현체 선택을 위해 @ConditionalOnProperty@Profile을 고려해보세요:

@Configuration
public class ImageServiceConfig {
    
    @Bean
    @ConditionalOnProperty(name = "club.image.provider", havingValue = "cloudflare")
    public ClubImageService cloudflareImageService() {
        return new CloudflareImageService();
    }
}
backend/src/test/java/moadong/media/service/GoogleDriveClubImageServiceLogoTest.java (1)

54-56: MockMultipartFile 설정을 개선할 수 있습니다.

테스트에서 더 현실적인 파일 데이터를 사용하는 것이 좋겠습니다.

-    private final MultipartFile mockFile = new MockMultipartFile(
-            "testFile", "testFile.jpg", "image/jpeg", "test".getBytes());
+    private final MultipartFile mockFile = new MockMultipartFile(
+            "file", "testFile.jpg", "image/jpeg", "actual image data".getBytes());
backend/src/main/java/moadong/club/entity/ClubApplication.java (1)

37-37: 타임존 설정이 하드코딩되어 있습니다.

타임존 정보를 설정 파일이나 상수로 관리하는 것이 좋겠습니다.

-LocalDateTime createdAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();
+LocalDateTime createdAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime();

애플리케이션 전체에서 사용할 타임존 상수를 정의하는 것을 권장합니다.

backend/src/main/java/moadong/media/service/GcsClubImageService.java (1)

147-147: 주석이 실제 검증 로직과 일치하지 않습니다.

containsInvalidChars는 한글뿐만 아니라 퍼센트 인코딩된 문자와 공백도 검증합니다. 주석을 업데이트하세요.

-// 한글이 포함된 파일 이름일 경우 랜덤 영어 문자열로 변환
+// 유효하지 않은 문자(한글, 공백, 퍼센트 인코딩 등)가 포함된 파일명일 경우 랜덤 영어 문자열로 변환
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)

111-124: switch 문에 default case를 추가하세요.

향후 새로운 질문 타입이 추가될 경우를 대비하여 default case를 추가하는 것이 좋습니다.

 private void validateAnswerLength(String value, ClubApplicationQuestionType type) {
     switch (type) {
         case SHORT_TEXT -> {
             if (value.length() > 30) {
                 throw new RestApiException(ErrorCode.SHORT_EXCEED_LENGTH);
             }
         }
         case LONG_TEXT -> {
             if (value.length() > 500) {
                 throw new RestApiException(ErrorCode.LONG_EXCEED_LENGTH);
             }
         }
+        default -> {
+            // 다른 타입은 길이 검증이 필요 없음
+        }
     }
 }
backend/src/main/java/moadong/media/service/CloudflareImageService.java (3)

48-50: 기존 파일 삭제 로직 중복

로고 업로드와 삭제에서 동일한 패턴의 기존 파일 삭제 로직이 반복됩니다.

공통 메서드로 추출하여 코드 중복을 제거할 수 있습니다:

+private void deleteExistingLogo(Club club) {
+    if (club.getClubRecruitmentInformation().getLogo() != null) {
+        deleteFile(club, club.getClubRecruitmentInformation().getLogo());
+    }
+}

Also applies to: 62-64


134-136: 파일명 처리 로직 개선 필요

잘못된 문자가 포함된 경우 원본 파일명을 완전히 버리고 랜덤 문자열로 대체하면 파일 식별이 어려워집니다.

파일 확장자는 보존하는 것이 좋습니다:

if (containsInvalidChars(fileName)) {
-    fileName = RandomStringUtil.generateRandomString(10);
+    String extension = fileName.substring(fileName.lastIndexOf('.'));
+    fileName = RandomStringUtil.generateRandomString(10) + extension;
}

164-166: 포괄적인 예외 처리

모든 Exception을 동일한 에러 코드로 처리하면 디버깅이 어려워집니다.

특정 예외 유형별로 더 구체적인 에러 처리를 고려하세요:

} catch (IOException e) {
    throw new RestApiException(ErrorCode.FILE_TRANSFER_ERROR);
+} catch (software.amazon.awssdk.core.exception.SdkException e) {
+    throw new RestApiException(ErrorCode.CLOUD_SERVICE_ERROR);
} catch (Exception e) {
    throw new RestApiException(ErrorCode.IMAGE_UPLOAD_FAILED);
}
backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)

21-21: 생성자 주입 어노테이션 권장 변경

@AllArgsConstructor 대신 @RequiredArgsConstructor를 사용하여 final 필드만 주입받는 것이 더 안전합니다.

-@AllArgsConstructor
+@RequiredArgsConstructor
backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java (2)

42-54: 코드 중복 제거 필요

uploadLogodeleteLogo 메서드에서 클럽 조회 로직이 중복됩니다.

공통 메서드로 추출하여 중복을 제거할 수 있습니다:

+private Club getClubById(String clubId) {
+    ObjectId objectId = ObjectIdConverter.convertString(clubId);
+    return clubRepository.findClubById(objectId)
+            .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
+}

이를 uploadFeedupdateFeeds 메서드에서도 활용할 수 있습니다.

Also applies to: 57-67


123-125: 파일 null 체크 개선

파일이 null인 경우만 체크하고 isEmpty() 체크가 누락되었습니다.

-if (file == null) {
+if (file == null || file.isEmpty()) {
    throw new RestApiException(ErrorCode.FILE_NOT_FOUND);
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 46ca647 and a5c6a16.

📒 Files selected for processing (68)
  • backend/.gitignore (1 hunks)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/moadong/MoadongApplication.java (1 hunks)
  • backend/src/main/java/moadong/club/controller/ClubApplyController.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/Club.java (4 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 (3 hunks)
  • backend/src/main/java/moadong/club/entity/ClubQuestion.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/ClubQuestionAnswer.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/ClubQuestionItem.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/ClubQuestionOption.java (1 hunks)
  • backend/src/main/java/moadong/club/entity/ClubRecruitmentInformation.java (3 hunks)
  • backend/src/main/java/moadong/club/enums/ApplicationStatus.java (1 hunks)
  • backend/src/main/java/moadong/club/enums/ClubApplicationQuestionType.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java (3 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplicationCreateRequest.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplicationEditRequest.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplyQuestion.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubApplyRequest.java (1 hunks)
  • backend/src/main/java/moadong/club/payload/request/ClubInfoRequest.java (2 hunks)
  • backend/src/main/java/moadong/club/payload/response/ClubApplicationResponse.java (1 hunks)
  • backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (1 hunks)
  • backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java (1 hunks)
  • backend/src/main/java/moadong/club/service/ClubApplyService.java (1 hunks)
  • backend/src/main/java/moadong/club/service/ClubMetricService.java (7 hunks)
  • backend/src/main/java/moadong/club/service/ClubProfileService.java (3 hunks)
  • backend/src/main/java/moadong/club/service/RecruitmentScheduler.java (0 hunks)
  • backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (1 hunks)
  • backend/src/main/java/moadong/global/config/SchedulerConfig.java (0 hunks)
  • backend/src/main/java/moadong/global/exception/ErrorCode.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/controller/ClubImageController.java (2 hunks)
  • backend/src/main/java/moadong/media/domain/FileType.java (1 hunks)
  • backend/src/main/java/moadong/media/dto/FeedUpdateRequest.java (1 hunks)
  • backend/src/main/java/moadong/media/service/CloudflareImageService.java (1 hunks)
  • backend/src/main/java/moadong/media/service/ClubImageService.java (1 hunks)
  • backend/src/main/java/moadong/media/service/GcsClubImageService.java (6 hunks)
  • backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java (1 hunks)
  • backend/src/main/java/moadong/media/util/ClubImageUtil.java (1 hunks)
  • backend/src/main/java/moadong/media/util/GoogleDriveConfig.java (1 hunks)
  • backend/src/main/java/moadong/media/util/S3Config.java (1 hunks)
  • backend/src/main/java/moadong/user/entity/User.java (1 hunks)
  • backend/src/main/java/moadong/user/entity/UserInformation.java (1 hunks)
  • backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java (1 hunks)
  • backend/src/main/java/moadong/user/repository/UserInformationRepository.java (0 hunks)
  • backend/src/main/java/moadong/user/service/UserCommandService.java (5 hunks)
  • backend/src/test/java/moadong/club/fixture/ClubFixture.java (1 hunks)
  • backend/src/test/java/moadong/club/fixture/MetricFixture.java (1 hunks)
  • backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java (1 hunks)
  • backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java (1 hunks)
  • backend/src/test/java/moadong/club/service/RecruitmentStateCheckerTest.java (1 hunks)
  • backend/src/test/java/moadong/fixture/ClubFixture.java (1 hunks)
  • backend/src/test/java/moadong/fixture/ClubRequestFixture.java (1 hunks)
  • backend/src/test/java/moadong/fixture/UserFixture.java (1 hunks)
  • backend/src/test/java/moadong/fixture/UserRequestFixture.java (1 hunks)
  • backend/src/test/java/moadong/media/service/GoogleDriveClubImageServiceFeedTest.java (1 hunks)
  • backend/src/test/java/moadong/media/service/GoogleDriveClubImageServiceLogoTest.java (1 hunks)
  • backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (1 hunks)
  • backend/src/test/java/moadong/unit/user/PasswordValidatorTest.java (1 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/TestTypeConstants.java (1 hunks)
  • backend/src/test/java/moadong/util/annotations/UnitTest.java (1 hunks)
💤 Files with no reviewable changes (3)
  • backend/src/main/java/moadong/global/config/SchedulerConfig.java
  • backend/src/main/java/moadong/user/repository/UserInformationRepository.java
  • backend/src/main/java/moadong/club/service/RecruitmentScheduler.java
🧰 Additional context used
🧠 Learnings (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)
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.
🧬 Code Graph Analysis (6)
backend/src/main/java/moadong/club/service/ClubProfileService.java (1)
backend/src/main/java/moadong/global/util/ObjectIdConverter.java (1)
  • ObjectIdConverter (7-17)
backend/src/test/java/moadong/unit/user/UserLoginTest.java (1)
backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1)
  • UnitTest (37-174)
backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java (2)
backend/src/test/java/moadong/club/fixture/ClubFixture.java (1)
  • ClubFixture (8-17)
backend/src/test/java/moadong/club/fixture/MetricFixture.java (1)
  • MetricFixture (9-31)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)
backend/src/main/java/moadong/club/entity/ClubQuestionAnswer.java (1)
  • AllArgsConstructor (7-16)
backend/src/main/java/moadong/club/entity/ClubQuestionOption.java (1)
backend/src/main/java/moadong/club/entity/ClubQuestionItem.java (1)
  • AllArgsConstructor (7-14)
backend/src/main/java/moadong/media/service/GcsClubImageService.java (3)
backend/src/main/java/moadong/media/util/ClubImageUtil.java (1)
  • ClubImageUtil (13-62)
backend/src/main/java/moadong/global/util/ObjectIdConverter.java (1)
  • ObjectIdConverter (7-17)
backend/src/main/java/moadong/global/util/RandomStringUtil.java (1)
  • RandomStringUtil (5-22)
🔇 Additional comments (75)
backend/.gitignore (1)

41-41: .gitignore 파일 끝에 개행 추가
파일 마지막에 개행을 추가하여 POSIX 표준을 준수하고 Git 무시 규칙이 올바르게 적용되도록 합니다.

backend/src/main/java/moadong/media/dto/FeedUpdateRequest.java (1)

1-1: 패키지 재배치 확인
FeedUpdateRequestmoadong.media.dto로 이동되었습니다. 관련 import 및 사용처가 모두 업데이트되었는지 검증해 주세요.

backend/src/main/java/moadong/media/domain/FileType.java (1)

1-1: 패키지 경로 일관성 유지
FileTypemoadong.media.domain으로 이동되어 미디어 처리 컴포넌트와의 일관성을 확보했습니다. 다른 참조들이 모두 변경되었는지 확인해 주세요.

backend/src/main/java/moadong/MoadongApplication.java (1)

7-7: 스케줄링 기능 활성화가 올바르게 구현되었습니다.

@EnableScheduling 어노테이션과 해당 import가 적절히 추가되어 Spring의 스케줄링 기능이 활성화됩니다. 새로운 RecruitmentStateChecker 컴포넌트를 지원하기 위한 변경으로 보입니다.

Also applies to: 11-11

backend/src/main/java/moadong/club/payload/response/ClubApplicationResponse.java (1)

8-13: 깔끔한 DTO 구현입니다.

record와 @Builder 어노테이션을 활용한 현대적인 Java 스타일의 DTO 구현이 적절합니다. 필드 구성도 동아리 지원서 응답에 적합하며, 불변 객체로 설계되어 안전합니다.

backend/src/main/java/moadong/club/entity/ClubMetric.java (2)

6-7: 타임존 관련 import 추가가 적절합니다.

명시적인 타임존 처리를 위한 ZoneIdZonedDateTime import가 추가되었습니다.


35-35: 타임존 일관성 개선이 훌륭합니다.

시스템 기본 타임존 대신 명시적으로 "Asia/Seoul" 타임존을 사용하도록 변경되어, 서로 다른 환경에서 실행될 때 발생할 수 있는 시간 관련 문제를 방지합니다. 한국 애플리케이션에 적합한 변경사항입니다.

Also applies to: 44-44

backend/src/main/java/moadong/club/payload/request/ClubInfoRequest.java (2)

5-5: Map 임포트 추가 확인됨.

socialLinks 필드를 위한 Map 임포트가 올바르게 추가되었습니다.


20-20:

Details

✅ Verification successful

recruitmentForm에서 socialLinks로의 구조적 변경 확인 필요.

recruitmentForm 필드에서 socialLinks Map으로 변경된 것은 주요한 구조적 변경사항입니다. 이 변경이 코드베이스 전체에서 일관되게 처리되었는지 확인이 필요합니다.


🏁 Script executed:

#!/bin/bash
# 설명: recruitmentForm 필드 사용처를 확인하여 호환성 문제가 없는지 검증

# 테스트: recruitmentForm 사용처 검색. 예상: 모든 사용처가 업데이트되었거나 제거되어야 함
echo "=== recruitmentForm 사용처 검색 ==="
rg -A 3 -B 3 "recruitmentForm"

echo "=== socialLinks 사용처 검색 ==="
rg -A 3 -B 3 "socialLinks"

Length of output: 3543


recruitmentForm → socialLinks 변경 검증 완료

소스 전체에서 recruitmentForm 잔여 참조가 모두 제거되었고, 새로운 socialLinks 필드는 요청(ClubInfoRequest), 엔티티(Club), DTO(ClubDetailedResult)에 일관되게 적용되어 있습니다. 추가 검토나 수정은 필요하지 않습니다.

backend/src/main/java/moadong/club/entity/ClubQuestionOption.java (1)

1-14: ClubQuestionOption 엔티티 구현이 간결하고 적절합니다.

단일 required 필드를 가진 간단한 구조로 현재 요구사항에 적합합니다. Lombok 어노테이션을 통한 불변성과 빌더 패턴 지원도 좋은 설계입니다.

backend/src/test/java/moadong/util/annotations/IntegrationTest.java (3)

1-17: 통합 테스트 어노테이션 구현이 우수합니다.

@SpringBootTest@Tag를 결합한 메타 어노테이션으로 테스트 분류를 효과적으로 지원합니다. TestTypeConstants를 사용한 중앙집중식 상수 관리도 좋은 설계입니다.


11-12: 어노테이션 타겟과 보존 정책이 적절합니다.

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME) 설정이 통합 테스트 어노테이션의 용도에 적합합니다.


14-14:

Details

✅ Verification successful

TestTypeConstants 클래스 존재 확인.

TestTypeConstants.INTEGRATION_TEST 상수가 올바르게 정의되어 있는지 확인이 필요합니다.


🏁 Script executed:

#!/bin/bash
# 설명: TestTypeConstants 클래스와 상수들이 올바르게 정의되었는지 확인

# 테스트: TestTypeConstants 클래스 검색
echo "=== TestTypeConstants 클래스 검색 ==="
fd "TestTypeConstants.java" --exec cat {}

echo "=== INTEGRATION_TEST 상수 사용처 검색 ==="
rg "INTEGRATION_TEST"

Length of output: 771


TestTypeConstants.INTEGRATION_TEST 상수 정의 확인 완료

TestTypeConstants 클래스에 INTEGRATION_TEST = "IntegrationTest" 상수가 올바르게 정의되어 있으며, @Tag(TestTypeConstants.INTEGRATION_TEST) 어노테이션에 정상적으로 적용되어 있습니다. 추가 검토나 수정이 필요하지 않습니다.

backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (2)

1-7: ClubApplicationRepository 구현이 깔끔하고 표준적입니다.

Spring Data MongoDB의 MongoRepository를 확장한 표준적인 구현으로, 기본 CRUD 작업을 효과적으로 지원합니다.


3-3:

Details

✅ Verification successful

ClubApplication 엔티티 존재 확인.

ClubApplication 엔티티가 올바르게 정의되어 있고 MongoDB 문서로 매핑되는지 확인이 필요합니다.


🏁 Script executed:

#!/bin/bash
# 설명: ClubApplication 엔티티가 올바르게 정의되었는지 확인

# 테스트: ClubApplication 엔티티 클래스 검색
echo "=== ClubApplication 엔티티 검색 ==="
fd "ClubApplication.java" --exec cat {}

echo "=== MongoDB Document 어노테이션 확인 ==="
rg "@Document" --type java -A 2 -B 2

Length of output: 3131


ClubApplication 엔티티 매핑 정상 확인

backend/src/main/java/moadong/club/entity/ClubApplication.java@Document("club_applications") 어노테이션이 적용되어 있어 MongoDB 문서와 올바르게 매핑됩니다. 추가 검토 사항 없습니다.

backend/src/main/java/moadong/club/enums/ClubApplicationQuestionType.java (1)

6-14: 동아리 지원서 질문 유형 정의가 잘 되어 있습니다.

각 질문 유형이 명확하고 일반적인 지원서에서 사용되는 타입들을 적절히 포함하고 있습니다. Lombok의 @Getter 사용도 적절합니다.

backend/src/test/java/moadong/club/fixture/ClubFixture.java (1)

8-17: 테스트 픽스처 구현이 깔끔합니다.

Mockito를 사용한 Club 객체 생성이 적절하고, 테스트에서 일관된 데이터 생성을 위한 좋은 패턴입니다.

backend/src/test/java/moadong/util/annotations/TestTypeConstants.java (1)

6-10: 유틸리티 클래스 구현이 모범적입니다.

private 생성자와 static final 상수 사용이 적절하며, 테스트 타입 분류를 위한 중앙집중식 상수 정의가 좋습니다.

backend/src/main/java/moadong/global/validator/PasswordValidator.java (2)

10-11: 정규식 보안 강화가 잘 적용되었습니다.

\d 대신 [0-9]를 사용하여 유니코드 숫자를 방지하고, (?!.*\\s)로 공백 문자를 명시적으로 차단한 것이 보안상 우수합니다. 정규식을 상수로 추출한 것도 유지보수성 향상에 도움이 됩니다.


15-17: null/empty 처리 방식 개선이 적절합니다.

이전에 null이나 빈 문자열을 유효한 것으로 처리했던 것을 false로 반환하도록 변경한 것이 보안상 더 안전합니다.

backend/src/test/java/moadong/util/annotations/UnitTest.java (1)

1-20: 테스트 분류를 위한 깔끔한 커스텀 어노테이션 구현!

단위 테스트와 통합 테스트를 명확히 분리할 수 있는 우수한 설계입니다. MockitoExtension 자동 적용으로 테스트 작성이 더 편리해질 것입니다.

backend/src/main/java/moadong/club/service/ClubMetricService.java (3)

6-7: 타임존 일관성 개선을 위한 적절한 import 추가

아시아/서울 타임존 적용을 위한 필요한 import가 추가되었습니다.


112-114: 입력 검증 로직 추가 - 적절한 개선사항

n <= 0인 경우에 대한 조기 반환 로직이 추가되어 불필요한 처리를 방지합니다.


140-142: 입력 검증 로직 추가 - 적절한 개선사항

n <= 0인 경우에 대한 조기 반환 로직이 추가되어 불필요한 처리를 방지합니다.

backend/src/main/java/moadong/media/service/ClubImageService.java (1)

7-19: 인터페이스 설계가 적절합니다.

클럽 이미지 관리를 위한 명확하고 간결한 인터페이스 설계입니다. 메서드 시그니처가 적절하고 단일 책임 원칙을 잘 따르고 있습니다.

backend/src/main/java/moadong/club/service/ClubProfileService.java (3)

24-33: LGTM! 클럽 생성 로직이 정확합니다.

Club 객체 생성과 저장 로직이 올바르게 구현되어 있습니다. 빌더 패턴 사용과 포맷팅도 깔끔합니다.


42-47: RecruitmentScheduler 의존성 제거가 적절히 처리되었습니다.

스케줄링 로직이 제거되면서 메서드가 단순해졌습니다. AI 요약에 따르면 RecruitmentStateChecker로 주기적 상태 확인 방식으로 변경된 것으로 보입니다.


60-67:

Details

✅ Verification successful

사용자 권한 검증 로직의 보안 취약점을 확인해 주세요.

현재 코드에서는 user.getId().equals(club.getUserId())로 권한을 검증하고 있습니다. 이 비교가 안전한지 확인이 필요합니다.

다음 스크립트로 User ID와 Club의 User ID 타입 일치성을 확인해 주세요:


🏁 Script executed:

#!/bin/bash
# Description: 사용자 ID와 클럽 사용자 ID의 타입 일치성 확인

# CustomUserDetails의 getId() 메서드 반환 타입 확인
ast-grep --pattern $'class CustomUserDetails {
  $$$
  getId() {
    $$$
  }
  $$$
}'

# Club 엔티티의 getUserId() 메서드 반환 타입 확인  
ast-grep --pattern $'class Club {
  $$$
  getUserId() {
    $$$
  }
  $$$
}'

Length of output: 164


아래 스크립트로 CustomUserDetailsClub 클래스 파일 경로를 찾은 뒤, 각각의 getId()·getUserId() 메서드 시그니처를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# CustomUserDetails 파일 경로 조회 및 getId() 메서드 시그니처 확인
CUD_FILE=$(rg -l "class CustomUserDetails" -R backend/src/main/java)
echo "=== CustomUserDetails 파일: $CUD_FILE ==="
rg -n "public .*getId" "$CUD_FILE"

# Club 엔티티 파일 경로 조회 및 getUserId() 메서드 시그니처 확인
CLUB_FILE=$(rg -l "class Club" -R backend/src/main/java)
echo "=== Club 파일: $CLUB_FILE ==="
rg -n "public .*getUserId" "$CLUB_FILE"

Length of output: 1199


아래 스크립트로 재시도하여 CustomUserDetailsClub 클래스 파일 위치를 찾고 각각의 getId()·getUserId() 메서드 시그니처를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# CustomUserDetails 클래스 파일 찾기 및 getId() 시그니처 확인
CUD_FILE=$(rg -l "class CustomUserDetails" backend/src/main/java)
echo "=== CustomUserDetails 파일: $CUD_FILE ==="
rg -n "public .*getId" "$CUD_FILE"

# Club 클래스 파일 찾기 및 getUserId() 시그니처 확인
CLUB_FILE=$(rg -l "class Club" backend/src/main/java)
echo "=== Club 파일: $CLUB_FILE ==="
rg -n "public .*getUserId" "$CLUB_FILE"

Length of output: 7247


아래 스크립트로 CustomUserDetails.javaClub.java 파일을 정확히 찾아 각각의 getId()·getUserId() 메서드 시그니처를 확인해주세요.


🏁 Script executed:

#!/bin/bash
# CustomUserDetails.java 경로 조회 및 getId() 시그니처 확인
CUD_PATH=$(find backend/src/main/java -type f -name CustomUserDetails.java)
echo "=== CustomUserDetails 파일: $CUD_PATH ==="
grep -n "getId" "$CUD_PATH" || echo "getId() 메서드를 찾을 수 없습니다."

# Club.java 경로 조회 및 getUserId() 시그니처 확인
CLUB_PATH=$(find backend/src/main/java -type f -name Club.java)
echo "=== Club 파일: $CLUB_PATH ==="
grep -n "getUserId" "$CLUB_PATH" || echo "getUserId() 메서드를 찾을 수 없습니다."

Length of output: 863


아래 스크립트로 CustomUserDetailsid 필드 타입과 Club 엔티티의 userId 필드 타입을 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# CustomUserDetails의 id 필드 타입 확인
echo "=== CustomUserDetails 필드 목록 ==="
grep -n "private" backend/src/main/java/moadong/user/payload/CustomUserDetails.java

# Club 엔티티의 userId 필드 타입 확인
echo
echo "=== Club 엔티티의 userId 필드 ==="
grep -n "private .*userId" backend/src/main/java/moadong/club/entity/Club.java

Length of output: 495


보안 검증 완료: 사용자 ID 타입 일치 확인됨

user.getId()club.getUserId()가 모두 String 타입으로 일치하므로, 타입 불일치로 인한 보안 취약점은 없습니다.

추가 검토 사항:

  • NPE(NullPointerException) 예방을 위해 Objects.equals(user.getId(), club.getUserId()) 사용을 고려할 수 있습니다.
backend/src/test/java/moadong/unit/user/PasswordValidatorTest.java (1)

15-19: 유효한 패스워드 테스트가 적절합니다.

UserFixture.collectPassword를 사용하여 유효한 패스워드를 테스트하는 방식이 좋습니다.

backend/src/main/java/moadong/club/payload/dto/ClubDetailedResult.java (1)

25-25: socialLinks 필드 추가가 적절합니다.

recruitmentForm에서 socialLinks Map으로 변경된 것은 더 유연한 소셜 미디어 링크 관리를 가능하게 합니다.

backend/src/test/java/moadong/fixture/ClubRequestFixture.java (1)

9-22: 테스트 픽스처가 잘 구성되어 있습니다.

ClubInfoRequest를 위한 유효한 테스트 데이터가 적절히 제공되고 있고, socialLinks Map 사용도 올바릅니다.

backend/src/main/java/moadong/user/entity/UserInformation.java (1)

31-34: 새로운 생성자가 추가되었습니다.

namephoneNumber만을 매개변수로 받는 생성자가 추가되었습니다. 이는 UserInformation이 이제 User 엔티티에 임베디드되는 구조 변경에 맞춰 추가된 것으로 보입니다.

backend/src/main/java/moadong/club/repository/ClubQuestionRepository.java (1)

1-14: 표준적인 Spring Data MongoDB 리포지토리 구현입니다.

ClubQuestionRepository는 Spring Data의 표준 패턴을 잘 따르고 있습니다. findByClubId 메서드는 쿼리 메서드 자동 생성 기능을 활용하여 구현이 깔끔합니다.

backend/src/main/java/moadong/club/entity/ClubRecruitmentInformation.java (2)

81-86: null 안전성이 개선되었습니다.

getRecruitmentStart() 메서드에 null 체크가 추가되어 recruitmentStart가 null일 때 안전하게 처리됩니다. 이는 defensive programming의 좋은 예시입니다.


89-95: null 안전성이 개선되었습니다.

getRecruitmentEnd() 메서드에도 동일한 null 체크가 추가되어 일관성이 유지되었습니다.

backend/src/test/java/moadong/club/fixture/MetricFixture.java (1)

9-31: 테스트 픽스처 구현이 잘 되어 있습니다.

Mockito를 활용한 ClubMetric 픽스처가 깔끔하게 구현되었습니다. 메서드 오버로딩을 통해 다양한 테스트 시나리오를 효과적으로 지원하고 있습니다.

backend/build.gradle (1)

67-92: 테스트 태스크 구성이 잘 개선되었습니다.

단위 테스트와 통합 테스트를 분리한 구성이 매우 좋습니다. 각 태스크에 적절한 설명과 그룹화가 되어 있어 개발자 경험을 향상시킵니다.

태그 기반 테스트 분리 전략이 효과적이며, 향후 CI/CD 파이프라인에서도 유용하게 활용될 수 있습니다.

backend/src/main/java/moadong/club/entity/Club.java (4)

9-9: 필요한 import 추가가 적절합니다.

새로운 기능을 위한 import 문들이 올바르게 추가되었습니다.

Also applies to: 13-13, 16-16


46-46: socialLinks 필드 추가가 적절합니다.

Map<String, String> 타입으로 소셜 링크 정보를 관리하는 구조가 recruitmentForm 필드보다 더 유연하고 구조화된 접근방식입니다.


82-82: socialLinks 설정 로직이 올바릅니다.

request에서 socialLinks를 직접 할당하는 방식이 간단하고 명확합니다.


98-100: 모집 상태 업데이트 메서드 추가가 적절합니다.

ClubRecruitmentInformation으로의 위임 패턴이 책임 분리 원칙에 부합합니다.

backend/src/main/java/moadong/user/payload/request/UserRegisterRequest.java (4)

3-3: 새로운 import 추가가 적절합니다.

@notblank 검증과 예외 처리, 패스워드 인코딩을 위한 import들이 올바르게 추가되었습니다.

Also applies to: 5-5, 10-11, 14-14


17-17: @notblank로 변경이 적절합니다.

@NotNull에서 @notblank로 변경하여 빈 문자열과 공백만 있는 문자열을 더 엄격하게 검증합니다.

Also applies to: 20-20, 23-23


29-33: 보안 검증 로직이 우수합니다.

compact constructor에서 userId와 password가 동일한지 검증하는 것은 보안 모범 사례입니다.


35-41: 의존성 주입 설계가 개선되었습니다.

PasswordEncoder를 파라미터로 받아 패스워드 인코딩을 처리하는 방식이 테스트 가능성과 의존성 관리 측면에서 우수합니다.

backend/src/test/java/moadong/club/service/RecruitmentStateCheckerTest.java (6)

1-30: 테스트 클래스 설정이 적절합니다.

MockitoExtension 사용과 의존성 모킹이 올바르게 구성되었고, Asia/Seoul 타임존 설정이 일관성 있게 처리되었습니다.


33-47: ALWAYS 상태 테스트가 우수합니다.

ALWAYS 상태일 때 상태 변경이 발생하지 않음을 verify(never())로 적절히 검증하고 있습니다.


49-67: UPCOMING 상태 테스트가 포괄적입니다.

14일 이내 모집 시작 시나리오를 정확히 시뮬레이션하고 적절한 상태 변경을 검증합니다.


69-87: OPEN 상태 테스트가 정확합니다.

모집 기간 중인 상황을 올바르게 모델링하고 상태 변경과 저장을 검증합니다.


89-107: CLOSED 상태 테스트가 적절합니다.

모집 종료 후 시나리오를 정확히 구현하고 상태 변경을 올바르게 검증합니다.


109-124: null 값 처리 테스트가 중요합니다.

null 날짜 처리 시나리오는 실제 운영에서 발생할 수 있는 엣지 케이스를 잘 다루고 있습니다.

backend/src/test/java/moadong/unit/user/UserLoginTest.java (4)

1-40: 테스트 클래스 구조가 일관성 있습니다.

UserRegisterTest와 동일한 패턴으로 import, 어노테이션, 모킹 설정이 구성되어 일관된 테스트 아키텍처를 유지하고 있습니다.


41-64: 테스트 설정이 적절합니다.

@UnitTest 어노테이션 사용과 Validator 설정, MockHttpServletResponse 인스턴스 생성이 올바르게 구성되었습니다.


66-96: 성공적인 로그인 테스트가 포괄적입니다.

모든 의존성이 적절히 모킹되고, 토큰 생성, 쿠키 설정, 사용자 저장까지 전체 로그인 플로우를 정확히 검증하고 있습니다.


98-112: 실패 시나리오 테스트가 적절합니다.

클럽을 찾을 수 없는 경우의 예외 처리를 올바르게 검증하여 로그인 실패 케이스를 다룹니다.

backend/src/test/java/moadong/unit/club/ClubProfileServiceTest.java (2)

25-46: 테스트 클래스 구조가 잘 설계되었습니다.

MockitoExtension 대신 커스텀 @UnitTest 어노테이션을 사용하고 있으며, 의존성 모킹과 검증이 적절히 구현되어 있습니다.


67-72:

Details

❌ Incorrect review comment

테스트에서 request 객체의 clubId 설정이 누락되었습니다.

ClubRequestFixture.createValidClubInfoRequest()로 생성된 request가 적절한 clubId를 가지고 있는지 확인이 필요합니다. clubRepository.findById(clubId)가 호출되는데 request의 clubId가 테스트의 clubId와 일치하지 않을 수 있습니다.

다음 스크립트로 ClubInfoRequest의 구조를 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# ClubInfoRequest와 ClubRequestFixture 구조 확인
ast-grep --pattern 'class ClubInfoRequest {
  $$$
}'

ast-grep --pattern 'class ClubRequestFixture {
  $$$
  createValidClubInfoRequest($$$) {
    $$$
  }
  $$$
}'

Length of output: 166


다음 스크립트로 ClubInfoRequestClubRequestFixture 정의 및 createValidClubInfoRequest 호출 위치를 다시 검색해보겠습니다.


🏁 Script executed:

#!/bin/bash
# ClubInfoRequest 클래스/레코드 정의 및 사용 검색
rg "class ClubInfoRequest" -n
rg "record ClubInfoRequest" -n
rg "ClubInfoRequest" -n

# ClubRequestFixture 클래스/오브젝트 정의 및 createValidClubInfoRequest 호출 검색
rg "class ClubRequestFixture" -n
rg "object ClubRequestFixture" -n
rg "createValidClubInfoRequest" -n

Length of output: 3111


다음 스크립트로 createValidClubInfoRequest() 구현부와 ClubInfoRequest 레코드 구조를 확인해보겠습니다.


🏁 Script executed:

#!/bin/bash
# Fixture 구현부 조회
sed -n '1,200p' backend/src/test/java/moadong/fixture/ClubRequestFixture.java

# 요청 레코드 정의 조회
sed -n '1,200p' backend/src/main/java/moadong/club/payload/request/ClubInfoRequest.java

Length of output: 2053


테스트에 클럽 ID 설정은 올바르게 적용되어 있습니다.

ClubRequestFixture.createValidClubInfoRequest()에서 id"club_123"으로 설정되어 있고,
when(clubRepository.findById(any()))를 사용해 어떤 ID로 호출되더라도 빈 Optional이 반환되도록 stub 처리하고 있습니다.
따라서 별도의 clubId 누락 이슈는 없습니다.

Likely an incorrect or invalid review comment.

backend/src/test/java/moadong/club/service/ClubSearchServiceTest.java (1)

76-103: 🛠️ Refactor suggestion

2차 정렬 테스트의 데이터가 일관성이 없습니다.

카테고리순 정렬을 테스트하는데 "봉사", "종교", "종교" 순서로 데이터를 만들고 결과적으로 "봉사", "종교", "종교" 순서를 기대하고 있습니다. 카테고리가 같을 때 이름순 정렬을 제대로 테스트하려면 동일 카테고리에 서로 다른 이름의 클럽이 있어야 합니다.

ClubSearchResult club1 = ClubSearchResult.builder().name("club1").recruitmentStatus("OPEN")
-        .division("중동").category("봉사").build();
+        .division("중동").category("종교").build();
ClubSearchResult club2 = ClubSearchResult.builder().name("club2").recruitmentStatus("OPEN")
        .division("중동").category("종교").build();
ClubSearchResult club3 = ClubSearchResult.builder().name("club3").recruitmentStatus("OPEN")
        .division("중동").category("종교").build();
⛔ Skipped due to learnings
Learnt from: Due-IT
PR: Moadong/moadong#329
File: backend/src/main/java/moadong/club/controller/ClubSearchController.java:0-0
Timestamp: 2025-05-01T08:32:09.398Z
Learning: ClubSearchController의 searchClubsByKeyword 메서드와 ClubSearchService의 searchClubsByKeyword 메서드 사이에 파라미터 순서 일치가 중요하다. 현재 컨트롤러의 매개변수 순서는 'keyword, recruitmentStatus, category, division'인 반면, 서비스 메서드의 순서는 'keyword, recruitmentStatus, division, category'이다. 컨트롤러에서 서비스 호출 시 'keyword, recruitmentStatus, division, category' 순서로 전달하면 category와 division 값이 바뀌어 검색 결과가 잘못될 수 있다.
backend/src/main/java/moadong/media/controller/ClubImageController.java (2)

1-8: 패키지 리팩토링이 적절히 수행되었습니다.

gcs 패키지에서 media 패키지로의 변경과 그에 따른 import 구문 업데이트가 올바르게 되었습니다.


36-44: API 문서에 Cloudflare 사용 명시가 잘 반영되었습니다.

@operation의 description에서 Cloudflare 사용을 명시한 것이 좋습니다. 사용자가 어떤 스토리지를 사용하는지 명확히 알 수 있습니다.

backend/src/main/java/moadong/user/service/UserCommandService.java (3)

40-48: 사용자 등록 메서드 개선이 잘 되었습니다.

메서드가 이제 생성된 User 객체를 반환하도록 변경되어 더 유용해졌고, 패스워드 인코딩 로직이 요청 객체의 변환 메서드로 이동한 것도 좋은 리팩토링입니다.


84-107: 리프레시 토큰 처리 로직이 개선되었습니다.

새로운 리프레시 토큰을 생성하고 사용자 정보를 업데이트한 후 쿠키에 설정하는 로직이 추가되어 보안성이 향상되었습니다.


109-121: 사용자 정보 업데이트 시 토큰 갱신 로직이 추가되었습니다.

사용자 정보 업데이트 후 새로운 리프레시 토큰을 생성하고 쿠키를 설정하는 것은 보안상 좋은 관행입니다.

backend/src/test/java/moadong/unit/user/UserRegisterTest.java (1)

61-87: 사용자 등록 성공 테스트가 잘 작성되었습니다.

모든 필요한 검증이 포함되어 있고, Mockito를 적절히 활용하여 의존성을 격리했습니다. 패스워드 인코딩 검증도 올바르게 수행되고 있습니다.

backend/src/test/java/moadong/club/service/ClubMetricServiceTest.java (3)

43-61: 메트릭 업데이트 테스트가 잘 작성되었습니다.

모킹과 검증이 적절히 수행되고 있으며, 타임존도 "Asia/Seoul"로 일관되게 설정되어 있습니다.


164-187: Club 정보 누락 시나리오 테스트가 훌륭합니다.

실제 운영 환경에서 발생할 수 있는 데이터 불일치 상황을 테스트하고 있어 매우 실용적입니다. null 처리 로직을 검증하는 것도 좋습니다.


212-237: IP 중복 제거 테스트가 잘 구현되었습니다.

실제 웹 애플리케이션에서 중요한 중복 IP 처리 로직을 정확히 검증하고 있습니다. 테스트 데이터와 예상 결과가 명확하게 설정되어 있습니다.

backend/src/test/java/moadong/media/service/GoogleDriveClubImageServiceLogoTest.java (1)

133-150: 파일명 검증 테스트 케이스가 잘 구성되어 있습니다.

다양한 엣지 케이스를 포함한 파일명 검증 테스트가 포괄적으로 작성되어 있어 ClubImageUtil.containsInvalidChars 메서드의 동작을 잘 검증하고 있습니다.

backend/src/main/java/moadong/global/exception/ErrorCode.java (2)

18-19: 파일 처리 관련 에러 코드가 적절하게 추가되었습니다.

새로 추가된 파일 처리 에러 코드들이 기존 패턴을 잘 따르고 있으며, HTTP 상태 코드와 메시지가 적절합니다.


27-31: 지원서 관련 에러 코드들이 체계적으로 정의되었습니다.

지원서 기능을 위한 에러 코드들이 논리적으로 그룹화되어 있고, 각각의 상황에 맞는 HTTP 상태 코드와 명확한 한국어 메시지를 제공하고 있습니다.

backend/src/main/java/moadong/media/util/ClubImageUtil.java (1)

17-20: 파일명 검증 로직이 잘 구현되었습니다.

유니코드 정규화와 정규식을 사용한 검증이 적절합니다.

backend/src/main/java/moadong/club/service/ClubApplyService.java (1)

126-192: 코드 중복이 존재합니다.

createQuestionsupdateQuestions 메서드 간의 코드 중복은 이미 인지된 문제이며 향후 리팩토링 예정임을 확인했습니다.

backend/src/main/java/moadong/media/service/CloudflareImageService.java (1)

24-24: 보안 주의: PUBLIC_READ ACL 사용

모든 업로드된 파일이 공개 읽기 권한으로 설정됩니다. 민감한 정보가 포함된 이미지가 업로드될 가능성을 고려하여 보안 정책을 검토해야 합니다.

backend/src/main/java/moadong/club/controller/ClubApplyController.java (1)

27-36: 응답 형식의 일관성

지원서 생성과 수정 엔드포인트가 동일한 응답 메시지를 반환합니다.

REST API 설계가 일관성 있게 구현되었고, 적절한 HTTP 상태 코드와 함께 명확한 응답 메시지를 제공합니다.

Also applies to: 38-47

backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java (1)

150-152: 보안 주의: 전체 공개 권한 설정

모든 업로드된 파일이 인터넷에 공개됩니다. 이는 심각한 보안 위험을 초래할 수 있습니다.

업로드되는 이미지가 모두 공개되어도 안전한지 확인이 필요합니다. 민감한 정보가 포함될 가능성을 검토해야 합니다.

String presidentName,
String presidentPhoneNumber,
String recruitmentForm
Map<String, String> socialLinks
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Map 필드에 대한 검증 어노테이션 누락.

socialLinks 필드는 Map 타입이지만 검증 어노테이션이 없습니다. 소셜 링크의 유효성을 보장하기 위해 @Valid 또는 맞춤형 검증 어노테이션을 추가하는 것을 고려해보세요.

+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Size;

-    Map<String, String> socialLinks
+    @Valid
+    @Size(max = 10, message = "소셜 링크는 최대 10개까지 허용됩니다")
+    Map<String, String> socialLinks
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/payload/request/ClubInfoRequest.java at
line 20, the Map field socialLinks lacks validation annotations. Add the @Valid
annotation to the socialLinks field to enable validation of its contents, or
implement a custom validation annotation if specific rules are needed for the
map entries. This ensures the social link data is properly validated during
request processing.

@@ -0,0 +1,43 @@
package moadong.media.util;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

deprecated된 GoogleCredential 클래스 사용 중단 필요

GoogleCredential 클래스는 deprecated되었으며 보안 취약점이 있을 수 있습니다. GoogleCredentials를 사용하도록 변경해야 합니다.

-import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;

현재 코드에서는 실제로 GoogleCredentials를 사용하고 있으므로 불필요한 import입니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
// (all lines removed)
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/media/util/GoogleDriveConfig.java at line 3,
remove the import statement for the deprecated GoogleCredential class since the
code already uses the recommended GoogleCredentials class. This eliminates the
use of deprecated and potentially insecure classes and cleans up unnecessary
imports.

Comment on lines +27 to +40
@Bean
public Drive googleDriveService() throws Exception {
InputStream in = ResourceUtils.getURL(credentialsLocation).openStream();
GoogleCredentials credentials = GoogleCredentials.fromStream(in)
.createScoped(Collections.singleton(DriveScopes.DRIVE));

return new Drive.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance(),
new HttpCredentialsAdapter(credentials))
.setApplicationName(applicationName)
.build();

}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

예외 처리 및 보안 강화 필요

현재 구현에서 다음 개선사항들이 필요합니다:

  1. 구체적인 예외 처리 부족
  2. credentials 파일 존재 여부 검증 없음
  3. 리소스 누수 가능성

다음과 같은 개선을 제안합니다:

 @Bean
 public Drive googleDriveService() throws Exception {
-    InputStream in = ResourceUtils.getURL(credentialsLocation).openStream();
-    GoogleCredentials credentials = GoogleCredentials.fromStream(in)
-            .createScoped(Collections.singleton(DriveScopes.DRIVE));
+    try (InputStream in = ResourceUtils.getURL(credentialsLocation).openStream()) {
+        GoogleCredentials credentials = GoogleCredentials.fromStream(in)
+                .createScoped(Collections.singleton(DriveScopes.DRIVE));
+        
+        return new Drive.Builder(
+                GoogleNetHttpTransport.newTrustedTransport(),
+                JacksonFactory.getDefaultInstance(),
+                new HttpCredentialsAdapter(credentials))
+                .setApplicationName(applicationName)
+                .build();
+    } catch (Exception e) {
+        throw new RuntimeException("Google Drive 설정 초기화 실패", e);
+    }

credentials 파일 경로와 권한 설정이 올바른지 확인해주세요.


Google Drive 초기화 메서드: 리소스 누수 방지 및 예외 처리 강화 필요

아래 개선 사항을 반영해주세요:

  • InputStream을 try-with-resources로 감싸 누수 방지
  • IOException, GeneralSecurityException 등 구체적 예외 처리
  • credentials 파일 존재 여부 및 권한 확인
 @Bean
 public Drive googleDriveService() throws Exception {
-    InputStream in = ResourceUtils.getURL(credentialsLocation).openStream();
-    GoogleCredentials credentials = GoogleCredentials.fromStream(in)
-            .createScoped(Collections.singleton(DriveScopes.DRIVE));
-
-    return new Drive.Builder(
-            GoogleNetHttpTransport.newTrustedTransport(),
-            JacksonFactory.getDefaultInstance(),
-            new HttpCredentialsAdapter(credentials))
-            .setApplicationName(applicationName)
-            .build();
+    try (InputStream in = ResourceUtils.getURL(credentialsLocation).openStream()) {
+        // TODO: credentials 파일 존재 여부 및 권한 체크 로직 추가 권장
+        GoogleCredentials credentials = GoogleCredentials.fromStream(in)
+                .createScoped(Collections.singleton(DriveScopes.DRIVE));
+
+        return new Drive.Builder(
+                GoogleNetHttpTransport.newTrustedTransport(),
+                JacksonFactory.getDefaultInstance(),
+                new HttpCredentialsAdapter(credentials))
+                .setApplicationName(applicationName)
+                .build();
+    } catch (IOException | GeneralSecurityException e) {
+        throw new RuntimeException("Google Drive 설정 초기화 실패: " + e.getMessage(), e);
+    }
  • 변경 파일:
    • backend/src/main/java/moadong/media/util/GoogleDriveConfig.java

credentialsLocation 경로 및 파일 권한 설정을 반드시 검증해 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Bean
public Drive googleDriveService() throws Exception {
InputStream in = ResourceUtils.getURL(credentialsLocation).openStream();
GoogleCredentials credentials = GoogleCredentials.fromStream(in)
.createScoped(Collections.singleton(DriveScopes.DRIVE));
return new Drive.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance(),
new HttpCredentialsAdapter(credentials))
.setApplicationName(applicationName)
.build();
}
@Bean
public Drive googleDriveService() throws Exception {
try (InputStream in = ResourceUtils.getURL(credentialsLocation).openStream()) {
// TODO: credentials 파일 존재 여부 및 권한 체크 로직 추가 권장
GoogleCredentials credentials = GoogleCredentials.fromStream(in)
.createScoped(Collections.singleton(DriveScopes.DRIVE));
return new Drive.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance(),
new HttpCredentialsAdapter(credentials))
.setApplicationName(applicationName)
.build();
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException("Google Drive 설정 초기화 실패: " + e.getMessage(), e);
}
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/media/util/GoogleDriveConfig.java around lines
27 to 40, improve the googleDriveService method by wrapping the InputStream in a
try-with-resources block to prevent resource leaks, add explicit checks to
verify the existence and accessibility of the credentials file before opening
it, and handle specific exceptions such as IOException and
GeneralSecurityException with appropriate catch blocks or throws declarations to
enhance error handling and security.

Comment on lines +7 to +9
@AllArgsConstructor
@Getter
@Builder(toBuilder = true)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

기본 생성자 누락 – 프레임워크 호환성 보강 필요
Lombok @AllArgsConstructor만 선언되어 있어 Jackson/MongoDB 등에서 디폴트 생성자가 없어 역직렬화나 리플렉션 기반 매핑 시 오류가 발생할 수 있습니다.
아래와 같이 @NoArgsConstructor를 추가해주세요.

 import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;

 @AllArgsConstructor
+@NoArgsConstructor
 @Getter
 @Builder(toBuilder = true)
 public class ClubQuestionAnswer { ... }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@AllArgsConstructor
@Getter
@Builder(toBuilder = true)
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder(toBuilder = true)
public class ClubQuestionAnswer {
// existing fields and methods...
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/entity/ClubQuestionAnswer.java around
lines 7 to 9, the class uses Lombok's @AllArgsConstructor but lacks a
no-argument constructor, which can cause issues with frameworks like Jackson or
MongoDB that require a default constructor for deserialization and reflection.
Add the Lombok annotation @NoArgsConstructor to the class to provide a default
constructor and ensure compatibility with these frameworks.

Comment on lines +7 to +9
@AllArgsConstructor
@Getter
@Builder(toBuilder = true)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

기본 생성자 누락 – 역직렬화 안정성 확보 필요
Lombok @AllArgsConstructor만 선언되어 있어 JSON 매핑이나 MongoDB 스키마 매핑 시 디폴트 생성자가 없어 실패할 수 있습니다.

 import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
 import lombok.Builder;
 import lombok.Getter;

 @AllArgsConstructor
+@NoArgsConstructor
 @Getter
 @Builder(toBuilder = true)
 public class ClubQuestionItem { ... }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@AllArgsConstructor
@Getter
@Builder(toBuilder = true)
import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
+@NoArgsConstructor
@Getter
@Builder(toBuilder = true)
public class ClubQuestionItem {
// existing fields and methods...
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/entity/ClubQuestionItem.java around lines
7 to 9, the class uses Lombok's @AllArgsConstructor but lacks a default
no-argument constructor, which can cause failures during JSON deserialization or
MongoDB schema mapping. Add Lombok's @NoArgsConstructor annotation to generate a
default constructor to ensure deserialization stability.

Comment on lines +49 to +53
@GetMapping("/apply")
@Operation(summary = "클럽 지원서 불러오기", description = "클럽 지원서를 불러옵니다")
public ResponseEntity<?> getClubApplication(@PathVariable String clubId) {
return clubApplyService.getClubApplication(clubId);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

인증 요구사항 불일치

지원서 조회(/apply GET)와 지원 제출(/apply POST)에 인증이 필요하지 않도록 설정되어 있습니다. 이는 보안상 위험할 수 있습니다.

지원서 조회와 제출에도 적절한 인증이 필요한지 검토해야 합니다:

@GetMapping("/apply")
@Operation(summary = "클럽 지원서 불러오기", description = "클럽 지원서를 불러옵니다")
+@PreAuthorize("isAuthenticated()")
+@SecurityRequirement(name = "BearerAuth")
public ResponseEntity<?> getClubApplication(@PathVariable String clubId) {

@PostMapping("/apply")
@Operation(summary = "클럽 지원", description = "클럽에 지원합니다")
+@PreAuthorize("isAuthenticated()")
+@SecurityRequirement(name = "BearerAuth")
public ResponseEntity<?>  applyToClub(@PathVariable String clubId,
+                                     @CurrentUser CustomUserDetails user,
                                      @RequestBody @Validated ClubApplyRequest request) {

Also applies to: 55-61

🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/controller/ClubApplyController.java around
lines 49 to 53 and 55 to 61, the GET and POST endpoints for /apply currently do
not enforce authentication, which poses a security risk. Review the security
requirements and add appropriate authentication annotations or checks to these
methods to ensure only authorized users can access or submit club applications.

Comment on lines +57 to +61
public ResponseEntity<?> applyToClub(@PathVariable String clubId,
@RequestBody @Validated ClubApplyRequest request) {
clubApplyService.applyToClub(clubId, request);
return Response.ok("success apply");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

클럽 지원 시 사용자 정보 누락

클럽 지원 시 어떤 사용자가 지원했는지 식별할 수 있는 정보가 전달되지 않습니다.

지원자 정보를 서비스 레이어에 전달해야 합니다:

public ResponseEntity<?>  applyToClub(@PathVariable String clubId,
+                                     @CurrentUser CustomUserDetails user,
                                      @RequestBody @Validated ClubApplyRequest request) {
-    clubApplyService.applyToClub(clubId, request);
+    clubApplyService.applyToClub(clubId, user, request);
    return Response.ok("success apply");
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ResponseEntity<?> applyToClub(@PathVariable String clubId,
@RequestBody @Validated ClubApplyRequest request) {
clubApplyService.applyToClub(clubId, request);
return Response.ok("success apply");
}
public ResponseEntity<?> applyToClub(@PathVariable String clubId,
@CurrentUser CustomUserDetails user,
@RequestBody @Validated ClubApplyRequest request) {
clubApplyService.applyToClub(clubId, user, request);
return Response.ok("success apply");
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/controller/ClubApplyController.java around
lines 57 to 61, the applyToClub method does not pass any user identification
information to the service layer, making it impossible to know who is applying.
Modify the method to obtain the current user's information (e.g., from security
context or session) and pass this user info along with the clubId and request to
the clubApplyService.applyToClub method to ensure the applicant is properly
identified.

@Override
public void deleteFile(Club club, String filePath) {
//"https://drive.google.com/file/d/{fileId}/view" -> {fileId}
String fileId = filePath.split("/")[5];
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

URL 파싱의 취약성

고정된 배열 인덱스로 URL을 파싱하는 방식은 URL 형식이 변경되면 오동작할 수 있습니다.

정규식을 사용한 더 안전한 파싱 방식을 권장합니다:

-String fileId = filePath.split("/")[5];
+String fileId = filePath.replaceAll(".*/d/([^/]+)/.*", "$1");

또는 더 명확한 방식:

Pattern pattern = Pattern.compile("/d/([^/]+)/");
Matcher matcher = pattern.matcher(filePath);
if (matcher.find()) {
    String fileId = matcher.group(1);
} else {
    throw new RestApiException(ErrorCode.INVALID_FILE_URL);
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java
at line 111, the current code extracts the fileId by splitting the URL with a
fixed index, which is fragile and can break if the URL format changes. Replace
this with a regex-based approach that matches the fileId pattern more reliably:
use a Pattern and Matcher to find the fileId between "/d/" and the next "/", and
if not found, throw an appropriate exception to handle invalid URLs safely.

Comment on lines +127 to +133
java.io.File tempFile;
try {
tempFile = java.io.File.createTempFile("upload-", file.getOriginalFilename());
file.transferTo(tempFile);
} catch (IOException e) {
throw new RestApiException(ErrorCode.FILE_TRANSFER_ERROR);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

임시 파일 생성 시 보안 위험

임시 파일 생성 시 파일명에 사용자 입력이 직접 포함되어 경로 조작 공격에 취약할 수 있습니다.

안전한 임시 파일 생성 방식을 사용하세요:

-tempFile = java.io.File.createTempFile("upload-", file.getOriginalFilename());
+tempFile = java.io.File.createTempFile("upload-", ".tmp");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
java.io.File tempFile;
try {
tempFile = java.io.File.createTempFile("upload-", file.getOriginalFilename());
file.transferTo(tempFile);
} catch (IOException e) {
throw new RestApiException(ErrorCode.FILE_TRANSFER_ERROR);
}
java.io.File tempFile;
try {
tempFile = java.io.File.createTempFile("upload-", ".tmp");
file.transferTo(tempFile);
} catch (IOException e) {
throw new RestApiException(ErrorCode.FILE_TRANSFER_ERROR);
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java
around lines 127 to 133, the temporary file is created using the original
filename from user input, which poses a security risk for path traversal
attacks. To fix this, generate the temporary file using only a safe prefix and
suffix without including the user-provided filename directly. Use createTempFile
with a fixed prefix and a safe suffix (e.g., a constant extension) to ensure the
filename is sanitized and secure.

Comment on lines +165 to +168
}finally {
// 임시 파일 삭제
tempFile.delete();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

리소스 정리 개선

finally 블록에서 임시 파일을 삭제하지만, try-with-resources 패턴을 사용하면 더 안전합니다.

임시 파일 관리를 더 안전하게 개선할 수 있습니다:

+// 임시 파일을 try-with-resources로 관리하거나
+// 업로드 완료 후 즉시 삭제하는 방식으로 개선
try {
    uploadedFile = googleDrive.files().create(fileMetadata, mediaContent)
            .setFields("id")
            .execute();

    googleDrive.permissions().create(uploadedFile.getId(), publicPermission)
            .setFields("id")
            .execute();
} catch (Exception e) {
    throw new RestApiException(ErrorCode.IMAGE_UPLOAD_FAILED);
} finally {
-    tempFile.delete();
+    if (tempFile != null && tempFile.exists()) {
+        tempFile.delete();
+    }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}finally {
// 임시 파일 삭제
tempFile.delete();
}
// 임시 파일을 try-with-resources로 관리하거나
// 업로드 완료 후 즉시 삭제하는 방식으로 개선
try {
uploadedFile = googleDrive.files().create(fileMetadata, mediaContent)
.setFields("id")
.execute();
googleDrive.permissions().create(uploadedFile.getId(), publicPermission)
.setFields("id")
.execute();
} catch (Exception e) {
throw new RestApiException(ErrorCode.IMAGE_UPLOAD_FAILED);
} finally {
// 임시 파일 삭제
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
}
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/media/service/GoogleDriveClubImageService.java
around lines 165 to 168, the temporary file is deleted in a finally block, but
this can be improved for safer resource management. Refactor the code to use a
try-with-resources statement for handling the temporary file or its stream,
ensuring automatic and safer cleanup of resources without relying solely on
manual deletion in finally.

- recruitmentStatus -> clubRecruitmentStatus
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (1)

47-47: 네이밍 컨벤션 일관성 개선

Java 카멜케이스 컨벤션에 맞춰 club_tags 대신 clubTags를 사용하는 것이 좋겠습니다.

-        operations.add(Aggregation.unwind("club_tags", true));
+        operations.add(Aggregation.unwind("clubTags", true));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5c6a16 and 4ea79f5.

📒 Files selected for processing (1)
  • backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Deploy
🔇 Additional comments (1)
backend/src/main/java/moadong/club/repository/ClubSearchRepository.java (1)

24-65: 전체적인 구현 검토

MongoDB 집계 파이프라인 구현이 잘 구조화되어 있고, 키워드 검색과 필터링 로직이 적절히 분리되어 있습니다. 정렬 기준도 division과 category 순으로 명확하게 설정되어 있어 사용자 경험을 고려한 구현입니다.

Aggregation.project("name", "state", "category", "division")
.and("recruitmentInformation.introduction").as("introduction")
.and("recruitmentInformation.recruitmentStatus").as("recruitmentStatus")
.and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

필드명 불일치로 인한 검색 필터링 오류 수정 필요

프로젝션 단계에서는 clubRecruitmentStatus를 사용하고 있지만, getMatchedCriteria 메서드의 73번째 라인에서는 여전히 recruitmentStatus를 사용하고 있습니다. 이는 모집 상태별 필터링이 제대로 작동하지 않을 수 있습니다.

다음과 같이 수정하여 일관성을 맞춰주세요:

-                Criteria.where("recruitmentInformation.recruitmentStatus").is(recruitmentStatus));
+                Criteria.where("recruitmentInformation.clubRecruitmentStatus").is(recruitmentStatus));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.and("recruitmentInformation.clubRecruitmentStatus").as("recruitmentStatus")
Criteria.where("recruitmentInformation.clubRecruitmentStatus").is(recruitmentStatus));
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/repository/ClubSearchRepository.java at
line 52, the projection uses "clubRecruitmentStatus" but the getMatchedCriteria
method at line 73 still references "recruitmentStatus". To fix the filtering
issue, update the getMatchedCriteria method to use "clubRecruitmentStatus"
instead of "recruitmentStatus" so the field names are consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신 작업 💾 BE Backend ✨ Feature 기능 개발 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants