Skip to content

약관 동의 API를 분리한다.#50

Merged
strongmhk merged 2 commits intodevelopfrom
refactor/#49-terms-api-separate
Aug 10, 2025
Merged

약관 동의 API를 분리한다.#50
strongmhk merged 2 commits intodevelopfrom
refactor/#49-terms-api-separate

Conversation

@strongmhk
Copy link
Copy Markdown
Member

@strongmhk strongmhk commented Aug 10, 2025

📄 PR 요약

약관 동의 API를 분리한다.

✍🏻 PR 상세

  1. 약관 동의 API 분리
  • 기존 PUT /api/members/terms -> PUT /api/members/terms/push-notifications, PUT /api/members/terms/privacy 로 분리

👀 참고사항

✅ 체크리스트

  • PR 양식에 맞게 작성했습니다.
  • 모든 테스트가 통과했습니다.
  • 프로그램이 정상적으로 작동합니다.
  • 적절한 라벨을 설정했습니다.
  • 불필요한 코드를 제거했습니다.

🚪 연관된 이슈 번호

Closes #49

Summary by CodeRabbit

  • 신규 기능

    • 개인정보 처리방침 동의 및 푸시 알림 수신 동의 상태를 각각 별도의 엔드포인트에서 개별적으로 수정할 수 있도록 기능이 분리되었습니다.
    • 회원 탈퇴 시 관련 토큰과 푸시 토큰이 기기 단위로 안전하게 삭제됩니다.
  • 버그 수정

    • 각 동의 항목에 대해 필수 입력값 검증이 강화되었습니다.
  • 문서화

    • API 문서에 각 동의 항목별 요청 형식이 명확하게 반영되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 10, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

기존에 하나로 통합되어 있던 약관 동의 API가 개인정보 제공 동의와 푸시 알림 수신 동의로 각각 분리되었습니다. 관련 DTO, 서비스, 유스케이스, 컨트롤러가 모두 해당 분리 구조에 맞게 수정 및 재구현되었습니다.

Changes

Cohort / File(s) Change Summary
Privacy Policy DTO 추가
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPrivacyPolicyModifyRequest.java
개인정보 제공 동의 요청을 위한 DTO(MemberPrivacyPolicyModifyRequest) 신규 생성.
푸시 알림 동의 DTO 추가
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPushNotificationPolicyModifyRequest.java
푸시 알림 수신 동의 요청을 위한 DTO(MemberPushNotificationPolicyModifyRequest) 신규 생성 및 유효성 검사 추가.
유스케이스 분리 및 수정
src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java
단일 약관 동의 수정 메서드 삭제, 개인정보/푸시 알림 동의 각각의 메서드로 분리 및 hardDeleteMember 메서드 파라미터 변경.
도메인 서비스 인터페이스 변경
src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandService.java
단일 약관 동의 수정 메서드 삭제, 개인정보/푸시 알림 동의 각각의 메서드로 분리, hardDeleteMember 메서드 파라미터에 deviceId 추가.
도메인 서비스 구현 변경
src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandServiceImpl.java
단일 약관 동의 수정 메서드 삭제, 개인정보/푸시 알림 동의 각각의 메서드로 분리 및 필드별 업데이트 구현, hardDeleteMember 메서드에 deviceId 추가 및 토큰 삭제 로직 구현.
컨트롤러 API 분리 및 수정
src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java
단일 약관 동의 수정 엔드포인트 삭제, 개인정보/푸시 알림 동의 각각의 엔드포인트로 분리 및 연결, hardDeleteMember 메서드 파라미터 변경.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant UseCase
    participant Service

    Client->>Controller: PUT /terms/privacy (privacyPolicy)
    Controller->>UseCase: modifyMemberPrivacyPolicy(memberId, request)
    UseCase->>Service: modifyPrivacyPolicy(memberId, privacyPolicy)
    Service-->>UseCase: 완료
    UseCase-->>Controller: 완료
    Controller-->>Client: 성공 응답

    Client->>Controller: PUT /terms/push-notifications (pushNotificationPolicy)
    Controller->>UseCase: modifyMemberPushNotificationPolicy(memberId, request)
    UseCase->>Service: modifyPushNotificationPolicy(memberId, pushNotificationPolicy)
    Service-->>UseCase: 완료
    UseCase-->>Controller: 완료
    Controller-->>Client: 성공 응답
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15 minutes

Assessment against linked issues

Objective Addressed Explanation
푸시 알림 수신 동의, 개인정보 제공 동의에 대한 각각의 API 구현 (#49)

Assessment against linked issues: Out-of-scope changes

(해당 변경사항 내에서 범위를 벗어나는 코드 변경은 발견되지 않았습니다.)

Suggested labels

:sparkles: feature

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Semgrep (1.128.1)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPushNotificationPolicyModifyRequest.java

{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]}

src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java

{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]}

src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandService.java

{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]}

  • 3 others

📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18337ba and 70b51b1.

📒 Files selected for processing (6)
  • src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPrivacyPolicyModifyRequest.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPushNotificationPolicyModifyRequest.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandService.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandServiceImpl.java (3 hunks)
  • src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#49-terms-api-separate

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3231b7c and 18337ba.

📒 Files selected for processing (6)
  • src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPrivacyPolicyModifyRequest.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPushNotificationPolicyModifyRequest.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandService.java (1 hunks)
  • src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandServiceImpl.java (2 hunks)
  • src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Files:

  • src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPrivacyPolicyModifyRequest.java
  • src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPushNotificationPolicyModifyRequest.java
  • src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java
  • src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java
  • src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandServiceImpl.java
  • src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandService.java
**/*{Controller,Service,Repository}.java

📄 CodeRabbit Inference Engine (AGENT.md)

Controller/Service는 getUser, createUser, removeUser 등 동사 기반으로 명명하고, Repository는 findByUser, insertUser, deleteUser처럼 명확히 구분합니다.

Files:

  • src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java
  • src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandService.java
🧬 Code Graph Analysis (6)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPrivacyPolicyModifyRequest.java (2)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberTermsModifyRequest.java (1)
  • MemberTermsModifyRequest (6-15)
src/main/java/akuma/whiplash/domains/member/persistence/entity/MemberEntity.java (2)
  • updatePrivacyPolicy (61-64)
  • Entity (25-70)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPushNotificationPolicyModifyRequest.java (2)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberTermsModifyRequest.java (1)
  • MemberTermsModifyRequest (6-15)
src/main/java/akuma/whiplash/domains/member/persistence/entity/MemberEntity.java (3)
  • updatePushNotificationPolicy (66-69)
  • Entity (25-70)
  • updatePrivacyPolicy (61-64)
src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java (2)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberTermsModifyRequest.java (1)
  • MemberTermsModifyRequest (6-15)
src/main/java/akuma/whiplash/domains/member/persistence/entity/MemberEntity.java (1)
  • Entity (25-70)
src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java (2)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberTermsModifyRequest.java (1)
  • MemberTermsModifyRequest (6-15)
src/main/java/akuma/whiplash/domains/member/persistence/entity/MemberEntity.java (1)
  • Entity (25-70)
src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandServiceImpl.java (2)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberTermsModifyRequest.java (1)
  • MemberTermsModifyRequest (6-15)
src/main/java/akuma/whiplash/domains/member/persistence/entity/MemberEntity.java (3)
  • Entity (25-70)
  • updatePushNotificationPolicy (66-69)
  • updatePrivacyPolicy (61-64)
src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandService.java (2)
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberTermsModifyRequest.java (1)
  • MemberTermsModifyRequest (6-15)
src/main/java/akuma/whiplash/domains/member/persistence/entity/MemberEntity.java (1)
  • Entity (25-70)
🔇 Additional comments (3)
src/main/java/akuma/whiplash/domains/member/domain/service/MemberCommandServiceImpl.java (1)

33-33: 동의 타임스탬프 갱신 정책 확인 요청

현재 MemberEntity#updatePrivacyPolicy(boolean) 구현은 true/false 상관없이 privacyAgreedAtLocalDateTime.now()로 갱신합니다. 이와 동일하게 updatePushNotificationPolicy(...)pushAgreedAt를 항상 갱신하고 있습니다.

도메인 의도에 맞춰 아래 중 하나로 결정해주세요:

  • “마지막 상태 변경 시각” → 현 구현 유지
  • “실제 동의 시각” → privacyAgreedAt(및 컬럼명)을 updatedAt 성격으로 변경하거나, true일 때만 갱신하도록 로직 수정

검토가 필요한 위치:

  • src/main/java/akuma/whiplash/domains/member/persistence/entity/MemberEntity.java
    • updatePrivacyPolicy 메서드 내 privacyAgreedAt = LocalDateTime.now()
    • updatePushNotificationPolicy 메서드 내 pushAgreedAt = LocalDateTime.now()

필요 시 true/false 분기 처리 예시나 컬럼명 변경안 제공해드리겠습니다.

src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java (2)

44-44: 회원 탈퇴 API 어노테이션 유지 확인

변경 사항 없음으로 보이며 현 구현과 일관됩니다. 별도 이슈 없습니다.


6-7: 검증 구성 완료: @NotNull 어노테이션 적용됨
MemberPrivacyPolicyModifyRequestMemberPushNotificationPolicyModifyRequest의 Boolean 필드에 이미 @NotNull이 적용되어 있어 null 입력 시 400 Bad Request가 반환됩니다. 추가 수정이 필요하지 않습니다.

Comment on lines +7 to +12
@Builder
public record MemberPrivacyPolicyModifyRequest(
@Schema(description = "개인정보 수집 동의 여부")
@NotNull(message = "개인정보 수집 동의 여부를 선택해주세요.")
Boolean privacyPolicy
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

DTO 분리 방향 적합 + 스키마 required 표기 제안

구현은 적절합니다. 문서화 정확도를 위해 필드에 required 명시를 권장합니다. 또한 단일 필드 record에서는 @builder가 과한 경우가 있어 제거 검토를 제안합니다.

적용 예:

-@Builder
 public record MemberPrivacyPolicyModifyRequest(
-    @Schema(description = "개인정보 수집 동의 여부")
+    @Schema(description = "개인정보 수집 동의 여부", requiredMode = Schema.RequiredMode.REQUIRED)
     @NotNull(message = "개인정보 수집 동의 여부를 선택해주세요.")
     Boolean privacyPolicy
 ) {
 }
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPrivacyPolicyModifyRequest.java
around lines 7 to 12, the @Builder annotation is unnecessary for a single-field
record and should be removed. Also, add the 'required = true' attribute to the
@Schema annotation on the privacyPolicy field to accurately reflect its
mandatory nature in the API documentation.

package akuma.whiplash.domains.member.application.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

DTO 분리 및 검증 추가 적절 + 스키마 required 표기 제안

  • @NotNull 추가는 적절합니다.
  • 문서 일관성을 위해 스웨거 스키마에 requiredMode 지정 권장.
  • 단일 필드 record에서 @builder는 불필요할 수 있어 제거 검토.

적용 예:

 public record MemberPushNotificationPolicyModifyRequest(
-    @Schema(description = "푸시 알림 수신 동의 여부")
+    @Schema(description = "푸시 알림 수신 동의 여부", requiredMode = Schema.RequiredMode.REQUIRED)
     @NotNull(message = "푸시 알림 수신 동의 여부를 선택해주세요.")
     Boolean pushNotificationPolicy
 ) {
 }

그리고 @builder 제거 고려:

-@Builder
 public record MemberPushNotificationPolicyModifyRequest( ... ) { }

Also applies to: 8-12

🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/member/application/dto/request/MemberPushNotificationPolicyModifyRequest.java
around lines 4 to 12, add requiredMode attribute to the Swagger schema
annotations for fields annotated with @NotNull to ensure the API documentation
correctly marks them as required. Also, since this is a single-field record,
remove the @Builder annotation as it is unnecessary and simplifies the code.

Comment on lines +16 to 22
public void modifyMemberPrivacyPolicy(Long memberId, MemberPrivacyPolicyModifyRequest request) {
memberCommandService.modifyPrivacyPolicy(memberId, request.privacyPolicy());
}

public void modifyMemberPushNotificationPolicy(Long memberId, MemberPushNotificationPolicyModifyRequest request) {
memberCommandService.modifyPushNotificationPolicy(memberId, request.pushNotificationPolicy());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

검증 경계 강화: @Valid 적용 및(또는) @validated 활성화 권장

DTO에 @NotNull이 있으므로 Controller에서 @Valid가 반드시 필요합니다. 방어적으로 UseCase에도 @Valid를 붙이고 클래스에 @validated를 추가하면, Controller에서 누락되어도 런타임 검증이 동작합니다.

메서드 시그니처 예:

- public void modifyMemberPrivacyPolicy(Long memberId, MemberPrivacyPolicyModifyRequest request) {
+ public void modifyMemberPrivacyPolicy(Long memberId, @jakarta.validation.Valid MemberPrivacyPolicyModifyRequest request) {
     memberCommandService.modifyPrivacyPolicy(memberId, request.privacyPolicy());
 }

- public void modifyMemberPushNotificationPolicy(Long memberId, MemberPushNotificationPolicyModifyRequest request) {
+ public void modifyMemberPushNotificationPolicy(Long memberId, @jakarta.validation.Valid MemberPushNotificationPolicyModifyRequest request) {
     memberCommandService.modifyPushNotificationPolicy(memberId, request.pushNotificationPolicy());
 }

클래스 레벨 활성화(파일 상단 import 필요):

import org.springframework.validation.annotation.Validated;

@UseCase
@Validated
@RequiredArgsConstructor
public class MemberUseCase { ... }

컨트롤러 검증/엔드포인트 분리 상태 점검 스크립트:


🏁 Script executed:

#!/bin/bash
set -e

echo "1) 컨트롤러에서 신규 엔드포인트 존재 여부 확인"
rg -n -A 3 -B 3 '@PutMapping\("?/api/members/terms/privacy"?\)'
rg -n -A 3 -B 3 '@PutMapping\("?/api/members/terms/push-notifications"?\)'

echo
echo "2) 컨트롤러에서 @Valid 적용 여부 확인"
rg -n -A 2 -B 2 'MemberPrivacyPolicyModifyRequest'
rg -n -A 2 -B 2 'MemberPushNotificationPolicyModifyRequest'
rg -n '@Valid' | wc -l

echo
echo "3) 구 DTO/메서드 잔존 여부 확인"
rg -n 'MemberTermsModifyRequest' || true
rg -n 'modifyMemberTermsInfo' || true

Length of output: 139


🏁 Script executed:

#!/bin/bash
set -e

echo "1) MemberUseCase.java 위치 확인"
MEMBER_USECASE=$(fd MemberUseCase.java -t f | head -n1)
echo "$MEMBER_USECASE"
echo

echo "2) MemberUseCase.java 상단 50줄 출력"
sed -n '1,50p' "$MEMBER_USECASE"
echo

echo "3) @Validated 어노테이션 존재 여부"
rg -n '@Validated' "$MEMBER_USECASE" || echo "→ @Validated 없음"
echo

echo "4) @Valid 어노테이션 존재 여부"
rg -n '@Valid' "$MEMBER_USECASE" || echo "→ @Valid 없음"
echo

echo "5) 컨트롤러에서 @PutMapping 확인"
rg -n '@PutMapping' src/main/java || echo "→ @PutMapping 호출 없음"
echo

echo "6) 컨트롤러 메서드 파라미터에 @Valid 적용 여부"
rg -n 'public .*(@Valid .*Request)' src/main/java || echo "→ 컨트롤러에 @Valid 미사용"

Length of output: 3771


MemberUseCase에 방어적 검증 적용(@validated@Valid)

컨트롤러에는 이미 @Valid가 적용되어 있으므로, UseCase 레이어에 @Validated와 파라미터별 @Valid를 추가해 검증 경계를 강화하세요.

  • 수정 대상:
    src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java
    • 클래스 상단에 @Validated 추가
    modifyMemberPrivacyPolicymodifyMemberPushNotificationPolicy 메서드 파라미터에 @jakarta.validation.Valid 추가
--- a/src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java
+++ b/src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java
@@
 @UseCase
-@RequiredArgsConstructor
+@Validated
 @RequiredArgsConstructor
 public class MemberUseCase {
@@
-    public void modifyMemberPrivacyPolicy(Long memberId, MemberPrivacyPolicyModifyRequest request) {
+    public void modifyMemberPrivacyPolicy(Long memberId, @jakarta.validation.Valid MemberPrivacyPolicyModifyRequest request) {
         memberCommandService.modifyPrivacyPolicy(memberId, request.privacyPolicy());
@@
-    public void modifyMemberPushNotificationPolicy(Long memberId, MemberPushNotificationPolicyModifyRequest request) {
+    public void modifyMemberPushNotificationPolicy(Long memberId, @jakarta.validation.Valid MemberPushNotificationPolicyModifyRequest request) {
         memberCommandService.modifyPushNotificationPolicy(memberId, request.pushNotificationPolicy());
     }
📝 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.

Suggested change
public void modifyMemberPrivacyPolicy(Long memberId, MemberPrivacyPolicyModifyRequest request) {
memberCommandService.modifyPrivacyPolicy(memberId, request.privacyPolicy());
}
public void modifyMemberPushNotificationPolicy(Long memberId, MemberPushNotificationPolicyModifyRequest request) {
memberCommandService.modifyPushNotificationPolicy(memberId, request.pushNotificationPolicy());
}
@UseCase
@Validated
@RequiredArgsConstructor
public class MemberUseCase {
public void modifyMemberPrivacyPolicy(Long memberId,
@jakarta.validation.Valid MemberPrivacyPolicyModifyRequest request) {
memberCommandService.modifyPrivacyPolicy(memberId, request.privacyPolicy());
}
public void modifyMemberPushNotificationPolicy(Long memberId,
@jakarta.validation.Valid MemberPushNotificationPolicyModifyRequest request) {
memberCommandService.modifyPushNotificationPolicy(memberId, request.pushNotificationPolicy());
}
}
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/member/application/usecase/MemberUseCase.java
around lines 16 to 22, add the @Validated annotation at the class level to
enable validation support in the UseCase layer. Then, add the
@jakarta.validation.Valid annotation to the request parameters of the
modifyMemberPrivacyPolicy and modifyMemberPushNotificationPolicy methods to
enforce validation on these inputs, strengthening the validation boundary beyond
the controller.

Comment on lines +29 to 34
@Operation(summary = "회원 개인정보 수집 동의 변경", description = "개인정보 수집 동의 여부를 변경합니다.")
@PutMapping("/terms/privacy")
public ApplicationResponse<Void> modifyPrivacyPolicy(@AuthenticationPrincipal MemberContext memberContext, @RequestBody @Valid MemberPrivacyPolicyModifyRequest request) {
memberUseCase.modifyMemberPrivacyPolicy(memberContext.memberId(), request);
return ApplicationResponse.onSuccess();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

엔드포인트 분리 및 메서드/경로 명명 LGTM — 동의 이력 및 idempotency 보완 제안

  • 동일 값으로 반복 호출 시에도 MemberEntity.updatePrivacyPolicy가 agreedAt를 매번 갱신할 수 있습니다. 서비스/유스케이스 층에서 현재 값과 변경 값 비교 후 변경 시에만 업데이트하도록 보완을 권장합니다.
  • 개인정보 동의는 감사 추적이 중요한 영역입니다. 동의 변경 이력을 별도 테이블/감사 로그로 적재하는 방안을 고려해 주세요.

다음 스크립트로 modifyMemberPrivacyPolicy 메서드 구현부를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
rg "modifyMemberPrivacyPolicy" -n -C5 src

Length of output: 3065


아래 스크립트로 MemberCommandService.modifyPrivacyPolicy 구현부를 확인해주세요.


🏁 Script executed:

#!/bin/bash
# 서비스 레이어 구현 확인
rg "class MemberCommandService" -n src
rg "modifyPrivacyPolicy" -n -C10 src

Length of output: 9547


아래 스크립트로 MemberEntity.updatePrivacyPolicy 구현부를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
rg "class MemberEntity" -n src
rg "updatePrivacyPolicy" -n -C5 src

Length of output: 3007


중복 호출 시 privacyAgreedAt 불필요 갱신 방지 및 감사 이력 관리 보완 필요

현재 MemberCommandServiceImpl.modifyPrivacyPolicyMemberEntity.updatePrivacyPolicy 로직은 요청값이 기존 값과 동일해도 매번 privacyAgreedAt을 갱신합니다. 또한, 개인정보 동의 변경 내역이 별도 감사 로그에 남지 않아 추후 이력 추적이 어렵습니다.

다음 지점을 중심으로 보완을 권장합니다.

  • MemberCommandServiceImpl.modifyPrivacyPolicy (src/main/java/…/MemberCommandServiceImpl.java:29~34)
    • 서비스 계층에서 현재 엔티티의 privacyPolicy 값과 파라미터 privacyPolicy를 비교
    • 변경이 있을 때만 member.updatePrivacyPolicy(…) 호출
  • MemberEntity.updatePrivacyPolicy (src/main/java/…/MemberEntity.java:61~64)
    • 변경 여부를 판단할 수 있는 로직이 없다면 상위 계층에서만 처리하거나, 엔티티 내부에서도 Conditional Setter 패턴 고려
  • 감사 이력 테이블 또는 별도 감사 로그(Audit Log) 도입
    • 변경 전·후 값, 변경 시각, 변경자 등 저장
    • JPA Envers 등 라이브러리 활용 또는 커스텀 테이블 설계

위 보완을 통해 불필요한 타임스탬프 갱신을 방지하고, 개인정보 동의 이력을 안전하게 관리하세요.

🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/member/application/MemberCommandServiceImpl.java
around lines 29 to 34 and
src/main/java/akuma/whiplash/domains/member/domain/MemberEntity.java around
lines 61 to 64, prevent unnecessary updates to privacyAgreedAt by comparing the
current privacyPolicy value with the incoming parameter before calling
updatePrivacyPolicy. Implement conditional logic either in the service layer or
inside the entity to update only when the value changes. Additionally, introduce
an audit log mechanism to record changes, including previous and new values,
timestamps, and the user making the change, using JPA Envers or a custom audit
table to ensure traceability of privacy policy consent modifications.

Comment on lines +36 to +42
@CustomErrorCodes(memberErrorCodes = {MEMBER_NOT_FOUND})
@Operation(summary = "회원 푸시 알림 수신 동의 변경", description = "푸시 알림 수신 동의 여부를 변경합니다.")
@PutMapping("/terms/push-notifications")
public ApplicationResponse<Void> modifyPushNotificationPolicy(@AuthenticationPrincipal MemberContext memberContext, @RequestBody @Valid MemberPushNotificationPolicyModifyRequest request) {
memberUseCase.modifyMemberPushNotificationPolicy(memberContext.memberId(), request);
return ApplicationResponse.onSuccess();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

스웨거 응답 명세 보강 제안

CustomErrorCodes로 404는 드러나지만, 유효성 실패(400), 인증/인가 실패(401/403) 등 응답을 @ApiResponses로 명시하면 클라이언트 연동에 도움이 됩니다.

 @CustomErrorCodes(memberErrorCodes = {MEMBER_NOT_FOUND})
 @Operation(summary = "회원 푸시 알림 수신 동의 변경", description = "푸시 알림 수신 동의 여부를 변경합니다.")
+@ApiResponses({
+    @ApiResponse(responseCode = "200", description = "성공"),
+    @ApiResponse(responseCode = "400", description = "요청 본문 유효성 실패"),
+    @ApiResponse(responseCode = "401", description = "인증 실패"),
+    @ApiResponse(responseCode = "403", description = "인가 실패"),
+    @ApiResponse(responseCode = "404", description = "회원 없음")
+})
 @PutMapping("/terms/push-notifications")
 public ApplicationResponse<Void> modifyPushNotificationPolicy(@AuthenticationPrincipal MemberContext memberContext, @RequestBody @Valid MemberPushNotificationPolicyModifyRequest request) {
     memberUseCase.modifyMemberPushNotificationPolicy(memberContext.memberId(), request);
     return ApplicationResponse.onSuccess();
 }

추가 import:

import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
🤖 Prompt for AI Agents
In
src/main/java/akuma/whiplash/domains/member/presentation/MemberController.java
around lines 36 to 42, the Swagger API response documentation lacks explicit
annotations for common HTTP error responses like 400, 401, and 403. To improve
client integration, add the @ApiResponses annotation with multiple @ApiResponse
entries specifying these status codes and their descriptions above the
modifyPushNotificationPolicy method. Also, import the necessary Swagger
annotations io.swagger.v3.oas.annotations.responses.ApiResponse and
io.swagger.v3.oas.annotations.responses.ApiResponses.

@strongmhk strongmhk merged commit be7f7d3 into develop Aug 10, 2025
1 of 2 checks passed
@strongmhk strongmhk deleted the refactor/#49-terms-api-separate branch August 14, 2025 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

약관 동의 API를 분리한다.

1 participant