Conversation
Walkthrough배포 워크플로우가 SSH/Compose 방식에서 AWS ASG 롤링 리프레시로 전환되었고, 다수의 컨트롤러 베이스 경로가 "/api" 프리픽스로 일괄 변경되었습니다. 메시지 저장 로직은 kakaoId 대신 userId를 사용하도록 업데이트되었고, Club 생성 시 멤버 수 증가 시점과 null 가드가 추가되었습니다. ES 매핑의 createdAt 형식이 초 단위로 통일되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Dev as Developer
participant GH as GitHub Actions
participant AWS as AWS (IAM)
participant ASG as Auto Scaling Group
Dev->>GH: Push to branch "stage"
GH->>GH: Build (Gradle) / Docker build & push
GH->>AWS: Configure AWS credentials
GH->>ASG: start-instance-refresh (Rolling, MinHealthyPercentage=50)
ASG-->>GH: Instance refresh started
sequenceDiagram
autonumber
participant API as MessageService.saveMessage
participant UR as UserRepository
participant CR as ChatRoomRepository
participant MSG as MessageRepository
API->>UR: findByUserId(userId)
UR-->>API: Optional<User>
API->>CR: findById(chatRoomId)
CR-->>API: Optional<ChatRoom>
API->>MSG: save(new Message(user, chatRoom, text))
MSG-->>API: Message
API-->>API: map to ChatMessageResponse
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/main/java/com/example/onlyone/global/filter/JwtAuthenticationFilter.java (3)
69-72: /api 마이그레이션 불일치로 로그아웃 차단 가능현재 코드가 "/auth/logout"만 허용합니다. "/api/auth/logout"로 접속하면 탈퇴 사용자 로그아웃이 차단될 수 있습니다.
다음과 같이 두 경로를 모두 허용하거나 프리픽스 기준으로 판별하세요.
- if (!"/auth/logout".equals(request.getRequestURI())) { + String uri = request.getRequestURI(); + if (!"/auth/logout".equals(uri) && !"/api/auth/logout".equals(uri)) { throw new CustomException(ErrorCode.USER_WITHDRAWN); - } + }
59-61: NPE 가능성: kakaoId 클레임 누락 처리claims.get("kakaoId")가 null이면 NPE로 500이 날 수 있습니다. 명시적으로 UNAUTHORIZED 처리하세요.
- String kakaoIdString = claims.get("kakaoId").toString(); + Object kakaoIdClaim = claims.get("kakaoId"); + if (kakaoIdClaim == null) { + throw new CustomException(ErrorCode.UNAUTHORIZED); + } + String kakaoIdString = kakaoIdClaim.toString();
53-57: 긴급: JJWT API/버전 충돌 — 수정 필요
- build.gradle에 io.jsonwebtoken:jjwt-api/impl/jackson 0.11.5와 0.12.4가 모두 선언되어 있습니다 (build.gradle, lines 58–65).
- JJWT 0.12.x에서는 파서 API가 변경되어 기존 호출(예: Jwts.parser().setSigningKey(key).build().parseClaimsJws(token))이 0.12.x와 호환되지 않습니다 — 0.12.x는 verifyWith()/parseSignedClaims() 등 새 API를 사용합니다.
- 조치(택1):
- 의존성을 0.11.5로 통일(0.12.4 제거)하여 기존 코드를 유지하거나,
- JwtAuthenticationFilter.java (src/main/java/com/example/onlyone/global/filter/JwtAuthenticationFilter.java, 해당 라인 약 53–57)에서 파서 호출을 0.12.x 규약으로 마이그레이션(예: Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt).getPayload()) 하여 코드 수정.
src/main/java/com/example/onlyone/domain/chat/controller/MessageRestController.java (1)
31-46: 요청 바디의 userId 신뢰는 가장 위험 — 인증 사용자에서 강제 주입 필요sendMessage가 request.getUserId()를 그대로 사용하면 임의 사용자 가장이 가능합니다. deleteMessage는 세션 사용자 기반이라 불일치입니다. 인증 사용자에서 userId를 취해 서비스로 전달하세요.
적용 예시:
- ChatMessageResponse response = - messageService.saveMessage(chatRoomId, request.getUserId(), request.getText()); + Long actorId = userService.getCurrentUser().getUserId(); + ChatMessageResponse response = + messageService.saveMessage(chatRoomId, actorId, request.getText());
🧹 Nitpick comments (22)
src/main/java/com/example/onlyone/domain/club/entity/Club.java (2)
59-61: builder 사용 시 memberCount null 위험 — 기본값 보존 필요
- Lombok @builder는 필드 초기값을 무시합니다. 현재 @NotNull + nullable=false인데 builder 경로에서 null이 들어갈 수 있어 제약 위반 위험이 남아있습니다.
아래처럼 @Builder.Default를 추가해 builder에서도 0L이 기본 적용되게 해주세요.
- @NotNull - private Long memberCount = 0L; + @NotNull + @Builder.Default + private Long memberCount = 0L;
100-106: increment/decrement 대칭성 및 null 가드 보완
- increment에서만 null을 가드하고 decrement는 null 시 NPE/음수 방지가 불완전합니다.
아래처럼 보완해 주세요.
public void decrementMemberCount() { - this.memberCount = Math.max(0L, this.memberCount - 1); + if (this.memberCount == null || this.memberCount <= 0) { + this.memberCount = 0L; + } else { + this.memberCount--; + } }src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java (2)
13-16: 미사용 import 정리
- LocalDateTime, JsonFormat, DateFormat(별도 import)은 더 이상 사용되지 않습니다. 제거해 경고를 없애 주세요.
-import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatter; - -import com.fasterxml.jackson.annotation.JsonFormat;또한
org.springframework.data.elasticsearch.annotations.DateFormat단일 import가 필요 없다면 제거하세요(와일드카드로 이미 포함 또는 미사용).
77-77: 포맷 상수화 및 null‑safe 포맷팅
- 포맷 문자열 하드코딩은 중복/오타 위험이 있습니다.
public class ClubDocument { + private static final DateTimeFormatter ES_DATE_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); ... - .createdAt(club.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))) + .createdAt(club.getCreatedAt() == null ? null : club.getCreatedAt().format(ES_DATE_FMT)).github/workflows/github-actions.yml (4)
60-65: ASG 롤링 리프레시 완료 대기 추가
- 현재는 트리거만 하고 종료합니다. 배포 안정성을 위해 완료까지 대기하세요.
- - name: Auto Scaling Group Refresh - run: | - aws autoscaling start-instance-refresh \ - --auto-scaling-group-name onlyone-buddkit-back-asg \ - --strategy "Rolling" \ - --preferences "MinHealthyPercentage=50" + - name: Start ASG Instance Refresh + id: asg_refresh + run: | + REFRESH_ID=$(aws autoscaling start-instance-refresh \ + --auto-scaling-group-name onlyone-buddkit-back-asg \ + --strategy "Rolling" \ + --preferences "MinHealthyPercentage=50" \ + --query 'InstanceRefreshId' --output text) + echo "id=$REFRESH_ID" >> $GITHUB_OUTPUT + + - name: Wait ASG Instance Refresh Completed + run: | + aws autoscaling wait instance-refresh-completed \ + --auto-scaling-group-name onlyone-buddkit-back-asg \ + --instance-refresh-id ${{ steps.asg_refresh.outputs.id }}추가로, 러너에 AWS CLI가 기본 탑재되어 있는지 확인해 주세요. 없으면 aws-cli 설치 스텝이 필요합니다.
45-51: latest 태그만 사용: 불변 이미지 태깅 권장
- latest만 사용하면 롤백/재현성 저하 및 캐시 이슈가 발생합니다. 커밋 SHA로 불변 태그를 함께 푸시하고, 런치 템플릿/유저데이터에서 해당 태그를 사용하도록 전환을 권장합니다.
- name: Server build & push uses: docker/build-push-action@v6 with: context: ./ file: ./Dockerfile platforms: linux/arm64 push: true - tags: ${{ secrets.DOCKER_REPO }}:latest + tags: | + ${{ secrets.DOCKER_REPO }}:latest + ${{ secrets.DOCKER_REPO }}:${{ github.sha }}또한 대상 인스턴스가 ARM(Graviton)인지 확인해 주세요. x86_64라면 플랫폼을 추가해야 합니다.
39-42: docker/login-action 최신 버전 권장
- v1은 구버전입니다. v3로 업데이트 권장.
- - name: Login to DockerHub - uses: docker/login-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v3
3-6: 수동 트리거 지원과 개행
- stage 브랜치 이외에 수동 실행(workflow_dispatch)도 함께 두면 운영 편의성이 높습니다. 또한 파일 말미 개행이 없습니다(YAMLlint).
on: push: branches: [ "stage" ] + workflow_dispatch: {}파일 끝에 개행 추가해 주세요.
src/main/java/com/example/onlyone/domain/user/repository/UserRepository.java (1)
12-12: findByUserId는 findById로 대체 가능
- 엔티티 PK가 userId라면 JPA 기본
findById로 동일 목적을 달성할 수 있습니다. 메서드 남기려면 호출부 일관성(예: OAuth 전용 findByKakaoId, 일반 조회 findById) 정리를 권장합니다.해당 메서드를 사용 중인 서비스에서
findById로 대체 가능한지 확인해 주세요.src/main/java/com/example/onlyone/domain/wallet/controller/WalletController.java (1)
8-8: 미사용 import 제거
jakarta.xml.bind.annotation.XmlType는 사용되지 않습니다. 제거하세요.src/main/resources/elasticsearch/club-create-settings.json (1)
98-101: 확인: createdAt 포맷(초 단위) 일치 — 선택적 개선 권장검증 결과 src/main/resources/elasticsearch/club-create-settings.json, src/main/resources/elasticsearch/club-mapping.json 및 src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java 모두 "yyyy-MM-dd'T'HH:mm:ss" 로 일치하므로 리포지토리 내에서는 포맷 불일치로 인한 인덱싱 실패 근거 없음.
- 권장(선택): 외부에서 밀리초 포함 타임스탬프가 유입될 가능성이 있으면 매핑에 멀티 포맷을 허용하세요. 예시(diff):
- "format": "yyyy-MM-dd'T'HH:mm:ss" + "format": "yyyy-MM-dd'T'HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS"
- 중복 정리 권고: mappings 정의가 두 곳에 중복되어 있으니 드리프트 방지를 위해 단일 소스로 관리(예: mapping.json은 mappings만, create-settings.json은 settings만)하세요. 파일: src/main/resources/elasticsearch/club-mapping.json, src/main/resources/elasticsearch/club-create-settings.json. 참고: src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java
src/main/java/com/example/onlyone/domain/payment/controller/PaymentController.java (1)
22-22: /api 프리픽스 적용 — 외부 연동 URL 동기화 필요토스 결제 성공/실패 리다이렉트·콜백 URL(관리 콘솔/프런트 환경변수)이 새 베이스 패스로 동기화됐는지 확인하세요.
src/main/java/com/example/onlyone/domain/notification/controller/NotificationController.java (1)
74-75: size 최대값 미검증(문서 불일치)주석에 “최대 100”이라 되어 있으나 검증이 없습니다. 쿼리 부하 방지를 위해 상한을 강제하세요.
- @RequestParam(defaultValue = "20") int size) { + @RequestParam(defaultValue = "20") int size) { + if (size > 100) size = 100;src/main/java/com/example/onlyone/domain/search/controller/SearchController.java (1)
44-48: 사양-구현 불일치: keyword 필수(2자 이상) 규칙 미강제문서상 keyword는 필수/2자 이상이지만 파라미터는 optional이며 길이 검증도 없습니다. 서비스 단에서 처리하지 않는다면 컨트롤러에서 400을 반환하도록 보완하세요. city/district 동시 제공 규칙도 함께 검증 권장.
예:
- public ResponseEntity<?> searchClubs( + public ResponseEntity<?> searchClubs( @RequestParam(required = false) String keyword, @RequestParam(required = false) String city, @RequestParam(required = false) String district, @RequestParam(required = false) Long interestId, @RequestParam(defaultValue = "MEMBER_COUNT") SearchFilterDto.SortType sortBy, @RequestParam(defaultValue = "0") int page) { - + if (keyword == null || keyword.trim().length() < 2) { + return ResponseEntity.badRequest().body(CommonResponse.fail("keyword must be at least 2 characters")); + } + if ((city == null) ^ (district == null)) { + return ResponseEntity.badRequest().body(CommonResponse.fail("city and district must be provided together")); + }(CommonResponse.fail 형태는 프로젝트 구현에 맞게 조정)
Also applies to: 51-56
src/main/java/com/example/onlyone/domain/image/controller/ImageController.java (2)
23-23: 개별 메서드에 /api 프리픽스 적용동작상 문제는 없으나, 일관성을 위해 클래스 레벨 @RequestMapping 적용을 고려해볼 수 있습니다.
24-33: 폴더 타입 검증 강화 제안imageFolderType을 String으로 받으면 오입력이 200/500로 이어질 수 있습니다. enum 바인딩 또는 사전 검증을 권장합니다.
예:
- public ResponseEntity<CommonResponse<PresignedUrlResponseDto>> generatePresignedUrl( - @PathVariable String imageFolderType, + public ResponseEntity<CommonResponse<PresignedUrlResponseDto>> generatePresignedUrl( + @PathVariable ImageFolderType imageFolderType, @Valid @RequestBody PresignedUrlRequestDto request) { - PresignedUrlResponseDto response = imageService.generatePresignedUrlWithImageUrl( - imageFolderType, + PresignedUrlResponseDto response = imageService.generatePresignedUrlWithImageUrl( + imageFolderType.name(), request.getFileName(), request.getContentType(), request.getImageSize() );서비스 시그니처가 String만 받는다면 enum→name() 변환으로 호환 유지 가능합니다.
src/main/java/com/example/onlyone/global/config/SecurityConfig.java (1)
76-83: CORS: 와일드카드/자격증명 조합은 위험 — 프로파일 분리 권장allowedOriginPatterns에 'https://*.ngrok-free.app'와 allowCredentials(true)가 함께 사용됩니다. 운영 프로파일에서는 정확한 도메인만 허용하고, 와일드카드는 dev/test로 제한하는 구성이 안전합니다.
src/main/java/com/example/onlyone/domain/club/service/ClubService.java (2)
127-158: 정원 체크 경쟁 상태 가능성 — 잠금/제약 고려count→비교→삽입 사이 경쟁으로 초과 가입 가능성이 있습니다. PESSIMISTIC_WRITE(클럽 행 잠금) 또는 ‘정원 초과’ 방지를 위한 최종 검증/DB 제약(조건부 업데이트) 적용을 검토하세요.
95-97: 에러코드 도메인 불일치(스케줄) — 의미 맞는 코드로 교체 권장MEMBER_CANNOT_MODIFY_SCHEDULE 대신 클럽 수정 문맥에 맞는 에러코드를 사용하세요.
src/main/java/com/example/onlyone/domain/chat/service/MessageService.java (2)
43-50: saveMessage의 userId 파라미터 — 컨트롤러에서 인증 사용자만 전달되도록 보장 필요컨트롤러 수정으로 위조 가능성은 해소되지만, 서비스 계층에서도 신뢰 경계가 명확해지도록 User를 직접 받거나 SecurityContext에서 조회하는 방식으로의 리팩터를 권장합니다.
76-86: 응답 senderId가 kakaoId — 식별자 일관성 재검토저장/검증은 userId 기반인데 응답은 kakaoId를 돌려줍니다. 클라이언트 계약 확인 후 userId로 통일하거나 둘 다 제공하도록 DTO를 조정하는 것이 혼란을 줄입니다.
src/main/java/com/example/onlyone/domain/user/controller/AuthController.java (1)
67-68: 로그 레벨 조정 제안성공 로그는 info로, 상세 정보는 debug로 제한해 로그 노이즈와 비용을 줄이세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (24)
.github/workflows/github-actions.yml(3 hunks)src/main/java/com/example/onlyone/domain/chat/controller/ChatRoomRestController.java(1 hunks)src/main/java/com/example/onlyone/domain/chat/controller/MessageRestController.java(1 hunks)src/main/java/com/example/onlyone/domain/chat/service/MessageService.java(1 hunks)src/main/java/com/example/onlyone/domain/club/controller/ClubController.java(1 hunks)src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java(3 hunks)src/main/java/com/example/onlyone/domain/club/entity/Club.java(1 hunks)src/main/java/com/example/onlyone/domain/club/service/ClubService.java(2 hunks)src/main/java/com/example/onlyone/domain/feed/controller/FeedController.java(1 hunks)src/main/java/com/example/onlyone/domain/feed/controller/FeedMainController.java(1 hunks)src/main/java/com/example/onlyone/domain/image/controller/ImageController.java(1 hunks)src/main/java/com/example/onlyone/domain/notification/controller/NotificationController.java(1 hunks)src/main/java/com/example/onlyone/domain/payment/controller/PaymentController.java(1 hunks)src/main/java/com/example/onlyone/domain/schedule/controller/ScheduleController.java(1 hunks)src/main/java/com/example/onlyone/domain/search/controller/SearchController.java(1 hunks)src/main/java/com/example/onlyone/domain/settlement/controller/SettlementController.java(1 hunks)src/main/java/com/example/onlyone/domain/user/controller/AuthController.java(3 hunks)src/main/java/com/example/onlyone/domain/user/controller/UserController.java(1 hunks)src/main/java/com/example/onlyone/domain/user/repository/UserRepository.java(1 hunks)src/main/java/com/example/onlyone/domain/wallet/controller/WalletController.java(1 hunks)src/main/java/com/example/onlyone/global/config/SecurityConfig.java(2 hunks)src/main/java/com/example/onlyone/global/filter/JwtAuthenticationFilter.java(1 hunks)src/main/resources/elasticsearch/club-create-settings.json(1 hunks)src/main/resources/elasticsearch/club-mapping.json(1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-16T00:20:44.262Z
Learnt from: ghkddlscks19
PR: GoormOnlyOne/OnlyOne-Back#145
File: src/main/java/com/example/onlyone/domain/user/service/UserService.java:84-99
Timestamp: 2025-09-16T00:20:44.262Z
Learning: JWT 인증에서 카카오 ID와 유저 ID 분리 시, JWT subject를 userId 기반으로 통일하고 findByKakaoId()는 OAuth 로그인 시에만 사용하는 것이 일관성 있는 아키텍처입니다.
Applied to files:
src/main/java/com/example/onlyone/domain/user/repository/UserRepository.java
🪛 actionlint (1.7.7)
.github/workflows/github-actions.yml
54-54: the runner of "aws-actions/configure-aws-credentials@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🪛 YAMLlint (1.37.1)
.github/workflows/github-actions.yml
[error] 65-65: no new line character at the end of file
(new-line-at-end-of-file)
🔇 Additional comments (17)
src/main/java/com/example/onlyone/domain/wallet/controller/WalletController.java (1)
20-20: 베이스 경로 변경에 따른 소비자 영향 점검
- "/api/users/wallet"로의 변경은 클라이언트/게이트웨이 라우팅/문서 스펙에 영향이 큽니다. 리다이렉트/백워드 호환(임시 alias) 필요 여부를 확인해 주세요.
src/main/java/com/example/onlyone/domain/club/controller/ClubController.java (1)
20-20: 베이스 경로 "/api/clubs" 변경 검증
- 외부 호출자, 스웨거 문서, 시큐리티 매칭(permitAll/인가 규칙) 업데이트가 모두 반영됐는지 확인 바랍니다. 구 경로에 대한 임시 호환이 필요한지 여부도 점검해주세요.
src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java (1)
19-21: @setting 파일명/경로 불일치 확인 필요src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java의 @setting이 '/elasticsearch/club-settings.json'을 참조하는데, 리포지토리에는 'club-create-settings.json'과 'club-settings.json' 둘 다 존재합니다. 다음 중 하나를 적용하세요:
- 의도한 파일이 club-create-settings.json이면 ClubDocument의 @setting을 '/elasticsearch/club-create-settings.json'로 수정.
- 의도한 파일이 club-settings.json이면 club-create-settings.json을 삭제하거나 파일명을 club-settings.json으로 통일.
- 빌드/배포 시 src/main/resources/elasticsearch/*.json이 최종 패키지에 포함되는지 확인.
src/main/resources/elasticsearch/club-mapping.json (1)
43-46: 확인 — createdAt 포맷(매핑 ↔ 앱) 일치; 운영 인덱스 혼재 여부 확인 필요
- 검증 결과: src/main/resources/elasticsearch/club-mapping.json 및 src/main/resources/elasticsearch/club-create-settings.json의 "format": "yyyy-MM-dd'T'HH:mm:ss"와 src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java에서 사용 중인 DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")가 일치합니다. 레포 내에서 createdAt 포맷 혼재 증거 없음.
- 리스크: Instant.now().toString() 등 다른 포맷을 생성하는 코드가 존재하므로, 외부에서 ES로 직접 쓰거나 과거에 다른 포맷으로 색인된 문서가 있다면 인덱싱 실패/정렬 왜곡 발생 가능 — 운영 인덱스(_source.createdAt 샘플) 및 ingestion 경로 확인 필요.
- 권장 (옵션): 외부 입력 가능성이 있으면 매핑을 관대하게 설정하여 호환성 확보. 예:
- "format": "yyyy-MM-dd'T'HH:mm:ss" + "format": "yyyy-MM-dd'T'HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS"운영 인덱스에서 createdAt 포맷 샘플(예: _source.createdAt 5건) 또는 인제스트 경로를 공유하세요.
src/main/java/com/example/onlyone/domain/chat/controller/ChatRoomRestController.java (1)
15-15: /api 프리픽스 적용 확인경로 변경 OK. 연관 클라이언트 호출·스웨거 경로 동기화만 확인 부탁드립니다.
src/main/java/com/example/onlyone/domain/feed/controller/FeedMainController.java (1)
28-28: /api 프리픽스 적용 확인문제 없습니다. 캐싱/SEO 등 외부 노출 경로가 있다면 리다이렉트 설정도 함께 점검하세요.
src/main/java/com/example/onlyone/domain/notification/controller/NotificationController.java (1)
20-20: /api 프리픽스 적용 확인정상입니다. 푸시/알림센터 링크가 하드코딩된 곳은 없는지 확인만 부탁드립니다.
src/main/java/com/example/onlyone/domain/search/controller/SearchController.java (1)
16-16: /api 프리픽스 적용 확인경로 변경 OK. 검색 페이지/딥링크의 베이스 URL 업데이트 여부만 확인해 주세요.
src/main/java/com/example/onlyone/domain/schedule/controller/ScheduleController.java (1)
22-22: /api 프리픽스 적용 확인 — 추가 검증 필요경로 변경 자체는 OK. 보안/문서/클라이언트 동기화(스웨거, 프런트, 게이트웨이/LB 리라이트, CORS) 점검 필요.
- 실행 결과: 원래 스크립트의 첫 정규식이 PCRE2 오류로 실패하여 클래스 레벨 @RequestMapping 중 '/api' 미사용 항목 전수검사가 완료되지 않았음.
- 로그아웃 경로 발견: src/main/java/com/example/onlyone/global/filter/JwtAuthenticationFilter.java (약 70행) 에서 "/auth/logout" 사용됨 — 이 경로를 '/api' 하위로 이동할지 예외로 둘지 의도 확인 필요.
- 재검증 권장(간단·안정형):
#!/bin/bash # 클래스 레벨 @RequestMapping 전수 출력(수동검토) rg -n --type java '^\s*@RequestMapping' -C2 # @RequestMapping 블록 중 '/api' 문자열이 없는 항목만 빠르게 필터링(출력은 수동검토 필요) rg -n --type java '^\s*@RequestMapping' -C2 | rg -v '/api'src/main/java/com/example/onlyone/domain/feed/controller/FeedController.java (1)
26-26: /api 프리픽스 변경에 따른 호환성 점검 요청보안/문서/클라이언트 동기화 확인 부탁드립니다: SecurityConfig 화이트리스트, CORS, Swagger 서버 URL, API 게이트웨이/프론트 호출 경로. 기존 비-/api 경로에 대한 일시적 리다이렉트/관용 처리 필요 여부도 검토해주세요.
src/main/java/com/example/onlyone/domain/chat/controller/MessageRestController.java (1)
23-23: 경로 변경 LGTM컨트롤러 베이스 경로의 /api 일괄 전환과 일치합니다.
src/main/java/com/example/onlyone/domain/club/service/ClubService.java (1)
53-55: 리더 UserClub 생성 전 memberCount 선증가 — 널 가드/롤백 보장 확인@transactional이라 예외 시 롤백되지만, Club.incrementMemberCount()가 null 초기화→증가를 안전하게 처리하는지 재확인 부탁드립니다(요청대로라면 1로 초기화).
src/main/java/com/example/onlyone/domain/settlement/controller/SettlementController.java (1)
20-20: 경로 변경 LGTM — 호출자/문서 동기화 확인클라이언트와 API 문서가 /api 프리픽스에 맞게 반영되었는지 확인 부탁드립니다.
src/main/java/com/example/onlyone/domain/user/controller/UserController.java (1)
28-28: 경로 변경 LGTM — 보안 화이트리스트 영향 없음 확인화이트리스트가 필요 없는 보호 API이므로 영향 미미하나, 프런트 호출 경로/Swagger 서버 URL 업데이트 여부만 확인 바랍니다.
src/main/java/com/example/onlyone/domain/chat/service/MessageService.java (1)
157-159: JDK 요구사항 확인(toList)Stream.toList()는 JDK 16+. 빌드 JDK가 17 이상인지 파이프라인에서 확인 바랍니다.
src/main/java/com/example/onlyone/domain/user/controller/AuthController.java (1)
71-76: 예외 로깅 시 민감정보 포함 여부 점검현재 메시지 중심 로깅이지만, 스택트레이스에 토큰/코드가 노출되지 않도록 상위 계층 로거 설정도 재검토 바랍니다.
src/main/java/com/example/onlyone/global/config/SecurityConfig.java (1)
55-70: AUTH_WHITELIST의 /api 전환 일관성 확인 — 로컬 재검증 필요샌드박스에서 자동 스크립트가 실패했습니다. 아래 수정된 스크립트를 로컬에서 실행해 /api로 통일되지 않은 공개(비-/api) 매핑이 있는지(의도적 제외: /api, /ws, /actuator, /swagger, /v3, /error, /favicon) 결과를 올려 주세요.
#!/bin/bash set -euo pipefail TMP=$(mktemp) rg -l --type java -S '@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|MessageMapping)\s*\(' src/main/java > "$TMP" || true if [ ! -s "$TMP" ]; then echo "NO_MAPPING_FILES" rm -f "$TMP" exit 0 fi python3 - "$TMP" <<'PY' import re,sys tmp = sys.argv[1] exclude_prefixes = ('/api','/ws','/actuator','/swagger','/v3','/error','/favicon') pattern = re.compile(r'@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|MessageMapping)\s*\(([^)]*)\)', re.S) found = [] with open(tmp,'r',encoding='utf-8') as fh: files = [l.strip() for l in fh if l.strip()] for f in files: try: s = open(f,'r',encoding='utf-8').read() except Exception as e: print(f"ERROR reading {f}: {e}", file=sys.stderr) continue for m in pattern.finditer(s): inside = m.group(2) lits = re.findall(r'"([^"]+)"', inside) for lit in lits: p = lit.strip() if not p.startswith('/'): continue if not any(p.startswith(pref) for pref in exclude_prefixes): lineno = s[:m.start()].count('\n') + 1 found.append(f"{f}:{lineno}:{m.group(1)} -> {p}") if not found: print("NO_NON_API_MAPPINGS_FOUND") else: for line in found: print(line) PY rm -f "$TMP"
| - name: AWS credentials | ||
| uses: aws-actions/configure-aws-credentials@v2 | ||
| with: |
There was a problem hiding this comment.
aws-actions/configure-aws-credentials 버전 업데이트
- actionlint 경고대로 v2는 더 이상 권장되지 않습니다. v4로 올리세요.
- - name: AWS credentials
- uses: aws-actions/configure-aws-credentials@v2
+ - name: AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ap-northeast-2가능하면 OIDC(Role to Assume)로 전환해 키 제거도 검토하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v2 | |
| with: | |
| - name: AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} | |
| aws-region: ap-northeast-2 |
🧰 Tools
🪛 actionlint (1.7.7)
54-54: the runner of "aws-actions/configure-aws-credentials@v2" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
| @Field(type = FieldType.Date, pattern = "yyyy-MM-dd'T'HH:mm:ss") | ||
| private String createdAt; |
There was a problem hiding this comment.
createdAt을 String으로 저장 + Date 필드 매핑: 타임존 주의
- 문자열로 포맷팅해 전송할 때 타임존 정보를 잃습니다. ES는 타임존 미지정 값을 UTC로 해석하므로, 서버 로컬(예: Asia/Seoul) 시간으로 포맷팅 시 9시간 오프셋 문제가 생길 수 있습니다.
대안:
- 가능하면
LocalDateTime유지 + 멀티 패턴(혹은 strict_date_optional_time)으로 파싱. - 문자열 유지 시 서버 표준 시간대를 UTC로 고정하여 포맷팅하거나 오프셋 포함 패턴 사용(운영 제약이 없다면).
🤖 Prompt for AI Agents
In src/main/java/com/example/onlyone/domain/club/document/ClubDocument.java
around lines 57-58, the createdAt field is declared as String while annotated as
an Elasticsearch Date, which loses timezone info when formatted; change the
field type to java.time.LocalDateTime (or java.time.OffsetDateTime/Instant if
you need explicit offsets) and update mapping to accept multi-patterns (or use
strict_date_optional_time with offset) so ES parses correctly; alternatively, if
you must keep String, ensure all code formats timestamps to UTC or include the
offset in the string (e.g., use ISO_OFFSET_DATE_TIME) before indexing and update
the @Field pattern to match the offset-aware format.
| log.error("🔍 [DEBUG] kakaoLogin 시작 - code={}", code); | ||
|
|
||
| try { | ||
| // 1. 인증 코드로 카카오 액세스 토큰 받기 | ||
| String kakaoAccessToken = kakaoService.getAccessToken(code); | ||
| log.error("✅ [DEBUG] kakaoAccessToken={}", kakaoAccessToken); | ||
|
|
||
| // 2. 카카오 액세스 토큰으로 사용자 정보 받기 | ||
| Map<String, Object> kakaoUserInfo = kakaoService.getUserInfo(kakaoAccessToken); | ||
| log.error("✅ [DEBUG] kakaoUserInfo={}", kakaoUserInfo); | ||
|
|
||
| // 3. 사용자 정보 저장 또는 업데이트 | ||
| Map<String, Object> loginResult = userService.processKakaoLogin(kakaoUserInfo, kakaoAccessToken); | ||
| User user = (User) loginResult.get("user"); | ||
| boolean isNewUser = (boolean) loginResult.get("isNewUser"); | ||
| log.error("✅ [DEBUG] userId={}, isNewUser={}", user.getUserId(), isNewUser); | ||
|
|
||
| // 4. JWT 토큰 생성 (Access + Refresh) | ||
| Map<String, String> tokens = userService.generateTokenPair(user); | ||
| log.error("✅ [DEBUG] tokens 생성됨 - accessToken={}, refreshToken={}", | ||
| tokens.get("accessToken"), tokens.get("refreshToken")); | ||
|
|
There was a problem hiding this comment.
심각: 인증 코드/토큰 등 민감정보 로그 유출
code, kakaoAccessToken, access/refreshToken, kakaoUserInfo를 그대로 로그로 남기고 있으며 log.error 레벨입니다. 즉시 제거/마스킹하세요.
적용 예시(민감정보 미출력, 로그 레벨 조정):
- log.error("🔍 [DEBUG] kakaoLogin 시작 - code={}", code);
+ log.debug("kakaoLogin 시작");
- log.error("✅ [DEBUG] kakaoAccessToken={}", kakaoAccessToken);
+ log.debug("kakaoAccessToken 수신");
- log.error("✅ [DEBUG] kakaoUserInfo={}", kakaoUserInfo);
+ log.debug("kakaoUserInfo 수신");
- log.error("✅ [DEBUG] userId={}, isNewUser={}", user.getUserId(), isNewUser);
+ log.debug("user resolved - userId={}, isNewUser={}", user.getUserId(), isNewUser);
- log.error("✅ [DEBUG] tokens 생성됨 - accessToken={}, refreshToken={}",
- tokens.get("accessToken"), tokens.get("refreshToken"));
+ log.debug("tokens 생성 완료");또한 성공 로그는 info, 실패는 warn/error로 일관되게 사용하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| log.error("🔍 [DEBUG] kakaoLogin 시작 - code={}", code); | |
| try { | |
| // 1. 인증 코드로 카카오 액세스 토큰 받기 | |
| String kakaoAccessToken = kakaoService.getAccessToken(code); | |
| log.error("✅ [DEBUG] kakaoAccessToken={}", kakaoAccessToken); | |
| // 2. 카카오 액세스 토큰으로 사용자 정보 받기 | |
| Map<String, Object> kakaoUserInfo = kakaoService.getUserInfo(kakaoAccessToken); | |
| log.error("✅ [DEBUG] kakaoUserInfo={}", kakaoUserInfo); | |
| // 3. 사용자 정보 저장 또는 업데이트 | |
| Map<String, Object> loginResult = userService.processKakaoLogin(kakaoUserInfo, kakaoAccessToken); | |
| User user = (User) loginResult.get("user"); | |
| boolean isNewUser = (boolean) loginResult.get("isNewUser"); | |
| log.error("✅ [DEBUG] userId={}, isNewUser={}", user.getUserId(), isNewUser); | |
| // 4. JWT 토큰 생성 (Access + Refresh) | |
| Map<String, String> tokens = userService.generateTokenPair(user); | |
| log.error("✅ [DEBUG] tokens 생성됨 - accessToken={}, refreshToken={}", | |
| tokens.get("accessToken"), tokens.get("refreshToken")); | |
| log.debug("kakaoLogin 시작"); | |
| try { | |
| // 1. 인증 코드로 카카오 액세스 토큰 받기 | |
| String kakaoAccessToken = kakaoService.getAccessToken(code); | |
| log.debug("kakaoAccessToken 수신"); | |
| // 2. 카카오 액세스 토큰으로 사용자 정보 받기 | |
| Map<String, Object> kakaoUserInfo = kakaoService.getUserInfo(kakaoAccessToken); | |
| log.debug("kakaoUserInfo 수신"); | |
| // 3. 사용자 정보 저장 또는 업데이트 | |
| Map<String, Object> loginResult = userService.processKakaoLogin(kakaoUserInfo, kakaoAccessToken); | |
| User user = (User) loginResult.get("user"); | |
| boolean isNewUser = (boolean) loginResult.get("isNewUser"); | |
| log.debug("user resolved - userId={}, isNewUser={}", user.getUserId(), isNewUser); | |
| // 4. JWT 토큰 생성 (Access + Refresh) | |
| Map<String, String> tokens = userService.generateTokenPair(user); | |
| log.debug("tokens 생성 완료"); |
🤖 Prompt for AI Agents
In src/main/java/com/example/onlyone/domain/user/controller/AuthController.java
around lines 34 to 55, remove or mask all sensitive values from logs (code,
kakaoAccessToken, tokens, full kakaoUserInfo) and stop logging them at error
level; instead log only non-sensitive context (e.g., "kakaoLogin started") and
safe identifiers (if needed, a non-sensitive userId or masked id) at info level
for success and use warn/error only for failures without sensitive details.
Update each debug log to omit the token/code/userinfo payloads (or replace with
a constant like "<masked>" if you must indicate presence) and change log.error
-> log.info for success traces; ensure any catch blocks log exception
messages/errors (not tokens) at warn/error level.
| // 5. refreshToken Redis에 저장 (local dev 시 주석 처리 가능) | ||
| // redisTemplate.opsForValue() | ||
| // .set(user.getUserId().toString(), tokens.get("refreshToken"), Duration.ofMillis(REFRESH_TOKEN_EXPIRE_TIME)); | ||
|
|
There was a problem hiding this comment.
리프레시 토큰 미보관(주석 처리) — 무효화/로테이션 불가
운영에서 refreshToken 저장이 비활성화되면 강제 로그아웃/블랙리스트가 불가합니다. 프로파일/환경변수로 저장 기능을 토글하도록 처리하거나 최소한 운영 프로파일에서는 활성화하세요.
🤖 Prompt for AI Agents
In src/main/java/com/example/onlyone/domain/user/controller/AuthController.java
around lines 56 to 59, the refresh token persistence to Redis is commented out
which prevents token invalidation/rotation in production; restore and gate this
behavior behind a configuration flag (e.g., application.profile or an env var
like ENABLE_REFRESH_TOKEN_STORE) so that: when the flag is true (and in
production profile) the code writes the refresh token to Redis with the proper
expiration, otherwise skip storing for local/dev; wire this flag from
application.properties or Environment, default to true for production profiles,
and add logging for success/failure of the Redis write so ops can detect storage
issues.
| // 사용자 상태 확인 - 탈퇴한 사용자인 경우 인증 거부, GUEST와 ACTIVE는 허용 | ||
| Optional<User> userOpt = userRepository.findByKakaoId(kakaoId); | ||
| log.error("userOpt = {}", userOpt.orElse(null)); | ||
| if (userOpt.isPresent()) { |
There was a problem hiding this comment.
PII/노이즈 로그: user 엔티티 직렬화 로깅 지양
에러 레벨로 엔티티 전체를 출력하면 PII 노출·로그 스팸 우려가 큽니다. presence 중심의 디버그 로그로 축소하세요.
다음 변경을 권장합니다.
- log.error("userOpt = {}", userOpt.orElse(null));
+ if (log.isDebugEnabled()) {
+ log.debug("User lookup by kakaoId={} present={}", kakaoId, userOpt.isPresent());
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (userOpt.isPresent()) { | |
| if (log.isDebugEnabled()) { | |
| log.debug("User lookup by kakaoId={} present={}", kakaoId, userOpt.isPresent()); | |
| } | |
| if (userOpt.isPresent()) { |
🤖 Prompt for AI Agents
In src/main/java/com/example/onlyone/global/filter/JwtAuthenticationFilter.java
around line 65, the code logs the entire user entity at error level which risks
PII exposure and noisy logs; replace that with a presence-focused, non-PII
debug/info log (e.g., "user present for token" or "user found" and optionally
log only a non-sensitive identifier or masked id), remove serialization of the
full entity from logs, and lower the log level from error to debug/info so only
presence is recorded without exposing sensitive fields.
#️⃣ Issue Number
📝 요약(Summary)
elasticsearch 수정 및 배포 적용
🛠️ PR 유형
어떤 변경 사항이 있나요?
📸스크린샷 (선택)
💬 공유사항 to 리뷰어
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
Summary by CodeRabbit