Conversation
…e-application-MOA-196 [refactor] 지원자를 여러명 삭제할 수 있다.
…pplicant-MOA-200 [refactor] 지원서를 여러개 수정할 수 있다. -BE
- 변경할 필요 없는 userId 필드 제거 - updateUserProfile 메서드에서 userId 제거
- /auth/user/reset - SecurePasswordGenerator 로 랜덤 비밀번호 생성 - 비밀번호는 TempPasswordResponse를 통해 응답 - user.resetPassword로 암호화된 비밀번로 저장
- 임시 비밀번호에 무조건 영어, 숫자, 특수문자가 포함되도록 보장
- db에 저장된 refresh 토큰 제거 - 클라이언트의 쿠키 삭제
- 최소 길이 검증 추가
[feature] 액세스 토큰 및 리프레시 토큰의 만료 시간을 수정하고, 로그인, 관리자 계정 관련의 동시성 문제를 해결한다
- 아이디와 동일한지, 이전 비밀번호와 동일한지 체크 - 각각 702-1, 702-2 ErrorCode 설정
- 로그인 여부와 상관없이 userId 만으로 초기화 하도록 수정 - 브라우저의 쿠키 제거 로직 삭제
…t-MOA-220 [feature] 비밀번호 변경 시 아이디/이전 비밀번호 동일 여부 검증 추가하고 초기화 api 비인증 사용자에게 제공한다
[fix] MOA-218 동아리 정보나 지원서 수정 시에 동시성 문제를 해결한다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
Gradle 빌드backend/build.gradle |
테스트 재시도 의존성 io.github.artsok:rerunner-jupiter:2.1.6 추가. |
지원자 API 배치 전환backend/src/main/java/moadong/club/controller/ClubApplyController.java |
PUT/DELETE 경로를 /applicant로 변경, 요청 본문 기반 배치 편집/삭제로 전환. 메서드 시그니처 변경 및 검증 추가. |
지원자 요청 DTObackend/src/main/java/moadong/club/payload/request/ClubApplicantEditRequest.java, .../ClubApplicantDeleteRequest.java |
Edit에 applicantId(@notblank) 추가. Delete용 레코드 ClubApplicantDeleteRequest(@NotEmpty List<String> applicantIds) 신규. |
응모 레포지토리 보강backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java |
findAllByIdInAndQuestionId(List<String>, String) 쿼리 메서드 추가. |
지원/질문 서비스 변경backend/src/main/java/moadong/club/service/ClubApplyService.java |
지원자 배치 편집/삭제로 리팩터링, 존재 검증 및 saveAll/벌크 삭제 적용. 질문 단건 수정 메서드 추가. 트랜잭션 적용. |
동시성/스케줄 조건화backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java |
@ConditionalOnProperty(name="scheduling.enabled", havingValue="true", matchIfMissing=true) 추가. |
클럽 프로필 트랜잭션backend/src/main/java/moadong/club/service/ClubProfileService.java |
updateClubInfo에 @Transactional 추가. |
Mongo 전환 및 버전 락킹backend/src/main/java/moadong/club/entity/Club.java, .../ClubQuestion.java |
Spring Data MongoDB로 마이그레이션, implements Persistable<String>, @Version Long version 및 isNew() 추가, JPA 전용 어노테이션 제거. |
Mongo 트랜잭션 구성backend/src/main/java/moadong/global/config/MongoConfig.java |
MongoTransactionManager 빈 추가. |
전역 에러/핸들러backend/src/main/java/moadong/global/exception/ErrorCode.java, .../GlobalExceptionHandler.java |
에러코드 3종 추가(동시성 충돌/비밀번호 규칙), 낙관적 락/WriteConflict 전역 핸들러(409) 추가 및 로깅. |
임시 비밀번호 생성 유틸backend/src/main/java/moadong/global/util/SecurePasswordGenerator.java |
최소 8자, 영문/숫자/특수 포함 비밀번호 generate(int length) 제공. |
유저 컨트롤러/서비스/엔티티backend/src/main/java/moadong/user/controller/UserController.java, .../service/UserCommandService.java, .../entity/User.java |
POST /auth/user/reset 추가. 서비스에 reset(String) 추가(임시 비번 생성/인코딩/토큰 초기화). update에 비번 검증(아이디 동일/이전 비번 동일 금지). User.resetPassword 추가. |
유저 DTObackend/src/main/java/moadong/user/payload/request/UserResetRequest.java, .../UserUpdateRequest.java, .../payload/response/TempPasswordResponse.java |
UserResetRequest(userId) 추가. UserUpdateRequest에서 userId 제거. TempPasswordResponse(tempPassword) 추가. |
테스트 및 픽스처backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java, .../ClubProfileServiceTest.java, .../fixture/ClubApplicationEditFixture.java, .../fixture/UserFixture.java |
동시성 충돌 검증 통합 테스트 2종 추가. 편의 픽스처 추가/수정(collectUserId 값 변경 및 오버로드 추가). |
Sequence Diagram(s)
sequenceDiagram
participant Client
participant Controller as ClubApplyController
participant Service as ClubApplyService
participant Repo as ClubApplicationRepository
Client->>Controller: PUT /clubs/{clubId}/applicant\n[List<EditRequest>]
Controller->>Service: editApplicantDetail(clubId, requests, user)
Service->>Repo: findAllByIdInAndQuestionId(ids, clubId)
Repo-->>Service: List<ClubApplication>
Service->>Service: 요청 ID 전체 존재 확인
Service->>Repo: saveAll(수정된 엔티티들)
Service-->>Controller: void
Controller-->>Client: 200 OK
sequenceDiagram
participant Client
participant Controller as UserController
participant Service as UserCommandService
participant Gen as SecurePasswordGenerator
participant Repo as UserRepository
Client->>Controller: POST /auth/user/reset\nUserResetRequest(userId)
Controller->>Service: reset(userId)
Service->>Repo: findByUserId(userId)
Repo-->>Service: User
Service->>Gen: generate(8)
Gen-->>Service: tempPassword
Service->>Service: encode & user.resetPassword\nrefreshToken 초기화
Service->>Repo: save(User)
Service-->>Controller: TempPasswordResponse(tempPassword)
Controller-->>Client: 200 OK + tempPassword
sequenceDiagram
participant T1 as Thread A
participant T2 as Thread B
participant Svc as Service
participant Repo as MongoDB
rect rgba(200,230,255,0.3)
note over T1,T2: 동시 업데이트 시나리오 (낙관적 락)
T1->>Repo: findById(id)
T2->>Repo: findById(id)
T1->>Repo: save(doc v=1→2)
Repo-->>T1: OK
T2->>Repo: save(doc v=1→2)
Repo-->>T2: WriteConflict/OptimisticLock
end
T2->>Svc: 예외 전파
Svc-->>T2: GlobalExceptionHandler 매핑 → 409 CONFLICT
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
- [refactor] 지원서를 여러개 수정할 수 있다. -BE #703: 지원자 편집/삭제 배치를 위한 동일 컨트롤러/서비스/DTO/레포지토리 변경과 직접적으로 중첩.
- [feature] 액세스 토큰 및 리프레시 토큰의 만료 시간을 수정하고, 로그인, 관리자 계정 관련의 동시성 문제를 해결한다 #713: Mongo 버전 관리, 낙관적 락, 전역 예외 처리 및 구성 변경이 동일 파일군에서 중복.
- [feature] 비밀번호 변경 시 아이디/이전 비밀번호 동일 여부 검증 추가하고 초기화 api 비인증 사용자에게 제공한다 #728: 비밀번호 초기화/변경 흐름(
UserController,UserCommandService,ErrorCode,TempPasswordResponse)과 코드 레벨로 일치.
Suggested labels
🔨 Refactor, ✅ Test
Suggested reviewers
- lepitaaar
- seongwon030
- oesnuj
- alsdddk
Pre-merge checks (2 passed, 1 warning)
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
✅ Passed checks (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | 제목 "[Release] BE v1.0.7 배포"은 PR의 주된 목적(백엔드 릴리스/배포)과 버전 정보를 간결하게 나타내며 불필요한 파일 목록, 이모지 또는 모호한 표현이 없습니다. 팀원이 히스토리를 스캔할 때 핵심 변경 의도를 즉시 파악할 수 있습니다. 따라서 제공된 기준에 부합합니다. |
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.
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
develop/be
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.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (1)
25-35: NPE 가능성 가드 추가 필요
club.getClubRecruitmentInformation()가 null이면 NPE가 발생합니다. 또한 ALWAYS 체크 전에 start/end를 먼저 읽고 있습니다.아래처럼 null 가드와 처리 순서를 조정해 주세요:
- for (Club club : clubs) { - ClubRecruitmentInformation recruitInfo = club.getClubRecruitmentInformation(); - ZonedDateTime recruitmentStartDate = recruitInfo.getRecruitmentStart(); - ZonedDateTime recruitmentEndDate = recruitInfo.getRecruitmentEnd(); - if (recruitInfo.getClubRecruitmentStatus() == ClubRecruitmentStatus.ALWAYS) { - continue; - } - RecruitmentStateCalculator.calculate(club, recruitmentStartDate, recruitmentEndDate); - clubRepository.save(club); - } + for (Club club : clubs) { + ClubRecruitmentInformation recruitInfo = club.getClubRecruitmentInformation(); + if (recruitInfo == null) { + continue; + } + if (recruitInfo.getClubRecruitmentStatus() == ClubRecruitmentStatus.ALWAYS) { + continue; + } + ZonedDateTime recruitmentStartDate = recruitInfo.getRecruitmentStart(); + ZonedDateTime recruitmentEndDate = recruitInfo.getRecruitmentEnd(); + if (recruitmentStartDate == null || recruitmentEndDate == null) { + // TODO: 로그/보정 로직이 필요하다면 여기에 추가 + continue; + } + RecruitmentStateCalculator.calculate(club, recruitmentStartDate, recruitmentEndDate); + clubRepository.save(club); + }null이 절대 나오지 않는 스키마 보장이 있다면 알려 주세요. 그러면 가드 범위를 축소하겠습니다.
backend/src/main/java/moadong/user/service/UserCommandService.java (1)
112-134: 비밀번호 변경 시 새 Refresh Token을 쿠키로만 발급하고 DB에 저장하지 않아 토큰 불일치가 발생합니다.refreshAccessToken 흐름은 DB에 저장된 토큰과 쿠키의 토큰 일치 여부를 검증합니다. 현재 저장 없이 쿠키만 갱신하면 이후 갱신 요청이 실패합니다. 저장 시점도 한 번으로 모아 I/O를 줄이는 편이 좋습니다.
다음과 같이 토큰 저장을 추가하고 save 호출을 재배치하세요.
- userRepository.save(user); - - String newRefreshToken = jwtProvider.generateRefreshToken(user.getUsername()).getToken(); - ResponseCookie cookie = cookieMaker.makeRefreshTokenCookie(newRefreshToken); - response.addHeader("Set-Cookie", cookie.toString()); + String newRefreshToken = jwtProvider.generateRefreshToken(user.getUsername()).getToken(); + user.updateRefreshToken(new RefreshToken(newRefreshToken, new Date())); + userRepository.save(user); + + ResponseCookie cookie = cookieMaker.makeRefreshTokenCookie(newRefreshToken); + response.addHeader("Set-Cookie", cookie.toString());
🧹 Nitpick comments (23)
backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (4)
18-18: 전역 토글 키와 혼동 가능 — 커스텀 prefix로 분리 권장
Spring Boot에는 이미 전역 스케줄러 토글spring.task.scheduling.enabled가 있습니다. 현재 키scheduling.enabled는 의미가 모호하고 충돌/혼동을 유발할 수 있습니다. 이 배치만 온오프하려는 의도라면 프로젝트 네임스페이스로 분리해 주세요.아래처럼 annotation만 수정하면 됩니다:
-@ConditionalOnProperty(name = "scheduling.enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnProperty(prefix = "moadong.scheduling.recruitment-checker", + name = "enabled", havingValue = "true", matchIfMissing = true)
33-35: 불필요한 쓰기 최소화(상태 변경 시에만 save)
상태가 바뀌지 않았다면 저장을 생략해 I/O를 줄일 수 있습니다.- RecruitmentStateCalculator.calculate(club, recruitmentStartDate, recruitmentEndDate); - clubRepository.save(club); + ClubRecruitmentStatus before = recruitInfo.getClubRecruitmentStatus(); + RecruitmentStateCalculator.calculate(club, recruitmentStartDate, recruitmentEndDate); + if (before != recruitInfo.getClubRecruitmentStatus()) { + clubRepository.save(club); + }(계산기가 다른 필드도 변경한다면 그 필드 기준으로 변경 여부를 판단하도록 조정해 주세요.)
21-36: 멀티 인스턴스 실행 시 단일 실행 보장 필요
서비스가 다중 인스턴스로 배포되면 스케줄이 각 인스턴스에서 동시에 돌 수 있습니다. 분산 락(ShedLock 등) 또는 리더 일렉션으로 단일 실행을 보장해 주세요.
25-26: 대량 데이터 대비 페이징/스트리밍 처리 고려
findAll()은 전체 목록을 메모리에 적재합니다. 데이터가 커지면 메모리 압박이 큽니다. 페이지 처리 혹은 커서 기반 스트리밍으로 변경을 검토해 주세요.간단 예시:
int page = 0, size = 500; Page<Club> slice; do { slice = clubRepository.findAll(PageRequest.of(page++, size)); slice.forEach(/* 기존 로직 */); } while (!slice.isEmpty());backend/src/main/java/moadong/club/service/ClubProfileService.java (1)
38-49: 동일 원자성 보장을 위해 모집정보 갱신에도 트랜잭션 적용 권장계산(Update) → 상태재계산 → 저장의 일련 과정을 하나의 경계로 묶는 편이 안전합니다(낙관적 락 예외도 동일하게 처리).
+ @Transactional("mongoTransactionManager") public void updateClubRecruitmentInfo(ClubRecruitmentInfoUpdateRequest request, CustomUserDetails user) {backend/src/main/java/moadong/club/payload/request/ClubApplicantEditRequest.java (1)
9-11: applicantId 형식 검증(예: ObjectId) 추가 제안ID가 Mongo ObjectId라면 형식까지 검증하면 400 단계에서 빠르게 거를 수 있습니다.
import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; @@ - @NotBlank - String applicantId, + @NotBlank + @Pattern(regexp = "^[0-9a-fA-F]{24}$", message = "유효한 ObjectId 형식이어야 합니다") + String applicantId,(만약 ObjectId가 아니라면 이 제안은 무시하셔도 됩니다.)
backend/src/main/java/moadong/club/payload/request/ClubApplicantDeleteRequest.java (1)
8-9: 배치 삭제 요청의 요소 검증/사이즈 제한/중복 제거 고려현재는 빈 리스트만 막습니다. 개별 요소 공백/널, 과도한 사이즈, 중복 값에 대한 방어가 있으면 안전합니다.
package moadong.club.payload.request; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import java.util.List; public record ClubApplicantDeleteRequest( - @NotEmpty - List<String> applicantIds + @NotEmpty + @Size(max = 200, message = "한 번에 최대 200건까지 삭제할 수 있습니다") + List<@NotBlank(message = "applicantId는 비어 있을 수 없습니다") String> applicantIds ) { }서비스 레이어에서는
new HashSet<>(applicantIds)로 중복 제거를 권장합니다.backend/src/main/java/moadong/global/util/SecurePasswordGenerator.java (2)
21-23: 예외 메시지 개선 필요예외 메시지에 실제 입력된 길이를 포함하면 디버깅에 도움이 됩니다.
- if (length < 8) { - throw new IllegalArgumentException("Length must be at least 8");} + if (length < 8) { + throw new IllegalArgumentException("Length must be at least 8, but was " + length); + }
13-13: 특수문자 세트 확대 고려현재 특수문자가 6개(
!@#$%^)로 제한되어 있습니다. 보안 강화를 위해 더 많은 특수문자를 포함하는 것을 고려해보세요.- private static final String SPECIAL = "!@#$%^"; + private static final String SPECIAL = "!@#$%^&*()-_=+[]{}|;:,.<>?";backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java (1)
4-4: 사용되지 않는 import 제거
Password어노테이션이 import되었지만 실제로 사용되지 않습니다.-import moadong.global.annotation.Password;backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java (1)
16-16: 파라미터 명칭 일관성 개선 필요
clubId파라미터가 실제로는questionId를 의미합니다. 학습된 내용에 따르면 이 두 식별자가 동일한 엔티티를 나타내지만, 코드 가독성을 위해 일관된 명칭 사용을 권장합니다.-List<ClubApplication> findAllByIdInAndQuestionId(List<String> ids, String clubId); +List<ClubApplication> findAllByIdInAndQuestionId(List<String> ids, String questionId);backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (4)
3-3: 사용되지 않는 import 제거
ObjectMapper가 import되었지만 테스트에서 사용되지 않습니다.-import com.fasterxml.jackson.databind.ObjectMapper;
19-19: 사용되지 않는 MockMvc 제거
MockMvc가 import되었지만@AutoConfigureMockMvc와 함께 사용되지 않습니다.-import org.springframework.test.web.servlet.MockMvc;@IntegrationTest -@AutoConfigureMockMvc public class ClubProfileServiceTest {
55-55: 주석에 참조 번호 제거주석의
[11][12]참조 번호가 의미가 불명확합니다.- CyclicBarrier barrier = new CyclicBarrier(numberOfThreads); // 모든 스레드의 동시 시작을 위함 [11][12] + CyclicBarrier barrier = new CyclicBarrier(numberOfThreads); // 모든 스레드의 동시 시작을 위함
40-44: 테스트 데이터 격리 개선 필요각 테스트마다 고정된 사용자 ID를 사용하면 병렬 테스트 실행 시 충돌이 발생할 수 있습니다.
테스트별로 고유한 사용자를 생성하도록 개선:
@BeforeEach void setUp() { - if (userRepository.findUserByUserId(UserFixture.collectUserId).isEmpty()) { - userRepository.save(UserFixture.createUser(passwordEncoder)); - } - User user = userRepository.findUserByUserId(UserFixture.collectUserId).get(); + String uniqueUserId = "test_" + System.currentTimeMillis(); + User user = UserFixture.createUser(passwordEncoder, uniqueUserId, + UserFixture.collectPassword, + UserFixture.collectName, + UserFixture.collectPhoneNumber); + user = userRepository.save(user); this.userDetails = new CustomUserDetails(user); }backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
12-19: 주석 처리된 코드 제거 권장주석 처리된 코드는 제거하거나, 향후 사용 예정이라면 TODO 코멘트로 대체하는 것이 좋습니다.
public static ClubApplicationEditRequest createClubApplicationEditRequest(){ -// ClubApplyQuestion clubApplyQuestion = new ClubApplyQuestion( -// 1, -// "타이틀", -// "설명", -// ClubApplicationQuestionType.CHOICE, -// new ClubApplyQuestion.Options(false), -// new ArrayList<>() -// ); + // TODO: 질문이 포함된 더 복잡한 테스트 데이터가 필요한 경우 추가 구현 예정 return new ClubApplicationEditRequest(backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (2)
41-41: 필드명 개선 제안주석 대신 의미 있는 변수명 사용을 권장합니다.
- private String clubQuestionId; // --- 변경: 객체 대신 ID만 저장 --- + private String clubQuestionIdForConcurrencyTest;
74-83: 서비스 메서드 직접 호출 권장테스트 목적상 이해는 되지만, 실제 서비스 메서드를 호출하는 것이 더 현실적인 테스트가 될 것입니다.
- // 서비스 메서드를 직접 호출하는 대신, 테스트에서 로직을 제어 - ClubApplicationEditRequest clubApplicationEditRequest = ClubApplicationEditFixture.createClubApplicationEditRequest(); - questionToUpdate.updateFormTitle(clubApplicationEditRequest.title()); // 엔티티 수정 - clubQuestionRepository.save(questionToUpdate); // 저장 (충돌 발생 지점) + // 실제 서비스 메서드 호출로 더 현실적인 테스트 + ClubApplicationEditRequest request = ClubApplicationEditFixture.createClubApplicationEditRequest(); + clubApplyService.editClubApplicationQuestion(this.clubQuestionId, userDetails, request);backend/src/main/java/moadong/user/service/UserCommandService.java (1)
117-126: 비밀번호 사전 검증 추가 좋습니다. 정책/검증 책임을 한 곳으로 모으는 것도 고려해 주세요.동일 아이디/기존 비번 방지 체크는 적절합니다. 다만 길이/복잡도/금지어(예: 연속문자) 등은 DTO Validator 또는 별도 PasswordPolicyValidator로 중앙화하면 재사용과 테스트가 수월합니다.
backend/src/main/java/moadong/club/service/ClubApplyService.java (2)
153-180: 배치 편집 로직 LGTM. 변수 명 가독성 소폭 개선 제안기능은 타당합니다. 변수명 복수형 사용이 읽기 쉬워집니다.
- List<ClubApplication> application = clubApplicationRepository.findAllByIdInAndQuestionId(applicationIds, clubId); + List<ClubApplication> applications = clubApplicationRepository.findAllByIdInAndQuestionId(applicationIds, clubId); - if (application.size() != applicationIds.size()) { + if (applications.size() != applicationIds.size()) { throw new RestApiException(ErrorCode.APPLICANT_NOT_FOUND); } - application.forEach(app -> { + applications.forEach(app -> { ClubApplicantEditRequest editRequest = requestMap.get(app.getId()); app.updateMemo(editRequest.memo()); app.updateStatus(editRequest.status()); }); - clubApplicationRepository.saveAll(application); + clubApplicationRepository.saveAll(applications);
183-198: 배치 삭제 흐름 적절소유자 검증 → 일괄 조회 → 개수 불일치 검증 → deleteAll 순서가 명확합니다. 필요 시 추후 효율화를 위해
deleteAllByIdInAndQuestionId리포지토리 메서드 도입을 고려해도 좋습니다.backend/src/main/java/moadong/club/controller/ClubApplyController.java (2)
74-86: 지원자 일괄 수정 엔드포인트 전환 좋습니다배치 요청 구조와 검증(@Valid, @notempty) 적용이 명확합니다. 성공 시 204(No Content)를 고려하면 REST 응답 의미가 더 분명해집니다.
88-99: DELETE 본문 사용 시 일부 클라이언트 호환성 고려Spring에서는 동작하지만, 일부 HTTP 클라이언트/프록시는 DELETE 본문을 제한합니다. 호환성 확보를 위해 쿼리 파라미터(예:
?ids=a,b,c) 또는 POST/applicant:delete대안도 제공하는 것을 검토하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (24)
backend/build.gradle(1 hunks)backend/src/main/java/moadong/club/controller/ClubApplyController.java(3 hunks)backend/src/main/java/moadong/club/entity/Club.java(4 hunks)backend/src/main/java/moadong/club/entity/ClubQuestion.java(4 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicantDeleteRequest.java(1 hunks)backend/src/main/java/moadong/club/payload/request/ClubApplicantEditRequest.java(1 hunks)backend/src/main/java/moadong/club/repository/ClubApplicationRepository.java(1 hunks)backend/src/main/java/moadong/club/service/ClubApplyService.java(3 hunks)backend/src/main/java/moadong/club/service/ClubProfileService.java(2 hunks)backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java(2 hunks)backend/src/main/java/moadong/global/config/MongoConfig.java(1 hunks)backend/src/main/java/moadong/global/exception/ErrorCode.java(2 hunks)backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java(2 hunks)backend/src/main/java/moadong/global/util/SecurePasswordGenerator.java(1 hunks)backend/src/main/java/moadong/user/controller/UserController.java(2 hunks)backend/src/main/java/moadong/user/entity/User.java(1 hunks)backend/src/main/java/moadong/user/payload/request/UserResetRequest.java(1 hunks)backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java(1 hunks)backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java(1 hunks)backend/src/main/java/moadong/user/service/UserCommandService.java(6 hunks)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(1 hunks)backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java(1 hunks)backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java(1 hunks)backend/src/test/java/moadong/fixture/UserFixture.java(2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: seongwon030
PR: Moadong/moadong#195
File: frontend/src/pages/AdminPage/AdminPage.tsx:7-7
Timestamp: 2025-03-19T05:18:07.818Z
Learning: AdminPage.tsx에서 현재 하드코딩된 클럽 ID('67d2e3b9b15c136c6acbf20b')는 로그인 기능 구현 후 동적으로 가져오는 방식으로 수정될 예정입니다.
📚 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/repository/ClubApplicationRepository.javabackend/src/main/java/moadong/club/service/ClubApplyService.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/test/java/moadong/fixture/ClubApplicationEditFixture.javabackend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/main/java/moadong/club/controller/ClubApplyController.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/main/java/moadong/club/service/ClubApplyService.java
🧬 Code graph analysis (5)
backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (3)
backend/src/test/java/moadong/fixture/ClubRequestFixture.java (1)
ClubRequestFixture(10-51)backend/src/test/java/moadong/fixture/UserFixture.java (1)
UserFixture(9-33)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
IntegrationTest(29-107)
backend/src/main/java/moadong/global/exception/ErrorCode.java (1)
backend/src/main/java/moadong/global/exception/RestApiException.java (1)
Getter(6-11)
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
backend/src/test/java/moadong/fixture/ClubRequestFixture.java (1)
ClubRequestFixture(10-51)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (3)
backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
ClubApplicationEditFixture(10-26)backend/src/test/java/moadong/fixture/UserFixture.java (1)
UserFixture(9-33)backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (1)
IntegrationTest(26-96)
backend/src/main/java/moadong/club/entity/Club.java (1)
frontend/src/types/club.ts (1)
Club(3-12)
🔇 Additional comments (17)
backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (2)
9-9:@ConditionalOnProperty임포트는 적절합니다.
해당 기능 게이팅을 위한 선택이 합리적입니다.
23-23: 주석/설정 불일치: fixedRate=3600000(60분) vs 주석 '5분'
현재 코드는 60분 주기로 동작합니다. 의도가 5분인지 60분인지 확인이 필요합니다.의도를 60분으로 확정하고, 동시에 설정 가능하도록 외부화하는 것을 권장합니다:
- @Scheduled(fixedRate = 60 * 60 * 1000) // 5분마다 실행 + @Scheduled(fixedRateString = "${moadong.scheduling.recruitment-checker.fixed-rate-ms:3600000}") // 60분마다 실행application.yml 예:
moadong: scheduling: recruitment-checker: enabled: true fixed-rate-ms: 3600000 # 필요 시 300000(5분) 등으로 조정backend/src/main/java/moadong/club/payload/request/ClubApplicantDeleteRequest.java (1)
7-10: 유효성 검사 적용 확인 — 해결ClubApplicantEditRequest는 컨트롤러에서 @RequestBody @Valid로 수신(backend/src/main/java/moadong/club/controller/ClubApplyController.java:82)되고, ClubApplicantDeleteRequest는 @RequestBody @validated로 수신(backend/src/main/java/moadong/club/controller/ClubApplyController.java:95)됩니다. @Valid/@validated 둘 다 Spring Bean Validation을 트리거하므로 DTO 제약(@notempty, @notblank 등)은 적용됩니다.
backend/src/main/java/moadong/global/config/MongoConfig.java (1)
1-14: Mongo 트랜잭션(Replica Set) 확인 및 @transactional 명시 필요요약: MongoTransactionManager 빈이 정의되어 있고 서비스에서 @transactional 사용이 확인됩니다. 그러나 리포지토리 내에서 application 설정에서 replicaSet 연결 문자열(spring.data.mongodb.uri 등)이 검색되지 않아 Mongo 트랜잭션 지원 여부가 불확실합니다. JPA와 Mongo가 공존하면 qualifier 없는 @transactional은 잘못된 트랜잭션 매니저를 선택할 위험이 있습니다.
발견 위치:
- backend/src/main/java/moadong/global/config/MongoConfig.java (MongoTransactionManager 빈 생성)
- backend/src/main/java/moadong/club/service/ClubProfileService.java (@transactional)
- backend/src/main/java/moadong/club/service/ClubApplyService.java (@transactional)
조치 권장(짧게):
- application.yml/properties 또는 배포 환경변수(MONGODB_URI 등)에서 replicaSet 파라미터(또는 connection string)를 확인해 Mongo가 replica set으로 동작하는지 검증. replica set이 아니면 MongoTransactionManager 사용 중단 또는 트랜잭션 코드 제거/수정.
- JPA/Mongo 둘 다 트랜잭션 매니저가 존재하면 빈을 명확히 이름 지정(@bean("mongoTransactionManager") / @bean("jpaTransactionManager"))하고, 서비스의 @transactional에 명시적 값(@transactional("jpaTransactionManager") 또는 @transactional("mongoTransactionManager"))을 사용하거나 적절히 @primary 지정.
backend/src/main/java/moadong/club/entity/ClubQuestion.java (1)
9-11: 낙관적 락 구현이 적절합니다.Spring Data MongoDB의
Persistable<String>인터페이스와@Version어노테이션을 활용한 낙관적 락 구현이 잘 되어 있습니다. 동시성 충돌 처리를 위한 버전 관리가 올바르게 설정되었습니다.Also applies to: 24-24, 48-50, 68-71
backend/src/main/java/moadong/user/payload/request/UserUpdateRequest.java (1)
8-16: userId 제거가 적절합니다.비밀번호 업데이트 요청에서 userId를 제거한 것은 보안상 좋은 결정입니다. 사용자 식별은 인증 토큰을 통해 이루어지므로 요청 본문에 userId를 포함할 필요가 없습니다.
backend/src/main/java/moadong/global/exception/ErrorCode.java (1)
9-9: 에러 코드 추가가 적절합니다.동시성 충돌과 비밀번호 정책 위반에 대한 에러 코드가 적절하게 추가되었습니다. 사용자 친화적인 메시지로 명확한 안내를 제공합니다.
Also applies to: 38-39
backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java (1)
28-34: 동시성 충돌 처리가 적절합니다.낙관적 락 실패와 MongoDB 관련 예외를 적절히 처리하고 있습니다. 로그 레벨(warn)과 HTTP 상태 코드(409 CONFLICT) 선택이 적절합니다.
backend/src/main/java/moadong/club/entity/Club.java (1)
12-14: 낙관적 락 구현이 일관성 있게 적용되었습니다.
ClubQuestion과 동일한 패턴으로Persistable<String>인터페이스와@Version어노테이션을 활용한 구현이 잘 되어 있습니다.Also applies to: 24-24, 44-46, 123-126
backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java (1)
6-9: 민감 정보 노출 — 임시 비밀번호를 API 응답으로 반환 중UserCommandService.reset에서 임시 비밀번호를 생성해 TempPasswordResponse를 만들고 UserController.reset이 그대로 반환합니다.
(파일: backend/src/main/java/moadong/user/payload/response/TempPasswordResponse.java, backend/src/main/java/moadong/user/service/UserCommandService.java, backend/src/main/java/moadong/user/controller/UserController.java)권장 조치:
- 평문 임시 비밀번호를 응답에 포함하지 말 것 — 이메일/문자 또는 재설정 토큰(링크)으로 전달하거나 응답에는 상태/토큰만 반환.
- 로그/응답에서 tempPassword가 남지 않도록 ResponseBodyAdvice/인터셉터 또는 로깅 필터로 마스킹/제거 구현.
- TLS(HTTPS) 사용은 전제이나 단독 대책이 아님.
리포지토리에서 logback/log4j 기반 마스킹 또는 응답 인터셉터 구현이 확인되지 않았음 — 적용 여부 확인 필요.
backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (1)
49-49: @RepeatedIfExceptionsTest 사용 재검토프로젝트 검색 결과 해당 어노테이션은 backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (라인 49)에만 사용됩니다. 재시도가 간헐적 실패를 숨길 수 있으므로 아래 중 하나 적용하세요:
- 근본 원인(동시성/타이밍)을 수정해 어노테이션 제거.
- 당분간 유지해야 하면 테스트에 재시도 이유·재현조건·만료일 또는 이슈 링크 주석으로 남기기.
- 대안으로 Awaitility/명시적 동기화(CountDownLatch 등)로 테스트 안정화.
backend/src/main/java/moadong/user/payload/request/UserResetRequest.java (1)
1-11: 잘 구현되었습니다!레코드를 사용한 불변 요청 객체 구현과 적절한 검증 어노테이션 사용이 좋습니다.
backend/src/test/java/moadong/fixture/UserFixture.java (2)
19-21: 팩토리 메서드 추가 승인파라미터가 없는 편의 메서드 추가가 적절합니다. 테스트 코드의 가독성을 향상시킵니다.
10-10: 검증 완료 — 변경된 collectUserId가 테스트에 영향 없음레포 전체에서 "test12345"를 검색한 결과 backend/src/test/java/moadong/fixture/UserFixture.java에서 collectPassword = "test12345@"(라인 11)만 발견되며, collectUserId는 "test123458"로 변경되어 다른 참조는 없습니다.
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
56-106: 동시성 테스트 구현이 적절합니다!CyclicBarrier를 사용한 동시 실행 보장과 AtomicInteger를 통한 결과 카운팅이 잘 구현되었습니다. 낙관적 락 동작을 검증하는 좋은 테스트입니다.
backend/src/main/java/moadong/user/service/UserCommandService.java (1)
136-151: 임시 비밀번호 길이 상향 및 리셋 엔드포인트 권한 검증 필요
- 임시 비밀번호 길이 8자는 짧음 — 최소 12–16자 권장(대/소문자·숫자·특수문자 포함 가정). 예시 변경:
- TempPasswordResponse tempPwdResponse = new TempPasswordResponse( - securePasswordGenerator.generate(8)); + TempPasswordResponse tempPwdResponse = new TempPasswordResponse( + securePasswordGenerator.generate(12));
- reset은 민감 작업입니다 — 컨트롤러에서 관리자 전용 또는 추가 본인확인(예: OTP/이메일 확인, @PreAuthorize/@secured 등)으로 보호되는지 확인 필요. 리포지토리 검색 결과가 없어 엔드포인트 위치·권한 어노테이션을 직접 확인해야 합니다.
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)
42-48: editClubApplication에 @transactional 추가 적절폼/질문 동시 수정 시 원자성 보장에 도움이 됩니다. 현재 save 1회로 마무리되는 것도 좋습니다.
| annotationProcessor 'org.projectlombok:lombok' | ||
|
|
||
|
|
||
| testImplementation 'io.github.artsok:rerunner-jupiter:2.1.6' |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
JUnit 재시도 라이브러리 추가 OK, 호환성만 확인해 주세요
rerunner-jupiter:2.1.6 추가 자체는 문제 없습니다. 현재 스프링 부트 3.3.x(= JUnit Platform 1.10.x 계열)와의 호환성을 한 번 확인해 주세요. 필요 시 Gradle test-retry 플러그인으로 빌드 레벨 재시도도 고려 가능합니다.
🌐 Web query:
Is io.github.artsok:rerunner-jupiter:2.1.6 compatible with JUnit 5.10.x / JUnit Platform 1.10?
💡 Result:
Short answer: No — rerunner-jupiter:2.1.6 is not guaranteed compatible with JUnit 5.10 / Platform 1.10. The artifact was built against very old JUnit/Jupiter/platform coordinates (see its published dependencies) and users report discovery/NoSuchMethod errors when extensions or plugins are used with newer JUnit Platform 1.10+ launches. [1][2][3]
Sources:
- rerunner-jupiter GitHub / pom (project and README). [1]
- Maven Central / mvnrepository page showing the artifact's older JUnit dependencies (junit-jupiter 5.4.2 / junit-platform-runner 1.4.2). [2]
- Examples of runtime discovery/NoSuchMethod errors when mixing older JUnit/Jupiter extensions with newer JUnit Platform versions (community reports). [3]
rerunner-jupiter:2.1.6는 JUnit Platform 1.10(JUnit 5.10)과 호환되지 않습니다 — 수정 필요
해당 아티팩트는 오래된 JUnit/Jupiter/Platform(예: junit-jupiter 5.4.2, junit-platform-runner 1.4.2)로 빌드되어 JUnit Platform 1.10+ 환경에서 discovery/NoSuchMethod 런타임 오류가 보고됩니다.
위치: backend/build.gradle 라인 44
- 즉시 조치: testImplementation 'io.github.artsok:rerunner-jupiter:2.1.6' 제거 또는 주석 처리.
- 권장 대체: Gradle 공식 test-retry 플러그인 사용 또는 JUnit Platform 1.10과 명시적으로 호환되는 재시도 확장으로 교체하고, 로컬/CI에서 테스트 실행을 검증.
🤖 Prompt for AI Agents
In backend/build.gradle around line 44, the dependency testImplementation
'io.github.artsok:rerunner-jupiter:2.1.6' is incompatible with JUnit Platform
1.10+ and causes discovery/NoSuchMethod errors; remove or comment out that
dependency, replace it with a compatible retry solution (for example apply
Gradle's official test-retry plugin or a retry extension explicitly compatible
with JUnit Platform 1.10+), update build configuration to use the chosen retry
mechanism, and run tests locally/CI to verify the failure is resolved.
| @Transactional | ||
| public void editClubApplicationQuestion(String questionId, CustomUserDetails user, ClubApplicationEditRequest request) { | ||
| ClubQuestion clubQuestion = clubQuestionRepository.findById(questionId) | ||
| .orElseThrow(() -> new RestApiException(ErrorCode.QUESTION_NOT_FOUND)); | ||
|
|
||
| updateQuestions(clubQuestion, request); | ||
| clubQuestion.updateEditedAt(); | ||
|
|
||
| clubQuestionRepository.save(clubQuestion); | ||
| } | ||
|
|
There was a problem hiding this comment.
질문 ID 기반 수정 메서드에 권한 검증이 누락되어 있습니다.
questionId만으로 엔티티를 로드하고 사용자 소유 검증을 하지 않아 다른 클럽의 폼을 수정할 수 있습니다. clubId 소유자 검증을 추가하세요.
public void editClubApplicationQuestion(String questionId, CustomUserDetails user, ClubApplicationEditRequest request) {
- ClubQuestion clubQuestion = clubQuestionRepository.findById(questionId)
- .orElseThrow(() -> new RestApiException(ErrorCode.QUESTION_NOT_FOUND));
+ ClubQuestion clubQuestion = clubQuestionRepository.findById(questionId)
+ .orElseThrow(() -> new RestApiException(ErrorCode.QUESTION_NOT_FOUND));
+ // 소유자 권한 검증
+ Club club = clubRepository.findById(clubQuestion.getClubId())
+ .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND));
+ if (!user.getId().equals(club.getUserId())) {
+ throw new RestApiException(ErrorCode.USER_UNAUTHORIZED);
+ }
updateQuestions(clubQuestion, request);
clubQuestion.updateEditedAt();
clubQuestionRepository.save(clubQuestion);
}📝 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.
| @Transactional | |
| public void editClubApplicationQuestion(String questionId, CustomUserDetails user, ClubApplicationEditRequest request) { | |
| ClubQuestion clubQuestion = clubQuestionRepository.findById(questionId) | |
| .orElseThrow(() -> new RestApiException(ErrorCode.QUESTION_NOT_FOUND)); | |
| updateQuestions(clubQuestion, request); | |
| clubQuestion.updateEditedAt(); | |
| clubQuestionRepository.save(clubQuestion); | |
| } | |
| @Transactional | |
| public void editClubApplicationQuestion(String questionId, CustomUserDetails user, ClubApplicationEditRequest request) { | |
| ClubQuestion clubQuestion = clubQuestionRepository.findById(questionId) | |
| .orElseThrow(() -> new RestApiException(ErrorCode.QUESTION_NOT_FOUND)); | |
| // 소유자 권한 검증 | |
| Club club = clubRepository.findById(clubQuestion.getClubId()) | |
| .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); | |
| if (!user.getId().equals(club.getUserId())) { | |
| throw new RestApiException(ErrorCode.USER_UNAUTHORIZED); | |
| } | |
| updateQuestions(clubQuestion, request); | |
| clubQuestion.updateEditedAt(); | |
| clubQuestionRepository.save(clubQuestion); | |
| } |
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/service/ClubApplyService.java around lines
50 to 60, the editClubApplicationQuestion method loads a ClubQuestion by
questionId but does not verify that the requesting user owns the club/form; add
an ownership check before updating: retrieve the clubId (or club entity) from
the loaded ClubQuestion, compare it against the user's club/owner id (from
CustomUserDetails), and if they do not match throw a RestApiException with the
appropriate error code (e.g., FORBIDDEN or custom NOT_AUTHORIZED); perform this
check immediately after or within the orElseThrow block and before calling
updateQuestions/updateEditedAt and saving.
| @Transactional | ||
| public void updateClubInfo(ClubInfoRequest request, CustomUserDetails user) { | ||
| Club club = clubRepository.findClubByUserId(user.getId()) | ||
| .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); | ||
|
|
||
| .orElseThrow(() -> new RestApiException(ErrorCode.CLUB_NOT_FOUND)); | ||
| club.update(request); | ||
| clubRepository.save(club); | ||
| } |
There was a problem hiding this comment.
다중 트랜잭션 매니저 환경에서는 명시적으로 Mongo TM을 지정해야 합니다
프로젝트가 JPA와 Mongo를 모두 사용합니다. 기본 빈 이름 transactionManager 충돌 시 잘못된 TM이 선택될 수 있습니다. Mongo 트랜잭션을 확실히 쓰도록 qualifier를 명시해 주세요.
다음과 같이 변경 제안:
- @Transactional
+ @Transactional("mongoTransactionManager")
public void updateClubInfo(ClubInfoRequest request, CustomUserDetails user) {🤖 Prompt for AI Agents
In backend/src/main/java/moadong/club/service/ClubProfileService.java around
lines 30 to 36, the method uses @Transactional but in a mixed JPA+Mongo
environment the default transaction manager may resolve incorrectly; update the
annotation to explicitly target the Mongo transaction manager by specifying the
transactionManager attribute (e.g. transactionManager =
"mongoTransactionManager") on the @Transactional annotation so the Mongo TM is
used for this method.
| @Bean | ||
| public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) { | ||
| return new MongoTransactionManager(dbFactory); | ||
| } |
There was a problem hiding this comment.
Bean 이름 충돌로 JPA 트랜잭션이 깨질 수 있습니다 — 별도 이름으로 분리 필요
현재 메서드명이 transactionManager라 기본 JPA TM(transactionManager)과 충돌/대체될 수 있습니다. Mongo 전용 이름으로 분리하고, 사용하는 곳에서 qualifier를 지정해 주세요.
- @Bean
- public MongoTransactionManager transactionManager(MongoDatabaseFactory dbFactory) {
- return new MongoTransactionManager(dbFactory);
- }
+ @Bean(name = "mongoTransactionManager")
+ public MongoTransactionManager mongoTransactionManager(MongoDatabaseFactory dbFactory) {
+ return new MongoTransactionManager(dbFactory);
+ }또한 Mongo를 사용하는 서비스 메서드에는 @Transactional("mongoTransactionManager")를 적용해 주세요.
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/global/config/MongoConfig.java around lines
10–13, the @Bean method is named transactionManager which can override the
default JPA TransactionManager bean; rename the bean to a Mongo-specific name
(e.g., method name mongoTransactionManager and/or
@Bean(name="mongoTransactionManager")) so it does not collide with JPA, return
the same MongoTransactionManager(dbFactory), and update all services that
require Mongo transactions to use @Transactional("mongoTransactionManager") (or
add @Qualifier where injecting) so Mongo transactions are explicitly targeted.
There was a problem hiding this comment.
🧩 Analysis chain
@alsdddk 좋은 질문입니다! 트랜잭션 매니저 충돌 가능성을 정확히 분석해보겠습니다.
🏁 Script executed:
#!/bin/bash
# Check if JPA is being used in this project
echo "=== Checking for JPA dependencies ==="
cat backend/build.gradle | grep -i jpa
echo -e "\n=== Checking for JPA annotations and imports ==="
rg -n --type=java "import.*jpa" backend/src/
echo -e "\n=== Checking for @Entity annotations ==="
rg -n --type=java "@Entity" backend/src/
echo -e "\n=== Checking current @Transactional usage ==="
rg -n --type=java "@Transactional" backend/src/ | head -10
echo -e "\n=== Checking for existing TransactionManager beans ==="
rg -n --type=java "TransactionManager" backend/src/
Length of output: 1216
🏁 Script executed:
#!/bin/bash
# Check what entities the @Transactional services are working with
echo "=== Checking ClubProfileService imports and entity usage ==="
rg -n --type=java -A5 -B5 "import.*entity" backend/src/main/java/moadong/club/service/ClubProfileService.java
echo -e "\n=== Checking ClubApplyService imports and entity usage ==="
rg -n --type=java -A5 -B5 "import.*entity" backend/src/main/java/moadong/club/service/ClubApplyService.java
echo -e "\n=== Checking UserInformation entity details ==="
cat backend/src/main/java/moadong/user/entity/UserInformation.java | head -30
echo -e "\n=== Checking if UserInformation is used with transactions ==="
rg -n --type=java "UserInformation" backend/src/ | grep -v "\.java:"
Length of output: 1791
🏁 Script executed:
#!/bin/bash
# Check UserInformation repository usage
echo "=== Checking UserInformation repository ==="
rg -n --type=java "UserInformation" backend/src/main/java/moadong/user/repository/
echo -e "\n=== Looking for UserInformation repository implementations ==="
fd "UserInformation.*Repository" backend/src/
echo -e "\n=== Checking what persistence UserInformation actually uses ==="
rg -n --type=java -C3 "UserInformation" backend/src/main/java/moadong/user/service/
echo -e "\n=== Checking current transaction manager configuration ==="
rg -n --type=java -A10 -B5 "TransactionManager" backend/src/main/resources/ || echo "No explicit transaction manager config in resources"
echo -e "\n=== Checking application properties for transaction settings ==="
find backend/src/main/resources -name "*.yml" -o -name "*.properties" | xargs grep -i "transaction" || echo "No transaction settings found"
Length of output: 380
네, 실제로 문제가 될 수 있습니다! 코드베이스를 분석한 결과를 말씀드리겠습니다:
🚨 확인된 문제점
-
JPA와 MongoDB 동시 사용:
spring-boot-starter-data-jpa의존성 존재UserInformation에@Entity어노테이션 존재
-
트랜잭션 매니저 충돌:
- Spring Boot가 JPA용으로
transactionManager빈을 자동 생성 - 현재 MongoConfig가 같은 이름으로 MongoDB용 빈 생성하여 덮어씀
- Spring Boot가 JPA용으로
-
현재 @transactional 사용 패턴:
ClubProfileService,ClubApplyService의 @transactional은 MongoDB 엔티티 작업- 하지만 qualifier 없이 사용하면 어떤 트랜잭션 매니저를 사용할지 불확실
✅ 권장 해결방안
@Configuration
public class MongoConfig {
@Bean(name = "mongoTransactionManager")
public MongoTransactionManager mongoTransactionManager(MongoDatabaseFactory dbFactory) {
return new MongoTransactionManager(dbFactory);
}
}그리고 MongoDB 트랜잭션이 필요한 서비스에서:
@Transactional("mongoTransactionManager")
public void updateClubInfo(...) { ... }
@Transactional("mongoTransactionManager")
public void editApplicantDetail(...) { ... }이렇게 하면 JPA와 MongoDB 트랜잭션이 각각 독립적으로 안전하게 작동합니다.
현재 상태로는 JPA 트랜잭션이 의도치 않게 깨질 가능성이 있으니 수정하시는 것이 좋겠습니다! 👍
| @@ -1,10 +1,12 @@ | |||
| package moadong.global.exception; | |||
|
|
|||
| import com.google.api.Http; | |||
There was a problem hiding this comment.
불필요한 import 제거 필요
com.google.api.Http import가 사용되지 않고 있습니다. 제거해 주세요.
-import com.google.api.Http;
import lombok.Getter;📝 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.
| import com.google.api.Http; | |
| import lombok.Getter; |
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/global/exception/ErrorCode.java around line 3,
the import statement for com.google.api.Http is unused; remove that import line
(or run an IDE/org.apache.maven plugin to organize imports) so the file no
longer contains the unnecessary import.
| .body(new Response("BAD_REQUEST", finalErrorMessage, null)); | ||
| } | ||
|
|
||
| @ExceptionHandler({OptimisticLockingFailureException.class, UncategorizedMongoDbException.class}) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
예외 처리 범위 검토 필요
UncategorizedMongoDbException은 너무 광범위한 예외입니다. 모든 MongoDB 예외가 동시성 충돌로 처리될 수 있습니다.
MongoDB의 특정 오류 코드(예: WriteConflict)를 확인하여 실제 동시성 충돌만 처리하도록 개선하는 것을 고려해보세요. 예외 핸들러를 다음과 같이 수정할 수 있습니다:
@ExceptionHandler(UncategorizedMongoDbException.class)
public ResponseEntity<Response> handleMongoException(UncategorizedMongoDbException ex) {
// MongoDB WriteConflict 오류 코드: 112
if (ex.getMessage() != null && ex.getMessage().contains("WriteConflict")) {
log.warn("데이터베이스 충돌 발생: {}", ex.getMessage());
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(new Response(ErrorCode.CONCURRENCY_CONFLICT.getCode(),
ErrorCode.CONCURRENCY_CONFLICT.getMessage(), null));
}
// 다른 MongoDB 예외는 다르게 처리
throw ex;
}다음 스크립트를 실행하여 코드베이스에서 MongoDB 예외 처리 패턴을 확인해주세요:
🏁 Script executed:
#!/bin/bash
# MongoDB 예외 처리 패턴 확인
rg -n "UncategorizedMongoDbException" --type java -C 3
# WriteConflict 관련 코드 확인
rg -n "WriteConflict" --type java -C 3Length of output: 3543
UncategorizedMongoDbException을 동시성 충돌만 처리하도록 범위 축소하세요
GlobalExceptionHandler의 @ExceptionHandler({OptimisticLockingFailureException.class, UncategorizedMongoDbException.class})는 너무 광범위합니다. UncategorizedMongoDbException을 별도 핸들러로 분리하여 ex.getMessage().contains("WriteConflict") 또는 가능한 경우 MongoDB 오류 코드를 확인할 때만 HttpStatus.CONFLICT(409)를 반환하고, 그렇지 않으면 예외를 재던지거나 다른 방식으로 처리하세요.
파일: backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java (라인 28)
| @PostMapping("/reset") | ||
| @Operation(summary = "사용자 비밀번호 초기화", description = "사용자 비밀번호를 초기화합니다.") | ||
| public ResponseEntity<?> reset(@RequestBody @Validated UserResetRequest userResetRequest) { | ||
| TempPasswordResponse tempPwdResponse = userCommandService.reset(userResetRequest.userId()); | ||
| return Response.ok(tempPwdResponse); | ||
| } |
There was a problem hiding this comment.
비밀번호 초기화 엔드포인트 보안 강화 필요
비밀번호 초기화는 매우 민감한 작업임에도 인증이나 권한 검증이 없습니다. 이는 누구나 다른 사용자의 비밀번호를 초기화할 수 있는 심각한 보안 취약점입니다.
관리자 권한 검증을 추가하세요:
@PostMapping("/reset")
@Operation(summary = "사용자 비밀번호 초기화", description = "사용자 비밀번호를 초기화합니다.")
+@PreAuthorize("hasRole('ADMIN')")
+@SecurityRequirement(name = "BearerAuth")
-public ResponseEntity<?> reset(@RequestBody @Validated UserResetRequest userResetRequest) {
+public ResponseEntity<?> reset(@AuthenticationPrincipal CustomUserDetails adminUser,
+ @RequestBody @Validated UserResetRequest userResetRequest) {
TempPasswordResponse tempPwdResponse = userCommandService.reset(userResetRequest.userId());
return Response.ok(tempPwdResponse);
}추가로 감사 로그 기록도 고려하세요.
📝 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.
| @PostMapping("/reset") | |
| @Operation(summary = "사용자 비밀번호 초기화", description = "사용자 비밀번호를 초기화합니다.") | |
| public ResponseEntity<?> reset(@RequestBody @Validated UserResetRequest userResetRequest) { | |
| TempPasswordResponse tempPwdResponse = userCommandService.reset(userResetRequest.userId()); | |
| return Response.ok(tempPwdResponse); | |
| } | |
| @PostMapping("/reset") | |
| @Operation(summary = "사용자 비밀번호 초기화", description = "사용자 비밀번호를 초기화합니다.") | |
| @PreAuthorize("hasRole('ADMIN')") | |
| @SecurityRequirement(name = "BearerAuth") | |
| public ResponseEntity<?> reset(@AuthenticationPrincipal CustomUserDetails adminUser, | |
| @RequestBody @Validated UserResetRequest userResetRequest) { | |
| TempPasswordResponse tempPwdResponse = userCommandService.reset(userResetRequest.userId()); | |
| return Response.ok(tempPwdResponse); | |
| } |
There was a problem hiding this comment.
이건 의도된 것입니다.
추후 이메일 및 SMS 인증 기능 도입 예정입니다.
There was a problem hiding this comment.
@alsdddk 설명해주셔서 감사합니다. 의도된 설계라는 점을 이해했습니다.
임시적인 구현이라면 코드에 TODO 주석이나 문서화를 통해 향후 인증 기능 추가 계획을 명시해두시는 것을 권장드립니다. 이렇게 하면 다른 개발자들도 현재 상태가 임시적임을 알 수 있을 것입니다.
✏️ Learnings added
Learnt from: alsdddk
PR: Moadong/moadong#734
File: backend/src/main/java/moadong/user/controller/UserController.java:97-102
Timestamp: 2025-09-11T23:31:34.304Z
Learning: 비밀번호 초기화 엔드포인트(/auth/user/reset)의 인증 없는 구현은 의도된 것이며, 추후 이메일 및 SMS 인증 기능을 도입할 예정이다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| public void updateUserProfile(UserUpdateRequest userUpdateRequest) { | ||
| this.userId = userUpdateRequest.userId(); | ||
| this.password = userUpdateRequest.password(); | ||
| } |
There was a problem hiding this comment.
비밀번호 직접 할당 보안 위험
updateUserProfile 메서드에서 인코딩되지 않은 비밀번호를 직접 할당하고 있습니다. 이는 평문 비밀번호가 데이터베이스에 저장될 수 있는 심각한 보안 문제입니다.
비밀번호는 반드시 인코딩된 후 저장되어야 합니다:
-public void updateUserProfile(UserUpdateRequest userUpdateRequest) {
- this.password = userUpdateRequest.password();
-}
+public void updateUserProfile(String encodedPassword) {
+ this.password = encodedPassword;
+}또는 서비스 레이어에서 인코딩된 비밀번호를 전달받도록 확인하세요.
📝 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.
| public void updateUserProfile(UserUpdateRequest userUpdateRequest) { | |
| this.userId = userUpdateRequest.userId(); | |
| this.password = userUpdateRequest.password(); | |
| } | |
| public void updateUserProfile(String encodedPassword) { | |
| this.password = encodedPassword; | |
| } |
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/user/entity/User.java around lines 76 to 78,
the updateUserProfile method assigns the raw password directly which risks
storing plaintext; change it to store an encoded password (e.g., call
passwordEncoder.encode(userUpdateRequest.password())) or require the service
layer to pass an already-encoded password and rename parameter accordingly;
inject or pass a PasswordEncoder to the place where encoding occurs, update the
method signature if you expect encoded input, and ensure unit/tests reflect
encoding behavior so no raw passwords are persisted.
| public void resetPassword(String encodedPassword) { //초기화된 비밀번호 업데이트 | ||
| this.password = encodedPassword; | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
메서드 접근 제어 검토 필요
비밀번호 초기화는 민감한 작업이므로 이 메서드가 적절한 권한 검증 후에만 호출되는지 확인이 필요합니다.
🏁 Script executed:
#!/bin/bash
# UserCommandService에서 resetPassword 메서드 호출 및 권한 검증 확인
ast-grep --pattern 'resetPassword($_)'
# UserController의 reset 엔드포인트 권한 설정 확인
rg -n -C3 '@PostMapping\("/reset"\)' backend/src/main/java/moadong/user/controller/UserController.javaLength of output: 373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== UserController reset endpoint snippet ==="
rg -n -C3 '@PostMapping\(\"/reset\"\)' backend/src/main/java -S || true
file="backend/src/main/java/moadong/user/controller/UserController.java"
if [ -f "$file" ]; then
echo "--- $file ---"
sed -n '1,240p' "$file"
else
echo "UserController file not found at expected path; searching..."
rg -n --hidden -S 'class\s+UserController' backend/src/main/java -C3 || true
file=$(rg -n --hidden -S 'class\s+UserController' backend/src/main/java -l || true)
if [ -n "$file" ]; then
sed -n '1,240p' "$file"
fi
fi
echo
echo "=== locate UserCommandService ==="
rg -n --hidden -S 'class\s+UserCommandService' backend/src/main/java -g '!**/test/**' -C3 || true
user_cmd_file=$(rg -n --hidden -S 'class\s+UserCommandService' backend/src/main/java -l || true)
if [ -n "$user_cmd_file" ]; then
echo "--- $user_cmd_file ---"
sed -n '1,300p' "$user_cmd_file"
fi
echo
echo "=== search for resetPassword usages ==="
rg -n --hidden -S 'resetPassword\s*\(' backend -g '!**/test/**' -C3 || true
echo
echo "=== search for reset(...) definitions/usages ==="
rg -n --hidden -S '\breset\s*\(' backend/src/main/java -g '!**/test/**' -C3 || true
echo
echo "=== Security annotations in user controller/service ==="
rg -n --hidden -S '@PreAuthorize|@Secured|@RolesAllowed' backend/src/main/java -g '!**/test/**' -C3 || true
echo
echo "=== Security config (method-level enable / HttpSecurity) ==="
rg -n --hidden -S '@EnableMethodSecurity|EnableGlobalMethodSecurity|SecurityFilterChain|HttpSecurity' backend/src/main/java -g '!**/test/**' -C3 || trueLength of output: 12423
비밀번호 초기화 엔드포인트에 인증/권한 검증 누락 — 즉시 수정 필요
검증 결과: POST /auth/user/reset (backend/src/main/java/moadong/user/controller/UserController.java의 reset 메서드)에 인증/권한 어노테이션이 없으며, userCommandService.reset(userId) → User.resetPassword(...)로 바로 임시 비밀번호를 설정합니다. 누구나 userId로 타인 비밀번호를 초기화할 수 있는 상태입니다.
- 영향 위치: backend/src/main/java/moadong/user/controller/UserController.java (reset 엔드포인트), backend/src/main/java/moadong/user/service/UserCommandService.java::reset, backend/src/main/java/moadong/user/entity/User.java (resetPassword, lines 80-82).
- 조치(권장): 엔드포인트 접근 제어 추가(@PreAuthorize 등 역할/인증 검증) 또는 이메일/토큰 기반 본인확인 흐름으로 재설계. 임시 비밀번호 전송 방식, 레이트리밋, 감사 로깅 등 보안 보강도 적용.
🤖 Prompt for AI Agents
In backend/src/main/java/moadong/user/entity/User.java around lines 80-82 and
the related reset flow, the User.resetPassword method is being invoked from an
unauthenticated endpoint allowing arbitrary password resets; fix by enforcing
access control and verification: add authentication/authorization to POST
/auth/user/reset (e.g., @PreAuthorize to require owner or admin role) or replace
the endpoint with an email/token-based verification flow so controllers/service
only accept a verified reset request; update UserCommandService.reset to
validate the token/email and rate-limit attempts, perform audit logging, and
only then call User.resetPassword with an encoded temp password; ensure the temp
password is sent via a secure channel and that all checks occur before mutating
the entity.
📝작업 내용
5차 MVP 기능 및 개선사항을 main 브랜치에 병합합니다
신기능
개선
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit