Skip to content

[TASK] dev 브랜치를 main 브랜치로 병합#64

Merged
HyeonBin2379 merged 48 commits into
mainfrom
dev
May 20, 2026
Merged

[TASK] dev 브랜치를 main 브랜치로 병합#64
HyeonBin2379 merged 48 commits into
mainfrom
dev

Conversation

@HyeonBin2379

@HyeonBin2379 HyeonBin2379 commented May 20, 2026

Copy link
Copy Markdown
Contributor

작업 배경

  • dev 브랜치의 작업 내역을 main 브랜치에 반영

작업 내용

  • dev 브랜치의 현재까지의 작업 내용을 main 브랜치로 병합

테스트 여부

  • 이미 테스트 통과되어 dev 브랜치에 병합된 작업이므로 별도 테스트 x

기타

  • main 브랜치로 병합할 시 user-service를 재배포하는 워크플로우가 진행됩니다.

이슈

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 사용자 회원가입, 로그인, 로그아웃 기능 추가
    • JWT 토큰 기반 인증 및 토큰 재발급 기능
    • 사용자 프로필 조회 및 수정 기능
    • 사용자별 채팅 가능 시간대 설정 및 조회
    • GitHub Actions 기반 자동 빌드 및 배포 파이프라인
  • Chores

    • 환경 변수 및 설정 파일 확장 (Kafka, Zipkin, Loki, Eureka 통합)
    • Docker 컨테이너 배포 구성 최적화
    • 테스트 환경 및 시드 데이터 추가

loveletheart and others added 30 commits April 23, 2026 16:05
* feat: 회원권한 vo 추가

- 회원권한은 USER, MANAGER, MASTER로 구분
- X-User-Role의 문자열에 해당하는 회원권한 enum을 찾아 반환하는 도메인 로직 추가

* feat: 채팅가능시간대 vo 추가

- 1개의 채팅가능 시간대는 요일, 시작시간, 종료시간 필드로 구성
- VO 생성에 필요한 정적 팩토리 메서드 추가

* feat: 회원 엔티티 추가

- 회원 도메인의 애그리거트 루트 역할을 하는 엔티티
- 회원의 username, name, nickname에 관한 복합 unique 제약조건 추가
- 회원 엔티티 생성에 필요한 정적 팩토리 메서드 추가
- 회원의 채팅가능시간대를 저장하는 도메인 로직은 인증 기능 구현 완료 흐 추가할 예정입니다.
* feat : 토큰 저장 및 만료 부분 구현
- 토큰 생성 및 유효성 검사 작성
- RefreshToken 구현은 해놓았으나 추후에 사용 예정

* feat : 코드 래빗 내용 참조 및 내용 수정
- string userid -> uuid로 변경
- UserRole를 참조해서 role를 가져오도록 설정
- TokenPair에서 검증하도록 로직 추가
- 토큰 저장,조회시 hash값을 사용하도록 수정

* fix : userId UUID로 통합 및 누락 부분 추가 수정

* fix : // 만료된 토큰에서도 클레임을 추출 부분 에서 토큰이 어떤 부분을 의미하는지 부정확함으로 수정

* fix : 토큰 사용자 식별값 string -> uuid로 변경
- domain 패키지에 UserRepository 추가 및 userId, username 기반 단일 회원 조회 메서드 추가
- infrastructure 패키지에 JpaUserRepository 추가
- application 패키지 내 UserDetailsInfo 추가
- presentation 패키지 내 GetUserDetailsResponse 추가
- 로그인용 회원정보 조회 요청에 관한 응답 시, User -> UserDetailsInfo -> GetUserDetailsResponse 로 진행되는 흐름을 구현
- 회원 엔티티의 복합 unique 제약 조건을 단일 unique 제약 조건으로 수정하여 username의 중복 방지
- isEnabled() 메서드 추가
- application 패키지 내 UserService 클래스 추가 및 로그인용 회원 조회 기능 구현
- presentation 패키지 내 UserInternalController 추가
- username 기반 회원 조회를 수행하기 위한 /internal/v1/users/{username} 엔드포인트 추가
- UserDetailInfo DTO 이름 오탈자 수정
- GetUserDetailsResponse에서 LoginUserResponse로 DTO명 변경
- UserDetailInfo 레코드 필드로 chatTimeRange 리스트 추가 및 관련 정적 팩토리 메서드 추가
- application 패키지에 ChatTimeRangeInfo DTO 클래스 추가
- presentation 패키지에 UserDetailResponse DTO 클래스 추가
- UserService 내 userId 기반 회원 단건 조회 기능 구현
- UserController 클래스 및 /api/v1/users/{userId} 엔드포인트 추가
- 누락된 @transactional(readOnly=true) 어노테이션 추가
- UserDetailInfo에서 내부 인증 호출 전용 LoginUserDetailInfo 분리
- UserDetailInfo의 메서드 getChatTimeTangeInfos()에 NPE 방지 로직 추가
- user/domain/service 패키지 내부에 RoleCheck를추가하여 현재 요청자가 메서드 파라미터에서 지정한 권한을 보유하는지를 확인하는 기능을 정의
- SecurityRoleCheck에서 hasRole 메서드의 내부 로직은 공통 모듈 배포 이후 SecurityUtil을 사용하여 구현할 예정
- /api/v1/users/{userId}는 관리자 전용 회원 조회 기능이므로 UserService의 기존 getUser를 getUserForAdmin 메서드와 getUser 메서드로 분리
- getUserForAdmin 내부에 RoleCheck를 활용한 관리자 권한 확인 기능 추가
- 잠재적인 경로 체크 문제 방지 및 API 가독성 개선 측면에서 UserInternalController의 `/internal/v1/users/{username}`을 `/internal/v1/users?username={username}`으로 수정
[Feat] 회원 단건 조회 기능 구현
* feat : 로그인 기능 구현
- 임시로 usersevies에서 username으로 검색하도록 구현
- LoginFilter 간소화 -> 추후 게이트 웨이에서 검증 부분 추가 필요
- 로그인시 암호 해독으로 config에 PasswordEncoder부분 추가

* Apply suggestions from code review

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* chore: dto 저장 경로 변경

- user/application/dto의 하위 패키지에 info, command 패키지를 추가함에 따라, 기존의 ~Info로 끝나는 DTO의 저장 경로를 user/application/dto/info로 변경
- user/presentation/dto의 하위 패키지로 request, response 패키지를 추가한 후 기존 응답 DTO의 저장 경로를 response 패키지 내부로 이전
- import문의 회원 도메인 관련 dto 패키지 경로 수정

* fix: UserAuthenticator 컴파일 에러 수정

- 메서드 중간에 import문이 삽입되어 컴파일 에러를 발생시켰던 부분을 정리

* feat: 회원등록 메서드 추가

- UserRepository save() 메서드 추가
- UserService createUser() 메서드 추가

* chore: domain 계층 패키지명 변경

- 인증, 회원 도메인의 domain/entity 패키지를 domain/model로 변경하고 import문에도 변경 사항을 반영

* fix: jwtProperties 관련 컴파일 에러 수정

- getSecret() 메서드 대신 getSecretKey()를 호출하면서 발생한 컴파일 에러 수정
* feat : 로그아웃 구현
- userId로 로그아웃이 되도록 작성함

* fix : UserAuthenticator.java파일에 이상하게 작성되어 있던 부분 수정
* feat: 신규 회원 생성용 DTO 추가

- 회원정보 생성, 회원의 채팅가능 시간대 추가용 DTO를 각각 CreateUserCommand, CreateChatTimeCommand로 구현

* feat: 신규 회원 생성 서비스 로직 구현

- User 엔티티 내부에 채팅가능 시간대를 저장하는 메서드 추가
- UserService의 createUser() 메서드 구현

* feat: 회원 등록 시 검증 로직 추가

- UserRepository에 중복된 username을 사용하는 회원을 조회하는 메서드를 추가하고 이를 UserService에 사용하여 회원가입 시 중복된 username 사용을 방지
- 현재 요청자가 총관리자 권한을 보유할 때만 관리자, 총관리자 등록 가능하도록 제한
- 최초로 총관리자를 등록하는 문제를 해결하기 위해, 회원 도메인 서버 구동 시 src/main/resources/data.sql의 insert문을 통해 총관리자를 등록 가능하도록 설정, WHERE NOT EXISTS 조건을 통해 중복 등록 방지

* fix: 코드래빗 수정사항 반영

- data.sql의 비밀번호 원문 관련 주석 삭제
- username 중복 여부 검사 시점과 회원 엔티티 저장 시점 간 차이로 인해 다른 사용자가 동일한 username으로 저장할 시 발생하는 데이터 무결성 문제에 관한 예외처리 코드 추가

* feat: 인증 도메인 회원가입 관련 DTO 추가

- auth/application/dto/command 패키지에 회원 도메인으로 사용자의 입력값들을 전송하기 위한 SignupUserCommand DTO 추가
- auth/presentation/dto/request 패키지에 회원가입 요청 데이터를 저장하기 위한 UserSignupRequest 추가

* feat: 회원가입 메서드 추가

- AuthService 클래스에 signup() 메서드를 추가
- signup() 메서드에서는 인증 도메인에서 수신한 데이터를 회원 도메인의 회원 등록 기능에서 사용할 DTO로 변환 후 회원 등록 진행

* feat: 회원가입 엔드포인트 추가

- AuthController에 POST /api/v1/auth/signup api를 추가

* feat: 인증 도메인 회원가입 응답 DTO 추가

- auth/presentation/dto/response에 UserSignupResponse를 추가하여 회원가입 요청에 관한 응답을 제공할 때 활용
- auth/application/dto/info 패키지에 SignupInfo를 추가하여 회원가입 완료된 사용자의 정보를 presentation 계층으로 전송할 때 사용

* feat: 회원가입 기능 관련 응답 DTO 적용

- 회원가입을 수행하는 signup() 메서드의 반환타입으로는 새로 추가한 회원가입 응답 DTO를 사용
- AuthService의 회원가입 메서드의 반환타입으로 SignupInfo를 지정
- AuthController의 회원가입용 엔드포인트의 반환타입으로 UserSignupResponse를 사용

* feat: 로그인/로그아웃 @transactional 추가

- AuthService의 login(), logout() 메서드에 @transactional 어노테이션 적용

* feat: 회원의 채팅 가능 시간대 검증 기능 추가

- dayOfWeek, startTime, endTime이 모두 null이 아니면서, startTime이 endTime보다 항상 앞서야 한다는 검증 규칙 추가

* refactor: CreateChatTimeCommand 정적 팩토리 메서드명 변경

- CreateChatTimeCommand의 메서드명을 to()에서 toChatTime()으로 변경하여 가독성 개선

* fix: 권한 문자열 매핑 로직 수정

- 권한의 한글 설명, enum 상수명, ROLE_ 접두사를 사용한 권한명에 대해 해당하는 UserRole enum을 매핑하도록 개선

* fix: 회원가입 요청 DTO 유효성 검증 로직 보강

- 정규표현식 기반 username, password, userRole, name, nickname 검증 추가
- name, nickname는 최대 20글자까지만 허용
- 채팅가능 시간이 null이 아닌 것만 Command로 변환하도록 null 체크 로직 보강
* chore: 공통모듈 적용에 필요한 build.gradle 설정 추가

- repositories에 사용할 공통 모듈 경로 추가
- github package를 통해 공통모듈을 제공받기 위해 필요한 github access token 관련 설정도 추가
- 공통 모듈 의존성('org:pgsg:common') 추가

* comment: 인증 도메인 TODO 주석 추가

- 인증 도메인에서 공통모듈 적용과 관련해서 추가 작업이 필요한 부분에 TODO 주석 작성

* feat: 회원 권한 검사 기능 구현

- SecurityRoleCheck에서 미구현된 hasRole() 메서드의 세부 기능 추가

* feat: 회원 엔티티 Auditing 필드 추가

- 회원 엔티티가 공통 모듈의 BaseEntity를 상속받도록 수정
- 회원 엔티티의 isEnabled() 조건 수정
- 회원 엔티티에 @SQLRestriction("deleted_at IS NULL") 어노테이션 추가

* chore: user-service 실행 환경 세팅

- src/main/resources/application.yml 파일 설정 세팅 및 관련 주석 추가
- src/test/resources/application.yml 파일 설정 세팅 및 관련 주석 추가
- .env.example 파일 내용 갱신
- build.gradle의 spring cloud 버전을 spring boot 3.5.13 버전에 호환되도록 수정
- 테스트용 h2 DB 의존성 추가

* chore: 통합 테스트 전용 설정 분리

- UserServiceApplicationTests에 src/test/resources/application.yml 파일의 설정을 적용하도록 설정 변경

* feat: 회원 조회 DTO Auditing 필드 추가

- 회원조회 DTO에 BaseEntity의 Auditing 필드를 추가하여 회원 조회 시 생성일자, 생성자, 수정일자, 수정자, 삭제일자, 삭제자 정보를 제공

* remove: global 패키지 삭제

- 공통모듈에서 제공하는 security 기능을 사용하게 되면서 더 이상 사용하지 않는 global 패키지를 삭제

* refactor: Config 클래스 수정

- auth/infrastructure/config 패키지의 SecurityConfig 클래스 이름이 공통모듈에서의 Config 클래스와 중복되므로, auth/infrastructure/config 패키지에 있던 SecurityConfig를 UserAuthConfig로 변경
- UserAuthConfig에 AuthenticationManager 빈 설정 추가

* refactor: 공통 모듈의 UserDetailsImpl 적용

- UserDetailsServiceImpl에서 사용하던 UserDetailsImpl을 공통모듈에서 제공하는 UserDetailsImpl로 교체
- auth/infrastructure 패키지에 있던 기존 UserDetailsImpl은 삭제

* refactor: UserAuthenticator에 공통 모듈 코드 적용

- UserAuthenticator를 인터페이스로 분리하고, 그 구현체인 UserAuthenticatorImpl 클래스는 auth/infrastructure 패키지로 이전
- UserAuthenticatorImpl의 verify() 메서드 호출 시, 내부적으로 AuthenticationManager.authenticate() 메서드를 사용하도록 코드 수정

* refactor: JWT 발급 기능에 공통모듈의 UserDetailsImpl 적용

- accessToken 발급 기능과 refreshToken 발급 기능 분리
- accessToken 발급 시 공통모듈의 UserDetailsImpl을 사용하도록 수정
- refreshToken 발급 시 회원의 UUID, 권한만 사용하도록 수정

* feat: 회원 도메인 내 userId 기반 인증 도메인용 회원조회 기능 추가

- 인증 도메인의 재발급 로직에서 활용
- LoginUserDetailInfo에 UserDetailImpl로의 전환에 활용할 toUserDetails() 메서드 추가
- 인증 처리용 회원조회 메서드와 일반 회원조회용 메서드 구분을 위해 인증 도메인에서 사용할 조회 메서드의 이름은 getUserForAuth()로 통일

* refactor: 회원 도메인 전용 커스텀 예외 클래스 추가

- UserServiceException 추가
- 회원도메인 에러메시지 관리용 application-error.yml 파일 추가
- 기존 예외 클래스를 회원 도메인 전용 커스텀 예외 클래스로 대체

* comment: TODO 주석 삭제

- 회원 도메인 전용 커스텀 예외 적용 관련 TODO 주석 삭제

* refactor: accessToken의 Bearer 접두사 추가 시점 변경

- 로그인 요청에 관한 응답 시점이 아닌 accessToken생성 시점으로 Bearer 접두사 추가 시점을 변경

* refactor: accessToken의 Bearer 접두사 추가 시점 변경

- 회원/인증 도메인 전용 security 설정에 관한 UserAuthConfig 추가
- UserAuthConfig 설정과 공통 모듈의 SecurityConfig 설정을 모두 적용

* chore: 배포 관련 설정 수정

- Dockerfile 설정에 공통모듈 사용에 필요한 github package 인증 관련 환경변수 추가
- 유레카 서버 연동 관련 bridge network 설정 추가
- UserServiceApplication 내 EnableEurekaClient 어노테이션 추가
- application.yml 파일 환경변수 설정 추가

* fix: 동기식 통신용 internal api 인가 설정 추가

