diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/controller/TermsAgreementController.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/controller/TermsAgreementController.java new file mode 100644 index 0000000..6f27ebe --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/controller/TermsAgreementController.java @@ -0,0 +1,38 @@ +package backend.greatjourney.domain.terms.controller; + +import backend.greatjourney.domain.terms.dto.TermsAgreementRequest; +import backend.greatjourney.domain.terms.service.TermsAgreementService; +import backend.greatjourney.global.exception.BaseResponse; +import backend.greatjourney.global.security.entitiy.CustomOAuth2User; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/terms-agreement") +@RequiredArgsConstructor +public class TermsAgreementController { + + private final TermsAgreementService termsAgreementService; + + @PostMapping + public ResponseEntity> agreeToTerms( + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @RequestBody TermsAgreementRequest request) { + + termsAgreementService.agreeToTerms(customOAuth2User, request); + + return ResponseEntity.ok( + BaseResponse.builder() + .isSuccess(true) + .code(200) + .message("약관 동의가 성공적으로 처리되었습니다.") + .data(null) + .build() + ); + } +} \ No newline at end of file diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/controller/TermsController.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/controller/TermsController.java new file mode 100644 index 0000000..40c4a4c --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/controller/TermsController.java @@ -0,0 +1,36 @@ +package backend.greatjourney.domain.terms.controller; + +import backend.greatjourney.domain.terms.domain.TermsType; +import backend.greatjourney.domain.terms.dto.TermsResponseDto; +import backend.greatjourney.domain.terms.service.TermsService; +import backend.greatjourney.global.exception.BaseResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/terms") +@RequiredArgsConstructor +public class TermsController { + + private final TermsService termsService; + + @GetMapping + public ResponseEntity> getTerms( + @RequestParam("type") TermsType type) { + + TermsResponseDto termsData = termsService.getLatestTerms(type); + + return ResponseEntity.ok( + BaseResponse.builder() + .isSuccess(true) + .code(200) + .message(type.name() + " 약관 조회에 성공했습니다.") + .data(termsData) + .build() + ); + } +} \ No newline at end of file diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/Terms.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/Terms.java new file mode 100644 index 0000000..48a92e5 --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/Terms.java @@ -0,0 +1,35 @@ +package backend.greatjourney.domain.terms.domain; + + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.joda.time.LocalDate; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Terms { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) // Enum 이름을 DB에 문자열로 저장 + @Column(nullable = false) + private TermsType type; // 약관 종류 + + @Column(nullable = false) + private String version; // 버전 (예: "1.0", "1.1") + + @Lob // CLOB, TEXT 등 DB의 대용량 텍스트 타입과 매핑 + @Column(nullable = false, columnDefinition = "TEXT") + private String content; // 약관 내용 (HTML) + + @Column(nullable = false) + private boolean isRequired; // 필수 동의 여부 + + private String effectiveDate; + +} \ No newline at end of file diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/TermsType.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/TermsType.java new file mode 100644 index 0000000..62ac259 --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/TermsType.java @@ -0,0 +1,7 @@ +package backend.greatjourney.domain.terms.domain; + +public enum TermsType { + TERMS_OF_SERVICE, // 서비스 이용약관 + PRIVACY_POLICY, // 개인정보 처리방침 + MARKETING_AGREEMENT // 마케팅 정보 수신 동의 (선택) +} diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/UserTermsAgreement.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/UserTermsAgreement.java new file mode 100644 index 0000000..cea7107 --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/domain/UserTermsAgreement.java @@ -0,0 +1,42 @@ +package backend.greatjourney.domain.terms.domain; + + +import backend.greatjourney.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) // @CreatedDate 자동 생성을 위해 필요 +public class UserTermsAgreement { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "terms_id", nullable = false) + private Terms terms; + + @CreatedDate // 엔티티가 생성될 때 시간이 자동으로 저장됩니다. + @Column(nullable = false, updatable = false) + private LocalDateTime agreedAt; // 동의한 시간 + + @Builder + public UserTermsAgreement(User user, Terms terms) { + this.user = user; + this.terms = terms; + } +} diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/dto/TermsAgreementRequest.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/dto/TermsAgreementRequest.java new file mode 100644 index 0000000..00f6081 --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/dto/TermsAgreementRequest.java @@ -0,0 +1,6 @@ +package backend.greatjourney.domain.terms.dto; + +// 약관 ID 목록을 받는 간단한 record DTO +public record TermsAgreementRequest( + java.util.List agreedTermsIds +) {} diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/dto/TermsResponseDto.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/dto/TermsResponseDto.java new file mode 100644 index 0000000..eea3a0b --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/dto/TermsResponseDto.java @@ -0,0 +1,22 @@ +package backend.greatjourney.domain.terms.dto; + +import backend.greatjourney.domain.terms.domain.Terms; +import backend.greatjourney.domain.terms.domain.TermsType; + +public record TermsResponseDto( + TermsType type, + String version, + String content, + boolean isRequired, + String effectiveDate +) { + public static TermsResponseDto from(Terms terms) { + return new TermsResponseDto( + terms.getType(), + terms.getVersion(), + terms.getContent(), + terms.isRequired(), + terms.getEffectiveDate() + ); + } +} diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/repository/TermsRepository.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/repository/TermsRepository.java new file mode 100644 index 0000000..15ebe2f --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/repository/TermsRepository.java @@ -0,0 +1,13 @@ +package backend.greatjourney.domain.terms.repository; + + +import backend.greatjourney.domain.terms.domain.Terms; +import backend.greatjourney.domain.terms.domain.TermsType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TermsRepository extends JpaRepository { + // 특정 타입의 약관 중 가장 최신 버전을 조회 (적용일과 버전으로 정렬) + Optional findFirstByTypeOrderByEffectiveDateDescVersionDesc(TermsType type); +} \ No newline at end of file diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/repository/UserTermsAgreementRepository.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/repository/UserTermsAgreementRepository.java new file mode 100644 index 0000000..58dafc5 --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/repository/UserTermsAgreementRepository.java @@ -0,0 +1,9 @@ +package backend.greatjourney.domain.terms.repository; + +import backend.greatjourney.domain.terms.domain.UserTermsAgreement; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserTermsAgreementRepository extends JpaRepository { + // 필요시 특정 유저의 동의 내역을 찾는 메서드 등을 추가할 수 있습니다. + // boolean existsByUserAndTerms(User user, Terms terms); +} diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/service/TermsAgreementService.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/service/TermsAgreementService.java new file mode 100644 index 0000000..e558acf --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/service/TermsAgreementService.java @@ -0,0 +1,52 @@ +package backend.greatjourney.domain.terms.service; + + +import backend.greatjourney.domain.terms.domain.Terms; +import backend.greatjourney.domain.terms.domain.UserTermsAgreement; +import backend.greatjourney.domain.terms.dto.TermsAgreementRequest; +import backend.greatjourney.domain.terms.repository.TermsRepository; +import backend.greatjourney.domain.terms.repository.UserTermsAgreementRepository; +import backend.greatjourney.domain.user.entity.User; +import backend.greatjourney.domain.user.repository.UserRepository; +import backend.greatjourney.global.exception.CustomException; +import backend.greatjourney.global.exception.ErrorCode; +import backend.greatjourney.global.security.entitiy.CustomOAuth2User; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class TermsAgreementService { + + private final UserRepository userRepository; + private final TermsRepository termsRepository; + private final UserTermsAgreementRepository userTermsAgreementRepository; + + public void agreeToTerms(CustomOAuth2User customOAuth2User, TermsAgreementRequest request) { + Long userId = Long.parseLong(customOAuth2User.getUserId()); + User user = userRepository.findByUserId(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // 요청받은 ID 목록으로 모든 Terms 엔티티를 조회 + List termsToAgree = termsRepository.findAllById(request.agreedTermsIds()); + + // 요청 ID 개수와 실제 조회된 엔티티 개수가 다르면 잘못된 ID가 포함된 것 + if (termsToAgree.size() != request.agreedTermsIds().size()) { + throw new CustomException(ErrorCode.TERMS_NOT_FOUND); // 혹은 다른 적절한 에러 코드 + } + + // 각 약관에 대해 동의 내역을 생성하고 저장 + List agreements = termsToAgree.stream() + .map(term -> UserTermsAgreement.builder() + .user(user) + .terms(term) + .build()) + .toList(); + + userTermsAgreementRepository.saveAll(agreements); + } +} diff --git a/greatjourney/src/main/java/backend/greatjourney/domain/terms/service/TermsService.java b/greatjourney/src/main/java/backend/greatjourney/domain/terms/service/TermsService.java new file mode 100644 index 0000000..0301e51 --- /dev/null +++ b/greatjourney/src/main/java/backend/greatjourney/domain/terms/service/TermsService.java @@ -0,0 +1,24 @@ +package backend.greatjourney.domain.terms.service; + +import backend.greatjourney.domain.terms.domain.TermsType; +import backend.greatjourney.domain.terms.dto.TermsResponseDto; +import backend.greatjourney.domain.terms.repository.TermsRepository; +import backend.greatjourney.global.exception.CustomException; +import backend.greatjourney.global.exception.ErrorCode; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Transactional +public class TermsService { + + private final TermsRepository termsRepository; + + public TermsResponseDto getLatestTerms(TermsType type) { + return termsRepository.findFirstByTypeOrderByEffectiveDateDescVersionDesc(type) + .map(TermsResponseDto::from) + .orElseThrow(() -> new CustomException(ErrorCode.TERMS_NOT_FOUND)); + } +} \ No newline at end of file diff --git a/greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java b/greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java index 36eff3d..0c61c68 100644 --- a/greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java +++ b/greatjourney/src/main/java/backend/greatjourney/global/exception/ErrorCode.java @@ -32,8 +32,8 @@ public enum ErrorCode { CERTIFICATION_CENTER_NOT_FOUND(HttpStatus.BAD_REQUEST, 400, "조건에 맞는 인증센터를 찾을 수 없습니다."), - PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, 404, "해당 장소를 찾을 수 없습니다.") - + PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, 404, "해당 장소를 찾을 수 없습니다."), + TERMS_NOT_FOUND(HttpStatus.NOT_FOUND, 404, "요청한 종류의 약관을 찾을 수 없습니다.") ;