Skip to content

[Release] Dev -> Prod 병합#105

Merged
soeun2537 merged 1 commit intoprodfrom
dev
Mar 6, 2026
Merged

[Release] Dev -> Prod 병합#105
soeun2537 merged 1 commit intoprodfrom
dev

Conversation

@soeun2537
Copy link
Member

@soeun2537 soeun2537 commented Mar 5, 2026

  • refactor: null 검증 예외명 통일

  • refactor: null 검증 Objects.isNull, nonNull로 변경

  • refactor: if 문 간 공백 제거

  • test: TestActor Builder 제거

  • test: Fixture 안 쓰는 곳 수정

  • refactor: 서비스, 컨트롤러 코드 메서드 순서 수정

  • refactor: Request to 메서드 제거

  • refactor: 테스트명 실제 메서드명 불일치 부분 수정

  • test: 커버리지 누락 보완

  • test: FestivalImageTest 누락된 도메인 검증 추가

  • test: DisplayNameGeneration 추가

  • refactor: 미사용 import 제거

  • docs: 컨벤션 엡데이트

  • refactor: PlaceAnnouncement 검증 테스트 추가

  • fix: Device 없을 경우도 알림 삭제

  • refactor: 미사용 import 제거

  • test: 예외명 컨벤션 수정

  • fix: 잘못된 메서드명 수정

  • test: 중복 display 어노테이션 제거

  • feat: 비밀번호 검증 추가

  • test: 토픽 검증 추가

  • test: 불필요한 검증 제거

  • test: 불필요한 null 검증 제거

  • refactor: 불필요한 공백 제거

  • feat: 동시성 제어 필요한 로직 인메모리락 추가

  • test: 테스트명 수정

  • test: 불필요 테스트 삭제

  • test: assertSoftly 추가

  • test: 비어 있는 것 검증 추가

  • test: 불필요한 테스트 삭제

  • test: 누락된 테스트 검증 추가

  • test: 불필요한 테스트 삭제

  • refactor: null 검증 == 비교로 변경

  • test: LockAspectTest 추가

  • test: 테스트 소속 이상한 부분 수정

  • feat: 아이디 한글 검증 추가

  • refactor: 미사용 import 제거

  • refactor: 의도대로 동작하는 Objects.isNull로 변경

  • feat: 예외 발생 락 해제 테스트 추가

#️⃣ 이슈 번호

ex) #이슈번호, #이슈번호


🛠️ 작업 내용

  • 구현한 기능을 작성해주세요.

🙇🏻 중점 리뷰 요청

  • 특히 확인이 필요한 부분, 고민했던 부분 등을 적어주세요.

📸 이미지 첨부 (Optional)

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 관리자 사용자명 중복 확인 기능 추가
    • 축제 관리자 권한 변경 엔드포인트 추가
    • 플레이스 접근 로그인 기능 추가
  • 개선 사항

    • 사용자명에 한글 포함 금지 규칙 적용
    • 동시성 제어를 위한 분산 잠금 메커니즘 적용
    • 오류 메시지 일관성 개선 및 표준화
  • 테스트

    • 동시성 시나리오에 대한 통합 테스트 추가

* refactor: null 검증 예외명 통일

* refactor: null 검증 Objects.isNull, nonNull로 변경

* refactor: if 문 간 공백 제거

* test: TestActor Builder 제거

* test: Fixture 안 쓰는 곳 수정

* refactor: 서비스, 컨트롤러 코드 메서드 순서 수정

* refactor: Request to 메서드 제거

* refactor: 테스트명 실제 메서드명 불일치 부분 수정

* test: 커버리지 누락 보완

* test: FestivalImageTest 누락된 도메인 검증 추가

* test: DisplayNameGeneration 추가

* refactor: 미사용 import 제거

* docs: 컨벤션 엡데이트

* refactor: PlaceAnnouncement 검증 테스트 추가

* fix: Device 없을 경우도 알림 삭제

* refactor: 미사용 import 제거

* test: 예외명 컨벤션 수정

* fix: 잘못된 메서드명 수정

* test: 중복 display 어노테이션 제거

* feat: 비밀번호 검증 추가

* test: 토픽 검증 추가

* test: 불필요한 검증 제거

* test: 불필요한 null 검증 제거

* refactor: 불필요한 공백 제거

* feat: 동시성 제어 필요한 로직 인메모리락 추가

* test: 테스트명 수정

* test: 불필요 테스트 삭제

* test: assertSoftly 추가

* test: 비어 있는 것 검증 추가

* test: 불필요한 테스트 삭제

* test: 누락된 테스트 검증 추가

* test: 불필요한 테스트 삭제

* refactor: null 검증 == 비교로 변경

* test: LockAspectTest 추가

* test: 테스트 소속 이상한 부분 수정

* feat: 아이디 한글 검증 추가

* refactor: 미사용 import 제거

* refactor: 의도대로 동작하는 Objects.isNull로 변경

* feat: 예외 발생 락 해제 테스트 추가
@soeun2537 soeun2537 requested review from changuii and taek2222 March 5, 2026 15:19
@soeun2537 soeun2537 self-assigned this Mar 5, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

코드 리뷰 분석

Walkthrough

이번 PR은 코드 스타일 표준화와 동시성 제어를 중심으로 광범위한 리팩토링을 진행합니다. 주요 변경은 null 체크 방식을 Objects.isNull()에서 직접 비교(== null)로 통일하고, 예외 메시지를 "null 중심"에서 "비어있음 중심"으로 변경하며, 분산 락 처리를 위해 @Lockable 어노테이션을 추가합니다. 또한 한글 username 검증, Organizer/Staff 서비스 확장, PlaceAccess 로그인 플로우 개선 등 기능 강화가 포함되어 있으며, 테스트 스위트가 대폭 확대됩니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

분석 근거:

  • 이질성(Heterogeneity): 단순 null 체크 변경만이 아닌 비즈니스 로직 변경(PlaceAccess 로그인 플로우, Staff 권한 관리, Organizer API 확장 등)이 섞여 있어 각 영역별로 별도의 검토 필요
  • 로직 밀도(Logic Density): 동시성 제어 메커니즘 추가, 새로운 유효성 검사 규칙(한글 검증), 복잡한 권한/인증 로직 변경으로 인해 높은 논리 복잡도
  • 파일 범위: 100개 이상의 프로덕션 코드 및 테스트 파일이 영향받음
  • 재구성 패턴: 메서드 재정렬, private 헬퍼 추가/제거 등으로 인한 구조적 변화가 다양함

Possibly related PRs


주요 고려사항 및 개선 제안

1. 광범위한 스타일 통일의 위험성

문제점:
이 PR은 약 100개 이상의 파일에서 Objects.isNull()== null 패턴으로 일괄 변경하고 있습니다. 이러한 기계적 변환은 다음의 한계를 드러냅니다:

  • 의도 불명확성: Objects.isNull()은 "명시적 null 체크"라는 의도를 함축하지만, == null은 더 단순한 구문 선택으로 보일 수 있습니다. 특히 신규 팀원이 코드 리뷰 시 왜 이 방식을 선택했는지 이해하기 어려울 수 있습니다.
  • IDE 인스펙션 활용 부족: 최신 Java IDE들은 Objects.isNull() 사용을 권장하는 인스펙션을 제공합니다. 역방향 변경은 이러한 가이드와 충돌합니다.

대안:

// 옵션 1: Java 15+ Optional의 isEmpty() 활용 (함수형 프로그래밍 지향)
if (actor == null) { ... }  // 현재 선택
// 개선: 
Optional.ofNullable(actor).ifPresentOrElse(
    act -> { /* 처리 */ },
    () -> throw new InternalServerException(...)
);
// 장점: 함수형 패러다임 일관성, null 처리 명시적
// 단점: 단순 null 체크에는 과도할 수 있음

// 옵션 2: `@Nullable/`@NonNull 어노테이션 도입
`@Nullable` Actor actor  // IDE 경고 활성화
if (actor == null) { ... }
// 장점: IDE 지원, 정적 분석 강화
// 단점: 추가 어노테이션 의존성

권장사항: 적어도 중요한 보안/권한 경로(ActorArgumentResolver, 권한 검증)에는 어노테이션을 추가하여 null 처리의 중요성을 강조하는 것이 좋습니다.


2. PlaceAccess 로그인 플로우 변경의 복잡성

문제점:
PlaceAccessService에서 기존 조회 방식(findByPlaceIdAndExpiresAtAfter)에서 로그인 기반 플로우(existsExpiredOrDeletedByAccessKey + findByAccessKeyAndExpiresAtAfter)로 전환했습니다.

// 기존: 장소 기반 전체 접근 조회
findByPlaceIdAndExpiresAtAfter(placeId, now)

// 신규: 키 기반 개별 접근 검증  
existsExpiredOrDeletedByAccessKey(accessKey)
findByAccessKeyAndExpiresAtAfter(accessKey, now)

이 변경의 세 가지 문제:

  1. 데이터베이스 조회 증가: 중복 조회 가능성 (existsExpiredOrDeletedByAccessKey + findByAccessKeyAndExpiresAtAfter)
  2. 에러 메시지 매핑의 불명확성: ForbiddenException vs BadRequestException vs NotFoundException 분기가 복잡
  3. 트랜잭션 안전성: 두 개의 DB 호출 사이에 경합 조건(race condition) 발생 가능성

개선 제안:

// 옵션 1: 단일 쿼리로 통합 (권장)
`@Query`("""
  SELECT pa FROM PlaceAccess pa 
  WHERE pa.accessKey = :accessKey 
  AND pa.expiresAt > :now
  AND pa.isDeleted = false
""")
Optional<PlaceAccess> findValidByAccessKey(String accessKey, LocalDateTime now);

// 사용처:
PlaceAccess pa = findValidByAccessKey(accessKey, now)
    .orElseThrow(() -> new BadRequestException("..."));
validatePasswordMatch(password, pa);
return generateLoginResponse(pa);

// 장점: 원자적(atomic) 조회, 단일 책임, 명확한 에러 처리
// 단점: 쿼리 복잡도 증가

// 옵션 2: 명시적 검증 래퍼 메서드
private PlaceAccess getValidAccessKey(String key, String password) {
    return findByAccessKey(key)
        .filter(pa -> !pa.isExpired())
        .filter(pa -> !pa.isDeleted())
        .filter(pa -> passwordMatches(password, pa))
        .orElseThrow(() -> new BadRequestException("..."));
}

// 장점: 체이닝 패턴으로 가독성 우수
// 단점: 다중 필터로 인한 성능 영향 가능

현재 코드는 에러 메시지 일관성도 개선이 필요합니다 ("만료된 플레이스 접근 링크" vs "비밀번호 불일치" 메시지 통일).


3. 한글 Username 검증의 정규식 범위

문제점:

Pattern KOREAN_USERNAME_PATTERN = Pattern.compile(".*[ㄱ-ㅎㅏ-ㅣ가-힣].*");

이 정규식은:

  • 초성/중성/종성만 감지: 복합 문자나 특수 한글 자모(쌍자음 등)를 누락할 수 있음
  • 성능 영향: 매번 정규식 컴파일 가능성 (static final로 정의는 했지만, 검증 메서드 호출 시마다 실행)
  • 문화적 고려 부족: 중국어 간체자, 일본어 등 다국어 username도 차단해야 하는지 불명확

개선 제안:

// 옵션 1: Unicode 블록 기반 (더 정확)
private static final Pattern KOREAN_USERNAME_PATTERN = 
    Pattern.compile(".*[\\p{Script=Hangul}].*");

// 테스트:
// 통과: "user123", "abc", "userABC"
// 실패: "사용자", "користувач", "用户"

// 옵션 2: 캐싱된 검증 (성능)
private static final Set<String> DISALLOWED_SCRIPTS = Set.of(
    "Hangul", "Han", "Hiragana", "Katakana", "Cyrillic"
);

private boolean containsDisallowedCharacters(String username) {
    return username.chars().anyMatch(ch -> {
        UnicodeScript script = UnicodeScript.of(ch);
        return DISALLOWED_SCRIPTS.contains(script.name());
    });
}

// 장점: 더 정확하고, 다국어 확장 가능
// 단점: 정규식보다 약간 느림 (대량 검증 시에만 영향)

권장사항: 정규식을 \p{Script=Hangul}로 변경하고, 향후 비즈니스 요구에 따라 다른 언어도 추가할 수 있도록 확장성 있는 구조로 설계하세요.


4. @Lockable 어노테이션의 SpEL 키 생성 일관성 부족

문제점:
다양한 서비스에서 서로 다른 SpEL 패턴을 사용하고 있습니다:

// EventDateService
`@Lockable`(spelKey = "'EventDateService'.concat(T(String).valueOf(`#festivalId`))", ...)

// LineupService  
`@Lockable`(spelKey = "'LineupService'.concat(T(String).valueOf(`#festivalId`))", ...)

// PlaceAccessService
`@Lockable`(spelKey = "'PlaceAccessService'.concat(T(String).valueOf(`#placeId`))", ...)

// AnnouncementService (String.valueOf 사용)
String.valueOf(`#festivalId`)

이 불일관성은:

  • 유지보수성 저하: 각 서비스마다 패턴을 기억해야 함
  • 성능 편차: SpEL 컴파일 오버헤드가 서비스마다 다를 수 있음
  • 버그 가능성: 타이핑 실수로 인한 락 키 오류

개선 제안:

// 옵션 1: 공통 유틸리티 메서드 생성 (권장)
public class LockKeyGenerator {
    public static String generateKey(String serviceName, String resourceId) {
        return serviceName + ":" + resourceId;
    }
}

// 사용처:
`@Lockable`(
    spelKey = "T(com.daedan.festabook.global.lock.LockKeyGenerator)" +
              ".generateKey(`#serviceName`, `#resourceId`)",
    useMethodScopeLock = false
)

// 장점: 중앙화, 일관성 보장, 테스트 용이
// 단점: 추가 클래스 필요

// 옵션 2: `@Lockable` 메타 어노테이션 확장
`@Target`(ElementType.METHOD)
`@Retention`(RetentionPolicy.RUNTIME)
`@Lockable`(useMethodScopeLock = false)
public `@interface` ResourceLock {
    String value();  // "serviceName:resourceId" 형식
}

// 사용:
`@ResourceLock`("EventDateService:`#festivalId`")
public EventDateResponse createEventDate(...) { }

// 장점: 어노테이션 간결화, IDE 자동완성
// 단점: 커스텀 어노테이션 처리 필요

5. 테스트의 과도한 확장

문제점:
이번 PR에서 테스트 코드가 매우 대폭 증가했습니다(파일당 100+ 라인 추가). 특히:

  • 새로운 concurrency 테스트 8개 추가
  • 기존 테스트 재구성 및 파라미터화
  • 동일한 시나리오에 대한 중복 테스트 가능성

예시:

// Staff 관련 테스트만 해도:
- StaffServiceTest: 443 라인 변경
- StaffControllerTest: 122 라인 변경  
- StaffConcurrencyTest: 92 라인 추가
- StaffTest: 27 라인 추가
- StaffAuthorityTest: 2 라인 변경

이는 다음 문제를 야기합니다:

  1. 테스트 유지비 증가: 매 변경 시 모든 시나리오 검증 필요
  2. 실행 시간 증가: concurrency 테스트는 고의적 지연으로 느림
  3. 중복 테스트 가능성: 같은 케이스를 여러 테스트에서 검증

권장사항:

// 옵션 1: 테스트 카테고리 분리 (권장)
`@Tag`("unit")  // 단위 테스트
class StaffServiceUnitTest { }

`@Tag`("concurrency")  // 동시성 테스트 (별도 실행)
class StaffConcurrencyTest { }

// Maven/Gradle에서 selective 실행
// mvn test -Dgroups="!concurrency"  (단위테스트만)
// mvn test -Dgroups="concurrency"   (동시성테스트만)

// 옵션 2: Parameterized Test로 중복 제거
`@ParameterizedTest`
`@CsvSource`({
    "valid_username, false",
    "123abc, false",
    "한글username, true"
})
void validateUsername(String username, boolean shouldFail) {
    // 하나의 테스트로 여러 케이스 커버
}

// 장점: 명확한 책임 분리, 실행 시간 제어
// 단점: CI/CD 파이프라인 복잡도 증가

종합 평가

이 PR은 기술 부채 정산과 기능 확장의 혼합입니다. 스타일 통일, 보안 강화(한글 검증), 동시성 제어는 긍정적이나, 다음 지점에서 신중한 재검토가 필요합니다:

  1. 좋은 점: 일관된 에러 메시지 정책, 분산 락 도입, 포괄적 테스트
  2. ⚠️ 개선 필요: SpEL 키 생성 중앙화, PlaceAccess 로그인 쿼리 최적화, 정규식 정확성
  3. 🔴 위험 요소: 광범위한 변경으로 인한 회귀 테스트 필요, 테스트 실행 시간 증가

다음 스텝:

  • 핵심 변경(PlaceAccess, Staff)에 대한 통합 테스트 추가 검증
  • SpEL 락 키 생성 유틸리티화 계획
  • 한글 검증 정규식 재평가 (Script 블록 활용)
✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

@github-actions
Copy link

github-actions bot commented Mar 5, 2026

Overall Project 99.84% 🍏
Files changed 100% 🍏

File Coverage
PlaceService.java 100% 🍏
PlaceGeographyService.java 100% 🍏
PlaceAccessService.java 100% 🍏
PlaceImageService.java 100% 🍏
PlaceAnnouncementService.java 100% 🍏
EventDateService.java 100% 🍏
OrganizerService.java 100% 🍏
Announcement.java 100% 🍏
NotificationSendRequest.java 100% 🍏
PlaceImage.java 100% 🍏
PlaceAccess.java 100% 🍏
PlaceAnnouncement.java 100% 🍏
Place.java 100% 🍏
FestivalCoordinateResponse.java 100% 🍏
InMemoryLockStorage.java 100% 🍏
LostItem.java 100% 🍏
Organizer.java 100% 🍏
StaffController.java 100% 🍏
FestivalNotificationService.java 100% 🍏
TestFestivalNotificationService.java 100% 🍏
PlaceCoordinateRequest.java 100% 🍏
StoreFile.java 100% 🍏
ActorArgumentResolver.java 100% 🍏
OrganizationIdArgumentResolver.java 100% 🍏
Lineup.java 100% 🍏
AccountDetailsExtractor.java 100% 🍏
PlaceAccessDetailsExtractStrategy.java 100% 🍏
StaffDetails.java 100% 🍏
OrganizerDetails.java 100% 🍏
OrganizerDetailsExtractStrategy.java 100% 🍏
PlaceAccessDetails.java 100% 🍏
StaffDetailsExtractStrategy.java 100% 🍏
EventDate.java 100% 🍏
Event.java 100% 🍏
Question.java 100% 🍏
LineupService.java 100% 🍏
TimeTagService.java 100% 🍏
AnnouncementService.java 100% 🍏
PlaceAccessController.java 100% 🍏
PlaceImageController.java 100% 🍏
PlaceTimeTag.java 100% 🍏
TimeTag.java 100% 🍏
JwtAuthenticationFilter.java 100% 🍏
Festival.java 100% 🍏
FestivalNotification.java 100% 🍏
Coordinate.java 100% 🍏
FestivalImage.java 100% 🍏
OrganizerController.java 100% 🍏
QuestionService.java 100% 🍏
StaffAuthority.java 100% 🍏
Staff.java 100% 🍏
Organization.java 100% 🍏
LostItemService.java 100% 🍏
ImageStoreService.java 100% 🍏
StaffService.java 99.5% 🍏

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 5, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
4 Security Hotspots

See analysis details on SonarQube Cloud

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 개발 환경의 변경 사항을 프로덕션 환경으로 병합하는 릴리스 PR입니다. 코드베이스의 전반적인 품질을 향상시키기 위한 다양한 리팩토링 작업과 테스트 개선이 포함되어 있습니다. 특히, null 검증 방식의 일관성을 확보하고, 메서드 정렬 규칙을 명확히 하여 코드 가독성을 높였습니다. 또한, 동시성 제어 로직을 도입하고 사용자 인증 관련 검증을 강화하여 시스템 안정성과 보안을 개선했습니다. 여러 테스트 케이스를 추가하고 기존 테스트를 정리하여 코드의 신뢰도를 높이는 데 중점을 두었습니다.

Highlights

  • 리팩토링: null 검증 로직을 Objects.isNull에서 == null로 변경하고, 예외 메시지를 '비어 있을 수 없습니다'로 통일했습니다. 또한, if 문 간의 불필요한 공백을 제거하고, 서비스 및 컨트롤러 코드의 메서드 순서를 CRUD 및 호출 순서에 맞춰 재정렬했습니다. 미사용 import 문과 Request to 메서드를 제거했습니다.
  • 테스트 개선: TestActor Builder를 제거하고, Fixture를 사용하지 않는 테스트를 수정했습니다. 커버리지 누락 부분을 보완하고, FestivalImageTest에 누락된 도메인 검증을 추가했습니다. DisplayNameGeneration을 추가하고, 테스트명 불일치 부분을 수정하며, 불필요한 검증 및 테스트를 삭제했습니다. assertSoftly를 도입하여 여러 단언을 한 번에 처리하도록 개선했습니다.
  • 신규 기능: 비밀번호 검증 로직을 추가하고, 아이디에 한글이 포함될 수 없도록 검증 로직을 추가했습니다. 동시성 제어가 필요한 로직에 인메모리 락을 적용하여 동시성 문제를 해결했습니다. 예외 발생 시 락이 해제되도록 테스트를 추가했습니다.
  • 버그 수정: Device 정보가 없을 경우에도 알림이 삭제되도록 수정하고, 잘못된 메서드명을 수정했습니다.
  • 문서 업데이트: 코드 컨벤션 문서를 업데이트하여 새로운 규칙들을 반영했습니다.
Changelog
  • code-style.md
    • 최종 수정일이 2026-03-04에서 2026-03-05로 업데이트되었습니다.
    • 미사용 코드 제거에 대한 규칙이 추가되었습니다.
    • Objects::nonNull 필터링 예시가 x -> x != null로 변경되었습니다.
    • null 처리 방식 및 도메인 검증 if 문 스타일에 대한 규칙이 추가되었습니다.
    • 도메인 검증 메서드 내 연속된 if 문 사이의 공백이 제거되었습니다.
    • BadRequestException 메시지 통일 규칙이 추가되었습니다.
    • Service 메서드 선언 순서 및 private 메서드 배치 규칙이 추가되었습니다.
    • 테스트 클래스 구조에 DisplayNameGeneration 및 Nested 클래스 순서 규칙이 추가되었습니다.
    • 도메인 객체 생성 방식에 Fixture 사용 규칙이 업데이트되었습니다.
    • 테스트 클래스 선언부 개행 규칙이 추가되었습니다.
    • 테스트에서 도메인/값객체 생성 방식에 대한 규칙이 추가되었습니다.
    • 코드 리뷰 체크리스트에 미사용 변수/메서드, Service/Controller 메서드 순서, DisplayNameGeneration 관련 항목이 추가되었습니다.
  • src/main/java/com/daedan/festabook/announcement/domain/Announcement.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateTitle 메서드 내 if 문 사이의 공백이 제거되었습니다.
    • validateContent 메서드 내 if 문 사이의 공백이 제거되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/announcement/service/AnnouncementService.java
    • @Lockable 어노테이션의 spelKeyT(String).valueOf(#festivalId)를 사용하도록 변경되었습니다.
    • private 메서드 validatePinnedLimitformatTitleWithOrganizationName의 순서가 변경되었습니다.
    • private 메서드 getAnnouncementByIdgetFestivalById의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/event/domain/Event.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateEventDate 메서드의 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTimes 메서드의 예외 메시지가 '시작 시간과 종료 시간은 null일 수 없습니다.'에서 '시작 시간과 종료 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 메서드의 예외 메시지가 '일정 제목은 공백이거나 null일 수 없습니다.'에서 '일정 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateLocation 메서드의 예외 메시지가 '일정 위치는 공백이거나 null일 수 없습니다.'에서 '일정 위치는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/event/domain/EventDate.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateDate 메서드의 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/event/service/EventDateService.java
    • com.daedan.festabook.global.lock.Lockable import가 추가되었습니다.
    • createEventDate 메서드에 @Lockable 어노테이션이 추가되었습니다.
    • updateEventDate 메서드에 @Lockable 어노테이션이 추가되었습니다.
    • private 메서드 getAllEventsById의 위치가 변경되었습니다.
  • src/main/java/com/daedan/festabook/festival/domain/Coordinate.java
    • validateLatitude 메서드의 예외 메시지가 '위도는 null일 수 없습니다.'에서 '위도는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateLongitude 메서드의 예외 메시지가 '경도는 null일 수 없습니다.'에서 '경도는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/festival/domain/Festival.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateOrganization 메서드의 예외 메시지가 '조직은 null일 수 없습니다.'에서 '조직은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateDates 메서드의 예외 메시지가 '시작일과 종료일은 null일 수 없습니다.'에서 '시작일과 종료일은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateZoom 메서드의 예외 메시지가 '줌은 null일 수 없습니다.'에서 '줌은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateCenterCoordinate 메서드의 예외 메시지가 '중심 좌표는 null일 수 없습니다.'에서 '중심 좌표는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePolygonHoleBoundary 메서드의 예외 메시지가 '폴리곤 내부 구멍 좌표 리스트는 비어있을 수 없습니다.'에서 '폴리곤 내부 구멍 좌표 리스트는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/festival/domain/FestivalImage.java
    • java.util.Objects import가 제거되었습니다.
    • updateSequenceBy 메서드 내 불필요한 공백 라인이 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateSequence 메서드의 예외 메시지가 '축제 이미지 순서는 null일 수 없습니다.'에서 '축제 이미지 순서는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/festival/domain/FestivalNotification.java
    • java.util.Objects import가 제거되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateDevice 메서드의 예외 메시지가 '디바이스는 null일 수 없습니다.'에서 '디바이스는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/festival/dto/FestivalCoordinateResponse.java
    • coordinate가 null일 경우 new FestivalCoordinateResponse(null, null)을 반환하는 로직이 제거되었습니다.
  • src/main/java/com/daedan/festabook/festival/service/FestivalNotificationService.java
    • unsubscribeFestivalNotification 메서드 내 festivalNotification.deleteBy() 호출 순서가 변경되었습니다.
    • private 메서드 getDeviceByIdgetFestivalById의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/festival/service/TestFestivalNotificationService.java
    • private 메서드 getDeviceByIdgetFestivalById의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(authentication)authentication == null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/argumentresolver/OrganizationIdArgumentResolver.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(organizationId)organizationId == null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/lock/infrastructure/InMemoryLockStorage.java
    • validateEmptyKey 메서드의 예외 메시지가 '락 키는 비어있을 수 없습니다.'에서 '락 키는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/authorization/AccountDetailsExtractor.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(strategy)strategy == null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java
    • Objects.nonNull(organizationId)organizationId != null로 변경되었습니다.
    • Objects.nonNull(festivalId)festivalId != null로 변경되었습니다.
    • Objects.nonNull(organizerId)organizerId != null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(subject) || Objects.isNull(organizationId) || Objects.isNull(organizerId)subject == null || organizationId == null || organizerId == null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetails.java
    • Objects.nonNull(placeId)placeId != null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsExtractStrategy.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(placeId)placeId == null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java
    • Objects.nonNull(organizationId)organizationId != null로 변경되었습니다.
    • Objects.nonNull(festivalId)festivalId != null로 변경되었습니다.
    • Objects.nonNull(staffId)staffId != null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/authorization/StaffDetailsExtractStrategy.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(subject) || Objects.isNull(organizationId) || Objects.isNull(staffId)subject == null || organizationId == null || staffId == null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/global/security/filter/JwtAuthenticationFilter.java
    • java.util.Objects import가 제거되었습니다.
    • !Objects.isNull(accessToken)accessToken != null로 변경되었습니다.
    • !Objects.isNull(userDetails)userDetails != null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/lineup/domain/Lineup.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePerformanceAt 메서드의 예외 메시지가 '공연 시간은 null일 수 없습니다.'에서 '공연 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/lineup/service/LineupService.java
    • com.daedan.festabook.global.lock.Lockable import가 추가되었습니다.
    • addLineup 메서드에 @Lockable 어노테이션이 추가되었습니다.
  • src/main/java/com/daedan/festabook/lostitem/domain/LostItem.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePickupStatus 메서드의 예외 메시지가 '수령 상태는 null일 수 없습니다.'에서 '수령 상태는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/lostitem/service/LostItemService.java
    • private 메서드 getFestivalByIdgetLostItemById의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/notification/dto/NotificationSendRequest.java
    • validateTitle 메서드의 예외 메시지가 '알림 제목은 비어있을 수 없습니다.'에서 '알림 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateBody 메서드의 예외 메시지가 '알림 본문은 비어있을 수 없습니다.'에서 '알림 본문은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/organization/domain/Organization.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
  • src/main/java/com/daedan/festabook/organizer/controller/OrganizerController.java
    • checkUsernameDuplicate 엔드포인트의 위치가 getOrganizerByOrganizationId 아래로 변경되었습니다.
  • src/main/java/com/daedan/festabook/organizer/domain/Organizer.java
    • java.util.Objects import가 제거되었습니다.
    • java.util.regex.Pattern import가 추가되었습니다.
    • KOREAN_USERNAME_PATTERN 상수가 추가되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateUsername 메서드에 한글 포함 여부 검증 로직이 추가되었습니다.
    • validateOrganization 메서드의 예외 메시지가 '조직 정보는 비어있을 수 없습니다.'에서 '조직 정보는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/organizer/service/OrganizerService.java
    • com.daedan.festabook.global.lock.Lockable import가 추가되었습니다.
    • org.springframework.util.StringUtils import가 추가되었습니다.
    • createOrganizer 메서드에 @Lockable 어노테이션이 추가되었습니다.
    • updatePassword 메서드에 validatePasswordByte 호출이 추가되었습니다.
    • private 메서드 validatePasswordMatch, validateUsernameNotBlank, getOrganizerByUsernameForAuthentication의 순서가 변경되었습니다.
    • validatePasswordByte 메서드에 StringUtils.hasText 검증이 추가되었습니다.
    • validateOrganizationId 메서드의 예외 메시지가 '조직 정보가 필요합니다.'에서 '조직은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateUsernameNotBlank 메서드에 StringUtils.hasText 검증 및 한글 포함 여부 검증 로직이 추가되었습니다.
  • src/main/java/com/daedan/festabook/place/controller/PlaceAccessController.java
    • loginPlaceAccess 엔드포인트의 위치가 createPlaceAccess 아래로 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/controller/PlaceImageController.java
    • import 문 순서가 변경되었습니다.
    • updateFestivalImagesSequence 메서드명이 updatePlaceImagesSequence로 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/domain/Place.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 'Festival은 null일 수 없습니다.'에서 'Festival은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePlaceCategory 메서드의 예외 메시지가 'PlaceCategory는 null일 수 없습니다.'에서 'PlaceCategory는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateCoordinate 메서드의 예외 메시지가 'Coordinate는 null일 수 없습니다.'에서 'Coordinate는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 메서드의 예외 메시지가 '플레이스의 이름은 공백이거나 null일 수 없습니다.'에서 '플레이스의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/domain/PlaceAccess.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateAccessKey 메서드의 예외 메시지가 '접근 키는 비어있을 수 없습니다.'에서 '접근 키는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePassword 메서드의 예외 메시지가 '비밀번호는 비어있을 수 없습니다.'에서 '비밀번호는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateExpiresAt 메서드의 예외 메시지가 '만료 일시는 null일 수 없습니다.'에서 '만료 일시는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePlace 메서드의 예외 메시지가 '플레이스는 비어있을 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/domain/PlaceAnnouncement.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validatePlace 메서드의 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 메서드의 예외 메시지가 '플레이스 공지의 제목은 비어있을 수 없습니다.'에서 '플레이스 공지 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 메서드의 예외 메시지 형식이 '플레이스 공지 제목의 길이는'에서 '플레이스 공지 제목 길이는'으로 변경되었습니다.
    • validateContent 메서드의 예외 메시지가 '플레이스 공지 내용은 null일 수 없습니다.'에서 '플레이스 공지 내용은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateContent 메서드의 예외 메시지 형식이 '플레이스 공지 내용의 길이는'에서 '플레이스 공지 내용 길이는'으로 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/domain/PlaceImage.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validatePlace 메서드의 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateSequence 메서드의 예외 메시지가 '플레이스 이미지 순서는 null일 수 없습니다.'에서 '플레이스 이미지 순서는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/dto/PlaceCoordinateRequest.java
    • com.daedan.festabook.festival.domain.Coordinate import가 제거되었습니다.
    • toCoordinate() 메서드가 제거되었습니다.
  • src/main/java/com/daedan/festabook/place/service/PlaceAccessService.java
    • com.daedan.festabook.global.lock.Lockable import가 추가되었습니다.
    • createPlaceAccess 메서드에 @Lockable 어노테이션이 추가되었습니다.
    • private 메서드 validatePasswordMatchgetActivePlaceAccessByPlaceId의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/service/PlaceAnnouncementService.java
    • com.daedan.festabook.global.lock.Lockable import가 추가되었습니다.
    • createPlaceAnnouncement 메서드에 @Lockable 어노테이션이 추가되었습니다.
    • private 메서드 validatePlaceAnnouncementMaxCount, getPlaceById, getPlaceAnnouncementById의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/service/PlaceGeographyService.java
    • updatePlaceCoordinate 메서드에서 request.toCoordinate() 대신 new Coordinate(request.latitude(), request.longitude())를 사용하도록 변경되었습니다.
  • src/main/java/com/daedan/festabook/place/service/PlaceImageService.java
    • TODO 주석이 제거되었습니다.
  • src/main/java/com/daedan/festabook/place/service/PlaceService.java
    • private 메서드 validateClonePlacesSize, validateExistsPlace, convertPlaceToResponse, deletePlaceTimeTags, getFestivalById, getPlaceById, getExistingTimeTagIdsByPlaceId의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/question/domain/Question.java
    • java.util.Objects import가 제거되었습니다.
    • org.springframework.util.StringUtils import가 추가되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 'Festival은 null일 수 없습니다.'에서 'Festival은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateAnswer 메서드에서 answer == null || answer.trim().isEmpty()!StringUtils.hasText(answer)로 변경되었습니다.
    • validateSequence 메서드의 예외 메시지가 '순서는 null일 수 없습니다.'에서 '순서는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/question/service/QuestionService.java
    • private 메서드 getQuestionByIdgetFestivalById의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/staff/controller/StaffController.java
    • updatePasswordupdateStaffAuthorities 엔드포인트의 순서가 변경되었습니다.
  • src/main/java/com/daedan/festabook/staff/domain/Staff.java
    • java.util.Objects import가 제거되었습니다.
    • java.util.regex.Pattern import가 추가되었습니다.
    • KOREAN_USERNAME_PATTERN 상수가 추가되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateUsername 메서드에 한글 포함 여부 검증 로직이 추가되었습니다.
    • validateOrganization 메서드의 예외 메시지가 '조직 정보는 비어있을 수 없습니다.'에서 '조직 정보는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/staff/domain/StaffAuthority.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateStaff 메서드의 예외 메시지가 '축제 관리자는 null일 수 없습니다.'에서 '축제 관리자는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/staff/service/StaffService.java
    • com.daedan.festabook.global.lock.Lockable import가 추가되었습니다.
    • createStaff 메서드에 @Lockable 어노테이션이 추가되었습니다.
    • updatePassword 메서드에 validatePasswordByte 호출이 추가되었습니다.
    • private 메서드 saveStaffAuthorities, createStaffAuthorities, deleteStaffAuthorities, validateStaffAuthorityUpdateRequest, getRequestedFestivals, validatePasswordMatch, validateUsernameNotBlank, getOrganizationByOrganizationId, getStaffById의 순서가 변경되었습니다.
    • createStaffAuthorities 메서드에서 Objects.isNull(festivalIds)festivalIds == null로 변경되었습니다.
    • validateStaffAuthorityUpdateRequest 메서드에서 Objects.isNull 검증이 == null로 변경되었습니다.
    • validateUsernameNotBlank 메서드에 한글 포함 여부 검증 로직이 추가되었습니다.
    • validatePasswordByte 메서드에 StringUtils.hasText 검증이 추가되었습니다.
  • src/main/java/com/daedan/festabook/storage/infrastructure/StoreFile.java
    • validateMultipartFile 메서드의 예외 메시지가 '파일은 비어있을 수 없습니다.'에서 '파일은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateStoragePath 메서드의 예외 메시지가 '파일 경로는 비어있을 수 없습니다.'에서 '파일 경로는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/storage/service/ImageStoreService.java
    • validateFile 메서드의 예외 메시지가 '파일은 비어있을 수 없습니다.'에서 '파일은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/timetag/domain/PlaceTimeTag.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validatePlace 메서드의 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTimeTag 메서드의 예외 메시지가 '시간 태그는 null일 수 없습니다.'에서 '시간 태그는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/timetag/domain/TimeTag.java
    • java.util.Objects import가 제거되었습니다.
    • Objects.isNull(actor)actor == null로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateName 메서드의 예외 메시지가 '시간 태그의 이름은 공백이거나 null일 수 없습니다.'에서 '시간 태그의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/main/java/com/daedan/festabook/timetag/service/TimeTagService.java
    • private 메서드 validateTimeTagNotInUsegetFestivalById, getTimeTagById의 순서가 변경되었습니다.
  • src/test/java/com/daedan/festabook/announcement/controller/AnnouncementControllerTest.java
    • io.restassured.http.Header import가 제거되었습니다.
    • org.junit.jupiter.params.provider.EnumSource.Mode import가 제거되었습니다.
  • src/test/java/com/daedan/festabook/announcement/domain/AnnouncementTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/announcement/infrastructure/AnnouncementJpaRepositoryTest.java
    • countByFestivalIdAndPinned 클래스명이 countByFestivalIdAndIsPinnedTrue로 변경되었습니다.
  • src/test/java/com/daedan/festabook/announcement/service/AnnouncementServiceTest.java
    • deleteByAnnouncementId 클래스명이 deleteAnnouncementByAnnouncementId로 변경되었습니다.
    • sendAnnouncementNotification 클래스에 '예외_존재하지_않는_축제' 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/event/concurrency/EventDateConcurrencyTest.java
    • 새로운 테스트 파일이 추가되었습니다. EventDate 생성 시 동시성 제어 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/event/controller/EventControllerTest.java
    • static org.mockito.BDDMockito.given import가 제거되었습니다.
    • io.restassured.http.Header import가 제거되었습니다.
    • java.time.Clock import가 제거되었습니다.
    • java.time.ZoneId import가 제거되었습니다.
    • updateEvent 및 deleteEventByEventId Nested 클래스의 위치가 getAllEventByEventDateId 아래로 변경되었습니다.
  • src/test/java/com/daedan/festabook/event/domain/EventDateTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateDate 메서드의 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateDateBy 클래스 내 validateDate 메서드의 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateDate 클래스 내 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/event/domain/EventTest.java
    • createBy 클래스 내 validateEventDate 메서드의 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateTimes 메서드의 예외 메시지가 '시작 시간과 종료 시간은 null일 수 없습니다.'에서 '시작 시간과 종료 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateTitle 메서드의 예외 메시지가 '일정 제목은 공백이거나 null일 수 없습니다.'에서 '일정 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateLocation 메서드의 예외 메시지가 '일정 위치는 공백이거나 null일 수 없습니다.'에서 '일정 위치는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateEventBy 클래스 내 validateEventDate 메서드의 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateEventBy 클래스 내 validateTimes 메서드의 예외 메시지가 '시작 시간과 종료 시간은 null일 수 없습니다.'에서 '시작 시간과 종료 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateEventBy 클래스 내 validateTitle 메서드의 예외 메시지가 '일정 제목은 공백이거나 null일 수 없습니다.'에서 '일정 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateEventBy 클래스 내 validateLocation 메서드의 예외 메시지가 '일정 위치는 공백이거나 null일 수 없습니다.'에서 '일정 위치는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateEventDate 클래스 내 예외 메시지가 '일정 날짜는 null일 수 없습니다.'에서 '일정 날짜는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTimes 클래스 내 예외 메시지가 '시작 시간과 종료 시간은 null일 수 없습니다.'에서 '시작 시간과 종료 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 클래스 내 예외 메시지가 '일정 제목은 공백이거나 null일 수 없습니다.'에서 '일정 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateLocation 클래스 내 예외 메시지가 '일정 위치는 공백이거나 null일 수 없습니다.'에서 '일정 위치는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/controller/AndroidFestivalNotificationSubscriptionControllerTest.java
    • subscribeAndroidFestivalNotification 클래스명이 subscribeFestivalNotification으로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/controller/FestivalControllerTest.java
    • com.daedan.festabook.festival.domain.CoordinateFixture import가 추가되었습니다.
    • createFestival 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
    • updateFestivalCenterCoordinate 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
    • updateFestivalPolygonHoleBoundary 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/controller/FestivalImageControllerTest.java
    • io.restassured.http.Header import가 제거되었습니다.
  • src/test/java/com/daedan/festabook/festival/controller/IosFestivalNotificationSubscriptionControllerTest.java
    • subscribeIosFestivalNotification 클래스명이 subscribeFestivalNotification으로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/controller/TestFestivalNotificationSubscriptionControllerTest.java
    • 새로운 테스트 파일이 추가되었습니다. TestFestivalNotificationSubscriptionController의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/festival/domain/CoordinateTest.java
    • validateLatitude 메서드의 예외 메시지가 '위도는 null일 수 없습니다.'에서 '위도는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateLongitude 메서드의 예외 메시지가 '경도는 null일 수 없습니다.'에서 '경도는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/domain/FestivalFixture.java
    • new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/domain/FestivalImageFixture.java
    • Festival, imageUrl, sequence를 인자로 받는 create 메서드가 추가되었습니다.
  • src/test/java/com/daedan/festabook/festival/domain/FestivalImageTest.java
    • 새로운 테스트 파일이 추가되었습니다. FestivalImage 도메인 로직의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/festival/domain/FestivalNotificationTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateDevice 메서드의 예외 메시지가 '디바이스는 null일 수 없습니다.'에서 '디바이스는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/domain/FestivalTest.java
    • createBy 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
    • createBy 클래스 내 validateOrganization 메서드의 예외 메시지가 '조직은 null일 수 없습니다.'에서 '조직은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateDates 메서드의 예외 메시지가 '시작일과 종료일은 null일 수 없습니다.'에서 '시작일과 종료일은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateZoom 메서드의 예외 메시지가 '줌은 null일 수 없습니다.'에서 '줌은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateCenterCoordinate 메서드의 예외 메시지가 '중심 좌표는 null일 수 없습니다.'에서 '중심 좌표는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validatePolygonHoleBoundary 메서드의 예외 메시지가 '폴리곤 내부 구멍 좌표 리스트는 비어있을 수 없습니다.'에서 '폴리곤 내부 구멍 좌표 리스트는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateFestivalInformationBy 클래스 내 validateDates 메서드의 예외 메시지가 '시작일과 종료일은 null일 수 없습니다.'에서 '시작일과 종료일은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateFestivalZoomBy 클래스 내 validateZoom 메서드의 예외 메시지가 '줌은 null일 수 없습니다.'에서 '줌은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateFestivalCenterCoordinateBy 클래스 내 validateCenterCoordinate 메서드의 예외 메시지가 '중심 좌표는 null일 수 없습니다.'에서 '중심 좌표는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateFestivalPolygonHoleBoundaryBy 클래스 내 validatePolygonHoleBoundary 메서드의 예외 메시지가 '폴리곤 내부 구멍 좌표 리스트는 비어있을 수 없습니다.'에서 '폴리곤 내부 구멍 좌표 리스트는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateOrganization 클래스 내 예외 메시지가 '조직은 null일 수 없습니다.'에서 '조직은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateDate 클래스명이 validateDates로 변경되었습니다.
    • validateDates 클래스 내 예외 메시지가 '시작일과 종료일은 null일 수 없습니다.'에서 '시작일과 종료일은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateZoom 클래스 내 예외 메시지가 '줌은 null일 수 없습니다.'에서 '줌은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateCenterCoordinate 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
    • validateCenterCoordinate 클래스 내 예외 메시지가 '중심 좌표는 null일 수 없습니다.'에서 '중심 좌표는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePolygonHoleBoundary 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
    • validatePolygonHoleBoundary 클래스 내 예외 메시지가 '폴리곤 내부 구멍 좌표 리스트는 비어있을 수 없습니다.'에서 '폴리곤 내부 구멍 좌표 리스트는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/dto/FestivalCreateRequestFixture.java
    • new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/dto/FestivalGeographyPolygonHoleBoundaryUpdateRequestFixture.java
    • com.daedan.festabook.festival.domain.CoordinateFixture import가 추가되었습니다.
    • new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/service/FestivalNotificationServiceTest.java
    • unsubscribeFestivalNotification 클래스 내 assertThat(festivalNotification.isDeleted()).isTrue() 단언이 추가되었습니다.
  • src/test/java/com/daedan/festabook/festival/service/FestivalServiceTest.java
    • createFestival 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
    • getFestivalGeographyByFestivalId 클래스 내 FestivalGeographyResponse expected = FestivalGeographyResponse.from(festival); 라인이 제거되고 assertSoftly를 사용한 상세 검증으로 대체되었습니다.
    • updateFestivalPolygonHoleBoundary 클래스 내 new Coordinate(...)CoordinateFixture.create(...)로 변경되었습니다.
  • src/test/java/com/daedan/festabook/festival/service/TestFestivalNotificationServiceTest.java
    • 새로운 테스트 파일이 추가되었습니다. TestFestivalNotificationService의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolverUnitTest.java
    • 새로운 테스트 파일이 추가되었습니다. ActorArgumentResolver의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/argumentresolver/FestivalIdArgumentResolverUnitTest.java
    • 새로운 테스트 파일이 추가되었습니다. FestivalIdArgumentResolver의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/exception/InvalidFileExceptionTest.java
    • constructor 클래스 내 예외 메시지가 '파일은 비어있을 수 없습니다.'에서 '파일은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/global/exception/LockExceptionTest.java
    • 새로운 테스트 파일이 추가되었습니다. LockException의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/infrastructure/ShuffleManagerTest.java
    • getShuffledList 클래스에 '성공_원소가_있으면_셔플된_리스트_반환' 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/global/lock/LockAspectTest.java
    • 새로운 테스트 파일이 추가되었습니다. LockAspect의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/lock/infrastructure/InMemoryLockStorageTest.java
    • 새로운 테스트 파일이 추가되었습니다. InMemoryLockStorage의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/logging/RequestBodyCachingFilterTest.java
    • 새로운 테스트 파일이 추가되었습니다. RequestBodyCachingFilter의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/logging/dto/LogDtoTest.java
    • 새로운 테스트 파일이 추가되었습니다. LogDto의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/security/authorization/ActorFixture.java
    • TestActor Builder를 사용하지 않고 직접 생성자를 호출하도록 변경되었습니다.
  • src/test/java/com/daedan/festabook/global/security/authorization/AdminDetailsTest.java
    • hasOrganizationAuthority 클래스명이 hasOrganizationId로 변경되었습니다.
    • hasFestivalAuthority 클래스명이 hasFestivalId로 변경되었습니다.
    • hasOrganizerAuthority 클래스명이 hasOrganizerId로 변경되었습니다.
    • hasStaffAuthority 클래스명이 hasStaffId로 변경되었습니다.
    • hasPlaceAccessAuthority 클래스명이 hasPlaceId로 변경되었습니다.
  • src/test/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsTest.java
    • hasOrganizationAuthority 클래스명이 hasOrganizationId로 변경되었습니다.
    • hasFestivalAuthority 클래스명이 hasFestivalId로 변경되었습니다.
    • hasOrganizerAuthority 클래스명이 hasOrganizerId로 변경되었습니다.
    • hasStaffAuthority 클래스명이 hasStaffId로 변경되었습니다.
    • hasPlaceAccessAuthority 클래스명이 hasPlaceId로 변경되었습니다.
  • src/test/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsTest.java
    • hasOrganizationAuthority 클래스명이 hasOrganizationId로 변경되었습니다.
    • hasFestivalAuthority 클래스명이 hasFestivalId로 변경되었습니다.
    • hasOrganizerAuthority 클래스명이 hasOrganizerId로 변경되었습니다.
    • hasStaffAuthority 클래스명이 hasStaffId로 변경되었습니다.
    • hasPlaceAccessAuthority 클래스명이 hasPlaceId로 변경되었습니다.
  • src/test/java/com/daedan/festabook/global/security/authorization/StaffDetailsTest.java
    • hasOrganizationAuthority 클래스명이 hasOrganizationId로 변경되었습니다.
    • hasFestivalAuthority 클래스명이 hasFestivalId로 변경되었습니다.
    • hasStaffAuthority 클래스명이 hasStaffId로 변경되었습니다.
    • hasOrganizerAuthority 클래스명이 hasOrganizerId로 변경되었습니다.
    • hasPlaceAccessAuthority 클래스명이 hasPlaceId로 변경되었습니다.
  • src/test/java/com/daedan/festabook/global/security/authorization/TestActor.java
    • Builder 패턴이 제거되고 직접 생성자를 호출하도록 변경되었습니다.
  • src/test/java/com/daedan/festabook/global/security/handler/CustomAccessDeniedHandlerTest.java
    • 새로운 테스트 파일이 추가되었습니다. CustomAccessDeniedHandler의 단위 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/global/security/util/JwtProviderTest.java
    • extractFestivalIds 클래스에 '성공_festivalIds_클레임이_null이면_빈셋' 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/lineup/concurrency/LineupConcurrencyTest.java
    • 새로운 테스트 파일이 추가되었습니다. Lineup 생성 시 동시성 제어 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/lineup/controller/LineupControllerTest.java
    • io.restassured.http.Header import가 제거되었습니다.
  • src/test/java/com/daedan/festabook/lineup/domain/LineupTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validatePerformanceAt 메서드의 예외 메시지가 '공연 시간은 null일 수 없습니다.'에서 '공연 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateLineupBy 클래스 내 validatePerformanceAt 메서드의 예외 메시지가 '공연 시간은 null일 수 없습니다.'에서 '공연 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateFestival 클래스 내 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePerformanceAt 클래스 내 예외 메시지가 '공연 시간은 null일 수 없습니다.'에서 '공연 시간은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/lostitem/controller/LostItemControllerTest.java
    • io.restassured.http.Header import가 제거되었습니다.
  • src/test/java/com/daedan/festabook/lostitem/domain/LostItemTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validatePickupStatus 메서드의 예외 메시지가 '수령 상태는 null일 수 없습니다.'에서 '수령 상태는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateStatusBy 클래스 내 validatePickupStatus 메서드의 예외 메시지가 '수령 상태는 null일 수 없습니다.'에서 '수령 상태는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateFestival 클래스 내 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePickupStatus 클래스 내 예외 메시지가 '수령 상태는 null일 수 없습니다.'에서 '수령 상태는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/notification/dto/NotificationSendRequestTest.java
    • validateTitle 메서드의 예외 메시지가 '알림 제목은 비어있을 수 없습니다.'에서 '알림 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateBody 메서드의 예외 메시지가 '알림 본문은 비어있을 수 없습니다.'에서 '알림 본문은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/notification/infrastructure/FcmNotificationManagerTest.java
    • TOPIC_PREFIX 상수가 추가되었습니다.
    • setUp 메서드에서 topicFestivalPrefix 필드 설정이 추가되었습니다.
    • subscribeAndroidFestivalTopic 클래스가 추가되었습니다.
    • subscribeIosFestivalTopic 클래스가 추가되었습니다.
    • subscribeFestivalTopic 클래스에 '성공' 테스트 케이스가 추가되었습니다.
    • unsubscribeFestivalTopic 클래스에 '성공' 테스트 케이스가 추가되었습니다.
    • sendToFestivalTopic 클래스에 '성공' 테스트 케이스가 추가되었습니다.
    • sendToFestivalTopic 클래스에 '예외_iOS_알림_전송_실패도_도메인_예외로_변환' 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/organization/controller/OrganizationControllerTest.java
    • getOrganizationByOrganizationId 클래스의 위치가 getFestivalsByOrganization 아래로 변경되었습니다.
    • getFestivalsByOrganization 클래스명이 getFestivalsByOrganizationId로 변경되었습니다.
  • src/test/java/com/daedan/festabook/organization/service/OrganizationServiceTest.java
    • getOrganizationByOrganizationId 클래스의 위치가 getFestivalsByOrganizationId 아래로 변경되었습니다.
  • src/test/java/com/daedan/festabook/organizer/concurrency/OrganizerConcurrencyTest.java
    • 새로운 테스트 파일이 추가되었습니다. Organizer 생성 시 동시성 제어 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/organizer/controller/OrganizerControllerTest.java
    • static org.assertj.core.api.Assertions.assertThat import가 제거되었습니다.
    • loginOrganizer 클래스명이 loginOrganization으로 변경되었습니다.
    • updatePassword 클래스명이 checkUsernameDuplicate로 변경되었습니다.
    • checkUsernameDuplicate 클래스명이 updatePassword로 변경되었습니다.
    • updateName 클래스명이 updateName으로 변경되었습니다.
  • src/test/java/com/daedan/festabook/organizer/domain/OrganizerTest.java
    • createBy 클래스 내 아이디 한글 포함 예외 테스트 케이스가 추가되었습니다.
    • createBy 클래스 내 validateOrganization 메서드의 예외 메시지가 '조직 정보는 비어있을 수 없습니다.'에서 '조직 정보는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateUsername 클래스 내 아이디 한글 포함 예외 테스트 케이스가 추가되었습니다.
    • validateOrganization 클래스 내 예외 메시지가 '조직 정보는 비어있을 수 없습니다.'에서 '조직 정보는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/organizer/service/OrganizerServiceTest.java
    • createOrganizer 클래스 내 validateOrganizationId 메서드의 예외 메시지가 '조직 정보가 필요합니다.'에서 '조직은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createOrganizer 클래스 내 비밀번호 null 및 blank 예외 테스트 케이스가 추가되었습니다.
    • getOrganizerByOrganizationId 클래스가 추가되었습니다.
    • checkUsernameDuplicate 클래스가 추가되었습니다.
    • updatePassword 클래스 내 새 비밀번호 null, blank, 바이트 초과 예외 테스트 케이스가 추가되었습니다.
    • updateName 클래스 내 예외 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/place/concurrency/PlaceAccessConcurrencyTest.java
    • 새로운 테스트 파일이 추가되었습니다. PlaceAccess 생성 시 동시성 제어 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/place/concurrency/PlaceAnnouncementConcurrencyTest.java
    • 새로운 테스트 파일이 추가되었습니다. PlaceAnnouncement 생성 시 동시성 제어 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/place/controller/PlaceAccessControllerTest.java
    • getPlaceAccess 클래스명이 loginPlaceAccess로 변경되었습니다.
    • loginPlaceAccess 클래스명이 deletePlaceAccess로 변경되었습니다.
    • deletePlaceAccess 클래스명이 getPlaceAccess로 변경되었습니다.
  • src/test/java/com/daedan/festabook/place/controller/PlaceControllerTest.java
    • updateMainPlaceByPlaceAccess 클래스명이 updateMainPlaceForPlaceAccess로 변경되었습니다.
  • src/test/java/com/daedan/festabook/place/domain/PlaceAccessTest.java
    • createBy 클래스 내 validateAccessKey 메서드의 예외 메시지가 '접근 키는 비어있을 수 없습니다.'에서 '접근 키는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validatePassword 메서드의 예외 메시지가 '비밀번호는 비어있을 수 없습니다.'에서 '비밀번호는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateExpiresAt 메서드의 예외 메시지가 '만료 일시는 null일 수 없습니다.'에서 '만료 일시는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validatePlace 메서드의 예외 메시지가 '플레이스는 비어있을 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateAccessKey 클래스 내 예외 메시지가 '접근 키는 비어있을 수 없습니다.'에서 '접근 키는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePassword 클래스 내 예외 메시지가 '비밀번호는 비어있을 수 없습니다.'에서 '비밀번호는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateExpiresAt 클래스 내 예외 메시지가 '만료 일시는 null일 수 없습니다.'에서 '만료 일시는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePlace 클래스 내 예외 메시지가 '플레이스는 비어있을 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/place/domain/PlaceAnnouncementTest.java
    • createBy 클래스 내 validatePlace 메서드의 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateContent 메서드의 예외 메시지가 '플레이스 공지 내용은 null일 수 없습니다.'에서 '플레이스 공지 내용은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updatePlaceAnnouncementBy 클래스 내 validateTitle 메서드의 예외 메시지가 '플레이스 공지의 제목은 비어있을 수 없습니다.'에서 '플레이스 공지 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updatePlaceAnnouncementBy 클래스 내 validateContent 메서드의 예외 메시지가 '플레이스 공지 내용은 null일 수 없습니다.'에서 '플레이스 공지 내용은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updatePlaceAnnouncementBy 클래스 내 validateTitle 메서드의 예외 메시지 형식이 '플레이스 공지 제목의 길이는'에서 '플레이스 공지 제목 길이는'으로 변경되었습니다.
    • updatePlaceAnnouncementBy 클래스 내 validateContent 메서드의 예외 메시지 형식이 '플레이스 공지 내용의 길이는'에서 '플레이스 공지 내용 길이는'으로 변경되었습니다.
    • validatePlace 클래스 내 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 클래스 내 예외 메시지가 '플레이스 공지의 제목은 비어있을 수 없습니다.'에서 '플레이스 공지 제목은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 클래스 내 예외 메시지 형식이 '플레이스 공지 제목의 길이는'에서 '플레이스 공지 제목 길이는'으로 변경되었습니다.
    • validateContent 클래스 내 예외 메시지가 '플레이스 공지 내용은 null일 수 없습니다.'에서 '플레이스 공지 내용은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateContent 클래스 내 예외 메시지 형식이 '플레이스 공지 내용의 길이는'에서 '플레이스 공지 내용 길이는'으로 변경되었습니다.
  • src/test/java/com/daedan/festabook/place/domain/PlaceImageTest.java
    • createBy 클래스 내 validatePlace 메서드의 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateSequence 메서드의 예외 메시지가 '플레이스 이미지 순서는 null일 수 없습니다.'에서 '플레이스 이미지 순서는 비어 있을 수 없습니다.'로 변경되었습니다.
    • cloneBy 클래스 내 validatePlace 메서드의 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateSequenceBy 클래스 내 validateSequence 메서드의 예외 메시지가 '플레이스 이미지 순서는 null일 수 없습니다.'에서 '플레이스 이미지 순서는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePlace 클래스 내 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateSequence 클래스 내 예외 메시지가 '플레이스 이미지 순서는 null일 수 없습니다.'에서 '플레이스 이미지 순서는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/place/domain/PlaceTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 'Festival은 null일 수 없습니다.'에서 'Festival은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validatePlaceCategory 메서드의 예외 메시지가 'PlaceCategory는 null일 수 없습니다.'에서 'PlaceCategory는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateTitle 메서드의 예외 메시지가 '플레이스의 이름은 공백이거나 null일 수 없습니다.'에서 '플레이스의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateCoordinateBy 클래스 내 validateCoordinate 메서드의 예외 메시지가 'Coordinate는 null일 수 없습니다.'에서 'Coordinate는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateMainPlaceForPlaceAccessBy 클래스 내 validateTitle 메서드의 예외 메시지가 '플레이스의 이름은 공백이거나 null일 수 없습니다.'에서 '플레이스의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateEtcPlaceBy 클래스 내 validateTitle 메서드의 예외 메시지가 '플레이스의 이름은 공백이거나 null일 수 없습니다.'에서 '플레이스의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePlace 클래스가 제거되었습니다.
    • validateFestival 클래스 내 예외 메시지가 'Festival은 null일 수 없습니다.'에서 'Festival은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePlaceCategory 클래스 내 예외 메시지가 'PlaceCategory는 null일 수 없습니다.'에서 'PlaceCategory는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateCoordinate 클래스 내 예외 메시지가 'Coordinate는 null일 수 없습니다.'에서 'Coordinate는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTitle 클래스 내 예외 메시지가 '플레이스의 이름은 공백이거나 null일 수 없습니다.'에서 '플레이스의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/place/infrastructure/PlaceAccessJpaRepositoryTest.java
    • @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 어노테이션이 추가되었습니다.
  • src/test/java/com/daedan/festabook/place/infrastructure/PlaceImageJpaRepositoryTest.java
    • @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) 어노테이션이 추가되었습니다.
  • src/test/java/com/daedan/festabook/place/service/PlaceAccessServiceTest.java
    • getPlaceAccess 클래스명이 loginPlaceAccess로 변경되었습니다.
    • loginPlaceAccess 클래스명이 getPlaceAccess로 변경되었습니다.
    • deletePlaceAccess 클래스명이 deletePlaceAccess로 변경되었습니다.
  • src/test/java/com/daedan/festabook/place/service/PlaceImageServiceTest.java
    • updatePlaceImagesSequence 클래스에 '성공_빈_요청이면_빈_응답' 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/place/service/PlaceServiceTest.java
    • getPlaceByPlaceId 클래스에 '성공_기타_플레이스는_이미지와_공지_빈_응답' 테스트 케이스가 추가되었습니다.
    • updateMainPlaceByPlaceAccess 클래스명이 updateMainPlaceForPlaceAccess로 변경되었습니다.
  • src/test/java/com/daedan/festabook/question/controller/QuestionControllerTest.java
    • io.restassured.http.Header import가 제거되었습니다.
  • src/test/java/com/daedan/festabook/question/domain/QuestionTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 'Festival은 null일 수 없습니다.'에서 'Festival은 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateSequence 메서드의 예외 메시지가 '순서는 null일 수 없습니다.'에서 '순서는 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateSequenceBy 클래스 내 validateSequence 메서드의 예외 메시지가 '순서는 null일 수 없습니다.'에서 '순서는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateFestival 클래스 내 예외 메시지가 'Festival은 null일 수 없습니다.'에서 'Festival은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateSequence 클래스 내 예외 메시지가 '순서는 null일 수 없습니다.'에서 '순서는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/question/service/QuestionServiceTest.java
    • createQuestion 클래스에 '성공_기존_질문이_없으면_첫_순서로_생성' 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/staff/concurrency/StaffConcurrencyTest.java
    • 새로운 테스트 파일이 추가되었습니다. Staff 생성 시 동시성 제어 테스트를 포함합니다.
  • src/test/java/com/daedan/festabook/staff/controller/StaffControllerTest.java
    • loginStaff 클래스가 추가되었습니다.
    • updatePassword 클래스명이 updateStaffAuthorities로 변경되었습니다.
    • updateStaffAuthorities 클래스명이 updateName으로 변경되었습니다.
    • updateName 클래스명이 updatePassword로 변경되었습니다.
  • src/test/java/com/daedan/festabook/staff/domain/StaffAuthorityTest.java
    • validateStaff 메서드의 예외 메시지가 '축제 관리자는 null일 수 없습니다.'에서 '축제 관리자는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/staff/domain/StaffTest.java
    • createBy 클래스 내 아이디 한글 포함 예외 테스트 케이스가 추가되었습니다.
    • createBy 클래스 내 validateOrganization 메서드의 예외 메시지가 '조직 정보는 비어있을 수 없습니다.'에서 '조직 정보는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateUsername 클래스 내 아이디 한글 포함 예외 테스트 케이스가 추가되었습니다.
    • validateOrganization 클래스 내 예외 메시지가 '조직 정보는 비어있을 수 없습니다.'에서 '조직 정보는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/staff/service/StaffServiceTest.java
    • createStaff 클래스에 '예외_존재하지_않는_조직' 테스트 케이스가 추가되었습니다.
    • createStaff 클래스 내 비밀번호 바이트 초과 예외 테스트 케이스가 비밀번호 null 및 blank 예외 테스트 케이스로 변경되었습니다.
    • deleteStaff 클래스명이 loginStaff로 변경되었습니다.
    • loginStaff 클래스명이 getAllByOrganizationId로 변경되었습니다.
    • updateStaffAuthorities 클래스명이 getStaffByStaffId로 변경되었습니다.
    • getAllByOrganizationId 클래스명이 checkUsernameDuplicate로 변경되었습니다.
    • getStaffByStaffId 클래스명이 updateStaffAuthorities로 변경되었습니다.
    • checkUsernameDuplicate 클래스명이 updatePassword로 변경되었습니다.
    • updatePassword 클래스명이 deleteStaff로 변경되었습니다.
  • src/test/java/com/daedan/festabook/storage/controller/ImageStoreControllerTest.java
    • io.restassured.http.Header import가 제거되었습니다.
    • UploadImage 클래스명이 uploadImage로 변경되었습니다.
  • src/test/java/com/daedan/festabook/storage/infrastructure/MockStorageManagerTest.java
    • uploadFile 클래스에 '성공_storagePath가_슬래시로_시작하면_상대경로_유지' 테스트 케이스가 추가되었습니다.
  • src/test/java/com/daedan/festabook/storage/infrastructure/StoreFileTest.java
    • validateMultipartFile 메서드의 예외 메시지가 '파일은 비어있을 수 없습니다.'에서 '파일은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateStoragePath 메서드의 예외 메시지가 '파일 경로는 비어있을 수 없습니다.'에서 '파일 경로는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/storage/service/ImageStoreServiceTest.java
    • validateFile 메서드의 예외 메시지가 '파일은 비어있을 수 없습니다.'에서 '파일은 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/timetag/controller/TimeTagControllerTest.java
    • io.restassured.http.Header import가 제거되었습니다.
  • src/test/java/com/daedan/festabook/timetag/domain/PlaceTimeTagTest.java
    • createBy 클래스 내 validatePlace 메서드의 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateTimeTag 메서드의 예외 메시지가 '시간 태그는 null일 수 없습니다.'에서 '시간 태그는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validatePlace 클래스 내 예외 메시지가 '플레이스는 null일 수 없습니다.'에서 '플레이스는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateTimeTag 클래스 내 예외 메시지가 '시간 태그는 null일 수 없습니다.'에서 '시간 태그는 비어 있을 수 없습니다.'로 변경되었습니다.
  • src/test/java/com/daedan/festabook/timetag/domain/TimeTagTest.java
    • createBy 클래스 내 validateFestival 메서드의 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • createBy 클래스 내 validateName 메서드의 예외 메시지가 '시간 태그의 이름은 공백이거나 null일 수 없습니다.'에서 '시간 태그의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
    • updateNameBy 클래스 내 validateName 메서드의 예외 메시지가 '시간 태그의 이름은 공백이거나 null일 수 없습니다.'에서 '시간 태그의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateFestival 클래스 내 예외 메시지가 '축제는 null일 수 없습니다.'에서 '축제는 비어 있을 수 없습니다.'로 변경되었습니다.
    • validateName 클래스 내 예외 메시지가 '시간 태그의 이름은 공백이거나 null일 수 없습니다.'에서 '시간 태그의 이름은 비어 있을 수 없습니다.'로 변경되었습니다.