- UserAuthConfig 클래스 내 filterChain() 메서드에 /internal/v1/users/** api 호출은 모두 허용하는 설정 추가

* comment: env.example 파일 설명 수정

- env.example 상단에 안내문 추가
- JWT 시크릿 관련 예시 값 수정

* chore: docker 설정 수정

- 빌드 시점에만 github package 인증 정보를 사용하도록 dockerfile 수정

* fix: 코드래빗 수정사항 반영

- DataIntegrityViolationException 발생 시 유니크 제약조건을 위반했을 때만 409 예외를 발생시키도록 유도
- UserAuthConfig /api/v1/auth/logout의 경우 인증된 사용자일 때만 로그아웃 가능하도록 수정
- src/test/resources/application.yml 파일 설정 수정

* fix: UserRole 관련 예외 처리 방식 수정

- application-error.yml 내 UserRoleNotFoundException 객체 추가
- UserRole에서 회원권한에 해당하는 enum을 찾지 못할 때 회원 도메인 전용 커스텀 예외를 던지도록 수정

* fix: 코드래빗 수정사항 반영

- docker-compose.yml 파일의 포트 매핑이 SERVER_PORT 환경 변수와 동기화되도록 수정
- AuthService 내 하드코딩된 refreshtoken TTL 수정

* fix: docker-compose.yml 수정

- docker-compose.yml 네트워크 설정 조정
* chore: user-service 실행 환경 설정 수정

- docker-compose.yml 네트워크 설정 및 서버 포트번호 매핑 설정 조정
- 프로젝트 폴더 내 .env 파일을 불러온 후 config-server의 설정을 불러오도록 수정
- README.md 파일 내 docker 파일 실행 방법 관련 설명 추가

* docs: README.md 파일 내용 보충

- 환경변수 설정 관련 내용(env.example 파일) 업데이트
-

* comment: README.md 수정
- docker run 명령어 실행 관련 주석 위치 수정

* docs: README.md 수정
- docker 명령어 실행 시 주의사항 추가

* docs: env.example 수정

- env.example 파일 환경변수명 수정
- 관련 README 내용 업데이트
- 누락된 application.yml 파일 설정 추가 및 README 업데이트
* chore: auth/infrastructure 내 하위 패키지 추가

- security 패키지와 redis 패키지로 분리
- 기존의 config 패키지는 security 패키지 내부로 이전

* feat: JWT 토큰 검증 및 토큰 파싱 기능 수정

- JwtTokenProvider에 parseClaims() 메서드 추가
- JwtTokenProviderImpl 내 parseClaims() 메서드의 접근제한자를 public으로 변경
- validateToken() 메서드 내부의 예외 처리 로직 세분화

* feat: Http 요청에 관한 Wrapper 추가

- Http 요청을 서블릿에서 처리할 때 원본 요청 헤더는 기본적으로 read-only이므로 커스텀 요청 헤더를 요청 메시지에 추가하려면 decorator 패턴에 기반한 전용 Wrapper 활용 필수
- 이 클래스에서는 기존의 요청 헤더에 JWT 토큰을 파싱하여 만든 커스텀 요청 헤더를 추가하는 작업을 진행
- 게이트웨이에 사용할 spring-cloud-gateway는 webflux 기반이므로 이 클래스는 게이트웨이로 JWT 인증 필터를 분리하기 전까지만 임시로 사용

* feat: 임시 JWT 인증 필터 기능 구현 및 적용

- 발급받은 JWT 토큰(accessToken)을 요청 헤더로 변조하는 작업을 수행하는 JwtAuthenticationFilter 추가
- JwtAuthenticationFilter를 먼저 실행한 다음 LoginFilter를 실행하도록 설정
- 필터 실행 순서 및 각 필터별 역할에 관한 주석 추가

* comment: redis 기반 토큰 저장소 관련 TODO 주석 추가

- 게이트웨이 인증 필터 구현 관련 주석 추가
- 토큰을 관리하기 위한 redis는 user-service에 두어 토큰 데이터가
  gateway, user-service에 각각 분산되지 않고 통합적으로 관리될 수 있게
  함

* refactor: auth/infrastructure 패키지 내부 구조 세분화 및 TokenProvider 클래스명 변경

- auth/infrastructure 패키지 내부를 filter, jwt, config로 세분하여 이후의 게이트웨이 인증 필터 분리 작업을 대비
- JwtAuthenticationFilter는 filter 패키지로 이동 -> 게이트웨이 인증 필터의 내부 로직으로 활용
- 기존 JwtTokenProvider 인터페이스의 이름을 TokenProvider로 변경함에 따라 JwtTokenProviderImpl -> JwtTokenProvider로 클래스명을 변경
- JwtProperties, JwtTokenProvider는 jwt 패키지로 이전 -> 공통모듈에 추가 예정

* comment: TokenProvider 내 게이트웨이 필터 분리 작업 계획 관련 TODO 주석

- gateway와 user-service에서 코드 중복 없이 TokenProvider를 사용하기 위해 TokenProvider를 공통모듈로 이전하기 위한 준비
- TokenProvider를 공통 모듈로 이전할 때 수행할 작업에 관한 주석 추가

* comment: 게이트웨이 인증 필터 분리 작업 계획 정리

- 현재는 임시로 user-service에서 JWT 인증 및 요청헤더 변조 작업까지 수행하도록 구현
- gateway-server에 JWT 인증 필터 기능을 이전하는 작업에 관한 TODO 주석 추가

* comment: 게이트웨이 인증 필터 분리 작업 관련 TODO 주석 추가

- 게이트웨이 인증 필터 분리 작업 진행 시 gateway-server와 user-service에 동일한  tokenProvider를 공유하기 위해 필요한 사항 정리
- 공통모듈에 추가된 JwtProperties, JwtTokenProvider 코드만 제공하고 자동 빈 등록, 수동 빈 등록 모두 수행하지 않는다는 TODO 주석 작성

* refactor: 패키지 경로 및 클래스명 변경사항 반영

- auth 패키지 내 일부 클래스명/인터페이스명 변경 및 패키지 저장 경로 관련 변경사항을 반영하여 import문 수정

* refactor: TODO 주석 미반영 사항 수정

- SignupInfo에 createdAt, createdBy 필드 추가

* fix: JWT 인증 필터 기능 보강

- 클라이언트가 Authorization 헤더 없이 X-User-* 헤더만 담긴 요청 메시지를 발송한 경우에는 해당 커스텀 요청 헤더를 위변조된 헤더로 판정하여 삭제하도록 수정

* comment: JWT 인증 필터 기능 보강 관련 주석 추가

- 클라이언트가 Authorization 헤더 없이 x-user- 로 시작하는 헤더를 보내는 것을 공격으로 간주하는 이유에 관한 주석 추가

* refactor: 코드래빗 수정사항 반영

- doFilterInternal() 메서드 내 try-catch문 추가를 통해 500 에러 예외처리
- X-User-Enabled 헤더 기본값 설정

* refactor: 코드래빗 수정사항 반영

- parseClaim() 메서드 관련 예외처리 및 doFilterInternal() 메서드 가독성 개선

* feat: 토큰 재발급용 DTO 추가

- auth/presentation 패키지 내 UserReissueRequest 추가
- auth/application 패키지 내 ReissueUserCommand 추가

* feat: 토큰 재발급 로직 구현

- AuthService 내 reissue() 메서드 추가
- AuthController 내 /api/v1/auth/reissue 엔드포인트 추가

* refactor: 사용자 인증 로직과 토큰 관리 로직 분리

- userAuthenticator에 토큰 검증 및 토큰 블랙리스트 검증 메서드 정의
- 인증 도메인 내 토큰 발급, 토큰 유효성(만료시간, 서명) 검사, 토큰 저장 등 토큰 관리 기능을 TokenService에 통합

* feat: 토큰 블랙리스트 검증 기능 추가

- 로그아웃한 사용자의 accessToken의 유효기간이 남은 경우, 이를 재사용하는 것을 방지하기 위해 남은 유효시간 동안만 accessToken을 블랙리스트에 저장
- accessToken의 남은 유효기간을 계산할 메서드(getRemainingTime()) 정의 및 기능 구현

* comment: 게이트웨이 인증 필터 분리 관련 TODO 주석 수정

- UserAuthenticatorImpl 내 토큰 블랙리스트 관련 주석 추가
- 작업 완료된 사항에 관한 주석 삭제

* fix: 코드래빗 수정사항 반영

- 토큰 재발급 엔드포인트 호출은 모든 사용자가 가능하도록 UserAuthConfig 수정
- ReissueUserCommand 빈 문자열, null 체크 추가
- TokenService의 토큰 검증 기능 관련 예외 처리 로직 보강

* fix: 토큰 블랙리스트 redis key 수정

- 로그아웃한 사용자의 accessToken을 토큰 블랙리스트에 저장할 시 accessToken의 subject인 userId를 redis key, accessToken은 redis value로 사용하도록 수정

* refactor: refreshTokenHash 파라미터명 수정

- 실제 동작과 불일치하므로 refreshToken으로 수정

* fix: 토큰 타입 구분용 enum 추가

- TokenType enum을 사용하여 발급된 토큰 타입을 구분함으로써 accessToken 대신 refreshToken이 오용되는 것을 방지

* refactor: JwtUtil 클래스 추가

- JWT 관련 중복되는 상수 및 기능 통합

* fix: 코드래빗 수정사항 반영

- 블랙리스트 저장/조회 시 토큰 형식 통합

* chore: 공통모듈 버전 업그레이드

- build.gradle에서 사용할 공통모듈의 버전을 0.1.0으로 업그레이드

* refactor: 요청 DTO 유효성 검사 규칙 수정

- String 타입 필드에 관한 유효성 검증 시 @notblank를 사용하도록 수정
- 입력받은 데이터에 관한 유효성 검사는 presentation 계층의 요청 DTO에서만 수행

* refactor: 회원 도메인 예외 처리 방식 변경

- 발생한 에러를 검출하기 위해, 기존의 application-error.yml에서 에러메시지를 관리하는 방식 대신 UserErrorCode enum을 통해 application-error.yml 파일의 key값을 관리하는 방향으로 수정
- 회원 도메인용 커스텀 예외 핸들러 추가
- UserServiceException 수정
- 기존의 application-error.yml 파일의 이름을 application-user-error.yml로 변경
- application-user-error.yml 파일의 설정을 불러오도록 application.yml 파일의 설정 수정

* refactor: 회원 도메인 예외 처리 방식 적용

- UserServiceException 예외 처리 시 UserErrorCode enum을 사용하도록 기존 예외 처리 로직을 수정

* fix: 코드래빗 수정사항 반영

- 유효성 검사 어노테이션 관련 피드백 반영
- UserExceptionHandler 관련 피드백 반영

* fix: 코드래빗 수정사항 반영

- 토큰 재발급 요청 DTO에 accessToken 또는 refreshToken이 null일 시 지정한 에러 메시지와 400 에러로 응답하도록 수정
- 최소 1개의 채팅 가능 시간대를 지정해야 한다는 규칙과 관련된 유효성 검증 로직 강화

* fix: 로그아웃 진행 시 블랙리스트 로직 미적용 문제 수정

- JwtAuthenticationFilter 내부에 UserAuthenticator를 주입하여, 내부에서 동작하는 UserAuthenticator가 블랙리스트 검증 로직을 수행하도록 위임

* fix: 코드래빗 피드백 반영

- 블랙리스트에 등록된 토큰일 때의 로그 메시지에서 accessToken이 노출되는 문제 수정
* chore: 공통모듈 버전 업그레이드

- 공통모듈에 추가한 JWT 토큰 발급 관련 기능을 사용하기 위해 build.gradle의 공통모듈의 버전을 0.2.0 버전으로 업데이트

* refactor: 공통모듈의 코드로 Jwt 토큰 발급 기능 대체

- auth 패키지에 작성된 기존의 JWT 토큰 발급 기능을 공통모듈에 추가한 Jwt 토큰 발급 기능으로 교체
- 기존의 auth 패키지에 작성된 JWT 토큰 발급 기능은 deprecated 처리

* fix: 빈 순환참조 문제 수정

- gradle build clean 명령어 실행 시 빈 순환참조 문제로 인해 contextLoad()에서 발생한 오류 해결
- UserAuthConfig 내부에서 빈 주입과 생성을 동시에 수행하는 코드를 수정하기 위해 JwtConfig, UserAuthenticatorConfig 클래스를 분리
- 기존 UserAuthConfig는 UserSecurityConfig로 명칭 변경

* remove: 공통모듈로 이전함에 따라 더 이상 사용되지 않는 파일 삭제

- 공통모듈에 추가한 Jwt 토큰 발급 기능으로 대체되어 삭제

* refactor: 공통모듈의 클래스 적용

- 공통모듈로 이전한 Jwt 관련 기능으로 대체

* chore: 테스트 실행환경 수정

- ddl-auto의 값을 create-drop으로 변경
- jwt secret-key 기본값을 게이트웨이와 통일

* chore: 사용하지 않는 임포트 및 주석 삭제

- 사용하지 않는 임포트 및 주석 삭제

* chore: 코드래빗 피드백 반영

- 테스트 환경의 config import 적용 순서를 일반 실행 환경과 동일하게 적용
* feat: 로그인한 회원 자신의 정보 조회 기능 추가

- GET /api/v1/users/me api 엔드포인트 추가

* fix: TODO 주석 작업 진행

- UserSignupResponse DTO 내 createdAt, createdBy 필드 추가
* feat: 블랙리스트 검증 요청/응답 DTO 추가

- UserVerifyRequest, UserVerifyResponse DTO 추가

* feat: /internal/v1/auth/verify api 추가

- AuthInternalController 클래스 추가
- 게이트웨이에서 전송한 accessToken이 로그아웃한 사용자의 토큰인지 검증하는 작업을 수행

* refactor: UserSecurityConfig 수정

- /internal/v1/**로 시작되는 api가 호출된 경우에는 모두 허용
- 내부 호출용 api로 FeignClient에서만 사용되고, 게이트웨이를 통해서는 해당 api 호출 불가능하도록 이미 처리함

* commend: isBlacklisted() 메서드 주석 수정

- try 블록 안에서 발생한, 검증 대상인 accessToken의 userId 추출 실패 등의 모든 예외 상황은 토큰이 블랙리스트에 포함된 것과 동일하게 취급하여 fast-fail 처리
* refactor: 로그인용 회원정보 조회 DTO명 수정

- LoginUserResponse에서 UserLoginResponse로 변경하여 DTO 명명 방식의 일관성 유지

* feat: 회원 목록 조회/검색용 DTO 추가

- user/application/dto/query 패키지에 검색조건에 관한 SearchUserQuery DTO 추가
- user/application/dto/query 패키지에 단건 검색결과에 관한 UserSearchResult DTO 추가
- user/presentation/dto 패키지에 검색 요청, 응답 DTO 추가

* feat: 회원 목록 조회/검색용 엔드포인트 추가

- GET /api/v1/users 요청을 처리하기 위한 엔드포인트 추가

* refactor: CQRS 패턴 적용

- 회원 도메인의 조회 기능은 용도에 따라 세분되어 있으나, 변경 기능은 용도가 한정되어 있으므로, 이 두 기능을 별도로 분리하여 관리하기 위해 CQRS 패턴을 적용
- 필요한 범위 내에서만 트랜잭션을 적용하여 관리할 수 있도록 Facade 계층 분리
- 기존의 UserService는 UserQueryFacade, UserCommandFacade로 분리

* refactor: CQRS 패턴 반영

- 회원/인증 도메인에서 회원 정보를 조회할 때는  UserQueryFacade를 호출하도록 변경
- 인증 도메인에서 회원 등록을 진행할 때는 UserCommandFacade를 사용하도록 변경

* chore: build.gradle 수정

- 회원/인증 도메인의 엔티티에 관한 Q클래스 생성용 annotationProcessor 설정

* feat: 회원목록 조회 및 검색 기능 구현

- QueryDsl 기반 회원목록 조회 및 검색 기능 구현
- 검색 조건으로는 keyword(통합검색), username, name, nickname, userRole을 지정

* refactor: UserRole 기능 분리

- UserRole의 문자열-enum 변환과 권한 검증 로직 분리
- 검색 조건에 관한 UserSearchRequest에서는 문자열-enum 변환만 수행하여 회원 권한을 검색조건으로 지정하지 않을 시 예외 발생 대신 null 처리를 수행

* feat: 권한 검증 로직 보강 및 적용

- SecurityUtil 기반 회원 본인 여부 확인 기능 추가
- 권한 검증 로직을 RoleCheck로 통합
- application 계층의 권한 검증 기능에 변경된 권한 검증 로직 적용

* feat: 커스텀 페이지네이션 DTO 적용

- api 명세서의 형식 준수
- UserController에 적용

* refactor: UserSecurity 필터 적용 순서 가독성 개선

- 코드 변경 전, 후 모두 필터 적용 순서는 JwtAuthenticationFilter -> LoginFilter -> UsernamePasswordAuthenticationFilter 순으로 동일
- 요청 메시지를 기준으로 했을 때의 필터 통과 순서를 명확히 드러내는 방향으로 수정

* fix: 회원 목록 조회 관련 코드래빗 개선사항 반영

- UserQueryCondition 오타 수정
- UserSearchRequest 필드 순서 수정
- 조회된 결과가 없더라도 예외 처리 없이 빈 페이지를 반환하도록 UserQueryService의 회원 목록 조회 로직 수정

* fix: 회원 등록 관련 코드래빗 개선사항 반영

- DataIntegrityViolationException 중 unique 제약조건 위반에 의해 발생한 예외는 DuplicateKeyException을 사용하여 처리

* fix: 코드래빗 수정사항 반영

- 에러코드 매핑 키 불일치 수정
* feat: 회원 수정, 삭제용 DTO 추가

- application 계층 내 회원정보 수정, 삭제용 DTO 추가
- presentation 계층 내 회원정보 수정, 삭제용 요청, 응답 DTO 추가

* feat: 회원정보 수정, 삭제 서비스 로직 구현

- UserCommandFacade, UserCommandService 내 updateUser(), deleteUser() 메서드 추가 및 구현

* feat: 회원정보 수정, 삭제 api 호출용 엔드포인트 추가

- PATCH /api/v1/users/{userId}, PATCH /api/v1/users/me 호출용 엔드포인트 추가
- DELETE /api/v1/users/{userId} 엔드포인트 추가

* feat: 회원정보 수정, 삭제 관련 도매인 로직 추가

- User 엔티티 내 update(), delete() 메서드 추가
- UserRepository 내 수정/삭제할 회원 조회용 메서드 정의

* feat: 회원정보 수정, 삭제 권한 관련 에러코드 추가

- 회원정보 수정, 삭제는 회원 본인 혹은 권한이 관리자, 총관리자인 회원만 가능
- 지정한 권한을 보유하지 않은 회원이 회원정보 수정이나 삭제를 요청하는 경우에 관한 에러코드 추가

* refactor: 회원정보 수정 요청 DTO 분리

- 회원정보 수정 DTO를 관리자 전용 회원정보 수정용 DTO와 회원 자신의 회원정보 수정용 DTO로 분리

* refactor: 채팅가능시간대 요청 DTO 분리

- 회원가입 DTO의 내부 클래스로 사용했던 채팅가능시간대 DTO를 별도의 public 클래스로 분리하여 회원정보 수정 기능에서 재사용

* feat: 회원정보 수정 관련 엔티티 메서드 추가

- 회원 권한 수정, 비밀번호 변경 기능 추가
- 리스트 단위 채팅 가능 시간대 저장, 변경, 삭제 기능 추가
- 채팅 가능 시간대의 변경은 기존 채팅가능 시간대의 일괄 삭제 후 변경할 시간대로 일괄 추가하는 방식

* feat: 회원정보 수정 시 채팅가능시간대 정렬 추가

- 회원정보 수정 시 채팅가능 시간대를 요일, 시작시간대 순으로 정렬
- 회원정보 수정 Command DTO 내부의 채팅가능 시간대 DTO를 엔티티로 변환하는 메서드에 정렬 로직 추가

* fix: 회원정보 수정 관련 응답 DTO 필드 추가

- UserUpdateResponse의 누락된 필드 추가

* feat: 회원정보 수정 기능 관련 서비스 로직 구현

- 회원정보 수정 기능을 회원 본인의 회원정보 수정 기능과 관리자 전용 회원정보 수정 기능으로 분리
- 회원 본인의 회원정보 수정 시, Facade 계층에 변경 범위에 비밀번호가 포함될 때의 비밀번호 암호화 로직을 추가

* refactor: 회원정보 수정 기능 관련 컨트롤러 메서드명 수정

- 각각의 회원정보 수정용 엔드포인트의 용도를 명확히 드러내는 방향으로 수정

* fix: 회원조회 기능의 권한 확인 로직 수정

- getUserWithAuthCheck() 메서드의 권한 확인 조건을 or 조건에서 and 조건으로 변경
- 총관리자/관리자 권한이거나 회원 본인인 경우에만 회원조회 기능을 이용 가능하도록 수정

* chore: 로컬/원격 application.yml 파일 설정 조정

- 로컬 application.yml 파일의 db, redis, jpa 관련 설정을 원격 configs의 user-service/application.yml로 이전
- 로컬 application.yml 파일 내 환경변수 우선순위 설정 조정
- jwt 만료시간 설정 추가 + 원격 kafka 서버 연동 관련 설정 추가

* docs: README 내용 업데이트

- docker 기반 실행 환경 세팅 관련 내용 업데이트

* feat: 임시 빈 등록 설정 추가

- OutboxService 빈 임시 등록용

* fix: 코드래빗 수정사항 반영

- 관리자 전용 삭제 기능 사용 시 회원 삭제가 불가능한 문제 수정
- UserSelfUpdateRequest chatTimeRange 관련 Null 검증 추가
- deleteUser() 메서드의 PathVariable 경로 변수 바인딩 대상 명시

* fix: docker 환경에서의 라우팅 오류 수정

- application.yml 파일 설정 수정 후 게이트웨이와 user-service 간 라우팅이 정상적으로 수행되지 않는 문제 수정
- application.yml 파일 내 prefer-ip-address: true 설정 추가

* fix: 코드래빗 수정사항 반영

- PathVariable 관련 피드백 반영
HyeonBin2379 and others added 18 commits May 7, 2026 21:12
* remove: 사용하지 않는 클래스 삭제

- common 모듈에 OutboxService 빈이 추가되어 그 이전까지 임시 사용 중이던 EventBeanConfig를 삭제

* refactor: UserExceptionHandler 재배치

- UserExceptionHandler를 domain 패키지에서 presentation 패키지로 이전

* chore: build.gradle common 모듈 버전 업그레이드

- 사용할 common 모듈의 버전을 0.2.6-SNAPSHOT에서 0.2.7-SNAPSHOT 버전으로 업그레이드

* chore: 테스트 환경 수정

- 테스트 진행 시 eureka server와의 연동을 비활성화하도록 수정

* refactor: ChatTimeRange 관련 예외처리 클래스 변경

- 이전에 사용했던 IllegalArgumentException을 UserServiceException으로 대체

* test: 회원 도메인 application 계층 관련 테스트코드 추가

- UserCommandFacade, UserCommandService, UserQueryFacade, UserQueryService에 관한 테스트코드 추가

* test: 회원 도메인 domain 계층 관련 테스트코드 추가

- User, UserRole, ChatTimeRange, UserErrorCode 관련 테스트코드 추가

* test: 회원 도메인 infrastructure 계층 관련 테스트코드 추가

- QueryDsl 회원 목록 검색 조건, 회원 목록 조회/검색 쿼리, 회원 목록 정렬 관련 테스트코드 추가
- SecurityRoleCheck의 권한 체크 로직 관련 테스트코드 추가

* test: 회원 엔티티 관련 테스트코드 보강

- 회원정보 수정용 메서드의 파라미터의 값이 null이면 User 엔티티의 대응되는 필드의 기존 값을 유지하는지 확인하는 테스트코드 추가
- User 엔티티가 삭제되지 않았을 때만 isEnabled()가 true를 반환하는지 확인하는 테스트코드 추가

* test: 회원 도메인 application 계층 관련 테스트코드 보강

- UserCommandFacade, UserCommandService, UserQueryFacade 관련 테스트코드 보강

* test: 회원 도메인 presentation 계층 관련 테스트코드 보강

- UserExceptionHandler 관련 테스트코드 보강

* fix: MethodArgumentNotValidException 관련 예외 처리 핸들러 수정

- UserErrorCode에 매핑할 수 없는 에러인 경우에도 4xx 상태코드를 반환하도록 수정

* fix: 코드래빗 피드백 반영

- 테스트코드 보강
* refactor: DTO 클래스를 record로 변경

- 별도의 설정 없이도 JSON으로 직렬화/역직렬화를 수행하기 위해 DTO 클래스를 record로 수정

* test: 인증 도메인 application 계층 테스트코드 추가

- AuthService, TokenService 관련 테스트코드 추가
- TokenService에 관한 테스트코드 중 블랙리스트 검증에 관한 테스트코드는 별도 클래스로 분리

* refactor: AuthController 수정 및 테스트코드 추가

- 회원가입, 로그아웃 성공 시 상태코드는 각각 201, 204를 반환
- 로그아웃 시 @AuthenticationPrincipal 대신 @RequestHeader를 사용하도록 메서드 파라미터 수정
- AuthController api 호출 성공 케이스 관련 테스트코드 추가

* test: AuthInternalController 테스트코드 추가

- 토큰의 블랙리스트 포함 여부를 검증하는 내부 호출용 api에 관한 테스트코드 추가

* test: UserAuthentication 관련 테스트코드 추가

- 아이디 & 비밀번호 검증 관련 테스트코드 추가
- 토큰 검증 관련 테스트코드 추가

* test: HttpRequestHeaderWrapper 관련 테스트코드 추가

- 헤더 스푸핑 방지 기능 관련 테스트코드 추가
- 정상적인 인증 흐름에서의 요청 헤더 추가 기능 관련 테스트코드 추가

* refactor: JwtAuthenticationFilter 세부 로직 수정 및 테스트코드 추가

- doFilterInternal()의 내부 로직에서 null 체크를 수행하는 코드를 Optional로 대체
- JwtAuthenticationFilter 실행 성공, 실패 케이스에 관한 테스트코드 추가

* test: HttpRequestHeaderWrapper 관련 테스트코드 보강

- getHeaders(), getHeaderNames() 관련 테스트코드 추가 및 테스트 진행

* fix: 코드래빗 수정사항 반영

- 토큰 발급 시 Duration 관련 검증 수행
- 토큰의 블랙리스트 포함 여부에 관한 테스트 방식의 일관성 유지

* refactor: JwtAuthenticationFilter 헤더 추가 로직 수정

- 값이 존재하지 않을 시 null을 반환하고, 헤더 추가 시 Optional을 사용하도록 수정
- 헤더 추가 로직 관련 테스트코드 수정
* feat: 회원의 채팅 가능 시간대 관련 에러코드 추가

- UserErrorCode에 회원의 채팅 가능 시간대 검색 조건, 중복 등록 관련 에러코드 추가

* feat: 회원의 채팅 가능 시간대 관련 도메인 로직 추가

- ChatTimeRange 내 채팅 가능 시간대 겹침 여부 판정용 도메인 로직 추가
- 채팅 시간대 겹침 여부 판정 관련 테스트코드 추가

* feat: ChatTimeRange 관련 제약조건 추가 및 채팅 시간대 검색 로직 추가

- ChatTimeRange에 관한 복합 유니크 키 제약조건 추가
(user_id, day_of_week, start_time으로 구성)
- 한 회원의 채팅 가능 시간대가 다른 회원의 채팅 가능 시간대를 모두 포괄하는 경우도 중복된 시간대로 간주
- 지정한 날짜, 시간 조건을 충족하는 채팅 가능 시간대 리스트 조회 관련 도메인 로직 추가
- 채팅 가능 시간대 일괄 등록 기능에 중복 여부 검증 로직 추가
- 추가된 도메인 로직에 관한 테스트코드 추가

* feat: 지정한 날짜, 시간을 충족하는 채팅 가능 시간대 검색 관련 서비스 로직 구현

- 지정한 날짜, 시간을 충족하는 채팅 가능 시간대 검색 관련 application 계층 서비스 로직 구현
- 테스트 코드 추가 및 단위 테스트 진행

* feat: 지정한 날짜, 시간 조건을 충족하는 채팅 가능 시간대 검색 관련 DTO 추가

- SearchChatTimeQuery, UserChatTimeListResponse, UserChatTimeSearchRequest DTO 추가

* refactor: DTO 필드명 수정

- 도메인 계층의 명명 규칙에 따라 DTO 필드명을 수정하여 명명 규칙의 일관성 유지

* feat: 지정한 날짜, 시간을 충족하는 채팅 가능 시간대 검색용 api 추가

- 내부호출용 GET /internal/v1/{userId}/chat-availability api 추가

* feat: 회원 도메인 에러코드 추가

- 회원의 채팅 가능 시간대 검색용 요청 DTO 필드의 유효성 검사 관련 에러코드 추가

* chore: 사용하지 않는 패키지 import문 삭제

- @AuthenticationPrincipal이 @RequestHeader로 대체되면서 더 이상 사용하지 않는 import문 삭제

* fix: 코드래빗 개선사항 반영

- ChatTimeRange 유니크 제약조건명 수정

* feat: 회원 도메인 테스트데이터 자동 추가 기능 구현

- 도메인 서비스 초기 구동 시 .csv 파일로 생성한 테스트 데이터를 자동 추가하기 위한 CsvLoader 구현

* refactor: 내부 인증 필터 로직 수정

- 한글 인코딩 로직을 게이트웨이의 JwtGatewayFilter와 동일한 방식으로 수행하도록 수정

* fix: 코드래빗 피드백 반영

- INSERT_SQL에 ON CONFLICT DO NOTHING 구문을 추가함으로써 멱등성을 확보하고, 이미 저장된 데이터가 존재하는 경우에는 다음 데이터 추가 작업을 진행
- CHECK_SQL의 하드코딩된 데이터를 limit문으로 대체
* chore: 회원 도메인 ci/cd 수행용 설정 추가

- 배포 전용 docker-compose.yaml 파일 추가
- 배포 전용 .env.template 파일 추가(deploy 폴더 내부에 .env 파일 생성)
- github action 워크플로우 스크립트 추가

* chore: 코드래빗 피드백 일부 반영

- Github Variables에 GCP_ZONE 환경변수 추가

* chore: 코드래빗 피드백 반영

- main 브랜치에 직접 push 시에는 배포되지 않도록 on 트리거 수정
- deploy 시 수동 배포만 가능하도록 수정
- dev -> main merge되어 배포된 경우에만 :stable 태그 부여

* chore: 코드래빗 피드백 반영

- 배포용 .env 파일 존재 여부 및 pgsg-network 생성 여부 확인

* chore: common 모듈 버전 업그레이드

- common 모듈의 버전을 0.2.7에서 0.2.8로 업그레이드

* chore: common 모듈 버전 업그레이드

- common 모듈의 버전을 0.2.8에서 0.2.10으로 업그레이드
* chore: github action 배포/롤백 관련 워크플로우 수정

- 배포/롤백 시 실행되는 docker compose 명령어를 수정

* fix: 코드래빗 수정사항 반영

- 롤백용 docker compose 관련 오기재된 환경변수명 수정
- 유레카 연동 및 헬스체크 관련 설정 추가
- config 서버 연동 관련 TODO 주석 추가
* chore: application.yaml 파일 수정

- 유레카 서버 연동 관련 설정 수정
- config-server 관련 todo 주석 추가

* chore: deploy/.env.template 관련 코드래빗 피드백 내용 반영

- eureka 서버 주소 관련 환경변수명 수정
- CONFIG_SERVER, JWT_SECRET 관련 주석 보충

* chore: deploy/.env.template 오타 수정

- eureka 서버 주소 관련 환경변수명을 EUREKA_SERVER_URL로 수정

* chore: application.yaml config 서버 연동 관련 설정 수정

- 유레카 서버에 등록된 config server와 연동되도록 설정 수정

* chore: ar-image-retention-policy.json 수정

- 유효하지 않은 코드 수정

* chore: promote-stable 수행 조건 수정

- main/dev 브랜치 병합 및 배포 성공 시에만 수행하도록 조건 변경

* chore: application.yaml 설정 수정

- config server 연동 완료 전까지만 임시로 이전 연동 방식을 적용

* chore: application.yaml 설정 수정

- 유레카 서버에 등록된 config server에 연동하도록 수정

* chore: 워크플로우 실행 중 오류 확인용 작업 추가

- 임시 확인용 작업 추가

* chore: 워크플로우 실행 중 오류 확인용 작업 추가

- 임시 확인용 작업 추가

* chore: 워크플로우 실행 중 오류 확인용 작업 추가

- 임시 확인용 작업 추가

* chore: 워크플로우 실행 중 오류 수정

- 임시 확인용 작업 추가

* chore: 워크플로우 실행 중 오류 수정

- 임시 확인용 작업 추가

* chore: 워크플로우 실행 중 오류 수정

- 임시 확인용 작업 추가

* chore: 워크플로우 실행 중 오류 수정

- gateway-server의 워크플로우를 user-service에 맞게 수정
- 이전 코드는 별도로 백업

* chore: 워크플로우 실행 중 오류 수정 및 코드리뷰 피드백 반영

- AR Image Retention Policy 한정으로 오류 발생하더라도 다음 작업 진행 허용
- promote-stable 수행 조건 수정
- prefer-ip-address: true로 변경
ip-address 관련 설정 추가
* fix: 모니터링 api 차단 문제 해결

- UserSecurityConfig의 설정 수정

* chore: 모니터링 관련 설정 추가

- prometheus 의존성 추가 및 zipkin 설정 추가

* chore: 빌드 및 배포 시 헬스체크 주기 조정

- 배포 속도 개선을 위해 헬스체크 주기를 조정

* chore: zipkin 서버 연동 관련 설정 추가

- deploy/promtail-config.yml 추가
- application.yaml 및 deploy/docker-compose.prod.yaml 파일에 zipkin, promtail 관련 설정 추가

* chore: zipkin 서버 연동 설정 관련 배포 오류 수정

- deploy.yaml에 promtail.yml 파일을 배포하기 위한 스크립트 추가

* fix: 코드래빗 수정사항 반영

- deploy.yaml에서 헬스체크 주기 및 횟수가 하드코딩된 부분을 수정
- zipkin, loki 엔드포인트를 환경변수로 변경 및 원격 서버에 환경변수 등록
- docker-compose.prod.yaml 파일 promtail 관련 명령어 수정
user-service-promtail에서 promtail로 수정
* chore: user-service 배포 시 로그 수집 관련 필요한 설정 추가

- docker-compose.prod.yml 파일에 user-service 기능 수행 중 생성된 로그파일의 저장 경로 지정
- docker-compose.prod.yml promtail-config.yml 파일 환경변수 값 치환 설정 추가
- deploy/.env.template 업데이트

* chore: deploy/.env.template 파일 수정

- LOG_FILE_PATH 환경변수 값 설정 형식 추가

* comment: deploy/.env.template 파일 수정

- 도메인 서비스별 로그 파일 경로명 관련 주석 추가

* chore: 코드래빗 피드백 반영

- .env.template 파일 수정
* fix: 회원 테스트 데이터 미적재 오류

- CHECK_SQL 조건 수정

* fix: 회원 테스트 데이터 미적재 오류

- .gitignore 수정
* chore: user-service-2 스케일아웃 기능 추가

- user-service 스케일아웃 워크플로우 추가

* chore: 코드래빗 리뷰 반영

- user-service 스케일아웃 워크플로우 수정
* docs: README 내용 업데이트

- user-service 관련 README 내용 최신화

* docs: 코드리뷰 피드백 반영

- 오칼자 수정
@HyeonBin2379 HyeonBin2379 self-assigned this May 20, 2026
@HyeonBin2379 HyeonBin2379 added the enhancement New feature or request label May 20, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in 8949 project May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

인증/사용자 도메인과 보안 필터·레포지토리·API·테스트를 대규모로 추가했다. GCP Artifact Registry 빌드/푸시 및 GCE VM 원격 배포·롤백 워크플로우와 VM 스케일 액션을 도입했다. 설정은 application.yml 재구성, 에러 매핑 YAML, .env 템플릿, Dockerfile/compose 수정이 포함된다.

Changes

유저 서비스 통합 업데이트

Layer / File(s) Summary
인증 도메인 및 보안 필터 흐름
src/main/java/org/pgsg/user_service/auth/...
AuthService/TokenService/UserAuthenticator, JwtAuthenticationFilter/JwtConfig/Security 설정, RedisTokenRepository, 인증 API 및 내부 검증 API와 DTO 추가.
사용자 도메인/쿼리/명령
src/main/java/org/pgsg/user_service/user/...
User 엔티티/역할/채팅시간, Command/Query Facade·Service, QueryDSL 조건/레포지토리 구현, 컨트롤러와 요청/응답 DTO, 예외/에러코드 추가.
데이터 로더와 시드
src/main/java/.../loader/*, src/main/resources/data.sql
CSV 기반 초기 데이터 로더(dev 프로파일)와 사용자/채팅시간 시드 SQL 추가.
설정/문서/도커
.env.example, application.yml, README.md, Dockerfile, docker-compose*, deploy/*
런타임/관측/Eureka/JWT 설정 재구성, 문서 전면 교체, Dockerfile 시크릿 마운트 빌드, compose(prod 포함) 및 promtail 설정 추가.
CI/CD 및 운영 워크플로우
.github/actions/scale-vm, .github/workflows/*
이미지 빌드·푸시·배포·롤백·stable 승격 파이프라인과 GCE VM 스케일 액션/워크플로우 추가.
테스트 추가
src/test/java/**, src/test/resources/**
인증/보안/도메인/레포지토리/예외 처리 단위·웹 테스트와 테스트용 설정/시드 추가.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(66, 135, 245, 0.5)
  participant Dev as GitHub Actions
  participant AR as Artifact Registry
  participant VM as GCE VM
  participant Svc as user-service
  end
  Dev->>AR: Build & Push image (SHA, latest)
  Dev->>VM: Sync compose/config via IAP
  Dev->>VM: docker pull/up (new tag)
  VM->>Svc: Start container
  Dev-->>Svc: Poll /actuator/health
  Svc-->>Dev: UP -> Tag promote to :stable
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • 89-49/user-service#27: TokenService/TokenRepository/재발급·블랙리스트·JwtAuthenticationFilter 경로가 본 PR과 동일.
  • 89-49/user-service#35: User 목록 검색/페이지 응답 및 Querydsl 조건/정렬 구현이 동일 코드 경로와 겹침.
  • 89-49/user-service#61: GCE VM 스케일/배포 파이프라인 연계 변경과 직접적으로 이어짐.

Suggested reviewers

  • loveletheart

Poem

토큰 두 알, 새벽에 깡총 깡총,
유저 들판에 권한을 심고,
로키와 집킨에 발자국 남겨,
구름 창고에 이미지 띄우고,
바람 탄 VM에 배를 올리네.
오늘도 배포는 UP! 🐇✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (18)
src/main/java/org/pgsg/user_service/auth/infrastructure/security/UserDetailsServiceImpl.java-29-29 (1)

29-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

예외 메시지에 사용자 식별자(아이디) 노출

인증 실패 로그/응답 경로에서 username 노출 위험이 있습니다. 고정 메시지로 바꾸고 원인 예외만 체이닝하세요.

🔒 제안 수정안
 		} catch (UserServiceException e) {
-            throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username);
+            throw new UsernameNotFoundException("사용자를 찾을 수 없습니다.", e);
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/auth/infrastructure/security/UserDetailsServiceImpl.java`
at line 29, Replace the exception that exposes the raw username in
UserDetailsServiceImpl by throwing a generic message and chain the original
cause only; locate where UsernameNotFoundException is constructed (the throw in
UserDetailsServiceImpl) and change the constructor argument to a fixed,
non-identifying message like "사용자를 찾을 수 없습니다" while passing the underlying
exception as the cause so the username is not included in the exception text.
src/main/java/org/pgsg/user_service/auth/domain/TokenRepository.java-11-21 (1)

11-21: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

리프레시 토큰 계약명을 해시 기준으로 일치시켜 주세요.

Line 11은 refreshTokenHash를 저장하지만 Line 20은 refreshToken으로 받아 계약이 혼동됩니다. 비교 기준이 해시라면 파라미터/메서드명을 해시 기준으로 맞추는 게 안전합니다.

제안 diff
- boolean existsByRefreshToken(UUID userId, String refreshToken);
+ boolean existsByRefreshTokenHash(UUID userId, String refreshTokenHash);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/pgsg/user_service/auth/domain/TokenRepository.java` around
lines 11 - 21, The interface mixes unhashed and hashed names:
saveRefreshToken(UUID userId, String refreshTokenHash) uses a hash but
existsByRefreshToken(UUID userId, String refreshToken) suggests an unhashed
token; align contract to use "hash" consistently. Rename existsByRefreshToken to
existsByRefreshTokenHash and its parameter to refreshTokenHash (and similarly
rename findRefreshToken to findRefreshTokenHash or at least its return/param
names if it represents a hash), then update all implementations and callers
(tests, services, repositories) to use the new method/parameter names so the
stored/compared value is unambiguously the refresh token hash.
deploy/ar-image-retention-policy.json-1-11 (1)

1-11: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delete 정책을 추가하여 오래된 이미지 삭제 조건을 명시하세요.

현재 Keep 정책만으로는 이미지가 실제로 삭제되지 않습니다. Google Artifact Registry 공식 문서에 따르면, Keep 정책은 삭제 대상 이미지를 "보호"하는 역할일 뿐이며, 오래된 이미지를 실제로 삭제하려면 Delete 정책에서 삭제 기준(예: 특정 기간 이상 된 이미지, 태그 패턴 등)을 정의해야 합니다.

[
  {
    "name": "delete-old-images",
    "action": {
      "type": "Delete"
    },
    "olderThan": {
      "days": 30
    }
  },
  {
    "name": "keep-latest-3-images",
    "action": {
      "type": "Keep"
    },
    "mostRecentVersions": {
      "keepCount": 3
    }
  }
]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/ar-image-retention-policy.json` around lines 1 - 11, Add a Delete
policy entry so old images are actually removed: create a policy object named
"delete-old-images" with "action.type" set to "Delete" and an "olderThan" rule
(e.g., "days": 30), and keep the existing "keep-latest-3-images" policy; ensure
both policy objects appear in the array (Delete policy and then the existing
keep policy) so the registry will remove images older than the specified period
while still preserving the most recent 3 versions.
.github/actions/scale-vm/action.yaml-33-36 (1)

33-36: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

targets 포맷 검증이 없어 운영 워크플로우 실패 원인이 됩니다.

현재 vm:zone 형식이 아닌 입력이 들어와도 바로 파싱해 진행합니다. 사전 검증을 추가해 잘못된 입력을 즉시 실패 처리하는 게 안전합니다.

제안 수정안
         for ENTRY in ${{ inputs.targets }}; do
+          if [[ ! "$ENTRY" =~ ^[^:[:space:]]+:[^:[:space:]]+$ ]]; then
+            echo "::error::Invalid target format: '$ENTRY' (expected vm-name:zone)"
+            exit 1
+          fi
           VM="${ENTRY%%:*}"
           ZONE="${ENTRY##*:}"

Also applies to: 66-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/scale-vm/action.yaml around lines 33 - 36, The for-loop that
parses inputs.targets into VM and ZONE (using VM="${ENTRY%%:*}" and
ZONE="${ENTRY##*:}") lacks validation and will silently mis-parse bad entries;
add a pre-parse check for each ENTRY to ensure it contains exactly one ':' and
neither side is empty (e.g., test for presence of ':' and non-empty VM and ZONE
after splitting or use shell pattern matching), and if validation fails emit a
clear error via echo/process logger and exit with non-zero status so the
workflow fails fast; apply the same validation in the second parsing block that
currently mirrors lines 66-69.
src/main/resources/application.yml-43-45 (1)

43-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Eureka IP 설정 값이 의미적으로 충돌합니다.

Line 43에서 prefer-ip-address: true인데 Line 44는 ${HOSTNAME}을 넣고 있어 실제 IP가 아닌 호스트명이 들어갈 수 있습니다. 서비스 디스커버리 등록 불안정의 원인이 됩니다.

제안 수정안
 eureka:
   instance:
     prefer-ip-address: true
-    ip-address: ${HOSTNAME:localhost}
-    hostname: ${HOSTNAME:localhost}
+    ip-address: ${EUREKA_INSTANCE_IP:127.0.0.1}
+    hostname: ${HOSTNAME:localhost}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/application.yml` around lines 43 - 45, 설정 충돌:
`prefer-ip-address: true`인데 `ip-address`에 `${HOSTNAME}`을 사용해 실제 IP가 아닌 호스트명이 들어갈
수 있으므로 서비스 등록 불안정이 발생합니다; 수정 방법은 `ip-address`를 실제 IP를 제공하는 환경변수(예: `HOST_IP`)로
바꾸거나 `prefer-ip-address`를 `false`로 변경해 의도를 일치시키는 것입니다—즉, 애플리케이션 설정에서
`prefer-ip-address`, `ip-address`(현재 값 `${HOSTNAME}`)와 `hostname` 값을 일관되게 조정해 IP
우선 동작을 보장하세요.
src/main/resources/application.yml-8-10 (1)

8-10: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

dev 프로필 하드코딩은 운영 배포 안정성을 해칩니다.

Line 9의 active: dev, kafka는 환경변수 누락 시 운영에서도 dev 설정이 활성화될 수 있습니다. 기본값은 비워두거나 환경변수 기반으로 두는 편이 안전합니다.

제안 수정안
 spring:
   profiles:
-    active: dev, kafka
+    active: ${SPRING_PROFILES_ACTIVE:}
     include: error, user-error
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/application.yml` around lines 8 - 10, profiles.active에
dev가 하드코딩되어 있어 운영에서 의도치 않게 dev 프로필이 활성화될 위험이 있습니다; profiles.active 설정(현재
"active: dev, kafka")에서 dev를 제거하고 환경변수 기반으로 변경하세요—예: profiles.active를 환경변수
플레이스홀더로 교체하여 ${SPRING_PROFILES_ACTIVE:}처럼 기본값을 비워 두거나 필요한 경우 kafka만 유지하도록
설정하십시오; 변경 대상 식별자는 profiles.active (현재 값 "dev, kafka") 와 include: error,
user-error를 참고하세요.
.github/workflows/deploy.yaml-99-102 (1)

99-102: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

push 트리거에서 실제 배포가 실행되지 않습니다.

Line [101] 조건 때문에 push(main/dev)에서는 deploy가 스킵되고, 따라서 promote-stable도 실행되지 않습니다. PR 목표(merge 후 재배포)와 동작이 어긋납니다.

수정 예시
   deploy:
     needs: [ build-and-push ]
-    if: github.event_name == 'workflow_dispatch'
+    if: github.event_name == 'workflow_dispatch' || github.event_name == 'push'

Also applies to: 302-307

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yaml around lines 99 - 102, The deploy job
currently has an if condition ("if: github.event_name == 'workflow_dispatch'")
which causes the deploy job (and downstream promote-stable) to be skipped on
push events; update the deploy job definition (the job named "deploy" that lists
needs: [ build-and-push ]) to allow push triggers — e.g., remove the strict if
or change it to include push (github.event_name == 'workflow_dispatch' ||
github.event_name == 'push') or to a more specific conditional that permits
merges to main/dev — and apply the same change to the other deploy-like block
referenced at lines 302-307 so pushes trigger the intended deployment flow.
.github/workflows/deploy.yaml-31-31 (2)

31-31: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GitHub Actions를 커밋 SHA로 고정하세요. 공급망 보안 위험이 있습니다.

현재 @v* 태그 참조는 업스트림 변경에 노출되어 공급망 위험을 초래합니다.

.github/workflows/deploy.yaml: 31, 35, 43, 76, 79, 86, 121, 125, 133, 316, 323
.github/workflows/scale-user-service.yaml: 29, 32, 39

모든 uses 지시어를 특정 커밋 SHA(예: actions/checkout@a5ac7e51b41094c5405dba89c1ab608f81b6884f)로 고정해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yaml at line 31, Replace all floating version tags
in GitHub Actions 'uses' directives (e.g., actions/checkout@v4,
actions/setup-node@v*, docker/build-push-action@v*, etc.) with specific commit
SHAs to mitigate supply-chain risk: locate every 'uses' entry referenced in the
review (the occurrences at lines listed for deploy.yaml and
scale-user-service.yaml) and update each to the corresponding pinned commit SHA
from the upstream repository (for example actions/checkout@<commit-sha>),
ensuring each replacement preserves the same repo and functionality but uses the
exact commit hash rather than a `@v` tag or other floating tag.

31-31: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

persist-credentials: false 설정을 추가하여 토큰 노출 위험을 줄이세요.

.github/workflows/deploy.yaml 31번과 121번 줄의 actions/checkout@v4에서 인증 토큰이 git 설정에 저장되어 이후 단계에서 의도치 않게 노출될 수 있습니다. persist-credentials: false를 명시적으로 설정하여 이를 방지해야 합니다.

수정 예시
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yaml at line 31, actions/checkout@v4가 기본적으로 인증 토큰을
Git 설정에 영구 저장할 수 있어 토큰 노출 위험이 있으니, 해당 액션 호출(모든 occurrences of "uses:
actions/checkout@v4")에 persist-credentials: false 옵션을 추가하여 토큰이 Git 환경에 남지 않도록
설정하세요; .github/workflows/deploy.yaml의 두 checkout 블록(현재 사용 중인
"actions/checkout@v4" 항목들)을 찾아 각각의 step에 persist-credentials: false를 동일한 들여쓰기
수준으로 추가하면 됩니다.
.github/workflows/scale-user-service.yaml-29-39 (1)

29-39: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GitHub Actions의 공급망 보안을 위해 커밋 SHA 고정과 checkout 자격증명 비활성화가 필요합니다.

현재 외부 액션이 버전 태그(@v2, @v4)로만 참조되어 있는데, 이러한 태그는 변경 가능하며 태그가 강제 푸시되는 방식으로 악의적 코드가 주입될 수 있습니다. GitHub의 공식 보안 강화 가이드라인에 따르면 전체 길이의 커밋 SHA로 고정하는 것이 공급망 변조 리스크를 완전히 제거하는 유일한 방법입니다.

또한 actions/checkout의 기본 자격증명은 같은 job 내 모든 후속 스텝에 제공되므로, 워크플로우가 손상된 경우 공격자가 이를 악용할 수 있습니다. persist-credentials: false를 설정하면 이러한 위험을 크게 줄일 수 있습니다.

모든 액션을 전체 길이 커밋 SHA로 고정하고 checkout에 persist-credentials: false를 적용해 주세요.

권장 수정 예시
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@<commit-sha>
+        with:
+          persist-credentials: false

-      - name: Authenticate to GCP
-        uses: google-github-actions/auth@v2
+      - name: Authenticate to GCP
+        uses: google-github-actions/auth@<commit-sha>

-      - name: Set up gcloud
-        uses: google-github-actions/setup-gcloud@v2
+      - name: Set up gcloud
+        uses: google-github-actions/setup-gcloud@<commit-sha>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/scale-user-service.yaml around lines 29 - 39, Replace
floating action tags with their full commit SHA pins and disable checkout
credentials: update the actions referenced (actions/checkout,
google-github-actions/auth, google-github-actions/setup-gcloud) to use the
full-length commit SHAs instead of `@v2/`@v4, and add persist-credentials: false
to the actions/checkout step so the checkout does not expose workflow
credentials to subsequent steps; ensure the SHA values match the commits for the
exact releases you intend to lock.
src/main/java/org/pgsg/user_service/auth/infrastructure/security/config/UserSecurityConfig.java-44-44 (1)

44-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

내부 API 전체 permitAll은 권한 우회 위험이 큽니다.

Line 44의 /internal/v1/** 전체 허용은 경계 설정 실수 시 외부 무인증 접근으로 이어질 수 있습니다. 최소한 서비스 간 인증(예: mTLS/서명 헤더) 또는 네트워크 기반 제한과 함께 좁혀 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/auth/infrastructure/security/config/UserSecurityConfig.java`
at line 44, UserSecurityConfig currently calls
requestMatchers("/internal/v1/**").permitAll(), which dangerously allows
unauthenticated access; change this to enforce service-to-service auth by
replacing permitAll() with an authenticated requirement and/or a stricter
matcher (e.g., require a specific authority via hasAuthority/hasRole, or use
hasIpAddress for network restrictions), or add a custom filter that validates
mTLS or a signature/header for requests to "/internal/v1/**" (keep the matcher
reference requestMatchers("/internal/v1/**") and update the security chain there
to authenticate and validate service credentials instead of permitting all).
src/main/java/org/pgsg/user_service/user/application/UserCommandFacade.java-76-79 (1)

76-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

삭제 권한 체크가 actorId를 사용하지 않아 주체 불일치 위험이 있습니다.

Line 76-79에서 self-check 기준이 targetUserId라, 메서드 시그니처로 받은 actorId와 권한 판단 기준이 분리됩니다. 내부 호출 경로에서 보안 컨텍스트 의존이 깨지면 오판 가능성이 있습니다.

수정 예시
 public UserDeleteResult deleteUser(UUID targetUserId, UUID actorId) {
-    if (!roleCheck.hasRole(List.of(UserRole.MANAGER, UserRole.MASTER)) && !roleCheck.checkUserSelf(targetUserId)) {
+    boolean isPrivileged = roleCheck.hasRole(List.of(UserRole.MANAGER, UserRole.MASTER));
+    boolean isSelf = actorId != null && actorId.equals(targetUserId);
+    if (!isPrivileged && !isSelf) {
         throw new UserServiceException(UserErrorCode.UNAUTHORIZED);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/pgsg/user_service/user/application/UserCommandFacade.java`
around lines 76 - 79, The authorization check in UserCommandFacade.deleteUser
uses checkUserSelf(targetUserId) which compares against the target instead of
the actor, risking subject mismatch; update the conditional to verify the actor
identity (e.g., call roleCheck.checkUserSelf(actorId) or change checkUserSelf to
accept the actorId) so the self-check uses the method parameter actorId (and
keep roleCheck.hasRole(UserRole.MANAGER, UserRole.MASTER) logic unchanged),
ensuring the permission decision in deleteUser uses the provided actorId rather
than targetUserId.
src/main/java/org/pgsg/user_service/user/presentation/dto/request/UserSearchRequest.java-13-20 (1)

13-20: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

잘못된 userRole 값을 조용히 무시하면 검색 조건이 의도치 않게 완화됩니다.

Line 16에서 파싱 실패를 null로 처리하면 잘못된 요청이 정상 검색으로 처리됩니다. 유효하지 않은 역할은 요청 오류로 반환하는 편이 안전합니다.

수정 예시
 public SearchUserQuery toQuery() {
+    UserRole parsedRole = null;
+    if (userRole() != null && !userRole().isBlank()) {
+        parsedRole = UserRole.of(userRole())
+                .orElseThrow(() -> new UserServiceException(UserErrorCode.INVALID_USER_ROLE));
+    }
     return new SearchUserQuery(
             keyword(),
-            UserRole.of(userRole()).orElse(null),
+            parsedRole,
             nickname(),
             name(),
             username()
     );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/presentation/dto/request/UserSearchRequest.java`
around lines 13 - 20, The current toQuery() in UserSearchRequest silently
converts an invalid userRole to null via UserRole.of(userRole()).orElse(null),
which weakens search filters; change it to validate the input and fail fast:
attempt to parse userRole with UserRole.of(userRole()) and if absent throw a
clear IllegalArgumentException (or a custom BadRequestException) so invalid role
values produce a request error instead of being ignored, then pass the validated
UserRole into SearchUserQuery; keep references to toQuery(), UserSearchRequest,
UserRole.of(...) and SearchUserQuery when locating the code.
src/main/java/org/pgsg/user_service/user/infrastructure/loader/AbstractCsvLoader.java-51-57 (1)

51-57: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

CSV 파싱이 인용부호/이스케이프를 처리하지 못합니다.

split(",") 기반 파싱은 "a,b" 같은 정상 CSV 값을 깨뜨려 잘못된 컬럼 수 판단/데이터 손실을 유발할 수 있습니다. CSV 전용 파서로 교체해 정확성을 보장해주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/infrastructure/loader/AbstractCsvLoader.java`
around lines 51 - 57, The current CSV parsing in AbstractCsvLoader uses
String.split(",") which breaks quoted/escaped fields; replace that logic with a
proper CSV parser (e.g., OpenCSV's CSVReader or Apache Commons CSV's CSVParser)
inside the same loop where line is read so quoted commas and escapes are handled
correctly; after parsing use the parser's returned String[] (or List<String>) to
perform the same columnCount check and logging (preserve the log.warn call and
variables) and ensure resources are closed/handled the same way as the
BufferedReader.
src/main/java/org/pgsg/user_service/auth/application/service/AuthService.java-39-42 (1)

39-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

로그아웃 토큰 무효화가 부분 실패 상태를 만들 수 있습니다.

현재 순서에서는 deleteRefreshToken 성공 후 addToBlacklist 실패 시, 액세스 토큰이 살아남아 보안적으로 불완전한 로그아웃이 됩니다. 블랙리스트 등록을 먼저 수행하거나, 실패 시 명확히 예외 처리/재시도 전략을 두는 게 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/auth/application/service/AuthService.java`
around lines 39 - 42, The logout flow can leave an access token valid if
deleteRefreshToken succeeds but addToBlacklist fails; update the
AuthService.logout to call tokenService.addToBlacklist(userId, accessToken)
first and only call tokenService.deleteRefreshToken(userId) after addToBlacklist
succeeds, and ensure addToBlacklist errors are handled (throw/return failure)
and retried/logged so deletion is not performed on partial failure;
alternatively implement a compensation/retry strategy around
tokenService.addToBlacklist in the logout method to guarantee atomic
invalidation.
src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserLoginResponse.java-9-27 (1)

9-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

응답 DTO에서 비밀번호 필드 노출을 제거해주세요.

UserLoginResponsepassword를 포함하면(해시라도) API/로그/모니터링 경로로 민감정보가 확산됩니다. 인증 처리에 꼭 필요한 값이 아니면 응답에서 제외해야 합니다.

🔧 제안 수정안
 public record UserLoginResponse(
 		UUID userId,
 		String username,
-		String password,	// 이미 BCrypt에 의해 암호화된 비밀번호
 		UserRole userRole,
 		String name,
 		String nickname,
 		boolean isEnabled
 ) {
 	public static UserLoginResponse from(LoginUserDetailInfo userDetailInfo) {
 		return new UserLoginResponse(
 				userDetailInfo.userId(),
 				userDetailInfo.username(),
-				userDetailInfo.password(),
 				userDetailInfo.userRole(),
 				userDetailInfo.name(),
 				userDetailInfo.nickname(),
 				userDetailInfo.isEnabled()
 		);
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserLoginResponse.java`
around lines 9 - 27, The UserLoginResponse record currently exposes a password
field; remove the sensitive field by deleting the password component from the
record declaration and all references, and update the factory method
UserLoginResponse.from(LoginUserDetailInfo) to stop passing
userDetailInfo.password() (return the remaining fields: userId, username,
userRole, name, nickname, isEnabled); also search for any callers that construct
UserLoginResponse directly and update them to the new constructor/signature so
no password value is propagated in responses.
src/main/java/org/pgsg/user_service/auth/application/service/TokenService.java-51-54 (1)

51-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

리프레시 토큰 null 입력 시 500이 발생합니다

Line 53의 refreshToken::equals와 Line 88의 Optional.of(refreshToken)refreshToken == null일 때 UserServiceException(UNAUTHORIZED)가 아니라 NPE로 실패합니다.

수정 예시
 public void validateRefreshToken(UUID userId, String refreshToken) {
+    if (refreshToken == null) {
+        throw new UserServiceException(UserErrorCode.UNAUTHORIZED);
+    }
     tokenRepository.findRefreshToken(userId)
-            .filter(refreshToken::equals)
+            .filter(saved -> saved.equals(refreshToken))
             .orElseThrow(() -> new UserServiceException(UserErrorCode.UNAUTHORIZED));
 }
@@
-UUID userIdFromRefresh = Optional.of(refreshToken)
+UUID userIdFromRefresh = Optional.ofNullable(refreshToken)
         .filter(tokenProvider::validateToken)

Also applies to: 88-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/auth/application/service/TokenService.java`
around lines 51 - 54, The validateRefreshToken flow can NPE when refreshToken is
null: replace the unsafe method reference refreshToken::equals in
TokenService.validateRefreshToken with a null-safe comparison (e.g., use
Objects.equals(refreshToken, stored) or a predicate t ->
Objects.equals(refreshToken, t)) so a null input yields the intended
UserServiceException; likewise replace/or adjust the use of
Optional.of(refreshToken) elsewhere (the code around the Optional.of(...) at
lines ~88-89) to Optional.ofNullable(refreshToken) or explicitly check for null
and throw UserServiceException(UserErrorCode.UNAUTHORIZED) so null refreshToken
inputs do not cause a NullPointerException.
src/main/java/org/pgsg/user_service/auth/application/service/TokenService.java-83-93 (1)

83-93: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

토큰 파싱 예외를 UNAUTHORIZED로 일관 매핑해 주세요

Line 85(UUID.fromString)와 Line 90~92(클레임/유저 ID 파싱)에서 런타임 예외가 발생하면 현재 500으로 전파될 수 있습니다. 인증 실패는 UserServiceException(UserErrorCode.UNAUTHORIZED)로 통일하는 편이 안전합니다.

수정 예시
 public UUID verifyTokenPair(String accessToken, String refreshToken) {
-    UUID userIdFromAccess = UUID.fromString(tokenProvider.getSubjectFromExpiredAccessToken(accessToken));
+    try {
+        UUID userIdFromAccess = UUID.fromString(tokenProvider.getSubjectFromExpiredAccessToken(accessToken));

-    UUID userIdFromRefresh = Optional.of(refreshToken)
+        UUID userIdFromRefresh = Optional.ofNullable(refreshToken)
                 .filter(tokenProvider::validateToken)
                 .filter(token -> TokenType.REFRESH.matches(tokenProvider.parseClaims(token).get(JwtUtils.CLAIM_TOKEN_TYPE, String.class)))
                 .map(tokenProvider::getUserId)
                 .filter(userIdFromAccess::equals)
                 .orElseThrow(() -> new UserServiceException(UserErrorCode.UNAUTHORIZED));

-    validateRefreshToken(userIdFromRefresh, refreshToken);
-    return userIdFromRefresh;
+        validateRefreshToken(userIdFromRefresh, refreshToken);
+        return userIdFromRefresh;
+    } catch (RuntimeException e) {
+        throw new UserServiceException(UserErrorCode.UNAUTHORIZED);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/auth/application/service/TokenService.java`
around lines 83 - 93, In verifyTokenPair (TokenService.verifyTokenPair) ensure
any runtime parsing errors (UUID.fromString on
tokenProvider.getSubjectFromExpiredAccessToken, tokenProvider.parseClaims,
tokenProvider.getUserId or other tokenProvider calls) are caught and translated
into new UserServiceException(UserErrorCode.UNAUTHORIZED); wrap the calls that
parse the expired access token and the refresh token claims/ID in a try-catch
(or use safe-optional helpers) and throw
UserServiceException(UserErrorCode.UNAUTHORIZED) on any Exception so parsing
failures no longer propagate as 500.
🟡 Minor comments (8)
src/main/java/org/pgsg/user_service/auth/infrastructure/web/HttpRequestHeaderWrapper.java-22-23 (1)

22-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

헤더 이름 null 입력 시 NPE가 발생합니다.

addHeader, getHeader, getHeaders 모두 name.toLowerCase()를 바로 호출해서 null 입력 시 500 에러로 떨어집니다. 방어 로직을 추가해 주세요.

수정 예시
 public void addHeader(String name, String value) {
+    if (name == null) {
+        return;
+    }
     headerMap.put(name.toLowerCase(), value);
 }

 `@Override`
 public String getHeader(String name) {
+    if (name == null) {
+        return null;
+    }
     String lowerName = name.toLowerCase();
     if (headerMap.containsKey(lowerName)) {
         return headerMap.get(lowerName);
     }
@@
 `@Override`
 public Enumeration<String> getHeaders(String name) {
+    if (name == null) {
+        return Collections.emptyEnumeration();
+    }
     // 추가된 헤더에 관한 목록 단위 값을 반환
     String lowerName = name.toLowerCase();

Also applies to: 27-29, 39-42

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/auth/infrastructure/web/HttpRequestHeaderWrapper.java`
around lines 22 - 23, In addHeader, getHeader, and getHeaders, guard against a
null header name before calling name.toLowerCase() by validating the name
parameter (e.g., if name == null throw new IllegalArgumentException("header name
must not be null") or return appropriately) and only then use name.toLowerCase()
to access headerMap; update the methods named addHeader, getHeader, and
getHeaders to perform this null check and use headerMap consistently with the
lower-cased key.
src/main/java/org/pgsg/user_service/user/presentation/UserInternalController.java-18-20 (1)

18-20: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

username 파라미터 공백 검증이 빠져 있습니다.

내부 API라도 빈 문자열 요청은 컨트롤러 레벨에서 즉시 400 처리하는 편이 안전합니다.

수정 예시
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
 import lombok.RequiredArgsConstructor;
@@
 import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
@@
 `@RestController`
 `@RequiredArgsConstructor`
+@Validated
 `@RequestMapping`("/internal/v1/users")
 public class UserInternalController {
@@
-    public UserLoginResponse getUser(`@RequestParam`(value = "username") String username) {
+    public UserLoginResponse getUser(`@RequestParam`(value = "username") `@NotBlank` String username) {

Also applies to: 26-27

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/presentation/UserInternalController.java`
around lines 18 - 20, Controller UserInternalController accepts a username
parameter but lacks blank-string validation; update the controller methods that
take the "username" request parameter (the endpoints in UserInternalController
around the annotations and the methods at the other noted location) to enforce
non-blank input—either annotate the parameter with a bean-validation annotation
like `@NotBlank` on the method signature and enable `@Validated` on the controller,
or perform an explicit check and throw a 400 (e.g., ResponseStatusException with
HttpStatus.BAD_REQUEST) when username is null/empty/blank so empty-string
requests are rejected at the controller layer.
README.md-10-25 (1)

10-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

코드 펜스 언어 지정 누락(MD040)을 수정해 주세요.

Line 10의 코드 블록 시작에 언어를 명시하면 문서 린트 경고가 사라집니다 (```text 권장).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 10 - 25, Update the fenced code block that starts
with "src/main/java/org/pgsg/user_service/" in README.md to include a language
tag (e.g., change ``` to ```text) at the opening fence so the Markdown linter
rule MD040 is satisfied; edit the README.md code block header only and leave the
block contents unchanged.
src/main/java/org/pgsg/user_service/user/infrastructure/repository/UserQueryCondition.java-57-58 (1)

57-58: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Sort null 입력 방어가 없어 NPE가 발생할 수 있습니다.

Line [57]~Line [58]에서 sort가 null이면 바로 예외가 납니다. 빈 정렬로 안전 처리하는 가드 추가를 권장합니다.

수정 예시
 public static OrderSpecifier<?>[] getOrderSpecifier(Sort sort) {
+    if (sort == null || sort.isUnsorted()) {
+        return new OrderSpecifier[0];
+    }
     return sort.stream()
             .map(orderSort -> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/infrastructure/repository/UserQueryCondition.java`
around lines 57 - 58, The getOrderSpecifier method in UserQueryCondition doesn't
guard against a null Sort and can NPE; update getOrderSpecifier(Sort sort) to
treat null as an empty Sort (or early-return an empty OrderSpecifier<?>[]),
i.e., add a null-check for the sort parameter at the start of getOrderSpecifier
and return an empty array (or convert to Sort.unsorted()) before streaming so
downstream logic (stream/map/toArray) never receives a null.
src/main/java/org/pgsg/user_service/user/domain/model/ChatTimeRange.java-39-43 (1)

39-43: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

overlapsWith에 null 입력 방어가 없습니다.

Line 39-43에서 other가 null이면 NPE가 발생합니다. 퍼블릭 메서드라서 명시적으로 차단하는 게 안전합니다.

수정 예시
 public boolean overlapsWith(ChatTimeRange other) {
+    if (other == null) {
+        throw new UserServiceException(UserErrorCode.CHAT_TIME_REQUIRED);
+    }
     if (this.dayOfWeek != other.dayOfWeek) {
         return false;
     }
     return this.startTime.isBefore(other.endTime) && other.startTime.isBefore(this.endTime);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/org/pgsg/user_service/user/domain/model/ChatTimeRange.java`
around lines 39 - 43, The public method ChatTimeRange.overlapsWith currently
dereferences the parameter 'other' without null checks; add an explicit null
guard at the start of overlapsWith (e.g., use Objects.requireNonNull(other,
"other must not be null") or throw IllegalArgumentException) so that callers get
a clear, intentional error instead of an NPE, then keep the existing logic that
compares dayOfWeek and startTime/endTime.
src/main/java/org/pgsg/user_service/user/infrastructure/security/SecurityRoleCheck.java-17-23 (1)

17-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

권한 체크 메서드에 null 가드가 필요합니다.

role 또는 allowedUserRoles가 null이면 NPE가 발생해 권한 거부(false) 대신 예외(500)로 끝납니다. 입력 null은 false 처리로 방어해주세요.

🔧 제안 수정안
 `@Override`
 public boolean hasRole(UserRole role) {
+		if (role == null) return false;
 		return hasRole(List.of(role));
 }

 `@Override`
 public boolean hasRole(List<UserRole> allowedUserRoles) {
+		if (allowedUserRoles == null || allowedUserRoles.isEmpty()) return false;
 		return SecurityUtil.getCurrentUser()
 				.map(userDetails -> {

Also applies to: 37-37

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/infrastructure/security/SecurityRoleCheck.java`
around lines 17 - 23, In SecurityRoleCheck, add null guards so null inputs
return false instead of throwing: in hasRole(UserRole role) check if role is
null and return false before calling hasRole(List<UserRole>); in
hasRole(List<UserRole> allowedUserRoles) return false if allowedUserRoles is
null or empty (and treat any null entries as non-matching), then proceed with
the existing role-check logic; apply the same null-safe pattern to any other
overloads referenced (e.g., the method around line 37).
src/test/java/org/pgsg/user_service/user/application/UserCommandServiceTest.java-128-131 (1)

128-131: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

관리자 수정 테스트에서 닉네임 검증이 누락되었습니다

Line 120에서 닉네임을 변경하도록 커맨드를 만들지만, Line 129~131에서는 닉네임 결과를 검증하지 않아 회귀를 놓칠 수 있습니다.

수정 예시
         assertThat(result.getUserRole()).isEqualTo(UserRole.MANAGER);
         assertThat(result.getName()).isEqualTo("관리자이름");
+        assertThat(result.getNickname()).isEqualTo("관리자닉네임");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/org/pgsg/user_service/user/application/UserCommandServiceTest.java`
around lines 128 - 131, The test UserCommandServiceTest is missing an assertion
for the nickname change; after the existing assertions on result.getUserRole()
and result.getName(), add a nickname verification that asserts the returned
result's nickname equals the value set in the command (e.g., assert that
result.getNickname() isEqualTo the expected nickname string used when
constructing the update command) so nickname regressions are caught.
src/test/java/org/pgsg/user_service/auth/infrastructure/security/filter/JwtAuthenticationFilterTest.java-83-85 (1)

83-85: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

성공 케이스의 이름 헤더 검증이 약합니다

Line 84는 헤더가 null이어도 통과합니다. 성공 시나리오라면 최소 null 아님을 함께 검증해야 합니다.

수정 예시
-        assertThat(wrappedRequest.getHeader(JwtUtils.HEADER_USER_NAME)).isNotEqualTo("홍길동");
+        assertThat(wrappedRequest.getHeader(JwtUtils.HEADER_USER_NAME)).isNotNull();
+        assertThat(wrappedRequest.getHeader(JwtUtils.HEADER_USER_NAME)).isNotEqualTo("홍길동");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/org/pgsg/user_service/auth/infrastructure/security/filter/JwtAuthenticationFilterTest.java`
around lines 83 - 85, The test's assertion for the user-name header is weak
because isNotEqualTo("홍길동") passes even if the header is null; update the
assertion in JwtAuthenticationFilterTest to explicitly verify the header is not
null and then that its value is not equal to the raw Korean string by calling
wrappedRequest.getHeader(JwtUtils.HEADER_USER_NAME) and asserting both notNull
and notEqualTo("홍길동") (or assertThat(value).isNotNull().isNotEqualTo("홍길동")),
leaving the enabled header assertion unchanged.
🧹 Nitpick comments (9)
src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserChatTimeListResponse.java (1)

10-11: ⚡ Quick win

리스트 방어적 복사로 응답 불변성 보장

현재 전달받은 List 참조를 그대로 보관해서 외부 변경이 응답 객체에 반영될 수 있습니다. 생성 시 List.copyOf(...)로 고정하는 편이 안전합니다.

♻️ 제안 수정안
 public record UserChatTimeListResponse(
 		List<ChatTimeRangeInfo> chatTimeRanges
 ) {
 	public static UserChatTimeListResponse from(List<ChatTimeRangeInfo> chatTimeRanges) {
-		return new UserChatTimeListResponse(chatTimeRanges);
+		return new UserChatTimeListResponse(List.copyOf(chatTimeRanges));
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserChatTimeListResponse.java`
around lines 10 - 11, The static factory UserChatTimeListResponse.from currently
stores the passed List<ChatTimeRangeInfo> reference directly, risking external
mutation; change it to create an immutable defensive copy (e.g., use
List.copyOf(chatTimeRanges)) when constructing the UserChatTimeListResponse so
the internal field is unmodifiable and response immutability is preserved;
update the constructor or from method to accept the copied list and ensure any
getter returns the immutable list as well.
src/test/java/org/pgsg/user_service/user/infrastructure/security/SecurityRoleCheckTest.java (1)

34-127: ⚡ Quick win

hasRole(List<UserRole>) 오버로드도 테스트를 추가해 주세요.

현재 테스트는 단일 Role 경로만 검증하고 있어, 리스트 권한 경로 회귀를 잡기 어렵습니다. List 입력 기준 성공/실패/비로그인 케이스를 최소 세트로 보강하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/org/pgsg/user_service/user/infrastructure/security/SecurityRoleCheckTest.java`
around lines 34 - 127, Add unit tests in SecurityRoleCheckTest for the overload
hasRole(List<UserRole>): create three tests that mirror existing single-role
cases—(1) returns true when current user role matches one role in the provided
List (use mocked SecurityUtil.getCurrentUser and a mocked UserDetailsImpl
returning the matching getUserRole()), (2) returns false when current user role
does not match any role in the List, and (3) returns false when no user is
logged in (mock SecurityUtil.getCurrentUser to Optional.empty()). Use the same
mocking pattern as existing tests and assert boolean results from
securityRoleCheck.hasRole(List<UserRole>).
src/test/java/org/pgsg/user_service/user/infrastructure/repository/UserQueryRepositoryImplTest.java (1)

41-42: ⚡ Quick win

where(any(), any()) 스텁은 구현 변경에 취약합니다.

현재 구현에서 findById()findByUsername()은 모두 .where()에 정확히 2개의 술어(userId/username 조건 + deletedAt 조건)를 전달합니다. 테스트 스텁도 이를 where(any(), any())로 정확히 일치시키고 있습니다. 하지만 JPAQuery.where(...)는 가변인자 API이므로, 향후 구현에서 술어 개수를 1개 또는 3개로 변경하면 테스트가 불필요하게 깨질 수 있습니다. 술어 개수에 덜 민감한 방식(예: any(Predicate[].class))으로 스텁을 개선하면 테스트의 탄력성이 높아집니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/org/pgsg/user_service/user/infrastructure/repository/UserQueryRepositoryImplTest.java`
around lines 41 - 42, The test UserQueryRepositoryImplTest is stubbing
query.where(any(), any()) which is fragile to varargs changes; update the stub
for the JPAQuery.where call to be varargs-safe by using any(Predicate[].class)
(or equivalent) instead of any(), so modify the given(query.where(any(),
any())).willReturn(query) in the test to use any(Predicate[].class) to match any
number of predicates and leave given(query.fetchOne()).willReturn(mockUser)
unchanged; reference symbols: UserQueryRepositoryImplTest, query.where(...),
findById, findByUsername.
src/test/java/org/pgsg/user_service/auth/infrastructure/security/UserAuthenticatorImplTest.java (1)

92-104: ⚡ Quick win

블랙리스트 실패 경로에서 userQueryFacade 미호출도 명시적으로 검증해 주세요.

보안 실패 경로 회귀를 더 강하게 막으려면 Line 104 이후 verifyNoInteractions(userQueryFacade)를 추가하는 편이 좋습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/org/pgsg/user_service/auth/infrastructure/security/UserAuthenticatorImplTest.java`
around lines 92 - 104, The test verifyToken_Blacklisted currently asserts
tokenService.isBlacklisted was called and no further interactions occurred, but
does not explicitly assert that userQueryFacade was never invoked; update the
test (in the verifyToken_Blacklisted method) to add a verification that
userQueryFacade had no interactions (e.g., call
verifyNoInteractions/userQueryFacade) after the existing
then(tokenService).shouldHaveNoMoreInteractions() to ensure userQueryFacade is
not called on blacklisted tokens.
src/main/java/org/pgsg/user_service/user/infrastructure/loader/ChatTimeCsvLoader.java (1)

10-10: ⚡ Quick win

로드 완료 판정값(10000) 하드코딩은 유지보수 리스크가 큽니다.

CSV 레코드 수가 변경되면 로더가 잘못 스킵될 수 있습니다. 상수 기반 임계치 대신 “데이터 존재 여부” 또는 “기대 건수 외부 설정화”로 바꾸는 쪽이 안전합니다.

개선 예시
-private static final String CHECK_SQL = "SELECT CASE WHEN COUNT(*) >= 10000 THEN 1 ELSE 0 END FROM p_chat_time_range";
+private static final String CHECK_SQL = "SELECT CASE WHEN EXISTS (SELECT 1 FROM p_chat_time_range LIMIT 1) THEN 1 ELSE 0 END";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/infrastructure/loader/ChatTimeCsvLoader.java`
at line 10, The CHECK_SQL hardcodes a 10000 threshold making ChatTimeCsvLoader
brittle; change the logic to either check for any existing rows or make the
threshold configurable: replace the CHECK_SQL constant in ChatTimeCsvLoader with
a presence check SQL (e.g. using EXISTS) or load a configurable threshold (from
env/config) and use a parameterized query, then update the loader logic that
uses CHECK_SQL to act on existence/configured value rather than the fixed 10000
literal; ensure the configuration key and the constant name (CHECK_SQL) or new
constant/field are updated so callers use the new behavior.
src/main/java/org/pgsg/user_service/user/infrastructure/loader/CsvLoader.java (1)

12-12: ⚡ Quick win

load()throws Exception은 예외 계약이 너무 넓습니다.

Line 12는 호출자에게 과도한 처리 부담을 주므로, 도메인/인프라 예외로 좁히는 게 유지보수에 유리합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/infrastructure/loader/CsvLoader.java`
at line 12, The CsvLoader.load() signature currently declares throws Exception
which is too broad; change it to throw specific, narrower exceptions (e.g.,
IOException and a domain-specific checked exception like CsvParseException or
CsvLoadException, or convert to unchecked domain/infrastructure exceptions if
preferred). Update the CsvLoader interface's load() signature accordingly,
implement the new exception types (e.g., CsvParseException or CsvLoadException)
and adjust all implementing classes and callers to catch/propagate those
concrete exceptions instead of Exception so the error contract is explicit and
maintainable.
src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserPageResponse.java (1)

18-18: ⚡ Quick win

content는 방어적 복사로 고정하는 편이 안전합니다

현재 page.getContent() 참조를 그대로 보관해 응답 객체의 불변성이 약합니다. List.copyOf(...)로 고정하면 예기치 않은 변경을 막을 수 있습니다.

수정 예시
-        this.content = page.getContent();
+        this.content = List.copyOf(page.getContent());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserPageResponse.java`
at line 18, The response object's content currently stores the mutable reference
returned by page.getContent(), weakening immutability; in the UserPageResponse
constructor replace the direct assignment to this.content with a defensive copy
(e.g., via List.copyOf(page.getContent()) or Collections.unmodifiableList(new
ArrayList<>(page.getContent()))) so the UserPageResponse.content cannot be
mutated by external callers.
build.gradle (1)

33-33: ⚡ Quick win

SNAPSHOT 의존성은 main 배포의 재현성을 깨뜨릴 수 있습니다.

Line 33의 0.3.2-SNAPSHOT은 동일 커밋에서도 시점별로 다른 아티팩트를 받을 수 있습니다. 워크플로우에서 main 브랜치 병합 시 GCP 프로덕션 배포가 트리거되므로, 릴리스 버전(0.3.2)으로 고정하는 것이 안전합니다.

변경 예시
-	implementation 'org.pgsg:common:0.3.2-SNAPSHOT'
+	implementation 'org.pgsg:common:0.3.2'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.gradle` at line 33, Replace the unstable snapshot dependency
declaration 'org.pgsg:common:0.3.2-SNAPSHOT' in the build.gradle dependency list
with the fixed release version 'org.pgsg:common:0.3.2' so the build uses a
reproducible, immutable artifact; locate the dependency line that currently uses
0.3.2-SNAPSHOT and change it to 0.3.2 (or, if a version variable is used, update
that variable to the release value).
Dockerfile (1)

8-12: ⚡ Quick win

BuildKit 의존성을 Dockerfile에 명시하는 편이 안전합니다.

RUN --mount=type=secret는 BuildKit 전제입니다. 상단에 syntax directive를 명시하면 로컬/CI 파서 차이로 인한 빌드 실패를 줄일 수 있습니다. 현재 Dockerfile에는 이 directive가 없어 로컬 빌드 시 BuildKit이 기본으로 활성화되지 않으면 실패할 수 있습니다.

제안 diff
+ # syntax=docker/dockerfile:1.7
  # 빌드 스테이지
  FROM gradle:8.7-jdk21 AS build
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 8 - 12, The Dockerfile uses RUN --mount=type=secret
(seen in the RUN --mount=type=secret,id=GPR_USER ... RUN
--mount=type=secret,id=GPR_TOKEN ... gradle bootJar) which requires BuildKit;
add a syntax directive at the top of the Dockerfile (e.g., starting line: #
syntax=docker/dockerfile:1) so Docker clients and CI that do not enable BuildKit
by default will still parse the secret mount syntax correctly; update the file
to include that single-line syntax directive before any FROM or RUN
instructions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cc123b3-fccc-4a34-97ea-6a92beb30f21

📥 Commits

Reviewing files that changed from the base of the PR and between dd53a45 and d2ed978.

⛔ Files ignored due to path filters (2)
  • src/main/resources/csv/p_chat_time_range.csv is excluded by !**/*.csv
  • src/main/resources/csv/p_user.csv is excluded by !**/*.csv
📒 Files selected for processing (117)
  • .env.example
  • .github/actions/scale-vm/action.yaml
  • .github/workflows/deploy.yaml
  • .github/workflows/scale-user-service.yaml
  • .gitignore
  • Dockerfile
  • README.md
  • build.gradle
  • deploy/.env.template
  • deploy/ar-image-retention-policy.json
  • deploy/docker-compose.prod.yaml
  • deploy/promtail-config.yml
  • docker-compose.yml
  • gradlew
  • src/main/java/org/pgsg/user_service/UserServiceApplication.java
  • src/main/java/org/pgsg/user_service/auth/application/dto/command/LoginUserCommand.java
  • src/main/java/org/pgsg/user_service/auth/application/dto/command/ReissueUserCommand.java
  • src/main/java/org/pgsg/user_service/auth/application/dto/command/SignupUserCommand.java
  • src/main/java/org/pgsg/user_service/auth/application/dto/info/AuthInfo.java
  • src/main/java/org/pgsg/user_service/auth/application/dto/info/SignupInfo.java
  • src/main/java/org/pgsg/user_service/auth/application/service/AuthService.java
  • src/main/java/org/pgsg/user_service/auth/application/service/TokenService.java
  • src/main/java/org/pgsg/user_service/auth/domain/TokenRepository.java
  • src/main/java/org/pgsg/user_service/auth/domain/UserAuthenticator.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/redis/RedisTokenRepository.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/security/UserAuthenticatorImpl.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/security/UserDetailsServiceImpl.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/security/config/JwtConfig.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/security/config/UserAuthenticatorConfig.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/security/config/UserSecurityConfig.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/security/filter/JwtAuthenticationFilter.java
  • src/main/java/org/pgsg/user_service/auth/infrastructure/web/HttpRequestHeaderWrapper.java
  • src/main/java/org/pgsg/user_service/auth/presentation/AuthController.java
  • src/main/java/org/pgsg/user_service/auth/presentation/AuthInternalController.java
  • src/main/java/org/pgsg/user_service/auth/presentation/dto/request/UserLoginRequest.java
  • src/main/java/org/pgsg/user_service/auth/presentation/dto/request/UserReissueRequest.java
  • src/main/java/org/pgsg/user_service/auth/presentation/dto/request/UserSignupRequest.java
  • src/main/java/org/pgsg/user_service/auth/presentation/dto/request/UserVerifyRequest.java
  • src/main/java/org/pgsg/user_service/auth/presentation/dto/response/UserLoginResponse.java
  • src/main/java/org/pgsg/user_service/auth/presentation/dto/response/UserSignupResponse.java
  • src/main/java/org/pgsg/user_service/auth/presentation/dto/response/UserVerifyResponse.java
  • src/main/java/org/pgsg/user_service/user/application/UserCommandFacade.java
  • src/main/java/org/pgsg/user_service/user/application/UserCommandService.java
  • src/main/java/org/pgsg/user_service/user/application/UserQueryFacade.java
  • src/main/java/org/pgsg/user_service/user/application/UserQueryService.java
  • src/main/java/org/pgsg/user_service/user/application/dto/command/CreateChatTimeCommand.java
  • src/main/java/org/pgsg/user_service/user/application/dto/command/CreateUserCommand.java
  • src/main/java/org/pgsg/user_service/user/application/dto/command/UpdateUserAdminCommand.java
  • src/main/java/org/pgsg/user_service/user/application/dto/command/UpdateUserSelfCommand.java
  • src/main/java/org/pgsg/user_service/user/application/dto/info/ChatTimeRangeInfo.java
  • src/main/java/org/pgsg/user_service/user/application/dto/info/LoginUserDetailInfo.java
  • src/main/java/org/pgsg/user_service/user/application/dto/info/UserDetailInfo.java
  • src/main/java/org/pgsg/user_service/user/application/dto/query/SearchChatTimeQuery.java
  • src/main/java/org/pgsg/user_service/user/application/dto/query/SearchUserQuery.java
  • src/main/java/org/pgsg/user_service/user/application/dto/result/UserDeleteResult.java
  • src/main/java/org/pgsg/user_service/user/application/dto/result/UserSearchResult.java
  • src/main/java/org/pgsg/user_service/user/application/dto/result/UserUpdateResult.java
  • src/main/java/org/pgsg/user_service/user/domain/exception/UserErrorCode.java
  • src/main/java/org/pgsg/user_service/user/domain/exception/UserServiceException.java
  • src/main/java/org/pgsg/user_service/user/domain/model/ChatTimeRange.java
  • src/main/java/org/pgsg/user_service/user/domain/model/User.java
  • src/main/java/org/pgsg/user_service/user/domain/model/UserRole.java
  • src/main/java/org/pgsg/user_service/user/domain/repository/UserQueryRepository.java
  • src/main/java/org/pgsg/user_service/user/domain/repository/UserRepository.java
  • src/main/java/org/pgsg/user_service/user/domain/service/RoleCheck.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/loader/AbstractCsvLoader.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/loader/ChatTimeCsvLoader.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/loader/CsvLoader.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/loader/UserCsvLoader.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/loader/UserServiceCsvDataLoader.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/repository/JpaUserRepository.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/repository/UserQueryCondition.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/repository/UserQueryRepositoryImpl.java
  • src/main/java/org/pgsg/user_service/user/infrastructure/security/SecurityRoleCheck.java
  • src/main/java/org/pgsg/user_service/user/presentation/UserController.java
  • src/main/java/org/pgsg/user_service/user/presentation/UserInternalController.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/request/ChatTimeRequest.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/request/UserAdminUpdateRequest.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/request/UserChatTimeSearchRequest.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/request/UserSearchRequest.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/request/UserSelfUpdateRequest.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserChatTimeListResponse.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserDeleteResponse.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserDetailResponse.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserLoginResponse.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserPageResponse.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserSearchResponse.java
  • src/main/java/org/pgsg/user_service/user/presentation/dto/response/UserUpdateResponse.java
  • src/main/java/org/pgsg/user_service/user/presentation/exception/UserExceptionHandler.java
  • src/main/resources/application-user-error.yml
  • src/main/resources/application.yaml
  • src/main/resources/application.yml
  • src/main/resources/data.sql
  • src/test/java/org/pgsg/user_service/UserServiceApplicationTests.java
  • src/test/java/org/pgsg/user_service/auth/application/service/AuthServiceTest.java
  • src/test/java/org/pgsg/user_service/auth/application/service/TokenBlacklistTest.java
  • src/test/java/org/pgsg/user_service/auth/application/service/TokenServiceTest.java
  • src/test/java/org/pgsg/user_service/auth/infrastructure/security/UserAuthenticatorImplTest.java
  • src/test/java/org/pgsg/user_service/auth/infrastructure/security/filter/JwtAuthenticationFilterTest.java
  • src/test/java/org/pgsg/user_service/auth/infrastructure/web/HttpRequestHeaderWrapperTest.java
  • src/test/java/org/pgsg/user_service/auth/presentation/AuthControllerTest.java
  • src/test/java/org/pgsg/user_service/auth/presentation/AuthInternalControllerTest.java
  • src/test/java/org/pgsg/user_service/user/application/UserCommandFacadeTest.java
  • src/test/java/org/pgsg/user_service/user/application/UserCommandServiceTest.java
  • src/test/java/org/pgsg/user_service/user/application/UserQueryFacadeTest.java
  • src/test/java/org/pgsg/user_service/user/application/UserQueryServiceTest.java
  • src/test/java/org/pgsg/user_service/user/domain/exception/UserErrorCodeTest.java
  • src/test/java/org/pgsg/user_service/user/domain/model/ChatTimeRangeTest.java
  • src/test/java/org/pgsg/user_service/user/domain/model/UserRoleTest.java
  • src/test/java/org/pgsg/user_service/user/domain/model/UserTest.java
  • src/test/java/org/pgsg/user_service/user/infrastructure/repository/UserOrderSpecifierTest.java
  • src/test/java/org/pgsg/user_service/user/infrastructure/repository/UserQueryRepositoryImplTest.java
  • src/test/java/org/pgsg/user_service/user/infrastructure/repository/UserSearchConditionTest.java
  • src/test/java/org/pgsg/user_service/user/infrastructure/security/SecurityRoleCheckTest.java
  • src/test/java/org/pgsg/user_service/user/presentation/exception/UserExceptionHandlerTest.java
  • src/test/resources/application.yml
  • src/test/resources/data-test.sql
💤 Files with no reviewable changes (1)
  • src/main/resources/application.yaml

Comment thread src/main/resources/data.sql
@HyeonBin2379 HyeonBin2379 merged commit 72f7589 into main May 20, 2026
4 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in 8949 project May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants