diff --git a/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java b/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java index 827a738..14d0a4c 100644 --- a/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java +++ b/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java @@ -288,5 +288,26 @@ public ResponseEntity> checkNicknameDuplic NicknameCheckResponseDTO nicknameCheckResponseDTO = memberService.checkNicknameDuplicate(nickname); return ApiResponse.success(SuccessStatus.CHECK_NICKNAME_DUPLICATE_SUCCESS, nicknameCheckResponseDTO); } + + /* + * + * 약관동의 API + * + * */ + @Operation( + summary = "이용약관동의 API", + description = "이용약관 동의 여부를 저장합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "닉네임 중복 체크 성공") + }) + @PostMapping("/agree-terms") + public ResponseEntity> agreeToTerms( + @AuthenticationPrincipal UserDetails userDetails, + @RequestBody AgreeTermsRequestDTO agreeTermsRequestDTO) { + + memberService.agreeToTerms(userDetails.getUsername(), agreeTermsRequestDTO); + return ApiResponse.success_only(SuccessStatus.TERMS_AGREE_SUCCESS); + } } diff --git a/src/main/java/com/moongeul/backend/api/member/dto/AgreeTermsRequestDTO.java b/src/main/java/com/moongeul/backend/api/member/dto/AgreeTermsRequestDTO.java new file mode 100644 index 0000000..e3ef795 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/dto/AgreeTermsRequestDTO.java @@ -0,0 +1,18 @@ +package com.moongeul.backend.api.member.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AgreeTermsRequestDTO { + + private boolean serviceTermsAgree; + private boolean privatePolicyAgree; + private boolean marketingAgree; +} diff --git a/src/main/java/com/moongeul/backend/api/member/entity/Agree.java b/src/main/java/com/moongeul/backend/api/member/entity/Agree.java new file mode 100644 index 0000000..f238761 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/entity/Agree.java @@ -0,0 +1,35 @@ +package com.moongeul.backend.api.member.entity; + +import com.moongeul.backend.common.entity.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션 +@NoArgsConstructor // 기본 생성자 +@AllArgsConstructor // 모든 필드를 포함한 생성자 +@Table(name = "AGREE") // 데이터베이스 테이블 이름 지정 +public class Agree extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "terms_id", nullable = false) + private Terms terms; + + private boolean isAgreed; // 동의 여부 (true/false) + + public void updateAgreement(boolean isAgreed) { + this.isAgreed = isAgreed; + } +} diff --git a/src/main/java/com/moongeul/backend/api/member/entity/Terms.java b/src/main/java/com/moongeul/backend/api/member/entity/Terms.java new file mode 100644 index 0000000..7b1040c --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/entity/Terms.java @@ -0,0 +1,27 @@ +package com.moongeul.backend.api.member.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션 +@NoArgsConstructor // 기본 생성자 +@AllArgsConstructor // 모든 필드를 포함한 생성자 +@Table(name = "TERMS") // 데이터베이스 테이블 이름 지정 +public class Terms { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private TermsType termsType; // 이용약관 타입 + + private String version; //v1.0, v1.1, v2.0 ... + private String content; // 약관 조항 + private boolean isRequired; // 필수 여부 +} diff --git a/src/main/java/com/moongeul/backend/api/member/entity/TermsType.java b/src/main/java/com/moongeul/backend/api/member/entity/TermsType.java new file mode 100644 index 0000000..bab54d4 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/entity/TermsType.java @@ -0,0 +1,15 @@ +package com.moongeul.backend.api.member.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TermsType { + + SERVICE_TERMS_AGREE("서비스 이용약관 동의"), + PRIVACY_POLICY_AGREE("개인 정보 수집 및 이용 동의"), + MARKETING_AGREE("마케팅 정보 수신 동의"); + + private final String key; +} diff --git a/src/main/java/com/moongeul/backend/api/member/repository/AgreeRepository.java b/src/main/java/com/moongeul/backend/api/member/repository/AgreeRepository.java new file mode 100644 index 0000000..439a5d5 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/repository/AgreeRepository.java @@ -0,0 +1,13 @@ +package com.moongeul.backend.api.member.repository; + +import com.moongeul.backend.api.member.entity.Agree; +import com.moongeul.backend.api.member.entity.Member; +import com.moongeul.backend.api.member.entity.Terms; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AgreeRepository extends JpaRepository { + + Optional findByMemberAndTerms(Member member, Terms terms); +} diff --git a/src/main/java/com/moongeul/backend/api/member/repository/TermsRepository.java b/src/main/java/com/moongeul/backend/api/member/repository/TermsRepository.java new file mode 100644 index 0000000..296b753 --- /dev/null +++ b/src/main/java/com/moongeul/backend/api/member/repository/TermsRepository.java @@ -0,0 +1,12 @@ +package com.moongeul.backend.api.member.repository; + +import com.moongeul.backend.api.member.entity.Terms; +import com.moongeul.backend.api.member.entity.TermsType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TermsRepository extends JpaRepository { + + Optional findByTermsType(TermsType type); +} diff --git a/src/main/java/com/moongeul/backend/api/member/service/MemberService.java b/src/main/java/com/moongeul/backend/api/member/service/MemberService.java index cc5b655..12cb231 100644 --- a/src/main/java/com/moongeul/backend/api/member/service/MemberService.java +++ b/src/main/java/com/moongeul/backend/api/member/service/MemberService.java @@ -3,14 +3,12 @@ import com.moongeul.backend.api.category.entity.Category; import com.moongeul.backend.api.category.repository.CategoryRepository; import com.moongeul.backend.api.member.dto.*; -import com.moongeul.backend.api.member.entity.Follow; -import com.moongeul.backend.api.member.entity.FollowStatus; -import com.moongeul.backend.api.member.entity.Member; -import com.moongeul.backend.api.member.entity.PrivacyLevel; -import com.moongeul.backend.api.member.entity.Role; +import com.moongeul.backend.api.member.entity.*; import com.moongeul.backend.api.member.jwt.dto.JwtTokenDTO; +import com.moongeul.backend.api.member.repository.AgreeRepository; import com.moongeul.backend.api.member.repository.FollowRepository; import com.moongeul.backend.api.member.repository.MemberRepository; +import com.moongeul.backend.api.member.repository.TermsRepository; import com.moongeul.backend.api.member.util.NicknameGenerator; import com.moongeul.backend.api.post.dto.CategoryPostListResponseDTO; import com.moongeul.backend.api.post.dto.PostDTO; @@ -33,9 +31,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; @Service @@ -52,6 +48,8 @@ public class MemberService { private final GoogleOAuthService googleOAuthService; private final KakaoOAuthService kakaoOAuthService; private final NicknameGenerator nicknameGenerator; + private final TermsRepository termsRepository; + private final AgreeRepository agreeRepository; // 인가코드 받아 JWT로 교환 및 회원가입/로그인 처리 @Transactional @@ -223,11 +221,6 @@ public PostStatsResponseDTO getPostStats(String email, Long userId) { .build(); } - private Member getMemberByEmail(String email) { - return memberRepository.findByEmail(email) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); - } - @Transactional public JwtTokenDTO reissueToken(String refreshToken){ @@ -428,4 +421,55 @@ private void validatePrivacyAccess(Member currentMember, Member targetMember) { } } } + + /* + * + * 약관동의 API + * + * */ + @Transactional + public void agreeToTerms(String email, AgreeTermsRequestDTO agreeTermsRequestDTO){ + + Member member = getMemberByEmail(email); + + // 예외처리: 필수 동의 여부 검증 + if (!agreeTermsRequestDTO.isServiceTermsAgree() || !agreeTermsRequestDTO.isPrivatePolicyAgree()) { + throw new BadRequestException(ErrorStatus.DISAGREE_REQUIRED_TERM.getMessage()); + } + + // 1. 약관 타입과 DTO의 동의 여부 매핑 + Map agreementData = Map.of( + TermsType.SERVICE_TERMS_AGREE, agreeTermsRequestDTO.isServiceTermsAgree(), + TermsType.PRIVACY_POLICY_AGREE, agreeTermsRequestDTO.isPrivatePolicyAgree(), + TermsType.MARKETING_AGREE, agreeTermsRequestDTO.isMarketingAgree() + ); + + agreementData.forEach((type, isAgreed) -> { + // 1. 해당 타입의 약관 마스터 정보 조회 + Terms terms = termsRepository.findByTermsType(type) + .orElseThrow(() -> new NotFoundException(ErrorStatus.TERMS_NOTFOUND_EXCEPTION.getMessage())); + + // 2. 기존 동의 내역이 있는지 조회 + agreeRepository.findByMemberAndTerms(member, terms) + .ifPresentOrElse( + // 이미 데이터가 있다면? -> 동의 여부 필드만 수정 (Dirty Checking 발생) + existingAgree -> existingAgree.updateAgreement(isAgreed), + // 데이터가 없다면? -> 새로 생성해서 저장 + () -> { + Agree newAgree = Agree.builder() + .member(member) + .terms(terms) + .isAgreed(isAgreed) + .build(); + agreeRepository.save(newAgree); + } + ); + }); + } + + // 회원 조회 메서드 + private Member getMemberByEmail(String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + } } diff --git a/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java b/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java index cc6cc8c..316b9d3 100644 --- a/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java +++ b/src/main/java/com/moongeul/backend/common/response/ErrorStatus.java @@ -25,6 +25,7 @@ public enum ErrorStatus { INVALID_ANSWER_VALUE(HttpStatus.BAD_REQUEST, "테스트 답변은 A 또는 B여야 합니다. (질문번호: %d, 현재답변: %s)"), NO_SCORE_RESULT(HttpStatus.BAD_REQUEST, "테스트 점수 계산 결과가 없습니다."), REVIEW_UNAUTHORIZED(HttpStatus.BAD_REQUEST, "수정하려는 회원의 리뷰가 아닙니다."), + DISAGREE_REQUIRED_TERM(HttpStatus.BAD_REQUEST, "필수 약관에 모두 동의해야 합니다."), /** * 401 UNAUTHORIZED @@ -48,6 +49,7 @@ public enum ErrorStatus { WEEKLY_RECOMMENDATION_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "주간 추천 기록을 찾을 수 없습니다."), QUESTION_NOTFOUND_EXCEPTION(HttpStatus.NOT_FOUND, "해당 질문을 찾을 수 없습니다."), ANSWER_NOTFOUND_EXCEPTION(HttpStatus.NOT_FOUND, "해당 답변을 찾을 수 없습니다."), + TERMS_NOTFOUND_EXCEPTION(HttpStatus.NOT_FOUND, "약관 정보를 찾을 수 없습니다."), /** * 400 BAD_REQUEST diff --git a/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java b/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java index 1993be7..c000e1f 100644 --- a/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java +++ b/src/main/java/com/moongeul/backend/common/response/SuccessStatus.java @@ -29,6 +29,7 @@ public enum SuccessStatus { CHECK_NICKNAME_DUPLICATE_SUCCESS(HttpStatus.OK, "닉네임 중복 체크 성공"), GET_PRIVACY_LEVEL_SUCCESS(HttpStatus.OK, "계정 공개 범위 조회 성공"), UPDATE_PRIVACY_LEVEL_SUCCESS(HttpStatus.OK, "계정 공개 범위 수정 성공"), + TERMS_AGREE_SUCCESS(HttpStatus.OK, "약관 동의 성공"), /* BOOK */ SEARCH_BOOK_SUCCESS(HttpStatus.OK, "도서 검색 성공"),