Activity
  • refactor: null 검증 예외명 통일
  • refactor: null 검증 Objects.isNull, nonNull로 변경
  • refactor: if 문 간 공백 제거
  • test: TestActor Builder 제거
  • test: Fixture 안 쓰는 곳 수정
  • refactor: 서비스, 컨트롤러 코드 메서드 순서 수정
  • refactor: Request to 메서드 제거
  • refactor: 테스트명 실제 메서드명 불일치 부분 수정
  • test: 커버리지 누락 보완
  • test: FestivalImageTest 누락된 도메인 검증 추가
  • test: DisplayNameGeneration 추가
  • refactor: 미사용 import 제거
  • docs: 컨벤션 엡데이트
  • refactor: PlaceAnnouncement 검증 테스트 추가
  • fix: Device 없을 경우도 알림 삭제
  • refactor: 미사용 import 제거
  • test: 예외명 컨벤션 수정
  • fix: 잘못된 메서드명 수정
  • test: 중복 display 어노테이션 제거
  • feat: 비밀번호 검증 추가
  • test: 토픽 검증 추가
  • test: 불필요한 검증 제거
  • test: 불필요한 null 검증 제거
  • refactor: 불필요한 공백 제거
  • feat: 동시성 제어 필요한 로직 인메모리락 추가
  • test: 테스트명 수정
  • test: 불필요 테스트 삭제
  • test: assertSoftly 추가
  • test: 비어 있는 것 검증 추가
  • test: 불필요한 테스트 삭제
  • test: 누락된 테스트 검증 추가
  • test: 불필요한 테스트 삭제
  • refactor: null 검증 == 비교로 변경
  • test: LockAspectTest 추가
  • test: 테스트 소속 이상한 부분 수정
  • feat: 아이디 한글 검증 추가
  • refactor: 미사용 import 제거
  • refactor: 의도대로 동작하는 Objects.isNull로 변경
  • feat: 예외 발생 락 해제 테스트 추가
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@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: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/main/java/com/daedan/festabook/timetag/service/TimeTagService.java (1)

53-56: ⚠️ Potential issue | 🟠 Major

exists 후 삭제 흐름은 동시성에서 원자성이 깨질 수 있습니다.

Line 55-56, Line 59-63 조합은 전형적인 Check-Then-Act 패턴이라, 동시 요청 시 “검증 시점엔 미사용 / 삭제 시점엔 사용 중” 상태가 생길 수 있습니다.

  • 옵션 A(성능/단순성): timeTagId 기준 인메모리 락(@Lockable)으로 deleteTimeTag를 직렬화
    • 장점: 구현 단순, 현재 운영 형태에서 효과적
    • 단점: 멀티 인스턴스로 확장 시 한계
  • 옵션 B(정합성 최우선): DB 레벨 제약/원자 쿼리(조건부 삭제)로 최종 방어
    • 장점: 인스턴스 수와 무관한 정합성 보장
    • 단점: 쿼리/스키마 복잡도 증가

Based on learnings: "festabook/backend 프로젝트는 단일 인스턴스(single JVM) 환경으로 운영된다. 따라서 인메모리 락(Lockable)으로 Check-Then-Act 동시성 문제를 충분히 제어할 수 있다."

Also applies to: 59-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/timetag/service/TimeTagService.java`
around lines 53 - 56, The delete flow uses a Check-Then-Act pattern
(getTimeTagById -> validateTimeTagNotInUse -> timeTag.deleteBy) which can break
atomicity under concurrency; wrap the deleteTimeTag method with the in-memory
lock mechanism used in this codebase (annotate or invoke the `@Lockable` keyed by
timeTagId) so the validation and deletion run serialized per timeTagId, e.g.,
apply `@Lockable` on deleteTimeTag or acquire the same lock inside deleteTimeTag
before calling getTimeTagById, validateTimeTagNotInUse, and timeTag.deleteBy to
prevent race conditions in single-JVM deployments.
src/main/java/com/daedan/festabook/global/argumentresolver/OrganizationIdArgumentResolver.java (1)

40-46: ⚠️ Potential issue | 🟡 Minor

parseOrganizationId 메서드는 private으로 변경하세요.

현재 parseOrganizationIdpublic으로 선언되어 있지만, 이 클래스 내부(line 31)에서만 호출되며 외부 어디에서도 사용되지 않습니다. 이는 불필요한 공개 인터페이스를 노출하는 것으로, 이 메서드는 HandlerMethodArgumentResolver의 구현 세부사항일 뿐 외부에서 직접 호출할 필요가 없습니다.

validateOrganizationId가 이미 private static으로 작성된 것처럼, parseOrganizationIdprivate으로 변경하여 클래스의 진정한 공개 계약(public interface)을 명확하게 정의하는 것이 좋습니다. 이렇게 하면 나중에 이 메서드를 리팩토링할 때 더 자유롭고, 의도하지 않은 외부 의존성이 생길 위험도 줄일 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/daedan/festabook/global/argumentresolver/OrganizationIdArgumentResolver.java`
around lines 40 - 46, The method parseOrganizationId in
OrganizationIdArgumentResolver is declared public but is only used internally;
change its visibility to private to match validateOrganizationId (which is
private static) and to avoid exposing implementation details of this
HandlerMethodArgumentResolver implementation—locate the parseOrganizationId
method and change its modifier from public to private, leaving its body and
exception behavior unchanged.
src/main/java/com/daedan/festabook/place/service/PlaceService.java (1)

61-68: ⚠️ Potential issue | 🟠 Major

클론 대상 검증이 축제 범위를 보장하지 않아 부분 복제가 조용히 발생할 수 있습니다.

Line 189는 “ID 존재 여부”만 확인하고, Line 65-68에서 축제 ID로 다시 필터링합니다. 이 조합이면 다른 축제 소속 ID가 섞여도 에러 없이 일부만 복제됩니다(정합성 저하).
옵션 A(성능 우선): 요청 ID를 distinctcountByIdInAndFestivalId로 수량 일치 검증.
옵션 B(운영/디버깅 우선): 조회 결과와 요청 ID 차집합을 계산해 어떤 ID가 문제인지 명시적으로 예외 반환.

