Applicant 도메인 기반 구조 추가#571
Conversation
|
Warning Review limit reached
More reviews will be available in 31 minutes and 23 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
Walkthrough지원자 도메인(Applicant Domain)의 전체 구현이 추가됩니다. PIN 기반 임시 등록, 초기 프로필 설정, PIN 관리를 제공하는 예외 처리, 엔티티, 저장소, DTO, 서비스, 컨트롤러 계층과 포괄적인 테스트가 포함됩니다. 또한 JWT 토큰 생성 기능 확장과 CI 워크플로 설정 변경이 함께 이루어집니다. ChangesApplicant 도메인 전체 구현
Sequence Diagram(s)sequenceDiagram
actor Client
participant ApplicantController
participant JwtTokenProvider
participant ApplicantService
participant PasswordEncoder
participant SemesterRepository
participant ApplicantRepository
Client->>ApplicantController: POST /apply (verification token, PIN)
ApplicantController->>JwtTokenProvider: parseVerificationToken(token)
JwtTokenProvider-->>ApplicantController: email
ApplicantController->>ApplicantService: registerTempApplicant(email, PIN)
ApplicantService->>ApplicantRepository: findByEmail(email)
ApplicantRepository-->>ApplicantService: Optional (check duplicate)
ApplicantService->>PasswordEncoder: encode(PIN)
PasswordEncoder-->>ApplicantService: encodedPin
ApplicantService->>SemesterRepository: findRecruitingSemester()
SemesterRepository-->>ApplicantService: semesterId
ApplicantService->>ApplicantRepository: save(new Applicant)
ApplicantRepository-->>ApplicantService: saved applicant
ApplicantService->>JwtTokenProvider: createAuthenticationByApplicant(applicant)
JwtTokenProvider-->>ApplicantService: Authentication token
ApplicantService-->>ApplicantController: Authentication
ApplicantController->>ApplicantController: CustomSuccessHandler.onAuthenticationSuccess()
ApplicantController->>ApplicantController: deleteCookie(verificationToken)
ApplicantController-->>Client: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
Test Results409 tests 409 ✅ 13s ⏱️ Results for commit 354afff. ♻️ This comment has been updated with latest results. |
📊테스트 커버리지
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/main/java/org/ject/support/domain/applicant/entity/Applicant.java (1)
114-116: ⚡ Quick win
isInitialed()와isProfileComplete()가 동일 로직으로 중복됩니다.두 메서드 모두
name != null && phoneNumber != null을 반환합니다. 의미가 같다면 하나로 통합하거나, 한쪽이 다른 쪽을 위임하도록 정리해 향후 조건 변경 시 불일치 위험을 줄이세요.♻️ 제안
public boolean isInitialed() { - return this.name != null && this.phoneNumber != null; + return isProfileComplete(); }Also applies to: 165-167
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/org/ject/support/domain/applicant/entity/Applicant.java` around lines 114 - 116, Applicant currently has duplicate logic in isInitialed() and isProfileComplete() (both return name != null && phoneNumber != null); remove duplication by having one canonical method (choose either isProfileComplete or isInitialed) implement the actual check and have the other simply delegate to it (e.g., isInitialed() -> return isProfileComplete()), or delete the redundant method and update callers to use the remaining method; update any references to the removed method accordingly (look for Applicant.isInitialed and Applicant.isProfileComplete).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/org/ject/support/domain/applicant/entity/Applicant.java`:
- Around line 49-50: Applicant.email is limited to `@Column`(length = 30) but
emails extracted in ApplicantController.extractEmailFromVerificationToken and
passed into ApplicantService.registerTempApplicant are not length-validated,
risking DB insert failures; fix by aligning model and validation: either
increase the column length on Applicant.email to a realistic max (e.g., 254) and
update migrations, or add a validation annotation (e.g., `@Size`(max=30) or a
larger agreed limit) plus explicit trimming/validation in
ApplicantService.registerTempApplicant (and ensure controller extraction
enforces same constraint) so DB schema and runtime validation match.
In
`@src/main/java/org/ject/support/domain/applicant/repository/ApplicantQueryRepositoryImpl.java`:
- Around line 29-37: The query in
ApplicantQueryRepositoryImpl.findEmailsByIdsAndNotSubmitted can over-include and
return duplicate emails because apply.status != SUBMITTED matches applicants who
also have submitted rows and multiple non-submitted rows; change the query to
select distinct applicant.email and replace the apply.status != SUBMITTED
predicate with a NOT EXISTS subquery that checks for any SUBMITTED apply for the
same applicant (use apply.member.id.eq(applicant.id) and
apply.status.eq(SUBMITTED) inside the subquery) so only applicants with no
submitted applications are returned.
In
`@src/main/java/org/ject/support/domain/applicant/service/ApplicantService.java`:
- Around line 40-54: registerTempApplicant currently does a check-then-act using
applicantRepository.findByEmail(...) then createTempApplicantWithPin(...) which
allows TOCTOU duplicates; add a unique constraint on Applicant.email (either via
`@Column`(unique=true)/@UniqueConstraint on the Applicant entity and/or a DB
migration) and wrap the save in createTempApplicantWithPin (or the caller) with
a catch for DataIntegrityViolationException (or the specific persistence
exception) to rethrow ApplicantException(ALREADY_EXIST_APPLICANT); also update
the else-branch comment near applicantRepository.findByEmail(...) to reflect
that an exception is thrown rather than “PIN only updated.”
In
`@src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java`:
- Around line 62-66: The test setup in ApplicantControllerTest uses
standaloneSetup(applicantController) but does not register the custom argument
resolver for `@AuthPrincipal`; update the setUp method to register
AuthenticatedMemberIdResolver via
standaloneSetup(applicantController).setCustomArgumentResolvers(new
AuthenticatedMemberIdResolver()) so `@AuthPrincipal` Long applicantId is bound
from SecurityContextHolder's CustomUserDetails (mirror
WebConfig#addArgumentResolvers), or alternatively convert the test to use a
Spring MVC slice (e.g., `@WebMvcTest`) so the WebConfig argument resolvers are
applied; ensure the change targets the setUp method and references
applicantController, standaloneSetup, setCustomArgumentResolvers,
AuthenticatedMemberIdResolver, `@AuthPrincipal` and SecurityContextHolder.
---
Nitpick comments:
In `@src/main/java/org/ject/support/domain/applicant/entity/Applicant.java`:
- Around line 114-116: Applicant currently has duplicate logic in isInitialed()
and isProfileComplete() (both return name != null && phoneNumber != null);
remove duplication by having one canonical method (choose either
isProfileComplete or isInitialed) implement the actual check and have the other
simply delegate to it (e.g., isInitialed() -> return isProfileComplete()), or
delete the redundant method and update callers to use the remaining method;
update any references to the removed method accordingly (look for
Applicant.isInitialed and Applicant.isProfileComplete).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: abf69d8c-597c-4dd2-9312-3a9cbaf3f585
📒 Files selected for processing (19)
.github/workflows/continuous-intergration.ymlsrc/main/java/org/ject/support/common/security/jwt/JwtTokenProvider.javasrc/main/java/org/ject/support/domain/applicant/controller/ApplicantApiSpec.javasrc/main/java/org/ject/support/domain/applicant/controller/ApplicantController.javasrc/main/java/org/ject/support/domain/applicant/dto/ApplicantAccountProjection.javasrc/main/java/org/ject/support/domain/applicant/dto/ApplicantDto.javasrc/main/java/org/ject/support/domain/applicant/dto/ApplicantProfileResponse.javasrc/main/java/org/ject/support/domain/applicant/entity/Applicant.javasrc/main/java/org/ject/support/domain/applicant/entity/ApplicantEditor.javasrc/main/java/org/ject/support/domain/applicant/exception/ApplicantErrorCode.javasrc/main/java/org/ject/support/domain/applicant/exception/ApplicantException.javasrc/main/java/org/ject/support/domain/applicant/repository/ApplicantQueryRepository.javasrc/main/java/org/ject/support/domain/applicant/repository/ApplicantQueryRepositoryImpl.javasrc/main/java/org/ject/support/domain/applicant/repository/ApplicantRepository.javasrc/main/java/org/ject/support/domain/applicant/service/ApplicantService.javasrc/test/java/org/ject/support/domain/applicant/ApplicantTest.javasrc/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.javasrc/test/java/org/ject/support/domain/applicant/repository/ApplicantRepositoryTest.javasrc/test/java/org/ject/support/domain/applicant/service/ApplicantServiceTest.java
| @Column(length = 30, nullable = false) | ||
| private String email; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 기존 member/legacy applicant의 email 컬럼 길이 정의 및 마이그레이션 확인
rg -nP '\bemail\b' --type=java -C2 -g '*Member*.java'
fd -e sql | xargs -r rg -niP 'email' -C1Repository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 50395
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Applicant.java의 email 컬럼 애노테이션 실제 값 확인
FILE="src/main/java/org/ject/support/domain/applicant/entity/Applicant.java"
if [ -f "$FILE" ]; then
echo "=== $FILE (lines 35-70) ==="
nl -ba "$FILE" | sed -n '35,70p'
else
echo "Missing file: $FILE" >&2
fi
# 2) Applicant 관련 엔티티/쿼리에서 email length(컬럼) 사용처 확인
echo "=== Applicant email length usages (in src/main/java) ==="
rg -n "class Applicant|Applicant.*email|`@Column`\\(.*email|private String email" src/main/java -S || true
# 3) DB 마이그레이션/DDL에서 applicant email 길이 확인 (sql/ddl 관련 폴더를 넓게 보되 검색은 가볍게)
echo "=== DB migration ddl: applicant + email length/definition ==="
for d in . src/main src/main/resources src/main/resources/db src/main/resources/database src/main/resources/migration src/main/resources/db/migration src/main/resources/flyway src/main/resources/liquibase; do
if [ -d "$d" ]; then
rg -n --hidden --no-ignore-vcs "(applicant|applicants).*email|email.*(applicant|applicants)|CREATE TABLE.*applicant|ALTER TABLE.*applicant" "$d" -S -g'*.sql' -g'*.ddl' -g'*.txt' 2>/dev/null | head -n 80 || true
fi
done
# 4) 코드에서 이메일 길이 제한(Bean Validation `@Size`, `@Pattern` 등) 확인
echo "=== Email length validations (`@Size` or similar) ==="
rg -n "`@Size`\\(|`@Length`\\(|`@Pattern`\\(.*email|email.*`@Size`|email.*`@Length`" src/main/java src/test/java -S || trueRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 236
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/main/java/org/ject/support/domain/applicant/entity/Applicant.java"
echo "=== $FILE (lines 1-120) preview (for locating email annotation) ==="
if [ -f "$FILE" ]; then
sed -n '1,120p' "$FILE" | nl -ba 2>/dev/null || sed -n '1,120p' "$FILE"
else
echo "Missing file: $FILE" >&2
fi
echo "=== Applicant entity email annotation (`@Column`...) ==="
rg -n "`@Column`\\([^\\)]*length\\s*=\\s*([0-9]+)[^\\)]*\\)|private String email" "$FILE" || true
echo "=== Applicant email usages (src/main/java) near email column mappings/validators ==="
rg -n "Applicant.*email|`@Column`\\(.*email|private String email|email.*`@Size`|email.*`@Length`|`@Size`\\(|`@Length`\\(|email.*`@Pattern`" src/main/java -S || true
echo "=== DB DDL/migrations: applicant email column length ==="
# Restrict to migration folders if they exist; otherwise search in resources.
SEARCH_DIRS=(
"src/main/resources/db"
"src/main/resources/database"
"src/main/resources/migration"
"src/main/resources/flyway"
"src/main/resources/liquibase"
"src/main/resources"
)
for d in "${SEARCH_DIRS[@]}"; do
[ -d "$d" ] || continue
echo "--- scanning: $d ---"
rg -n --hidden --no-ignore-vcs "(applicant|applicants).*email|email.*(applicant|applicants)|CREATE TABLE.*applicant|ALTER TABLE.*applicant" "$d" \
-S -g'*.sql' -g'*.ddl' -g'*.txt' 2>/dev/null | head -n 120 || true
doneRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 8350
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Migration에서 applicant.email 컬럼 타입/길이 확인
MIG="src/main/resources/db/migration/V28__create_applicant_and_copy_member.sql"
echo "=== Inspect migration: $MIG (email column definition) ==="
if [ -f "$MIG" ]; then
# Show around CREATE TABLE applicant ... email
rg -n "CREATE TABLE applicant|email" "$MIG" || true
# Print a focused window around the first "email" occurrence
line=$(rg -n "email" "$MIG" | head -n 1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$((line-20)); end=$((line+20))
sed -n "${start},${end}p" "$MIG" | nl -ba 2>/dev/null || sed -n "${start},${end}p" "$MIG"
fi
else
echo "Missing file: $MIG" >&2
fi
# 2) Applicant 관련 DTO/요청에서 email 길이 검증(`@Size/`@Length 등) 있는지 확인
echo "=== Applicant DTO/request email validations ==="
rg -n "package .*applicant|domain\\.applicant" src/main/java/org/ject/support/domain/applicant -S || true
rg -n "@(Size|Length)\\([^\\)]*max\\s*=\\s*[0-9]+|email.*`@Size`|email.*`@Length`|`@Size`\\([^\\)]*max[^\\)]*\\).*email" src/main/java/org/ject/support/domain/applicant -S || true
rg -n "@(Size|Length)\\([^\\)]*max\\s*=\\s*[0-9]+|email" src/main/java/org/ject/support/domain/applicant/dto src/main/java/org/ject/support/domain/applicant/controller src/main/java/org/ject/support/domain/applicant/service -S || true
# 3) 혹시 applicant와 연결되는 공용 요청(예: register, apply)에서 email 길이 제한이 있는지 추가로 확인
echo "=== Global search: email validations mentioning max length (near applicant flow) ==="
rg -n "`@Size`\\(max\\s*=\\s*[0-9]+[^\\)]*\\).*이메일|이메일.*`@Size`\\(max\\s*=\\s*[0-9]+|email.*`@Size`\\(max\\s*=\\s*[0-9]+" src/main/java/org/ject/support -S || trueRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 4376
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Applicant flow: find email validations (`@Size/`@Length) ==="
rg -n "@(Size|Length)\\([^\\)]*max\\s*=\\s*[0-9]+" src/main/java/org/ject/support/domain/applicant -S || true
rg -n "email.*@(Size|Length)|@(Size|Length).*email" src/main/java/org/ject/support/domain/applicant -S || true
rg -n "class .*Applicant|RegisterRequest|ApplicantDto|ApplyProfileRequest" src/main/java/org/ject/support/domain/applicant src/main/java/org/ject/support/domain/apply -S || true
echo "=== Applicant request/DTO fields mentioning email ==="
rg -n "(email\\b)" src/main/java/org/ject/support/domain/applicant/dto src/main/java/org/ject/support/domain/applicant/controller src/main/java/org/ject/support/domain/applicant/service -S || true
echo "=== Global search: email max length validators around applicant code paths ==="
rg -n "@(Size|Length)\\([^\\)]*max\\s*=\\s*[0-9]+[^\\)]*\\)\\s*(?:`@Email`\\b)?|`@Email`\\b" src/main/java/org/ject/support/domain/applicant src/main/java/org/ject/support/domain/apply -S || trueRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 6740
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Find any `@Size/`@Length validators for email (max=30 etc.) across codebase ==="
rg -n "@(Size|Length)\\([^\\)]*max\\s*=\\s*[0-9]+[^\\)]*\\).*이메일|이메일.*@(Size|Length)\\([^\\)]*max\\s*=\\s*[0-9]+|`@Email`\\b" src/main/java -S || true
rg -n "이메일 길이|email length|max = 30|varchar\\(30\\)|length = 30" src/main/java -S || true
echo "=== ApplicantDto.java (to check whether request body contains email with validations) ==="
FILE="src/main/java/org/ject/support/domain/applicant/dto/ApplicantDto.java"
if [ -f "$FILE" ]; then
sed -n '1,200p' "$FILE" | nl -ba 2>/dev/null || sed -n '1,200p' "$FILE"
fi
echo "=== Find where applicant email token is extracted and generated ==="
rg -n "extractEmailFromVerificationToken|VerificationToken|verification token|email verification" src/main/java -S || true
echo "=== Member entity email column length for consistency (varchar(30) etc.) ==="
FILE="src/main/java/org/ject/support/domain/member/entity/Member.java"
if [ -f "$FILE" ]; then
rg -n "`@Column`\\([^\\)]*length\\s*=\\s*30[^\\)]*\\)" "$FILE" || true
sed -n '35,80p' "$FILE" | nl -ba 2>/dev/null || sed -n '35,80p' "$FILE"
fiRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 7921
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Applicant flow: ApplicantController 관련 email 검증/DTO 사용처 확인
rg -n "ApplicantController|registerTempApplicant|createVerificationToken|extractEmailFromVerificationToken" src/main/java/org/ject/support/domain/applicant src/main/java/org/ject/support/common/security/jwt -S
# 2) JwtTokenProvider: createVerificationToken/extractEmailFromVerificationToken 구현에서 email 길이/검증 로직 유무 확인
FILE="src/main/java/org/ject/support/common/security/jwt/JwtTokenProvider.java"
if [ -f "$FILE" ]; then
rg -n "createVerificationToken\\(|extractEmailFromVerificationToken\\(" "$FILE" -n
# print around those methods (rough window)
for m in "createVerificationToken" "extractEmailFromVerificationToken"; do
line=$(rg -n "$m" "$FILE" | head -n 1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$((line-30)); end=$((line+40))
echo "=== $FILE around $m ($start-$end) ==="
sed -n "${start},${end}p" "$FILE" | head -n 120
fi
done
fi
# 3) 회원/인증에서 email을 받는 DTO/요청에 `@Size`(max=30) 유무 확인(회원/로그인/검증 토큰 생성 전 단계)
rg -n "AuthDto|`@Email`|`@Size`\\(max\\s*=\\s*[0-9]+\\).*email|email.*`@Size`\\(max\\s*=\\s*[0-9]+\\)" src/main/java/org/ject/support/domain/auth src/main/java/org/ject/support/common/security src/main/java/org/ject/support/domain/member src/main/java/org/ject/support/domain/applicant -S || true
# 4) AuthDto 내용 확인
FILE="src/main/java/org/ject/support/domain/auth/dto/AuthDto.java"
if [ -f "$FILE" ]; then
sed -n '1,120p' "$FILE" | nl -ba 2>/dev/null || sed -n '1,120p' "$FILE"
fiRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 7354
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "`@Size`\\(max\\s*=\\s*30\\)" src/main/java -S || true
rg -n "email.*`@Size`\\(max\\s*=\\s*30\\)|`@Size`\\(max\\s*=\\s*30\\).*email" src/main/java -S || true
rg -n "`@Schema`\\([^\\)]*maxLength\\s*=\\s*30[^\\)]*\\)" src/main/java -S || true
rg -n "`@NotBlank`\\s*`@Email`|`@Email`" src/main/java/org/ject/support/domain -S | head -n 200Repository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 355
Applicant email 컬럼(30) 길이 제약으로 인한 DB 삽입 실패 가능성
Applicant.email이 @Column(length = 30)이고 마이그레이션에서도 applicant.email varchar(30) not null로 생성돼 있습니다. 그런데 applicant 등록 흐름은 검증 토큰의 email을 그대로 사용하고(ApplicantController의 extractEmailFromVerificationToken → ApplicantService.registerTempApplicant), 이메일 값에 @Size(max=30) 같은 길이 검증이 코드에서 확인되지 않아 30자를 초과하는(표준 범위의) 이메일이 들어오면 DB 삽입 단계에서 실패할 수 있습니다. DB 길이/입력 검증 정책을 명확히 맞추는 조정이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/org/ject/support/domain/applicant/entity/Applicant.java` around
lines 49 - 50, Applicant.email is limited to `@Column`(length = 30) but emails
extracted in ApplicantController.extractEmailFromVerificationToken and passed
into ApplicantService.registerTempApplicant are not length-validated, risking DB
insert failures; fix by aligning model and validation: either increase the
column length on Applicant.email to a realistic max (e.g., 254) and update
migrations, or add a validation annotation (e.g., `@Size`(max=30) or a larger
agreed limit) plus explicit trimming/validation in
ApplicantService.registerTempApplicant (and ensure controller extraction
enforces same constraint) so DB schema and runtime validation match.
| public List<String> findEmailsByIdsAndNotSubmitted(List<Long> applicantIds) { | ||
| return queryFactory.select(applicant.email) | ||
| .from(applicant) | ||
| .join(apply) | ||
| .on(applicant.id.eq(apply.member.id)) | ||
| .where(applicant.id.in(applicantIds), | ||
| applicant.isDeleted.eq(false), | ||
| apply.status.eq(SUBMITTED).not()) | ||
| .fetch(); |
There was a problem hiding this comment.
findEmailsByIdsAndNotSubmitted 조건이 의도와 다르게 결과를 과포함할 수 있습니다.
현재 조건(apply.status != SUBMITTED)은 동일 지원자에게 제출/미제출 이력이 함께 있을 때도 미제출 row 때문에 포함되고, 미제출 row가 여러 개면 이메일이 중복 반환됩니다. 메서드 명세(미제출 지원자 이메일)와 불일치할 수 있습니다. distinct + submitted apply 부재(not exists) 조건으로 바꾸는 쪽이 안전합니다.
수정 예시
`@Override`
public List<String> findEmailsByIdsAndNotSubmitted(List<Long> applicantIds) {
- return queryFactory.select(applicant.email)
+ QApply submittedApply = new QApply("submittedApply");
+
+ return queryFactory.selectDistinct(applicant.email)
.from(applicant)
.join(apply)
.on(applicant.id.eq(apply.member.id))
.where(applicant.id.in(applicantIds),
applicant.isDeleted.eq(false),
- apply.status.eq(SUBMITTED).not())
+ apply.status.ne(SUBMITTED),
+ JPAExpressions.selectOne()
+ .from(submittedApply)
+ .where(
+ submittedApply.member.id.eq(applicant.id),
+ submittedApply.status.eq(SUBMITTED)
+ )
+ .notExists())
.fetch();
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/org/ject/support/domain/applicant/repository/ApplicantQueryRepositoryImpl.java`
around lines 29 - 37, The query in
ApplicantQueryRepositoryImpl.findEmailsByIdsAndNotSubmitted can over-include and
return duplicate emails because apply.status != SUBMITTED matches applicants who
also have submitted rows and multiple non-submitted rows; change the query to
select distinct applicant.email and replace the apply.status != SUBMITTED
predicate with a NOT EXISTS subquery that checks for any SUBMITTED apply for the
same applicant (use apply.member.id.eq(applicant.id) and
apply.status.eq(SUBMITTED) inside the subquery) so only applicants with no
submitted applications are returned.
| @BeforeEach | ||
| void setUp() { | ||
| objectMapper = new ObjectMapper(); | ||
| mockMvc = standaloneSetup(applicantController).build(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# `@AuthPrincipal` 리졸버 구현 확인
rg -nP 'AuthPrincipal' --type=java -C3
ast-grep --pattern 'class $_ implements HandlerMethodArgumentResolver {
$$$
}'Repository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 40446
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) ApplicantControllerTest 관련 코드 확인 (62-66 및 MockMvc perform 부분)
sed -n '1,140p' src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java
# 2) `@AuthPrincipal` 리졸버 구현 확인
sed -n '1,200p' src/main/java/org/ject/support/common/security/AuthenticatedMemberIdResolver.java
# 3) 리졸버가 WebMvcConfigurer에 등록되는지 확인
rg -n "addArgumentResolvers|WebMvcConfigurer|setCustomArgumentResolvers|HandlerMethodArgumentResolver" src/main/java src/test/java -S --type=java
# 4) ApplicantControllerTest에서 standaloneSetup에 setCustomArgumentResolvers를 쓰는지 확인
rg -n "standaloneSetup\\(|setCustomArgumentResolvers|AuthenticatedMemberIdResolver|AuthPrincipal" src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java -S --type=javaRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 10519
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# AuthPrincipal 정의 확인
sed -n '1,120p' src/main/java/org/ject/support/common/security/AuthPrincipal.java
# WebConfig에서 addArgumentResolvers 등록 여부 확인
sed -n '1,200p' src/main/java/org/ject/support/common/security/config/WebConfig.java
# ApplicantControllerTest에서 setCustomArgumentResolvers 사용 여부 확인
rg -n "setCustomArgumentResolvers|AuthenticatedMemberIdResolver|AuthPrincipal" \
src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java -S --type=java
# AuthenticatedMemberIdResolver 외에 `@AuthPrincipal` 대응 리졸버가 더 있는지 확인
rg -n "class .*Resolver.*implements HandlerMethodArgumentResolver|`@AuthPrincipal`" \
src/main/java/org/ject/support/common/security -S --type=javaRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 1050
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# ApplicantControllerTest 내부에서 setCustomArgumentResolvers 사용 여부 확인
rg -n "setCustomArgumentResolvers\\(|AuthenticatedMemberIdResolver|AuthPrincipal" src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java -S --type=java
# setUp() 메서드 주변(약 40~90라인)만 다시 확인
sed -n '40,110p' src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.javaRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 65
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 파일 전체(대부분 100줄 내외일 가능성) 확인
wc -l src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java
sed -n '1,220p' src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java
# setCustomArgumentResolvers 존재 여부(없으면 아무 출력도 안 나올 수 있으니 ok)
rg -n "setCustomArgumentResolvers\\(" src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java -S --type=java || trueRepository: JECT-Study/JECT-Official-WebSite-Server
Length of output: 10015
standaloneSetup에서 @AuthPrincipal용 HandlerMethodArgumentResolver 등록 여부를 확인하세요.
WebConfig#addArgumentResolvers(...)에서AuthenticatedMemberIdResolver를 등록하고,AuthenticatedMemberIdResolver는@AuthPrincipal이 붙은Long파라미터를SecurityContextHolder의CustomUserDetails에서 꺼내 바인딩합니다.- 그런데
ApplicantControllerTest는standaloneSetup(applicantController)만 사용하며setCustomArgumentResolvers(...)로AuthenticatedMemberIdResolver를 명시적으로 추가하지 않습니다(62~66의 setUp). 따라서@AuthPrincipal Long applicantId바인딩이 실제로 이뤄지는지/인증 경로가 검증되는지 불명확하니,standaloneSetup(...).setCustomArgumentResolvers(new AuthenticatedMemberIdResolver())추가 또는@WebMvcTest등 스프링 MVC 구성을 포함하는 방식으로 테스트 구성을 정리하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/test/java/org/ject/support/domain/applicant/controller/ApplicantControllerTest.java`
around lines 62 - 66, The test setup in ApplicantControllerTest uses
standaloneSetup(applicantController) but does not register the custom argument
resolver for `@AuthPrincipal`; update the setUp method to register
AuthenticatedMemberIdResolver via
standaloneSetup(applicantController).setCustomArgumentResolvers(new
AuthenticatedMemberIdResolver()) so `@AuthPrincipal` Long applicantId is bound
from SecurityContextHolder's CustomUserDetails (mirror
WebConfig#addArgumentResolvers), or alternatively convert the test to use a
Spring MVC slice (e.g., `@WebMvcTest`) so the WebConfig argument resolvers are
applied; ensure the change targets the setUp method and references
applicantController, standaloneSetup, setCustomArgumentResolvers,
AuthenticatedMemberIdResolver, `@AuthPrincipal` and SecurityContextHolder.
#️⃣연관된 이슈
close: #562
Parent: #561
📝 작업 내용
domain.apply.domain.Applicant를 독립domain.applicant.entity.Applicant로 이동domain.membercontroller/service/repository/entity는 유지🙏 리뷰 요구사항 (선택)
코멘트