수정 예시 (옵션 A)
@@
     `@Transactional`
     public PlacesCloneResponse clonePlaces(Actor actor, Long festivalId, PlacesCloneRequest request) {
         validateClonePlacesSize(request.originalPlaceIds().size());
-        validateExistsPlace(request.originalPlaceIds());
+        List<Long> originalPlaceIds = request.originalPlaceIds().stream()
+                .distinct()
+                .toList();
+        validateExistsPlaceInFestival(originalPlaceIds, festivalId);

         List<Place> originalPlaces = placeJpaRepository.findAllByIdInAndFestivalId(
-                request.originalPlaceIds(),
+                originalPlaceIds,
                 festivalId
         );
@@
-    private void validateExistsPlace(List<Long> originalPlaceIds) {
-        if (placeJpaRepository.countByIdIn(originalPlaceIds) != originalPlaceIds.size()) {
+    private void validateExistsPlaceInFestival(List<Long> originalPlaceIds, Long festivalId) {
+        long count = placeJpaRepository.countByIdInAndFestivalId(originalPlaceIds, festivalId);
+        if (count != originalPlaceIds.size()) {
             throw new NotFoundException(Place.class);
         }
     }

Also applies to: 188-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/place/service/PlaceService.java` around
lines 61 - 68, The clonePlaces flow currently validates only existence
(validateExistsPlace) and then re-filters by festival when loading via
placeJpaRepository.findAllByIdInAndFestivalId, allowing IDs from other festivals
to be silently skipped; fix by ensuring the requested originalPlaceIds all
belong to the given festival: either (A) dedupe request.originalPlaceIds(), call
placeJpaRepository.countByIdInAndFestivalId(dedupedIds, festivalId) and compare
counts, throwing an exception if mismatch, or (B) load originalPlaces with
placeJpaRepository.findAllByIdInAndFestivalId(...) and compute the set
difference between the requested IDs and
originalPlaces.stream().map(Place::getId).collect(...); if any missing IDs
exist, throw an exception listing which IDs are invalid so partial clones cannot
occur. Ensure this replaces or augments the current validateExistsPlace check
used in clonePlaces.
src/main/java/com/daedan/festabook/event/service/EventDateService.java (1)

58-67: ⚠️ Potential issue | 🟠 Major

update 경로에서 festivalId 신뢰로 락/중복검증 범위가 어긋날 수 있습니다.

Line 64~67에서 실제 수정 대상은 eventDateId로 조회한 엔티티인데, 중복 검증은 외부 입력 festivalId를 사용합니다. 두 값이 불일치하면 잘못된 축제를 기준으로 검증/락 스코프가 잡힐 수 있습니다.
옵션 A(권장): 조회된 eventDate의 소속 축제를 기준으로 검증하고, 불일치 시 예외 처리.
옵션 B: 업데이트 API에서 festivalId를 제거하고 eventDateId 기반으로만 처리.

🔧 보강 예시
 public EventDateUpdateResponse updateEventDate(
         Actor actor,
         Long festivalId,
         Long eventDateId,
         EventDateUpdateRequest request
 ) {
     EventDate eventDate = getEventDateById(eventDateId);
+    Long ownerFestivalId = eventDate.getFestival().getId();
+    if (!ownerFestivalId.equals(festivalId)) {
+        throw new NotFoundException(EventDate.class);
+    }
     if (!eventDate.getDate().isEqual(request.date())) {
-        validateDuplicatedEventDate(festivalId, request.date());
+        validateDuplicatedEventDate(ownerFestivalId, request.date());
     }
     eventDate.updateDateBy(actor, request.date());
     return EventDateUpdateResponse.from(eventDate);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/event/service/EventDateService.java`
around lines 58 - 67, The updateEventDate method currently uses the external
festivalId input for duplicate/lock checks which can diverge from the retrieved
EventDate's actual festival; change the logic to derive the festival id from the
loaded EventDate (use getEventDateById(eventDateId) ->
eventDate.getFestival().getId()) when calling validateDuplicatedEventDate and
when establishing any locks, and if the provided festivalId does not match
eventDate.getFestival().getId() throw a clear validation exception; ensure all
references to festivalId inside updateEventDate are replaced with the
authoritative id from the EventDate entity (or remove the incoming festivalId
per Option B).
src/main/java/com/daedan/festabook/organizer/service/OrganizerService.java (1)

46-59: ⚠️ Potential issue | 🔴 Critical

락 키가 조직 단위 불변식을 완전히 보호하지 못합니다.

현재 락 키가 username 기준이라, 같은 조직에 서로 다른 username으로 동시 요청이 들어오면 validateOrganizationHasNoOrganizer()와 저장 사이에서 경쟁 조건이 생길 수 있습니다.

  • 옵션 A: 락 키를 organizationId 기준으로 변경
    • 장점: “조직당 관리자 1명” 불변식을 직접 보호합니다.
    • 단점: 같은 조직 내 생성 요청 직렬화로 처리량이 약간 줄 수 있습니다.
  • 옵션 B: DB unique 제약(organization_id)을 최종 안전장치로 추가 + 예외를 Conflict로 매핑
    • 장점: 애플리케이션 락 누락에도 데이터 무결성을 보장합니다.
    • 단점: 마이그레이션/예외 변환 코드가 추가됩니다.
🔧 제안 패치 (옵션 A)
 `@Lockable`(
-        spelKey = "'OrganizerService'.concat(T(String).valueOf(`#request.username`()))",
+        spelKey = "'OrganizerService:organization:'.concat(T(String).valueOf(`#request.organizationId`()))",
         useMethodScopeLock = false
 )
 `@Transactional`
 public OrganizerResponse createOrganizer(Actor actor, OrganizerRequest request) {

Based on learnings: festabook/backend 프로젝트는 단일 인스턴스(single JVM) 환경으로 운영되며 인메모리 락으로 Check-Then-Act 동시성 문제를 제어한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/organizer/service/OrganizerService.java`
around lines 46 - 59, The lock key on the Lockable annotation in createOrganizer
should use the organization identifier instead of username to prevent
check-then-act races across different usernames in the same organization: update
the spelKey expression on the Lockable annotation to derive the key from
request.organizationId() (so the lock covers the organization), keep the
transactional boundary and calls to validateOrganizationHasNoOrganizer and
getOrganizationByOrganizationId unchanged, and ensure tests exercising
concurrent createOrganizer calls for the same organization pass; alternatively,
if you prefer DB-enforced safety instead, add a unique constraint on
organization_id for organizers and map the resulting constraint-violation
exception to a conflict response in the createOrganizer flow.
src/test/java/com/daedan/festabook/place/controller/PlaceAccessControllerTest.java (1)

152-225: ⚠️ Potential issue | 🟠 Major

컨트롤러 통합 테스트 범위를 서비스 규칙 검증과 분리해 주세요.

Line 152-225의 만료/삭제 링크 실패 시나리오는 HTTP 라우팅/권한보다 비즈니스 규칙 검증 성격이 강해서, 컨트롤러 테스트가 과도하게 두꺼워지고 서비스 테스트와 중복될 리스크가 있습니다.

  • 옵션 A: 컨트롤러 테스트는 성공 + 권한 중심으로 유지
    • 장점: API 계약 검증에 집중, 유지보수 단순
    • 단점: 에러 비즈니스 검증은 다른 계층으로 이동 필요
  • 옵션 B: 만료/삭제 규칙은 PlaceAccessServiceTest로 이동
    • 장점: 도메인 규칙 변경 시 영향 범위가 명확
    • 단점: 컨트롤러 단에서의 세부 메시지 검증은 축소됨

As per coding guidelines: "Controller 통합 테스트 범위: Controller 테스트(통합 테스트)는 성공 케이스와 권한 테스트만 작성한다. 비즈니스 로직 검증은 Service/Domain 단위 테스트에서 수행한다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/place/controller/PlaceAccessControllerTest.java`
around lines 152 - 225, These two controller tests (PlaceAccessControllerTest
methods 실패_만료된_플레이스_접근_링크 and 실패_삭제된_플레이스_접근_링크) are testing business rules
(expiration/deletion) and should be removed from the controller integration test
and moved to a service-level test (PlaceAccessServiceTest) that asserts the same
error conditions and messages; alternatively, if you want to keep controller
coverage, reduce the controller tests to only assert routing/authorization and
the HTTP status (e.g., success and auth failure) and delete the business-rule
setup/assertions from PlaceAccessControllerTest so that PlaceAccessServiceTest
owns the detailed rule validations.
🧹 Nitpick comments (21)
src/test/java/com/daedan/festabook/global/logging/dto/LogDtoTest.java (3)

17-20: 각 테스트에 // given 구간도 명시해 주시면 컨벤션 일관성이 더 좋아집니다.

지금도 테스트는 읽히지만, 팀 규칙상 given/when/then 3구간을 고정하면 이후 셋업 추가 시 구조가 흔들리지 않아 유지보수가 편해집니다.

As per coding guidelines "모든 테스트에 // given, // when, // then 주석을 작성하여 구간을 구분한다."

Also applies to: 44-47, 61-64, 78-81

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/global/logging/dto/LogDtoTest.java` around
lines 17 - 20, Add explicit "// given" comments to each test to follow the
given/when/then convention: in LogDtoTest.java (test methods like 성공() and the
other test cases referenced) add a "// given" section before any setup code
(e.g., constructing inputs or stubbing), keep the existing "// when" before
ApiLog.from(...) or action calls, and ensure a "// then" before assertions so
each test clearly has // given, // when, // then markers for consistency.

14-16: @nested 클래스명은 테스트 대상 메서드명과 동일하게 맞추는 편이 좋겠습니다.

현재 구조는 의도는 명확하지만, 팀 규칙 기준으로는 메서드명 일치성이 약합니다.
옵션 A) DTO별 테스트 클래스를 분리하고 각 파일에서 @Nested class from 사용: 규칙 부합도가 높고 탐색이 쉽습니다(파일 수 증가 단점).
옵션 B) 한 파일 유지 시 DTO별 상위 중첩 클래스를 두고 그 아래 from 중첩 클래스로 구성: 파일 수는 유지하면서 규칙도 맞출 수 있습니다(중첩 깊이 증가 단점).

As per coding guidelines "테스트하려는 메서드마다 @Nested 클래스를 작성하며 @Nested 클래스명은 테스트하려는 메서드명과 동일하게 작성한다."

Also applies to: 41-43, 58-60, 75-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/global/logging/dto/LogDtoTest.java` around
lines 14 - 16, Rename the `@Nested` classes so their names exactly match the
method under test ("from") — e.g., change class ApiLogFrom to class from (or
restructure into a DTO-specific outer `@Nested` class and an inner `@Nested` class
named from) for each DTO test in LogDtoTest (replace the other similarly named
nested classes in this file the same way); ensure each `@Nested` targets the
method "from" to comply with the guideline and maintain test discoverability.

84-84: 2단계 체이닝은 개행하면 코드베이스 스타일과 더 잘 맞습니다.

기능 영향은 없고 가독성/일관성 개선 제안입니다.

제안 diff
-            assertThat(result).isEqualTo(new MethodEventLog(LogType.METHOD_EVENT, "Controller", "getAll"));
+            assertThat(result)
+                    .isEqualTo(new MethodEventLog(LogType.METHOD_EVENT, "Controller", "getAll"));

As per coding guidelines "메서드 체이닝이 2단계 이상이면 반드시 개행한다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/global/logging/dto/LogDtoTest.java` at
line 84, 테스트의 메서드 체이닝이 두 단계 이상이므로 한 줄로 작성된 assert 구문을 개행해 읽기 좋게 만드세요:
LogDtoTest의 assertThat(result).isEqualTo(new MethodEventLog(...))를
assertThat(result) 다음 줄로 개행하고 인덴트된 .isEqualTo(new
MethodEventLog(LogType.METHOD_EVENT, "Controller", "getAll")) 형태로 바꿔 일관된 체이닝
스타일을 적용하세요.
src/main/java/com/daedan/festabook/global/security/filter/JwtAuthenticationFilter.java (1)

53-61: 설계 관점 참고: setAuthentication()의 방어적 null 체크

현재 userDetails != null 체크는 accountDetailsExtractor.extract()가 null을 반환할 경우를 대비한 방어적 코드입니다. 그러나 팀의 JWT 검증 컨벤션에 따르면, 토큰이 유효하나 필수 claim이 누락된 경우 AuthenticationException을 던지도록 되어 있습니다.

고려할 수 있는 두 가지 방향:

옵션 설명 장점 단점
A) 현행 유지 방어적 null 체크 유지 예상치 못한 null 반환에도 NPE 방지 extractor의 버그가 묻힐 수 있음
B) null 체크 제거 + 계약 명확화 extract()가 non-null 반환 보장 의도가 명확하고 버그 조기 발견 extractor 구현이 계약을 위반하면 NPE 발생

현재 PR 범위에서 변경이 필요한 것은 아니며, 향후 리팩토링 시 accountDetailsExtractor.extract()의 반환 계약을 @NonNull로 명시하고 이 방어 코드를 제거하는 방향을 검토해 볼 수 있습니다. (Based on learnings: JWT claim 누락 시 AuthenticationException을 던지도록 되어 있으므로, extractor가 null을 반환하는 경우는 서버 측 버그로 간주됩니다.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/daedan/festabook/global/security/filter/JwtAuthenticationFilter.java`
around lines 53 - 61, The current defensive null check in
setAuthentication(AccountDetails userDetails) masks possible contract violations
from accountDetailsExtractor.extract(); remove the if (userDetails != null)
guard so a null return will surface (causing an NPE) and instead ensure
accountDetailsExtractor.extract() declares/guarantees a non-null return (e.g.,
annotate with `@NonNull`) and throws AuthenticationException when required claims
are missing; update any extractor implementations to throw
AuthenticationException on invalid/missing claims rather than returning null so
setAuthentication and UsernamePasswordAuthenticationToken always receive a valid
AccountDetails.
src/test/java/com/daedan/festabook/global/logging/RequestBodyCachingFilterTest.java (1)

38-49: 래핑 “타입” 외에 “원본 요청 연결성”도 함께 검증해두면 더 안전합니다.

현재도 충분히 유의미하지만, 향후 구현이 바뀔 때 의도와 다른 객체를 전달하는 회귀를 더 잘 잡으려면 한 단계만 더 확인하는 것이 좋습니다.

  • 옵션 A(현재 유지): 단순하고 테스트가 가볍습니다.
  • 옵션 B(권장): wrapper 내부의 원본 request가 동일한지까지 검증해 안정성이 높아집니다.
제안 diff (옵션 B)
         // then
         assertThat(filterChain.getRequest()).isInstanceOf(ContentCachingRequestWrapper.class);
+        ContentCachingRequestWrapper result = (ContentCachingRequestWrapper) filterChain.getRequest();
+        assertThat(result.getRequest()).isSameAs(request);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/global/logging/RequestBodyCachingFilterTest.java`
around lines 38 - 49, Update the test 성공_wrapper가_아니면_새로_감싸서_전달 to not only
assert the request type but also verify the wrapper delegates to the original
request: cast filterChain.getRequest() to ContentCachingRequestWrapper and
assert that wrapper.getRequest() (or equivalent accessor) is the same instance
as the original MockHttpServletRequest `request`; this ensures both type and
original-request connectivity are validated.
src/test/java/com/daedan/festabook/question/service/QuestionServiceTest.java (1)

73-74: 기대값을 expected로 분리하면 테스트 의도가 더 명확해집니다.

현재도 동작은 정확합니다. 다만 리터럴 직접 비교보다 기대값을 변수로 분리하면 규칙 일관성과 유지보수성이 좋아집니다.

✏️ 제안 변경
             // then
-            assertThat(result.sequence()).isEqualTo(1);
+            int expected = 1;
+            assertThat(result.sequence()).isEqualTo(expected);

As per coding guidelines "테스트 실행 결과 값 변수명: result. 기대값 변수명: expected."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/question/service/QuestionServiceTest.java`
around lines 73 - 74, Extract the literal assertion value into an expected
variable to follow the testing naming convention: declare an int expected = 1
(or appropriate type) and then use
assertThat(result.sequence()).isEqualTo(expected); keep the actual variable
named result (from result.sequence()) and the expected variable named expected
to improve clarity and maintain consistency with other tests.
src/test/java/com/daedan/festabook/place/service/PlaceServiceTest.java (1)

335-361: 테스트가 Mockito 기본 반환값에 의존해 의도 고정이 약합니다.

현재는 placeImageJpaRepository, placeAnnouncementJpaRepository를 stub 하지 않아도(기본 empty 반환) 통과할 수 있습니다.
의도에 따라 아래 중 하나로 고정해두는 쪽을 권장드립니다.

  • 옵션 A(성능/분기 보장): 기타 플레이스에서 조회를 아예 생략한다면 shouldHaveNoInteractions()로 단락 실행을 검증
  • 옵션 B(가독성/단순성): 조회는 수행하되 빈 결과를 기대한다면 두 repository를 명시적으로 given(...).willReturn(List.of()) stub
예시 (옵션 A)
             // when
             PlaceResponse result = placeService.getPlaceByPlaceId(placeId);

             // then
             assertSoftly(s -> {
                 s.assertThat(result.placeId()).isEqualTo(placeId);
                 s.assertThat(result.placeImages().responses()).isEmpty();
                 s.assertThat(result.placeAnnouncements().responses()).isEmpty();
                 s.assertThat(result.timeTags().responses()).hasSize(1);
                 s.assertThat(result.timeTags().responses().getFirst().name()).isEqualTo(timeTag.getName());
             });
+            then(placeImageJpaRepository).shouldHaveNoInteractions();
+            then(placeAnnouncementJpaRepository).shouldHaveNoInteractions();

As per coding guidelines, "테스트는 검증하려는 의도만 드러내야 한다. given에는 해당 테스트에서 검증하는 값만 노출" 규칙을 더 강하게 만족시키기 위한 제안입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/place/service/PlaceServiceTest.java`
around lines 335 - 361, The test 성공_기타_플레이스는_이미지와_공지_빈_응답() relies on Mockito's
default empty returns for placeImageJpaRepository and
placeAnnouncementJpaRepository which weakens intent; either explicitly stub
those repos to return empty lists with
given(placeImageJpaRepository.findAllByPlaceId(placeId)).willReturn(List.of())
and
given(placeAnnouncementJpaRepository.findAllByPlaceId(placeId)).willReturn(List.of())
so the expectation is explicit, or assert no calls were made with
verifyNoInteractions/placeImageJpaRepository and placeAnnouncementJpaRepository
(or verify(placeImageJpaRepository, never()).findAllByPlaceId(...)) to document
that getPlaceByPlaceId(placeId) must not query them.
src/test/java/com/daedan/festabook/global/security/handler/CustomAccessDeniedHandlerTest.java (1)

26-27: 테스트 대상 주입을 @InjectMocks로 통일해 주세요.

현재 방식은 각 테스트에서 핸들러를 반복 생성해서 중복이 생기고, 추후 생성자 의존성이 늘어날 때 테스트별 초기화 누락 위험이 있습니다.

  • 옵션 A(권장): @InjectMocks로 일관 주입 → 중복 감소, 설정 일관성 확보
  • 옵션 B: 현 방식 유지 → 생성 과정이 명시적이라는 장점은 있지만, 팀 테스트 컨벤션과 어긋나고 유지보수 비용이 커집니다.
변경 예시
 import org.mockito.Mock;
+import org.mockito.InjectMocks;
 import org.mockito.junit.jupiter.MockitoExtension;

 `@ExtendWith`(MockitoExtension.class)
 `@DisplayNameGeneration`(DisplayNameGenerator.ReplaceUnderscores.class)
 class CustomAccessDeniedHandlerTest {

     `@Mock`
     private ObjectMapper objectMapper;
+
+    `@InjectMocks`
+    private CustomAccessDeniedHandler handler;

     `@Nested`
     class handle {

         `@Test`
         void 성공_forbidden_json_응답() throws IOException {
             // given
-            CustomAccessDeniedHandler handler = new CustomAccessDeniedHandler(objectMapper);
             MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test/uri");
             MockHttpServletResponse response = new MockHttpServletResponse();
             AccessDeniedException exception = new AccessDeniedException("denied");
@@
         `@Test`
         void 예외_objectMapper_직렬화_실패() throws IOException {
             // given
-            CustomAccessDeniedHandler handler = new CustomAccessDeniedHandler(objectMapper);
             MockHttpServletRequest request = new MockHttpServletRequest("POST", "/test/uri");
             MockHttpServletResponse response = new MockHttpServletResponse();
             AccessDeniedException exception = new AccessDeniedException("denied");

As per coding guidelines "**/*Test.java: @Mock으로 모킹하고 @InjectMocks로 테스트 대상에 주입한다."

Also applies to: 35-36, 56-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/global/security/handler/CustomAccessDeniedHandlerTest.java`
around lines 26 - 27, The tests currently create the handler per test and only
mock ObjectMapper with `@Mock`; change to use `@InjectMocks` to inject mocks into
the test subject: add an `@InjectMocks` field for CustomAccessDeniedHandler in
CustomAccessDeniedHandlerTest and remove repeated in-test instantiations
(references: CustomAccessDeniedHandler, objectMapper), ensure Mockito
initialization is present (e.g., MockitoExtension or
MockitoAnnotations.openMocks) so the `@InjectMocks` injection works, and remove
duplicate manual constructions at the other indicated spots.
src/main/java/com/daedan/festabook/staff/domain/Staff.java (1)

170-172: matches() 대신 find()를 사용하고 패턴을 단순화하면 의도가 더 명확해집니다.

현재 패턴 .*[ㄱ-ㅎㅏ-ㅣ가-힣].*은 의도(한글 문자 존재 여부 검사)를 직접적으로 표현하기보다 전체 문자열 매칭에 맞춰져 있어, .*라는 불필요한 와일드카드가 추가되었습니다. find()를 사용하면 패턴이 더 간결해지고 검사 로직이 직관적입니다.

  • 옵션 A(권장): 패턴을 [ㄱ-ㅎㅏ-ㅣ가-힣]으로 단순화하고 find() 사용
    • 장점: 의도 명확, 패턴 가독성 향상, 불필요한 앞뒤 .* 제거
    • 단점: 없음에 가까움
  • 옵션 B: 현재 그대로 유지
    • 장점: 현재 동작하고 있음
    • 단점: 과도한 패턴 표현으로 인한 가독성 저하
제안 diff
-    private static final Pattern KOREAN_USERNAME_PATTERN = Pattern.compile(".*[ㄱ-ㅎㅏ-ㅣ가-힣].*");
+    private static final Pattern KOREAN_USERNAME_PATTERN = Pattern.compile("[ㄱ-ㅎㅏ-ㅣ가-힣]");
...
-        if (KOREAN_USERNAME_PATTERN.matcher(username).matches()) {
+        if (KOREAN_USERNAME_PATTERN.matcher(username).find()) {
             throw new BadRequestException("아이디는 한글을 포함할 수 없습니다.");
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/staff/domain/Staff.java` around lines 170
- 172, The current KOREAN_USERNAME_PATTERN usage in Staff.java should be
simplified: replace the pattern that uses leading/trailing ".*" with a concise
character class "[ㄱ-ㅎㅏ-ㅣ가-힣]" and use KOREAN_USERNAME_PATTERN.find() against
username instead of matches(); update the conditional that throws
BadRequestException("아이디는 한글을 포함할 수 없습니다.") to check
KOREAN_USERNAME_PATTERN.find(username) so the intent (presence of any Korean
character) is clearer and more readable.
src/test/java/com/daedan/festabook/place/service/PlaceImageServiceTest.java (1)

171-185: 빈 요청 테스트는 “저장소 미호출”까지 검증하면 더 견고합니다.

현재 테스트는 빈 요청에서도 findAllById 스텁을 두고 있어, 서비스가 불필요한 조회를 해도 통과할 수 있습니다.

  • 옵션 A(성능 우선): 빈 요청이면 즉시 반환하고 저장소 미호출을 검증
  • 옵션 B(현 구조 유지): 저장소 조회를 의도한 설계라면 테스트명/의도를 명확히 표현
♻️ 제안 diff (옵션 A)
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anySet;
 import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.never;
@@
         void 성공_빈_요청이면_빈_응답() {
             // given
             Actor adminActor = ActorFixture.admin();
             List<PlaceImageSequenceUpdateRequest> requests = List.of();
-
-            given(placeImageJpaRepository.findAllById(Set.of()))
-                    .willReturn(new ArrayList<>());
@@
             // then
             assertThat(result.responses()).isEmpty();
+            then(placeImageJpaRepository).should(never()).findAllById(anySet());
         }

As per coding guidelines "Service 테스트 — BDDMockito 패턴: ... then().should(never()) 호출되지 않음 검증."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/place/service/PlaceImageServiceTest.java`
around lines 171 - 185, The test 성공_빈_요청이면_빈_응답 should assert the repository is
not called for an empty request: change the test to treat an empty requests list
as a fast-path and verify placeImageJpaRepository.findAllById is never invoked
after calling placeImageService.updatePlaceImagesSequence(adminActor, requests)
by using BDDMockito
then(placeImageJpaRepository).should(never()).findAllById(anySet()) (or the
equivalent never() verification); if the design intentionally calls the repo,
instead update the test name/intent to reflect that behavior.
src/main/java/com/daedan/festabook/question/domain/Question.java (1)

142-149: validateQuestion에서도 StringUtils.hasText() 사용을 고려해 보세요.

validateAnswer 메서드(Line 152)에서는 StringUtils.hasText(answer)를 사용하여 null, 빈 문자열, 공백만 있는 문자열을 한 번에 검증하고 있습니다. 반면 validateQuestion 메서드에서는 여전히 question == null || question.trim().isEmpty() 패턴을 사용하고 있어 일관성이 떨어집니다.

두 가지 선택지가 있습니다:

  • 옵션 A: validateQuestionStringUtils.hasText()로 변경하여 일관성 확보
  • 옵션 B: 현재 상태 유지 (동작은 동일하므로 기능적 문제는 없음)

동작 결과는 같지만, 같은 클래스 내에서 동일한 검증 패턴을 사용하면 코드 가독성과 유지보수성이 향상됩니다.

♻️ 일관성 개선을 위한 제안
 private void validateQuestion(String question) {
-    if (question == null || question.trim().isEmpty()) {
+    if (!StringUtils.hasText(question)) {
         throw new BadRequestException("질문은 비어 있을 수 없습니다.");
     }
     if (question.length() > MAX_QUESTION_LENGTH) {
         throw new BadRequestException(String.format("질문은 %d자를 초과할 수 없습니다.", MAX_QUESTION_LENGTH));
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/question/domain/Question.java` around
lines 142 - 149, The validateQuestion method currently uses question == null ||
question.trim().isEmpty(), which is inconsistent with validateAnswer's use of
StringUtils.hasText(answer); update validateQuestion to use
StringUtils.hasText(question) for the null/empty/whitespace check (throw the
same BadRequestException when it returns false) so both validation methods use
the same utility; locate the method validateQuestion in class Question and
replace the existing null/trim check with a hasText-based check while keeping
the MAX_QUESTION_LENGTH length validation unchanged.
src/test/java/com/daedan/festabook/organization/controller/OrganizationControllerTest.java (1)

92-114: ROLE_PLACE_ACCESS 거부 시나리오를 별도 고정 테스트로 두는 편이 안전합니다.

Line 93에서 성공 대상 Role만 검증하고 있어, 보안 설정이 완화되어도 회귀를 바로 잡기 어렵습니다.
옵션 A: ROLE_PLACE_ACCESS 단건 403 테스트를 추가(가독성 좋고 의도 명확).
옵션 B: Role/ExpectedStatus를 매핑한 파라미터 테스트로 통합(중복 감소, 다만 케이스 가독성은 다소 낮아짐).

As per coding guidelines **/*ControllerTest.java: Controller 테스트(통합 테스트)는 성공 케이스와 권한 테스트만 작성한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/organization/controller/OrganizationControllerTest.java`
around lines 92 - 114, The parameterized test method 성공 in
OrganizationControllerTest currently excludes ROLE_PLACE_ACCESS but doesn't
assert the denied-role behavior; add a separate fixed test (e.g.,
권한_없음_PLACE_ACCESS_403) that builds the same request headers using
createAuthorizationHeader(organization.getId(), RoleType.ROLE_PLACE_ACCESS) and
createOrganizationIdHeader(organization.getId()), performs GET "/organizations",
and asserts HttpStatus.FORBIDDEN (403) to ensure ROLE_PLACE_ACCESS denial is
explicitly verified; alternatively, if you prefer a single parameterized test,
convert 성공 to supply pairs of (RoleType, expectedStatus) and assert the response
status accordingly, but prefer the explicit single-case test for clarity.
src/test/java/com/daedan/festabook/organization/service/OrganizationServiceTest.java (1)

108-117: 실행 결과 변수명을 result로 통일해 주세요.

Line 108의 response는 팀 테스트 컨벤션과 달라 검색/리뷰 일관성이 떨어집니다. 기능 이슈는 아니므로 다음 정리 때 맞추면 충분합니다.

As per coding guidelines **/*Test.java: 테스트 실행 결과 값 변수명: result. 기대값 변수명: expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/organization/service/OrganizationServiceTest.java`
around lines 108 - 117, Rename the local test result variable from response to
result in the OrganizationServiceTest method that calls
organizationService.getOrganizationByOrganizationId (replace the
OrganizationResponse response declaration and every usage inside the
assertSoftly block with OrganizationResponse result), keeping the same value and
assertions; ensure the variable name follows the test convention (result for
actual, expected for expected) and update any imports or references within the
method accordingly.
src/main/java/com/daedan/festabook/place/service/PlaceAccessService.java (1)

69-73: 로그인 시 accessKey를 2번 조회해 일관성과 성능에서 손해가 있습니다.

Line 7173, 109117 구조는 exists + find 이중 조회라 DB 왕복이 늘고, 만료 경계 시점에는 두 조회 결과가 달라 예외 의미가 흔들릴 수 있습니다.
옵션 A(권장): 1회 조회 후 상태(삭제/만료/비밀번호)를 분기 처리해 예외를 일관화.
옵션 B: 현재 구조를 유지하되, 경계 시점 정책(어떤 예외를 우선할지)을 명시하고 테스트로 고정.

Also applies to: 109-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/place/service/PlaceAccessService.java`
around lines 69 - 73, The code performs two DB lookups for the same accessKey
(validatePlaceAccessIsNotExpiredOrDeleted(...) then
getActivePlaceAccessByAccessKey(...)), causing extra roundtrips and inconsistent
behavior at expiry boundaries; fix by consolidating to a single retrieval in
loginPlaceAccess (e.g., fetch an Optional/nullable PlaceAccess once via the
repository lookup used by getActivePlaceAccessByAccessKey), then inspect that
single PlaceAccess instance to decide whether to throw the
deleted/expired/password-mismatch exceptions (replace
validatePlaceAccessIsNotExpiredOrDeleted(...) and the second find), ensuring all
decision branches (deleted, expired, wrong password) are derived from that one
result so behavior and tests remain deterministic.
code-style.md (1)

1376-1381: 동일 규칙이 중복 정의되어 문서 드리프트 위험이 있습니다.

Line 13761381(12.22)와 Line 15821588(13.8)이 사실상 동일 규칙이고, 체크리스트도 Line 1771~1772와 Line 1787에서 같은 항목이 반복됩니다.
옵션 A(권장): 규칙 본문은 한 곳(예: 12.22)만 유지하고 다른 섹션은 참조 링크로 대체.
옵션 B: 중복 유지 시 “원본 섹션”을 명시해 동기화 기준을 고정.

📝 정리 예시
-### 13.8 테스트에서 도메인/값객체 생성 방식
-- 적용 범위: `**/*Test.java` (모든 테스트)
-- 테스트 본문에서는 도메인(Entity, VO)을 `new`로 직접 생성하지 않는다.
-- 도메인/값객체는 반드시 Fixture를 통해 생성한다.
-- 값객체(`Coordinate` 등)도 동일하게 `CoordinateFixture.create(...)` 형태로 사용한다.
+### 13.8 테스트에서 도메인/값객체 생성 방식
+- 본 규칙은 12.22를 단일 원본(Source of Truth)으로 따른다.
+- 부록 체크리스트도 12.22 기준 단일 항목으로 유지한다.

Also applies to: 1582-1588, 1771-1772, 1787-1787

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code-style.md` around lines 1376 - 1381, The rule "테스트 도메인/값객체 생성 방식" is
duplicated across sections 12.22 and 13.8 and repeated in checklist entries
around lines 1771 and 1787; consolidate by keeping a single canonical definition
(e.g., retain 12.22) and replace the duplicate section (13.8) with a
cross-reference to 12.22, and either remove or replace the repeated checklist
items with a reference to the canonical checklist entry; if you choose to keep
duplicates, explicitly mark one as "원본(참조 기준)" and add a "see: 12.22" note in
the other locations so the synchronization source is clear.
src/test/java/com/daedan/festabook/organizer/controller/OrganizerControllerTest.java (1)

117-117: @Nested 클래스명은 테스트 대상 메서드명과 맞추면 탐색성이 더 좋아집니다.

loginOrganization은 테스트 본문 의도(organizer 로그인)와 살짝 어긋나 보여, 실제 메서드명 기준으로 맞추면 추적이 쉬워집니다.

As per coding guidelines: 테스트하려는 메서드마다 @nested클래스를 작성하며@nested 클래스명은 테스트하려는 메서드명과 동일하게 작성한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/organizer/controller/OrganizerControllerTest.java`
at line 117, The `@Nested` class name loginOrganization does not match the tested
method name; rename the nested test class in OrganizerControllerTest from
loginOrganization to the actual method name under test (e.g., loginOrganizer or
the exact controller method name) so it follows the guideline that each `@Nested`
class name equals the method being tested and improves discoverability; update
any references/imports and test annotations accordingly.
src/test/java/com/daedan/festabook/place/concurrency/PlaceAnnouncementConcurrencyTest.java (1)

74-81: 동시성 테스트가 helper 기본 요청 수에 암묵 의존하고 있습니다.

지금 구조는 helper 기본 설정이 바뀌면 테스트 이름/의도(“3개 이상 동시 요청”)와 실제 검증이 쉽게 어긋날 수 있습니다.

  • 옵션 A: 요청 수를 명시적으로 설정하고(예: 5 이상) 결과를 검증
    • 장점: 테스트 의도가 고정됩니다.
    • 단점: helper 오버로드 사용이 필요할 수 있습니다.
  • 옵션 B: ConcurrencyTestResult를 받아 실제 requestCount를 함께 검증
    • 장점: helper 설정 변경에 덜 취약합니다.
    • 단점: assertion이 1~2개 늘어납니다.
🔍 제안 패치 (옵션 B)
+import com.daedan.festabook.support.ConcurrencyTestResult;
 ...
-            concurrencyTestHelper.execute(httpRequest);
+            ConcurrencyTestResult concurrencyResult = concurrencyTestHelper.execute(httpRequest);

             // then
-            Integer result = placeAnnouncementJpaRepository.countByPlaceId(place.getId());
-            assertThat(result).isEqualTo(expectedPlaceAnnouncementCount);
+            assertThat(concurrencyResult.getRequestCount()).isGreaterThanOrEqualTo(3);
+            Integer result = placeAnnouncementJpaRepository.countByPlaceId(place.getId());
+            assertThat(result).isEqualTo(expectedPlaceAnnouncementCount);

Based on learnings: 동시성 테스트는 concurrencyTestHelper를 활용한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/place/concurrency/PlaceAnnouncementConcurrencyTest.java`
around lines 74 - 81, The test currently relies on concurrencyTestHelper's
implicit default request count; update PlaceAnnouncementConcurrencyTest to
capture the ConcurrencyTestResult returned by
concurrencyTestHelper.execute(httpRequest) (use the overload that returns
ConcurrencyTestResult or change execute to return it), then assert the actual
requestCount from that result meets the test's intent (e.g., >= 3) before
asserting the DB state via
placeAnnouncementJpaRepository.countByPlaceId(place.getId()); reference
concurrencyTestHelper.execute, ConcurrencyTestResult.getRequestCount (or
equivalent), and placeAnnouncementJpaRepository.countByPlaceId to locate and
change the assertions.
src/test/java/com/daedan/festabook/staff/controller/StaffControllerTest.java (1)

515-545: 컨트롤러 통합 테스트 범위를 조금만 줄이면 유지보수가 더 좋아집니다.

현재 구간은 “비밀번호 실제 변경 여부”와 “비밀번호 불일치 비즈니스 예외”까지 컨트롤러 테스트가 직접 검증하고 있어, 계층 책임이 섞여 보입니다.

  • 옵션 A: 해당 검증을 StaffServiceTest로 이동
    • 장점: 테스트 실패 원인(라우팅/권한 vs 비즈니스)이 분리되어 디버깅이 빨라집니다.
    • 단점: HTTP 레벨에서의 상세 행위 검증은 줄어듭니다.
  • 옵션 B: 컨트롤러 테스트에는 성공/권한만 유지하고, 비밀번호 규칙은 서비스 단위 테스트로 집중
    • 장점: 팀 컨벤션과 가장 일관적입니다.
    • 단점: 테스트 파일이 분산됩니다.

As per coding guidelines: **/*ControllerTest.java: Controller 테스트(통합 테스트)는 성공 케이스와 권한 테스트만 작성한다. 비즈니스 로직 검증은 Service/Domain 단위 테스트에서 수행한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/staff/controller/StaffControllerTest.java`
around lines 515 - 545, Controller test currently asserts business logic
(password actually changed and password mismatch handling) inside
StaffControllerTest; move those business-logic assertions into unit tests on the
service layer (StaffServiceTest) and keep the controller integration test
focused only on routing/auth and status codes. Specifically: remove assertions
that inspect persistence or use passwordEncoder.matches from StaffControllerTest
(e.g., the block using staffJpaRepository.findById(...) and
passwordEncoder.matches(...) and the detailed “비밀번호_불일치” logic) and instead add
service-level tests that call StaffService#updatePassword (or the service method
used by the controller) to verify that the password is updated in the repository
and that wrong current passwords throw the expected exception; in the controller
tests (StaffControllerTest) only assert HTTP status codes for success and
forbidden/unauthorized and keep the request setup (StaffPasswordUpdateRequest
fixture) and authorization header scaffolding to validate routing/auth behavior.
src/main/java/com/daedan/festabook/staff/service/StaffService.java (1)

133-142: updateStaffAuthorities 검증 경계(TODO)를 이번 릴리즈에서 확정해두는 게 안전합니다.

현재는 null 체크 중심이라 “어디서 권한/소속 정합성을 최종 보장하는지”가 코드만으로 명확히 드러나지 않습니다.

  • 옵션 A: Service에서 staffId - organizationId - festivalIds 소속 일치 선검증
    • 장점: API 경계에서 실패 원인이 명확합니다.
    • 단점: 서비스 로직이 다소 비대해질 수 있습니다.
  • 옵션 B: StaffAuthority.createBy/deleteBy 도메인 메서드에서 불변식 강제 + 서비스 TODO 제거
    • 장점: 권한/불변식 책임이 도메인에 모여 누락 위험이 줄어듭니다.
    • 단점: 도메인 테스트 보강이 필요합니다.

As per coding guidelines: 권한 검증 누락 방지를 위해 CRUD 권한 검증을 도메인 내부로 강제한다. Service는 orchestration만 담당하고 권한 판단은 엔티티가 수행한다.

Also applies to: 277-286

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/staff/service/StaffService.java` around
lines 133 - 142, 현재 TODO는 권한·소속 정합성 검증 경계가 불명확하므로, 코딩 가이드에 따라 검증 책임을 도메인으로 이동해
확정하세요: 제거하려는 사전 검사(validateStaffAuthorityUpdateRequest)를
서비스(updateStaffAuthorities)에서 없애고 대신 StaffAuthority.createBy(...) 및
StaffAuthority.deleteBy(...) 내부에 staffId↔organizationId↔festivalIds 불변식(소속 일치,
null/존재 검사 등)을 강제로 구현하여 예외를 던지도록 하며 서비스는 해당 도메인 메서드를 호출해 예외를 핸들링(or rethrow)하는
오케스트레이션만 담당하게 만드세요; 동일한 방식으로 다른 케이스(참조된 277-286 범위)도 도메인 검증으로 이관해 TODO를 제거하세요.
src/test/java/com/daedan/festabook/place/service/PlaceAccessServiceTest.java (1)

243-244: 토큰 생성 인자 검증을 더 구체화하면 회귀 탐지가 강화됩니다.

Line 243-244에서 createPlaceAccessToken(any(), eq(placeAccess.getRoles()))로 첫 번째 인자를 느슨하게 잡아, 토큰 주체/클레임 연결이 잘못되어도 테스트가 통과할 수 있습니다.

  • 옵션 A: eq(expectedValue)로 엄격 검증
    • 장점: 회귀 탐지력이 가장 강함
    • 단점: 구현 변경 시 테스트 수정 빈도 증가
  • 옵션 B: ArgumentCaptor로 캡처 후 핵심 필드만 검증
    • 장점: 의도 검증 + 구현 변화 내성 균형
    • 단점: 코드가 약간 길어짐

Also applies to: 247-254

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/place/service/PlaceAccessServiceTest.java`
around lines 243 - 244, The mock for jwtProvider.createPlaceAccessToken in
PlaceAccessServiceTest is using any() for the first argument which weakens
regression detection; replace the loose matcher by either (A) using
eq(expectedSubjectOrClaims) to strictly verify the exact subject/claims passed
into createPlaceAccessToken (use the expected value derived from your test
fixture) or (B) use an ArgumentCaptor for the first parameter, capture the token
payload passed to jwtProvider.createPlaceAccessToken, and assert the critical
fields (e.g., subject, place id, roles) match expectations; apply the same
stricter verification to the other occurrences mentioned around lines 247-254.
src/test/java/com/daedan/festabook/global/lock/LockAspectTest.java (1)

145-165: @nested 클래스명을 테스트 대상 메서드명과 맞추는 편이 탐색성이 좋습니다.

현재 parameterMetadata는 범주형 이름이라, 어떤 메서드를 검증하는지 한 번 더 읽어야 합니다. 메서드명 기준으로 맞추면 유지보수 시 추적이 빨라집니다.

🔧 제안 diff
-    class parameterMetadata {
+    class createPlaceAccess {

As per coding guidelines "테스트하려는 메서드마다 @Nested 클래스를 작성하며 Nested 클래스명은 테스트하려는 메서드명과 동일하게 작성한다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/global/lock/LockAspectTest.java` around
lines 145 - 165, The `@Nested` class name parameterMetadata is too generic; rename
it to match the tested method name to improve discoverability — change the
nested class currently named parameterMetadata to createPlaceAccess (or
CreatePlaceAccess) so it clearly indicates it contains tests for
PlaceAccessService.createPlaceAccess; update the nested class declaration that
wraps the test method 성공_PlaceAccessService_메서드_파라미터명이_보존된다 to the new name and
adjust any references/imports accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 64a5963a-ef05-4ad4-a0dc-0d2dc7985f55

📥 Commits

Reviewing files that changed from the base of the PR and between 18be698 and 8202430.

📒 Files selected for processing (138)
  • code-style.md
  • src/main/java/com/daedan/festabook/announcement/domain/Announcement.java
  • src/main/java/com/daedan/festabook/announcement/service/AnnouncementService.java
  • src/main/java/com/daedan/festabook/event/domain/Event.java
  • src/main/java/com/daedan/festabook/event/domain/EventDate.java
  • src/main/java/com/daedan/festabook/event/service/EventDateService.java
  • src/main/java/com/daedan/festabook/festival/domain/Coordinate.java
  • src/main/java/com/daedan/festabook/festival/domain/Festival.java
  • src/main/java/com/daedan/festabook/festival/domain/FestivalImage.java
  • src/main/java/com/daedan/festabook/festival/domain/FestivalNotification.java
  • src/main/java/com/daedan/festabook/festival/dto/FestivalCoordinateResponse.java
  • src/main/java/com/daedan/festabook/festival/service/FestivalNotificationService.java
  • src/main/java/com/daedan/festabook/festival/service/TestFestivalNotificationService.java
  • src/main/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolver.java
  • src/main/java/com/daedan/festabook/global/argumentresolver/OrganizationIdArgumentResolver.java
  • src/main/java/com/daedan/festabook/global/lock/infrastructure/InMemoryLockStorage.java
  • src/main/java/com/daedan/festabook/global/security/authorization/AccountDetailsExtractor.java
  • src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java
  • src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java
  • src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetails.java
  • src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsExtractStrategy.java
  • src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java
  • src/main/java/com/daedan/festabook/global/security/authorization/StaffDetailsExtractStrategy.java
  • src/main/java/com/daedan/festabook/global/security/filter/JwtAuthenticationFilter.java
  • src/main/java/com/daedan/festabook/lineup/domain/Lineup.java
  • src/main/java/com/daedan/festabook/lineup/service/LineupService.java
  • src/main/java/com/daedan/festabook/lostitem/domain/LostItem.java
  • src/main/java/com/daedan/festabook/lostitem/service/LostItemService.java
  • src/main/java/com/daedan/festabook/notification/dto/NotificationSendRequest.java
  • src/main/java/com/daedan/festabook/organization/domain/Organization.java
  • src/main/java/com/daedan/festabook/organizer/controller/OrganizerController.java
  • src/main/java/com/daedan/festabook/organizer/domain/Organizer.java
  • src/main/java/com/daedan/festabook/organizer/service/OrganizerService.java
  • src/main/java/com/daedan/festabook/place/controller/PlaceAccessController.java
  • src/main/java/com/daedan/festabook/place/controller/PlaceImageController.java
  • src/main/java/com/daedan/festabook/place/domain/Place.java
  • src/main/java/com/daedan/festabook/place/domain/PlaceAccess.java
  • src/main/java/com/daedan/festabook/place/domain/PlaceAnnouncement.java
  • src/main/java/com/daedan/festabook/place/domain/PlaceImage.java
  • src/main/java/com/daedan/festabook/place/dto/PlaceCoordinateRequest.java
  • src/main/java/com/daedan/festabook/place/service/PlaceAccessService.java
  • src/main/java/com/daedan/festabook/place/service/PlaceAnnouncementService.java
  • src/main/java/com/daedan/festabook/place/service/PlaceGeographyService.java
  • src/main/java/com/daedan/festabook/place/service/PlaceImageService.java
  • src/main/java/com/daedan/festabook/place/service/PlaceService.java
  • src/main/java/com/daedan/festabook/question/domain/Question.java
  • src/main/java/com/daedan/festabook/question/service/QuestionService.java
  • src/main/java/com/daedan/festabook/staff/controller/StaffController.java
  • src/main/java/com/daedan/festabook/staff/domain/Staff.java
  • src/main/java/com/daedan/festabook/staff/domain/StaffAuthority.java
  • src/main/java/com/daedan/festabook/staff/service/StaffService.java
  • src/main/java/com/daedan/festabook/storage/infrastructure/StoreFile.java
  • src/main/java/com/daedan/festabook/storage/service/ImageStoreService.java
  • src/main/java/com/daedan/festabook/timetag/domain/PlaceTimeTag.java
  • src/main/java/com/daedan/festabook/timetag/domain/TimeTag.java
  • src/main/java/com/daedan/festabook/timetag/service/TimeTagService.java
  • src/test/java/com/daedan/festabook/announcement/controller/AnnouncementControllerTest.java
  • src/test/java/com/daedan/festabook/announcement/domain/AnnouncementTest.java
  • src/test/java/com/daedan/festabook/announcement/infrastructure/AnnouncementJpaRepositoryTest.java
  • src/test/java/com/daedan/festabook/announcement/service/AnnouncementServiceTest.java
  • src/test/java/com/daedan/festabook/event/concurrency/EventDateConcurrencyTest.java
  • src/test/java/com/daedan/festabook/event/controller/EventControllerTest.java
  • src/test/java/com/daedan/festabook/event/domain/EventDateTest.java
  • src/test/java/com/daedan/festabook/event/domain/EventTest.java
  • src/test/java/com/daedan/festabook/festival/controller/AndroidFestivalNotificationSubscriptionControllerTest.java
  • src/test/java/com/daedan/festabook/festival/controller/FestivalControllerTest.java
  • src/test/java/com/daedan/festabook/festival/controller/FestivalImageControllerTest.java
  • src/test/java/com/daedan/festabook/festival/controller/IosFestivalNotificationSubscriptionControllerTest.java
  • src/test/java/com/daedan/festabook/festival/controller/TestFestivalNotificationSubscriptionControllerTest.java
  • src/test/java/com/daedan/festabook/festival/domain/CoordinateTest.java
  • src/test/java/com/daedan/festabook/festival/domain/FestivalFixture.java
  • src/test/java/com/daedan/festabook/festival/domain/FestivalImageFixture.java
  • src/test/java/com/daedan/festabook/festival/domain/FestivalImageTest.java
  • src/test/java/com/daedan/festabook/festival/domain/FestivalNotificationTest.java
  • src/test/java/com/daedan/festabook/festival/domain/FestivalTest.java
  • src/test/java/com/daedan/festabook/festival/dto/FestivalCreateRequestFixture.java
  • src/test/java/com/daedan/festabook/festival/dto/FestivalGeographyPolygonHoleBoundaryUpdateRequestFixture.java
  • src/test/java/com/daedan/festabook/festival/service/FestivalNotificationServiceTest.java
  • src/test/java/com/daedan/festabook/festival/service/FestivalServiceTest.java
  • src/test/java/com/daedan/festabook/festival/service/TestFestivalNotificationServiceTest.java
  • src/test/java/com/daedan/festabook/global/argumentresolver/ActorArgumentResolverUnitTest.java
  • src/test/java/com/daedan/festabook/global/argumentresolver/FestivalIdArgumentResolverUnitTest.java
  • src/test/java/com/daedan/festabook/global/exception/InvalidFileExceptionTest.java
  • src/test/java/com/daedan/festabook/global/exception/LockExceptionTest.java
  • src/test/java/com/daedan/festabook/global/infrastructure/ShuffleManagerTest.java
  • src/test/java/com/daedan/festabook/global/lock/LockAspectTest.java
  • src/test/java/com/daedan/festabook/global/lock/infrastructure/InMemoryLockStorageTest.java
  • src/test/java/com/daedan/festabook/global/logging/RequestBodyCachingFilterTest.java
  • src/test/java/com/daedan/festabook/global/logging/dto/LogDtoTest.java
  • src/test/java/com/daedan/festabook/global/security/authorization/ActorFixture.java
  • src/test/java/com/daedan/festabook/global/security/authorization/AdminDetailsTest.java
  • src/test/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsTest.java
  • src/test/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsTest.java
  • src/test/java/com/daedan/festabook/global/security/authorization/StaffDetailsTest.java
  • src/test/java/com/daedan/festabook/global/security/authorization/TestActor.java
  • src/test/java/com/daedan/festabook/global/security/handler/CustomAccessDeniedHandlerTest.java
  • src/test/java/com/daedan/festabook/global/security/util/JwtProviderTest.java
  • src/test/java/com/daedan/festabook/lineup/concurrency/LineupConcurrencyTest.java
  • src/test/java/com/daedan/festabook/lineup/controller/LineupControllerTest.java
  • src/test/java/com/daedan/festabook/lineup/domain/LineupTest.java
  • src/test/java/com/daedan/festabook/lostitem/controller/LostItemControllerTest.java
  • src/test/java/com/daedan/festabook/lostitem/domain/LostItemTest.java
  • src/test/java/com/daedan/festabook/notification/dto/NotificationSendRequestTest.java
  • src/test/java/com/daedan/festabook/notification/infrastructure/FcmNotificationManagerTest.java
  • src/test/java/com/daedan/festabook/organization/controller/OrganizationControllerTest.java
  • src/test/java/com/daedan/festabook/organization/service/OrganizationServiceTest.java
  • src/test/java/com/daedan/festabook/organizer/concurrency/OrganizerConcurrencyTest.java
  • src/test/java/com/daedan/festabook/organizer/controller/OrganizerControllerTest.java
  • src/test/java/com/daedan/festabook/organizer/domain/OrganizerTest.java
  • src/test/java/com/daedan/festabook/organizer/service/OrganizerServiceTest.java
  • src/test/java/com/daedan/festabook/place/concurrency/PlaceAccessConcurrencyTest.java
  • src/test/java/com/daedan/festabook/place/concurrency/PlaceAnnouncementConcurrencyTest.java
  • src/test/java/com/daedan/festabook/place/controller/PlaceAccessControllerTest.java
  • src/test/java/com/daedan/festabook/place/controller/PlaceControllerTest.java
  • src/test/java/com/daedan/festabook/place/domain/PlaceAccessTest.java
  • src/test/java/com/daedan/festabook/place/domain/PlaceAnnouncementTest.java
  • src/test/java/com/daedan/festabook/place/domain/PlaceImageTest.java
  • src/test/java/com/daedan/festabook/place/domain/PlaceTest.java
  • src/test/java/com/daedan/festabook/place/infrastructure/PlaceAccessJpaRepositoryTest.java
  • src/test/java/com/daedan/festabook/place/infrastructure/PlaceImageJpaRepositoryTest.java
  • src/test/java/com/daedan/festabook/place/service/PlaceAccessServiceTest.java
  • src/test/java/com/daedan/festabook/place/service/PlaceImageServiceTest.java
  • src/test/java/com/daedan/festabook/place/service/PlaceServiceTest.java
  • src/test/java/com/daedan/festabook/question/controller/QuestionControllerTest.java
  • src/test/java/com/daedan/festabook/question/domain/QuestionTest.java
  • src/test/java/com/daedan/festabook/question/service/QuestionServiceTest.java
  • src/test/java/com/daedan/festabook/staff/concurrency/StaffConcurrencyTest.java
  • src/test/java/com/daedan/festabook/staff/controller/StaffControllerTest.java
  • src/test/java/com/daedan/festabook/staff/domain/StaffAuthorityTest.java
  • src/test/java/com/daedan/festabook/staff/domain/StaffTest.java
  • src/test/java/com/daedan/festabook/staff/service/StaffServiceTest.java
  • src/test/java/com/daedan/festabook/storage/controller/ImageStoreControllerTest.java
  • src/test/java/com/daedan/festabook/storage/infrastructure/MockStorageManagerTest.java
  • src/test/java/com/daedan/festabook/storage/infrastructure/StoreFileTest.java
  • src/test/java/com/daedan/festabook/storage/service/ImageStoreServiceTest.java
  • src/test/java/com/daedan/festabook/timetag/controller/TimeTagControllerTest.java
  • src/test/java/com/daedan/festabook/timetag/domain/PlaceTimeTagTest.java
  • src/test/java/com/daedan/festabook/timetag/domain/TimeTagTest.java
💤 Files with no reviewable changes (9)
  • src/main/java/com/daedan/festabook/place/service/PlaceImageService.java
  • src/test/java/com/daedan/festabook/timetag/controller/TimeTagControllerTest.java
  • src/test/java/com/daedan/festabook/question/controller/QuestionControllerTest.java
  • src/test/java/com/daedan/festabook/lineup/controller/LineupControllerTest.java
  • src/test/java/com/daedan/festabook/festival/controller/FestivalImageControllerTest.java
  • src/test/java/com/daedan/festabook/lostitem/controller/LostItemControllerTest.java
  • src/main/java/com/daedan/festabook/place/dto/PlaceCoordinateRequest.java
  • src/main/java/com/daedan/festabook/festival/dto/FestivalCoordinateResponse.java
  • src/test/java/com/daedan/festabook/announcement/controller/AnnouncementControllerTest.java

Comment on lines +125 to +129
private void validatePinnedLimit(Long festivalId) {
Long pinnedCount = announcementJpaRepository.countByFestivalIdAndIsPinnedTrue(festivalId);
if (pinnedCount >= MAX_PINNED_ANNOUNCEMENTS) {
throw new BadRequestException(String.format("공지글은 최대 %d개까지 고정할 수 있습니다.", MAX_PINNED_ANNOUNCEMENTS));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

고정 공지 제한 검증이 요청 festivalId 신뢰에 의존해 우회될 수 있습니다.

현재 제한 검증은 전달받은 festivalId 기준으로만 수행됩니다. announcementId가 속한 실제 축제와 요청 festivalId가 다를 때, 다른 축제의 카운트를 검사한 뒤 원래 공지를 고정할 수 있어 데이터 무결성이 깨질 수 있습니다.

  • 옵션 A (안전성 우선): 실제 공지의 축제 ID(announcement.getFestival().getId())로만 제한 검증하고, 요청 ID와 불일치하면 즉시 예외 처리
    • 장점: 우회 차단, 규칙 일관성 확보
    • 단점: 검증 코드 소폭 증가
  • 옵션 B (인터페이스 단순화): updateAnnouncementPin에서 festivalId 인자를 제거하고 announcementId로 축제를 유도
    • 장점: 호출 실수/오용 가능성 제거
    • 단점: API 시그니처 변경 영향 존재
🔧 제안 수정안 (옵션 A)
@@
     public AnnouncementPinUpdateResponse updateAnnouncementPin(
             Actor actor,
             Long festivalId,
             Long announcementId,
             AnnouncementPinUpdateRequest request
     ) {
         Announcement announcement = getAnnouncementById(announcementId);
+        Long targetFestivalId = announcement.getFestival().getId();
+        if (!targetFestivalId.equals(festivalId)) {
+            throw new BadRequestException("요청 축제 정보가 공지의 축제와 일치하지 않습니다.");
+        }
         if (announcement.isUnpinned() && request.pinned()) {
-            validatePinnedLimit(festivalId);
+            validatePinnedLimit(targetFestivalId);
         }
 
         announcement.updatePinnedBy(actor, request.pinned());
         return AnnouncementPinUpdateResponse.from(announcement);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/daedan/festabook/announcement/service/AnnouncementService.java`
around lines 125 - 129, The current validatePinnedLimit(Long festivalId) trusts
the passed festivalId and can be bypassed; change it to verify against the
announcement's actual festival id: in the flow that pins an announcement (e.g.,
updateAnnouncementPin), load the Announcement entity, get its festival id via
announcement.getFestival().getId(), compare it to the request festivalId and
throw BadRequestException on mismatch, then call
announcementJpaRepository.countByFestivalIdAndIsPinnedTrue(actualFestivalId) and
enforce MAX_PINNED_ANNOUNCEMENTS using that actualFestivalId (i.e., update
validatePinnedLimit to accept/validate the announcement's festival id or inline
the check in updateAnnouncementPin so countByFestivalIdAndIsPinnedTrue and
MAX_PINNED_ANNOUNCEMENTS operate on announcement.getFestival().getId()).

Comment on lines +29 to 30
if (subject == null || organizationId == null || organizerId == null) {
throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

# 1) OrganizerDetailsExtractStrategy 전체 구현 확인
cat -n src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

     1	package com.daedan.festabook.global.security.authorization;
     2	
     3	import com.daedan.festabook.global.exception.ForbiddenException;
     4	import com.daedan.festabook.global.security.role.RoleType;
     5	import com.daedan.festabook.global.security.util.JwtProvider;
     6	import io.jsonwebtoken.Claims;
     7	import java.util.Set;
     8	import lombok.RequiredArgsConstructor;
     9	import org.springframework.stereotype.Component;
    10	
    11	`@Component`
    12	`@RequiredArgsConstructor`
    13	public class OrganizerDetailsExtractStrategy implements AccountDetailsExtractStrategy {
    14	
    15	    private final JwtProvider jwtProvider;
    16	
    17	    `@Override`
    18	    public RoleType getSupportedRole() {
    19	        return RoleType.ROLE_ORGANIZER;
    20	    }
    21	
    22	    `@Override`
    23	    public AccountDetails extract(Claims claims, Set<RoleType> roleTypes) {
    24	        String subject = claims.getSubject();
    25	        Long organizationId = jwtProvider.extractOrganizationId(claims);
    26	        Long organizerId = jwtProvider.extractOrganizerId(claims);
    27	        Set<Long> festivalIds = jwtProvider.extractFestivalIds(claims);
    28	
    29	        if (subject == null || organizationId == null || organizerId == null) {
    30	            throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
    31	        }
    32	
    33	        return new OrganizerDetails(
    34	                subject,
    35	                organizationId,
    36	                organizerId,
    37	                festivalIds,
    38	                roleTypes
    39	        );
    40	    }
    41	}

Script executed:

# 2) 다른 추출 전략 구현체들 확인 (일관성 검증)
find . -name "*ExtractStrategy.java" -type f | head -20

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

./src/main/java/com/daedan/festabook/global/security/authorization/AccountDetailsExtractStrategy.java
./src/main/java/com/daedan/festabook/global/security/authorization/AdminDetailsExtractStrategy.java
./src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java
./src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsExtractStrategy.java
./src/main/java/com/daedan/festabook/global/security/authorization/StaffDetailsExtractStrategy.java

Script executed:

# 3) 전역 예외 핸들러 및 보안 엔트리포인트 확인
rg -A5 '@ExceptionHandler.*ForbiddenException|@RestControllerAdvice|@ControllerAdvice' --type=java -B2

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java-
src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java-@Slf4j
src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java:`@RestControllerAdvice`
src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java-public class GlobalExceptionHandler {
src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java-
src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java-    private static final String INTERNAL_ERROR_MESSAGE = "서버에 오류가 발생하였습니다. 관리자에게 문의해주세요.";
src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java-
src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java-    `@ExceptionHandler`(DatabaseException.class)

Script executed:

# 4) JWT claim 누락 관련 로깅 흔적 확인
rg -n 'subject|organizerId|organizationId' src/main/java/com/daedan/festabook/global/security --type=java -B2 -A2 | head -100

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-20-public class JwtProvider {
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-21-
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:22:    private static final String CLAIM_ORGANIZATION_ID = "organizationId";
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:23:    private static final String CLAIM_ORGANIZER_ID = "organizerId";
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-24-    private static final String CLAIM_STAFF_ID = "staffId";
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-25-    private static final String CLAIM_PLACE_ID = "placeId";
--
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-42-    public String createOrganizerToken(
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-43-            String username,
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:44:            Long organizationId,
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:45:            Long organizerId,
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-46-            Set<RoleType> roles,
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-47-            Set<Long> festivalIds
--
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-52-
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-53-        return Jwts.builder()
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:54:                .subject(username)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:55:                .claim(CLAIM_ORGANIZATION_ID, organizationId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-56-                .claim(CLAIM_ROLES_TYPE, roles)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-57-                .claim(CLAIM_FESTIVAL_IDS, normalizedFestivalIds)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:58:                .claim(CLAIM_ORGANIZER_ID, organizerId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-59-                .issuedAt(now)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-60-                .expiration(expiry)
--
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-65-    public String createStaffToken(
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-66-            String username,
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:67:            Long organizationId,
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-68-            Long staffId,
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-69-            Set<RoleType> roles,
--
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-75-
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-76-        return Jwts.builder()
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:77:                .subject(username)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:78:                .claim(CLAIM_ORGANIZATION_ID, organizationId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-79-                .claim(CLAIM_ROLES_TYPE, roles)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java-80-                .claim(CLAIM_FESTIVAL_IDS, normalizedFestivalIds)
--
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-22-    `@Override`
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-23-    public AccountDetails extract(Claims claims, Set<RoleType> roleTypes) {
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java:24:        String subject = claims.getSubject();
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java:25:        Long organizationId = jwtProvider.extractOrganizationId(claims);
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java:26:        Long organizerId = jwtProvider.extractOrganizerId(claims);
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-27-        Set<Long> festivalIds = jwtProvider.extractFestivalIds(claims);
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-28-
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java:29:        if (subject == null || organizationId == null || organizerId == null) {
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-30-            throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-31-        }
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-32-
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-33-        return new OrganizerDetails(
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java:34:                subject,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java:35:                organizationId,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java:36:                organizerId,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-37-                festivalIds,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java-38-                roleTypes
--
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-8-
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-9-    private final String username;
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java:10:    private final Long organizationId;
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-11-    private final Long staffId;
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-12-    private final Set<Long> festivalIds;
--
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-14-    public StaffDetails(
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-15-            String username,
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java:16:            Long organizationId,
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-17-            Long staffId,
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-18-            Set<Long> festivalIds,
--
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-21-        super(roles);
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-22-        this.username = username;
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java:23:        this.organizationId = organizationId;
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-24-        this.staffId = staffId;
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-25-        this.festivalIds = Objects.requireNonNullElse(festivalIds, Set.of());
--
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-32-
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-33-    `@Override`
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java:34:    public boolean hasOrganizationId(Long organizationId) {
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java:35:        return organizationId != null && Objects.equals(this.organizationId, organizationId);
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-36-    }
src/main/java/com/daedan/festabook/global/security/authorization/StaffDetails.java-37-
--
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-8-
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-9-    private final String username;
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java:10:    private final Long organizationId;
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java:11:    private final Long organizerId;
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-12-    private final Set<Long> festivalIds;
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-13-
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-14-    public OrganizerDetails(
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-15-            String username,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java:16:            Long organizationId,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java:17:            Long organizerId,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-18-            Set<Long> festivalIds,
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-19-            Set<RoleType> roles
--
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-21-        super(roles);
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-22-        this.username = username;
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java:23:        this.organizationId = organizationId;
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java:24:        this.organizerId = organizerId;
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-25-        this.festivalIds = Objects.requireNonNullElse(festivalIds, Set.of());
src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetails.java-26-    }
--

Script executed:

# 5) 토큰 생성 부분에서 claim 설정 확인
rg -n 'claim|subject|organizerId|organizationId' src/main/java/com/daedan/festabook --type=java | grep -E '(claim|withClaim|claim\(|setClaim)' | head -20

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:55:                .claim(CLAIM_ORGANIZATION_ID, organizationId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:56:                .claim(CLAIM_ROLES_TYPE, roles)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:57:                .claim(CLAIM_FESTIVAL_IDS, normalizedFestivalIds)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:58:                .claim(CLAIM_ORGANIZER_ID, organizerId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:78:                .claim(CLAIM_ORGANIZATION_ID, organizationId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:79:                .claim(CLAIM_ROLES_TYPE, roles)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:80:                .claim(CLAIM_FESTIVAL_IDS, normalizedFestivalIds)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:81:                .claim(CLAIM_STAFF_ID, staffId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:93:                .claim(CLAIM_PLACE_ID, placeId)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:94:                .claim(CLAIM_ROLES_TYPE, roles)
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:105:    public Set<RoleType> extractRoles(Claims claims) {
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:107:                        claims.get(CLAIM_ROLES_TYPE, List.class),
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:118:    public Long extractOrganizationId(Claims claims) {
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:119:        return claims.get(CLAIM_ORGANIZATION_ID, Long.class);
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:122:    public Set<Long> extractFestivalIds(Claims claims) {
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:123:        List<?> festivalIds = claims.get(CLAIM_FESTIVAL_IDS, List.class);
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:133:    public Long extractOrganizerId(Claims claims) {
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:134:        return claims.get(CLAIM_ORGANIZER_ID, Long.class);
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:137:    public Long extractStaffId(Claims claims) {
src/main/java/com/daedan/festabook/global/security/util/JwtProvider.java:138:        return claims.get(CLAIM_STAFF_ID, Long.class);

Script executed:

# 1) GlobalExceptionHandler 전체 확인
cat -n src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java | head -100

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

     1	package com.daedan.festabook.global.exception;
     2	
     3	import static net.logstash.logback.argument.StructuredArguments.kv;
     4	
     5	import com.daedan.festabook.global.logging.dto.ExceptionLog;
     6	import java.io.IOException;
     7	import java.io.PrintWriter;
     8	import java.io.StringWriter;
     9	import lombok.extern.slf4j.Slf4j;
    10	import org.springframework.dao.DataAccessException;
    11	import org.springframework.dao.DataAccessResourceFailureException;
    12	import org.springframework.dao.DataIntegrityViolationException;
    13	import org.springframework.data.jpa.repository.query.BadJpqlGrammarException;
    14	import org.springframework.http.HttpStatus;
    15	import org.springframework.http.ResponseEntity;
    16	import org.springframework.jdbc.BadSqlGrammarException;
    17	import org.springframework.security.access.AccessDeniedException;
    18	import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
    19	import org.springframework.web.bind.annotation.ExceptionHandler;
    20	import org.springframework.web.bind.annotation.RestControllerAdvice;
    21	
    22	`@Slf4j`
    23	`@RestControllerAdvice`
    24	public class GlobalExceptionHandler {
    25	
    26	    private static final String INTERNAL_ERROR_MESSAGE = "서버에 오류가 발생하였습니다. 관리자에게 문의해주세요.";
    27	
    28	    `@ExceptionHandler`(DatabaseException.class)
    29	    public ResponseEntity<ExceptionResponse> handleDatabaseException(DatabaseException databaseException) {
    30	        try {
    31	            return ResponseEntity
    32	                    .status(databaseException.getStatus())
    33	                    .body(databaseException.toResponse());
    34	        } finally {
    35	            ExceptionLog exceptionLog = ExceptionLog.from(
    36	                    databaseException.getStatus().value(),
    37	                    databaseException.getMessage(),
    38	                    databaseException.getClass().getSimpleName(),
    39	                    databaseException.getOriginalExceptionMessage()
    40	            );
    41	
    42	            logDatabaseExceptionAsErrorIf5xx(databaseException, exceptionLog);
    43	        }
    44	    }
    45	
    46	    `@ExceptionHandler`(BusinessException.class)
    47	    public ResponseEntity<ExceptionResponse> handleBusinessException(BusinessException businessException) {
    48	        try {
    49	            return ResponseEntity
    50	                    .status(businessException.getStatus())
    51	                    .body(businessException.toResponse());
    52	        } finally {
    53	            ExceptionLog exceptionLog = ExceptionLog.from(
    54	                    businessException.getStatus().value(),
    55	                    businessException.getMessage(),
    56	                    businessException.getClass().getSimpleName(),
    57	                    ""
    58	            );
    59	            log.info("", kv("event", exceptionLog));
    60	        }
    61	    }
    62	
    63	    `@ExceptionHandler`(AuthenticationCredentialsNotFoundException.class)
    64	    public ResponseEntity<ExceptionResponse> handleAuthenticationCredentialsNotFoundException(
    65	            AuthenticationCredentialsNotFoundException exception
    66	    ) {
    67	        try {
    68	            return ResponseEntity
    69	                    .status(HttpStatus.UNAUTHORIZED.value())
    70	                    .body(new ExceptionResponse("인증되지 않은 사용자입니다."));
    71	        } finally {
    72	            ExceptionLog exceptionLog = ExceptionLog.from(
    73	                    HttpStatus.UNAUTHORIZED.value(),
    74	                    exception.getMessage(),
    75	                    exception.getClass().getSimpleName(),
    76	                    ""
    77	            );
    78	            log.info("", kv("event", exceptionLog));
    79	        }
    80	    }
    81	
    82	    `@ExceptionHandler`(AccessDeniedException.class)
    83	    public ResponseEntity<ExceptionResponse> handleAccessDeniedException(AccessDeniedException exception) {
    84	        try {
    85	            return ResponseEntity
    86	                    .status(HttpStatus.FORBIDDEN.value())
    87	                    .body(new ExceptionResponse("접근 권한이 없습니다."));
    88	        } finally {
    89	            ExceptionLog exceptionLog = ExceptionLog.from(
    90	                    HttpStatus.FORBIDDEN.value(),
    91	                    exception.getMessage(),
    92	                    exception.getClass().getSimpleName(),
    93	                    ""
    94	            );
    95	            log.info("", kv("event", exceptionLog));
    96	        }
    97	    }
    98	
    99	    `@ExceptionHandler`({
   100	            DataIntegrityViolationException.class,

Script executed:

# 2) PlaceAccessDetailsExtractStrategy 확인 (PR 93 관련)
cat -n src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsExtractStrategy.java

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

     1	package com.daedan.festabook.global.security.authorization;
     2	
     3	import com.daedan.festabook.global.exception.ForbiddenException;
     4	import com.daedan.festabook.global.security.role.RoleType;
     5	import com.daedan.festabook.global.security.util.JwtProvider;
     6	import io.jsonwebtoken.Claims;
     7	import java.util.Set;
     8	import lombok.RequiredArgsConstructor;
     9	import org.springframework.stereotype.Component;
    10	
    11	`@Component`
    12	`@RequiredArgsConstructor`
    13	public class PlaceAccessDetailsExtractStrategy implements AccountDetailsExtractStrategy {
    14	
    15	    private final JwtProvider jwtProvider;
    16	
    17	    `@Override`
    18	    public RoleType getSupportedRole() {
    19	        return RoleType.ROLE_PLACE_ACCESS;
    20	    }
    21	
    22	    `@Override`
    23	    public AccountDetails extract(Claims claims, Set<RoleType> roleTypes) {
    24	        Long placeId = jwtProvider.extractPlaceId(claims);
    25	
    26	        if (placeId == null) {
    27	            throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
    28	        }
    29	
    30	        return new PlaceAccessDetails(placeId, roleTypes);
    31	    }
    32	}

Script executed:

# 3) StaffDetailsExtractStrategy 확인
cat -n src/main/java/com/daedan/festabook/global/security/authorization/StaffDetailsExtractStrategy.java

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

     1	package com.daedan.festabook.global.security.authorization;
     2	
     3	import com.daedan.festabook.global.exception.ForbiddenException;
     4	import com.daedan.festabook.global.security.role.RoleType;
     5	import com.daedan.festabook.global.security.util.JwtProvider;
     6	import io.jsonwebtoken.Claims;
     7	import java.util.Set;
     8	import lombok.RequiredArgsConstructor;
     9	import org.springframework.stereotype.Component;
    10	
    11	`@Component`
    12	`@RequiredArgsConstructor`
    13	public class StaffDetailsExtractStrategy implements AccountDetailsExtractStrategy {
    14	
    15	    private final JwtProvider jwtProvider;
    16	
    17	    `@Override`
    18	    public RoleType getSupportedRole() {
    19	        return RoleType.ROLE_STAFF;
    20	    }
    21	
    22	    `@Override`
    23	    public AccountDetails extract(Claims claims, Set<RoleType> roleTypes) {
    24	        String subject = claims.getSubject();
    25	        Long organizationId = jwtProvider.extractOrganizationId(claims);
    26	        Long staffId = jwtProvider.extractStaffId(claims);
    27	        Set<Long> festivalIds = jwtProvider.extractFestivalIds(claims);
    28	
    29	        if (subject == null || organizationId == null || staffId == null) {
    30	            throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
    31	        }
    32	
    33	        return new StaffDetails(
    34	                subject,
    35	                organizationId,
    36	                staffId,
    37	                festivalIds,
    38	                roleTypes
    39	        );
    40	    }
    41	}

Script executed:

# 4) AdminDetailsExtractStrategy 확인
cat -n src/main/java/com/daedan/festabook/global/security/authorization/AdminDetailsExtractStrategy.java

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

     1	package com.daedan.festabook.global.security.authorization;
     2	
     3	import com.daedan.festabook.global.security.role.RoleType;
     4	import io.jsonwebtoken.Claims;
     5	import java.util.Set;
     6	import org.springframework.stereotype.Component;
     7	
     8	`@Component`
     9	public class AdminDetailsExtractStrategy implements AccountDetailsExtractStrategy {
    10	
    11	    `@Override`
    12	    public RoleType getSupportedRole() {
    13	        return RoleType.ROLE_ADMIN;
    14	    }
    15	
    16	    `@Override`
    17	    public AccountDetails extract(Claims claims, Set<RoleType> roleTypes) {
    18	        return new AdminDetails(roleTypes);
    19	    }
    20	}

Script executed:

# 5) 로깅 및 모니터링 현황 확인
rg -n 'logger|log\.|Logger|monitoring|metric|alert' src/main/java/com/daedan/festabook/global/security --type=java | head -30

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

src/main/java/com/daedan/festabook/global/security/handler/CustomAuthenticationEntryPoint.java:47:            log.info("", kv("event", securityLog));
src/main/java/com/daedan/festabook/global/security/handler/CustomAccessDeniedHandler.java:47:            log.info("", kv("event", securityLog));
src/main/java/com/daedan/festabook/global/security/filter/JwtAuthenticationFilter.java:46:            log.warn("", kv("event", securityLog));

Script executed:

# ForbiddenException 정의 및 상속 확인
cat -n src/main/java/com/daedan/festabook/global/exception/ForbiddenException.java

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

     1	package com.daedan.festabook.global.exception;
     2	
     3	import lombok.Getter;
     4	import org.springframework.http.HttpStatus;
     5	
     6	`@Getter`
     7	public class ForbiddenException extends BusinessException {
     8	
     9	    private static final String message = "접근 권한이 없습니다.";
    10	
    11	    public ForbiddenException() {
    12	        super(message, HttpStatus.FORBIDDEN);
    13	    }
    14	
    15	    public ForbiddenException(String message) {
    16	        super(message, HttpStatus.FORBIDDEN);
    17	    }
    18	}

Script executed:

# BusinessException 정의 확인
cat -n src/main/java/com/daedan/festabook/global/exception/BusinessException.java

Repository: festabook/backend


Repository: festabook/backend
Exit code: 0

stdout:

     1	package com.daedan.festabook.global.exception;
     2	
     3	import lombok.Getter;
     4	import org.springframework.http.HttpStatus;
     5	
     6	`@Getter`
     7	public class BusinessException extends RuntimeException {
     8	
     9	    private final HttpStatus status;
    10	
    11	    public BusinessException(String message, HttpStatus status) {
    12	        super(message);
    13	        this.status = status;
    14	    }
    15	
    16	    public ExceptionResponse toResponse() {
    17	        return new ExceptionResponse(getMessage());
    18	    }
    19	}

필수 JWT claim 누락 시 운영 가시성이 부족합니다.

현재 방식은 외부적으로 접근 차단(403)은 되지만, subject/organizationId/organizerId 누락이 토큰 발급 버그인지 판단할 신호(ERROR 로그/모니터링)가 남지 않습니다. 토큰 생성 시 이 필드들이 필수이므로, null 값은 서버 쪽 문제의 조기 신호입니다.

옵션 A (권장): ForbiddenException은 유지하고 claim 누락 시 ERROR 로그 + 메트릭(알람) 추가

  • 장점: 기존 응답 정책(403)은 유지하면서 장애 탐지력 강화, 팀 컨벤션 일관성 유지
  • 단점: 예외 처리 코드가 조금 증가

옵션 B: 모든 extract strategy에 공통 검증 로직 추출 (중복 제거)

  • 장점: 유지보수성 향상, null 체크 로직 한 곳에서 관리
  • 단점: 추가 추상화 필요

현재 PlaceAccessDetailsExtractStrategy, StaffDetailsExtractStrategy에서도 동일한 패턴이므로, 한 곳에서 개선하면 나머지도 함께 해결할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/daedan/festabook/global/security/authorization/OrganizerDetailsExtractStrategy.java`
around lines 29 - 30, OrganizerDetailsExtractStrategy currently throws
ForbiddenException when subject/organizationId/organizerId are null but provides
no ERROR log or metric; update OrganizerDetailsExtractStrategy to log an ERROR
(including which claim(s) are missing and the token context) and emit a
monitoring metric/trace before throwing ForbiddenException to preserve the 403
response while surfacing server-side failures; additionally refactor the
repeated null-check logic used in PlaceAccessDetailsExtractStrategy and
StaffDetailsExtractStrategy into a shared validator/helper (e.g., a new
JwtClaimValidator utility) that returns details of missing claims so all three
strategies can log, emit metrics, and then throw ForbiddenException
consistently.

Comment on lines +26 to 27
if (placeId == null) {
throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# 1) ForbiddenException과 다른 인증 관련 예외 정의 확인
echo "=== 1. Exception classes and their status codes ==="
rg -A 3 "class.*Exception" src/main/java/com/daedan/festabook/global/exception --type java

# 2) PlaceAccessDetailsExtractStrategy 전체 파일 확인
echo "=== 2. PlaceAccessDetailsExtractStrategy full content ==="
cat -n src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsExtractStrategy.java

# 3) JWT claim 추출 관련 전략들 확인
echo "=== 3. Other JWT extraction strategies ==="
fd ".*DetailsExtractStrategy\|.*AccessAccountDetailsStrategy" src/main/java --type f

# 4) 예외 핸들러 확인 (401/403 매핑)
echo "=== 4. Exception handlers and their mappings ==="
rg -B 2 -A 5 "ExceptionHandler|ControllerAdvice|ResponseStatus" src/main/java/com/daedan/festabook/global --type java | head -100

Repository: festabook/backend

Length of output: 15630


🏁 Script executed:

#!/bin/bash

# 1) BusinessException과 ForbiddenException 전체 코드 확인
echo "=== 1. BusinessException definition ==="
cat -n src/main/java/com/daedan/festabook/global/exception/BusinessException.java

echo -e "\n=== 2. ForbiddenException definition ==="
cat -n src/main/java/com/daedan/festabook/global/exception/ForbiddenException.java

# 2) UnauthorizedException 전체 코드 확인
echo -e "\n=== 3. UnauthorizedException definition ==="
cat -n src/main/java/com/daedan/festabook/global/exception/UnauthorizedException.java

# 3) JWT 관련 다른 추출 전략 검색
echo -e "\n=== 4. All details extraction strategies ==="
find src/main/java -name "*DetailsExtractStrategy*" -o -name "*AccountDetailsStrategy*" | xargs cat -n

# 4) GlobalExceptionHandler의 BusinessException 핸들러 상세 확인
echo -e "\n=== 5. GlobalExceptionHandler BusinessException handler ==="
sed -n '/handleBusinessException/,/^    `@ExceptionHandler/p`' src/main/java/com/daedan/festabook/global/exception/GlobalExceptionHandler.java | head -20

Repository: festabook/backend

Length of output: 8351


JWT claim 누락은 403보다 401(인증 실패)로 분리해야 합니다. 이는 세 개의 추출 전략 모두에 적용됩니다.

현재 PlaceAccessDetailsExtractStrategy, OrganizerDetailsExtractStrategy, StaffDetailsExtractStrategy가 일관되게 JWT claim 누락을 ForbiddenException(403)으로 처리하고 있습니다. 하지만 서명 검증은 통과했지만 필수 claim이 누락된 경우는 서버의 토큰 생성 버그를 의미하므로, 인가 실패(403)와는 의미가 다릅니다.

  • 옵션 A(권장): 401로 매핑되는 UnauthorizedException(UnauthorizedType.???) 사용 + ERROR 로그/알람 추가

    • 장점: 인증 경계가 명확하고, 클라이언트가 재인증을 정확히 수행, 운영 탐지 향상
    • 단점: 3개 전략 모두 수정 필요, UnauthorizedType 새 타입 추가 가능
  • 옵션 B: 403 유지 + 내부 모니터링 알람만 추가

    • 장점: 변경 폭이 작음
    • 단점: 클라이언트 재인증 플로우 정확도 저하, 인증/인가 경계 모호
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/daedan/festabook/global/security/authorization/PlaceAccessDetailsExtractStrategy.java`
around lines 26 - 27, The three extract strategies currently throw
ForbiddenException when required JWT claims are missing; change this to throw an
UnauthorizedException (mapped to HTTP 401) to reflect authentication failure and
add an ERROR-level log/monitoring alarm when a claim is absent; update
PlaceAccessDetailsExtractStrategy (replace throw new ForbiddenException(...)
when placeId==null), OrganizerDetailsExtractStrategy, and
StaffDetailsExtractStrategy similarly, and if needed add a new UnauthorizedType
enum value to classify “missing_jwt_claim” so the UnauthorizedException can
carry that type for metrics/alerts.

Comment on lines +29 to 30
if (subject == null || organizationId == null || staffId == null) {
throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

JWT 필수 클레임 누락 시 ForbiddenException만 던지고 종료되어 원인 추적 신호가 없습니다.

Line 29~30은 인증 실패 처리 자체는 맞지만, 이 케이스는 사용자 오입력보다 토큰 발급/서버 결함 신호일 가능성이 커서 내부 ERROR 로그(및 알림 연계)가 필요합니다.

  • 옵션 A (변경 범위 최소): 이 전략 내부에서 누락 클레임명을 ERROR로 로깅 후 현재처럼 403 반환
    • 장점: 빠르게 반영 가능, 운영 가시성 즉시 확보
    • 단점: 다른 ExtractStrategy에도 유사 코드가 중복될 수 있음
  • 옵션 B (일관성 우선): JwtProvider 또는 공통 인증 계층에서 필수 클레임 검증/로깅을 중앙화하고 여기서는 결과만 사용
    • 장점: 정책 일관성/중복 제거
    • 단점: 리팩토링 범위가 커짐
예시 패치 (옵션 A)
+import lombok.extern.slf4j.Slf4j;
...
 `@Component`
 `@RequiredArgsConstructor`
+@Slf4j
 public class StaffDetailsExtractStrategy implements AccountDetailsExtractStrategy {
...
-        if (subject == null || organizationId == null || staffId == null) {
-            throw new ForbiddenException("토큰 필드에 문제가 있습니다.");
+        if (subject == null || organizationId == null || staffId == null) {
+            log.error(
+                    "Invalid staff JWT claims. subjectPresent={}, organizationIdPresent={}, staffIdPresent={}",
+                    subject != null,
+                    organizationId != null,
+                    staffId != null
+            );
+            throw new ForbiddenException("접근 권한이 없습니다.");
         }

Based on learnings: JWT 필수 클레임 누락은 인증 실패로 처리하되 내부적으로 ERROR 로그/모니터링을 남겨야 하며, 이 프로젝트의 인증 실패 외부 응답은 HttpStatus.FORBIDDEN + "접근 권한이 없습니다." 컨벤션을 따릅니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/daedan/festabook/global/security/authorization/StaffDetailsExtractStrategy.java`
around lines 29 - 30, The current null-check in StaffDetailsExtractStrategy that
throws new ForbiddenException("토큰 필드에 문제가 있습니다.") should also emit an ERROR log
with the missing claim names before throwing to aid ops; update the check in
StaffDetailsExtractStrategy (where subject, organizationId, staffId are
validated) to determine which claims are null, call the class logger at ERROR
with a clear message including the missing claim identifiers (e.g., "Missing JWT
claims: subject, organizationId"), then throw the existing ForbiddenException so
the external response remains HttpStatus.FORBIDDEN/"접근 권한이 없습니다."; keep the
change local (option A) to minimize scope.

Comment on lines +143 to 147
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
@PatchMapping("/{staffId}/password")
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "🟨 축제 관리자 권한 변경")
@Operation(summary = "🟪 축제 관리자 비밀번호 변경")
@ApiResponses(value = {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

권한 이모지와 실제 접근 Role 범위를 맞춰 주세요.

Line 143-147에서 @Operation(summary = "🟪 ...")는 STAFF/ORGANIZER/ADMIN 범위를 암시하는데, 실제 @PreAuthorize는 STAFF/ADMIN만 허용하고 있어 API 문서 해석이 혼동될 수 있습니다.

  • 옵션 A: 의도가 “STAFF 이상”이면 ORGANIZERhasAnyRole에 포함
    • 장점: 문서/정책 일치, 호출자 혼란 감소
    • 단점: 실제 권한 범위가 넓어짐
  • 옵션 B: 의도가 STAFF/ADMIN 한정이면 summary 권한 표기를 그에 맞게 조정
    • 장점: 정책 유지, 문서 정확도 개선
    • 단점: 팀 이모지 체계와의 추가 합의 필요

As per coding guidelines: "Operation summary 권한 접두사 이모지: 🟪 STAFF/ORGANIZER/ADMIN"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/daedan/festabook/staff/controller/StaffController.java`
around lines 143 - 147, The `@Operation` summary emoji in StaffController (method
with `@PatchMapping`("/{staffId}/password")) implies STAFF/ORGANIZER/ADMIN but the
`@PreAuthorize` on that handler currently allows only STAFF and ADMIN; fix by
making them consistent: either add ORGANIZER to the
`@PreAuthorize`(hasAnyRole(...)) expression to grant ORGANIZER the same access, or
change the `@Operation`(summary=...) emoji/text to reflect only STAFF/ADMIN;
update the annotation(s) on the method (the `@PreAuthorize` or the `@Operation`
summary) accordingly so documentation and enforcement match.

Comment on lines +210 to +227
@Test
void 예외_iOS_알림_전송_실패도_도메인_예외로_변환() throws FirebaseMessagingException {
// given
Long festivalId = 1L;
NotificationSendRequest request = NotificationSendRequest.builder()
.title("title")
.body("body")
.putData("announcementId", "1")
.build();
given(firebaseMessaging.send(any()))
.willReturn("android-ok")
.willThrow(FirebaseMessagingException.class);

// when & then
assertThatThrownBy(() -> fcmNotificationManager.sendToFestivalTopic(festivalId, request))
.isInstanceOf(InternalServerException.class)
.hasMessage("FCM 메시지 전송을 실패했습니다.");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

iOS 실패 테스트가 ‘iOS 토픽 실패’ 자체를 직접 증명하지 못합니다.

지금은 “두 번째 send 호출이 실패한다”를 검증하므로, 호출 순서가 바뀌어도 테스트가 통과할 수 있습니다.

  • 옵션 A(정확성 우선): Answer에서 Message.topic을 보고 iOS 토픽일 때만 예외 발생 → 테스트 의도와 1:1 매핑
  • 옵션 B(단순성 우선): 현재 순차 stubbing 유지 + 테스트명을 “두 번째 전송 실패”로 변경 → 구현 의존적이지만 코드 단순
옵션 A 예시 diff
-            given(firebaseMessaging.send(any()))
-                    .willReturn("android-ok")
-                    .willThrow(FirebaseMessagingException.class);
+            given(firebaseMessaging.send(any()))
+                    .willAnswer(invocation -> {
+                        Message message = invocation.getArgument(0);
+                        String topic = (String) ReflectionTestUtils.getField(message, "topic");
+                        if ((TOPIC_PREFIX + festivalId + "-ios").equals(topic)) {
+                            throw new FirebaseMessagingException("send-failed", "ios send failed");
+                        }
+                        return "ok";
+                    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/notification/infrastructure/FcmNotificationManagerTest.java`
around lines 210 - 227, The test currently stubs firebaseMessaging.send
sequentially which doesn't guarantee the exception is tied to the iOS topic;
change the stub to inspect the Message argument and throw only when the message
topic matches the iOS topic so the test proves iOS-specific failure: replace the
chained willReturn/willThrow on firebaseMessaging.send(...) with a willAnswer
that reads the invocation argument (Message msg), checks msg.getTopic() (the
topic name used for iOS in sendToFestivalTopic / how NotificationSendRequest is
mapped) and throws FirebaseMessagingException for that topic while returning a
success id for other topics; keep the assertion on
fcmNotificationManager.sendToFestivalTopic(...) and adjust the thrown exception
instance/message if needed.

Comment on lines +56 to +76
try (ExecutorService executorService = Executors.newFixedThreadPool(threadCount)) {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);

for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
startLatch.await();
staffService.createStaff(actor, organization.getId(), request);
successCount.incrementAndGet();
} catch (ConflictException e) {
conflictCount.incrementAndGet();
} catch (Exception ignore) {
} finally {
endLatch.countDown();
}
});
}

startLatch.countDown();
endLatch.await();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

동시성 테스트는 공용 헬퍼 사용으로 일관성을 맞추는 편이 안전합니다.

Line 56~76의 수동 스레드 제어는 동작은 가능하지만, 프로젝트 표준(concurrencyTestHelper 활용)과 달라 향후 유지보수 시 테스트 패턴이 분산됩니다.
옵션 A(권장): concurrencyTestHelper로 실행/집계 로직 통일.
옵션 B: 지금 구조를 유지하려면 헬퍼에 동일 패턴을 흡수해 중복 구현을 제거.

As per coding guidelines: 동시성 테스트 클래스는 XXXConcurrencyTest로 명명한다. AcceptanceTestSupport를 상속받으며 concurrencyTestHelper를 활용한다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/staff/concurrency/StaffConcurrencyTest.java`
around lines 56 - 76, The concurrency test in StaffConcurrencyTest uses manual
ExecutorService/CountDownLatch logic (executorService, startLatch, endLatch)
instead of the project-standard concurrencyTestHelper; refactor the test to call
the shared concurrencyTestHelper to run concurrent invocations of
staffService.createStaff(actor, organization.getId(), request) and aggregate
success/conflict counts, or move the current latch/submit pattern into
concurrencyTestHelper and invoke that helper from StaffConcurrencyTest so the
test class uses the standard helper and matches naming/Inheritance conventions
(ensure class still extends AcceptanceTestSupport).

Comment on lines +68 to +69
} catch (Exception ignore) {
} finally {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

예외/인터럽트 무시는 동시성 테스트 신뢰도를 크게 떨어뜨립니다.

Line 6869, 7778에서 예외를 무시하면 실패 원인이 사라져 “실패해야 할 상황이 통과”로 보일 수 있습니다.
옵션 A(권장): 예상 외 예외를 수집해 마지막에 assertThat(unexpectedErrors).isEmpty()로 실패 처리.
옵션 B: 최소한 인터럽트는 Thread.currentThread().interrupt()로 복구하고 AssertionError로 즉시 실패 처리.

🔧 최소 보강 예시 (인터럽트 처리)
-            } catch (InterruptedException ignore) {
-            }
+            } catch (InterruptedException exception) {
+                Thread.currentThread().interrupt();
+                throw new AssertionError("동시성 테스트가 인터럽트되었습니다.", exception);
+            }

Also applies to: 77-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/staff/concurrency/StaffConcurrencyTest.java`
around lines 68 - 69, In StaffConcurrencyTest replace empty exception catches
with logic that records unexpected exceptions and properly handles interrupts:
create a thread-safe List<Throwable> unexpectedErrors (or use
ConcurrentLinkedQueue) and in each catch(Exception e) add e to unexpectedErrors;
for InterruptedException specifically call Thread.currentThread().interrupt()
and add the exception, and for other exceptions add them and/or rethrow an
AssertionError to fail fast; after the concurrent work completes
assertThat(unexpectedErrors).isEmpty() to surface failures instead of silently
swallowing them.

Comment on lines +87 to 108
@Test
void 예외_존재하지_않는_조직() {
// given
Actor adminActor = ActorFixture.admin();

Long organizationId = 10L;
String username = "staff";
String password = "1234";

given(organizationJpaRepository.findById(organizationId))
.willReturn(Optional.empty());

StaffRequest request = StaffRequestFixture.create(username, password);

// when & then
assertThatThrownBy(() -> staffService.createStaff(adminActor, organizationId, request))
.isInstanceOf(NotFoundException.class)
.hasMessage("존재하지 않는 Organization 입니다.");
}

@Test
void 성공() {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

createStaff Nested의 첫 테스트를 성공()으로 맞추는 편이 좋겠습니다.

Line 87에서 예외 케이스가 먼저 와서, 팀 공통 읽기 순서(정상 흐름 → 예외 흐름)와 어긋납니다. 기능 영향은 없지만 리뷰/탐색 비용이 커집니다.

  • 옵션 A: 현재 테스트 순서만 재배치
    • 장점: 변경량 최소, 컨벤션 즉시 정렬
    • 단점: 구조적 의미 개선은 제한적
  • 옵션 B: 성공/입력검증을 하위 Nested로 분리
    • 장점: 의도 구분이 더 선명
    • 단점: 클래스 깊이가 1단계 늘어남

As per coding guidelines: "도메인 권한/입력 검증 테스트: createBy, updateXxxBy, deleteBy Nested 클래스의 첫 번째 테스트는 반드시 '성공()'으로 작성."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/daedan/festabook/staff/service/StaffServiceTest.java`
around lines 87 - 108, Reorder the tests inside the createStaff Nested in
StaffServiceTest so the happy-path test method 성공() is the first test: locate
the nested block containing the tests 예외_존재하지_않는_조직() and 성공() and move the 성공()
test before 예외_존재하지_않는_조직(); alternatively, if you prefer clearer separation,
split happy-path and validation/error cases into separate inner Nested classes
(e.g., Success and Validation/Exception) so createStaff's first test is always
성공() per the guideline.

Comment on lines +59 to +64
StorageUploadResponse response = mockStorageManager.uploadFile(request);

// then
assertSoftly(s -> {
s.assertThat(response.accessRelativePath()).isEqualTo(storagePath);
s.assertThat(response.storagePath()).isEqualTo(storagePath);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

테스트 결과/식별자 네이밍을 가이드에 맞추면 일관성이 더 좋아집니다.

Line 59의 response는 테스트 결과 변수 규칙(result)과 다르고, Line 62의 s는 축약 식별자라 팀 규칙과 어긋납니다.
선택지는 아래처럼 보시면 됩니다.

  • 옵션 A: responseresult로 변경 (변경 범위 최소, 빠른 정합성 확보)
  • 옵션 B: response + 람다 파라미터(s)까지 함께 정리 (가독성/팀 컨벤션 일관성 극대화)
제안 diff (옵션 B)
-            StorageUploadResponse response = mockStorageManager.uploadFile(request);
+            StorageUploadResponse result = mockStorageManager.uploadFile(request);

             // then
-            assertSoftly(s -> {
-                s.assertThat(response.accessRelativePath()).isEqualTo(storagePath);
-                s.assertThat(response.storagePath()).isEqualTo(storagePath);
+            assertSoftly(assertions -> {
+                assertions.assertThat(result.accessRelativePath()).isEqualTo(storagePath);
+                assertions.assertThat(result.storagePath()).isEqualTo(storagePath);
             });

As per coding guidelines, "테스트 실행 결과 값 변수명: result." 및 "**/*.{java,ts,tsx,js}: 모든 식별자에서 줄임말을 사용하지 않는다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/test/java/com/daedan/festabook/storage/infrastructure/MockStorageManagerTest.java`
around lines 59 - 64, Rename the test result variable response to result and
replace the abbreviated lambda parameter s with a descriptive name (e.g.,
softAssertions or assertions) in the MockStorageManagerTest.uploadFile assertion
block so it follows the test naming rule and avoids abbreviations; specifically
update the variable returned by mockStorageManager.uploadFile(request)
(currently StorageUploadResponse response) to StorageUploadResponse result and
change the assertSoftly lambda parameter (currently s) to a full identifier,
then update all uses (result.accessRelativePath(), result.storagePath(), and the
lambda body) accordingly.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이번 릴리스를 위한 대규모 병합 PR이네요. 코드 스타일 가이드에 맞춰 전반적인 코드를 개선하고, 동시성 제어를 위한 분산락을 도입하는 등 시스템 안정성을 크게 향상시키는 중요한 변경사항들이 많이 포함되어 있습니다. 특히, 새로운 코드 컨벤션을 code-style.md에 명확히 정의하고 이를 코드베이스 전반에 일관되게 적용하려는 노력이 돋보입니다. 전체적으로 훌륭한 작업이라고 생각합니다. 한 가지 잠재적인 NullPointerException 발생 가능성에 대한 리뷰 의견을 남겼으니 확인 부탁드립니다.

Note: Security Review did not run due to the size of the PR.

@soeun2537 soeun2537 merged commit 255eb71 into prod Mar 6, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants