Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.dreamteam.alter.adapter.inbound.general.terms.controller;

import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
import com.dreamteam.alter.adapter.inbound.general.terms.dto.PublishedTermsItemResponseDto;
import com.dreamteam.alter.domain.terms.port.inbound.GetPublishedTermsListUseCase;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/public/terms")
@RequiredArgsConstructor
@Validated
public class TermsPublicController implements TermsPublicControllerSpec {

@Resource(name = "getPublishedTermsList")
private final GetPublishedTermsListUseCase getPublishedTermsList;
Comment thread
hodoon marked this conversation as resolved.

@Override
@GetMapping
public ResponseEntity<CommonApiResponse<List<PublishedTermsItemResponseDto>>> getPublishedTermsList() {
return ResponseEntity.ok(CommonApiResponse.of(
getPublishedTermsList.execute()
.stream()
.map(PublishedTermsItemResponseDto::from)
.toList()
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.dreamteam.alter.adapter.inbound.general.terms.controller;

import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse;
import com.dreamteam.alter.adapter.inbound.general.terms.dto.PublishedTermsItemResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

import java.util.List;

@Tag(name = "Public - 약관")
public interface TermsPublicControllerSpec {

@Operation(summary = "게시된 약관 목록 조회")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "약관 목록 조회 성공")
})
ResponseEntity<CommonApiResponse<List<PublishedTermsItemResponseDto>>> getPublishedTermsList();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.dreamteam.alter.adapter.inbound.general.terms.dto;

import com.dreamteam.alter.adapter.inbound.common.dto.DescribedEnumDto;
import com.dreamteam.alter.domain.terms.result.GetPublishedTermsListResult;
import com.dreamteam.alter.domain.terms.type.TermsType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Schema(description = "게시된 약관 항목 응답 DTO")
public class PublishedTermsItemResponseDto {

@Schema(description = "약관 ID", example = "1")
private Long id;

@Schema(description = "약관 유형")
private DescribedEnumDto<TermsType> type;

@Schema(description = "약관 버전", example = "v1.0")
private String version;

@Schema(description = "약관 제목", example = "서비스 이용약관")
private String title;

@Schema(description = "약관 문서 URL", example = "https://example.com/terms/service/v1.0")
private String docUrl;

@Schema(description = "필수 동의 여부", example = "true")
private boolean required;

@Schema(description = "게시 일시", example = "2025-01-01T12:00:00")
private LocalDateTime effectiveAt;

public static PublishedTermsItemResponseDto from(GetPublishedTermsListResult result) {
return PublishedTermsItemResponseDto.builder()
.id(result.id())
.type(DescribedEnumDto.of(result.type(), TermsType.describe()))
.version("v" + result.version())
Comment thread
hodoon marked this conversation as resolved.
.title(result.title())
.docUrl(result.docUrl())
.required(result.required())
.effectiveAt(result.effectiveAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dreamteam.alter.adapter.inbound.general.user.dto;

import com.dreamteam.alter.domain.terms.type.TermsType;
import com.dreamteam.alter.domain.user.type.UserGender;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
Expand All @@ -10,6 +11,8 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Set;

@Getter
@NoArgsConstructor
@AllArgsConstructor
Expand Down Expand Up @@ -58,4 +61,8 @@ public class CreateUserRequestDto {
@Schema(description = "야간 알림 수신 동의 여부", example = "false")
private Boolean nightNotificationConsent;

@NotNull
@Schema(description = "동의한 약관 타입 목록", example = "[\"SERVICE\", \"PRIVACY\"]")
private Set<TermsType> agreedTermsTypes;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dreamteam.alter.adapter.inbound.general.user.dto;

import com.dreamteam.alter.domain.terms.type.TermsType;
import com.dreamteam.alter.domain.user.type.PlatformType;
import com.dreamteam.alter.domain.user.type.SocialProvider;
import com.dreamteam.alter.domain.user.type.UserGender;
Expand All @@ -13,6 +14,8 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.Set;

@Getter
@NoArgsConstructor
@AllArgsConstructor
Expand Down Expand Up @@ -66,6 +69,10 @@ public class CreateUserWithSocialRequestDto {
@Schema(description = "야간 알림 수신 동의 여부", example = "false")
private Boolean nightNotificationConsent;

@NotNull
@Schema(description = "동의한 약관 타입 목록", example = "[\"SERVICE\", \"PRIVACY\"]")
private Set<TermsType> agreedTermsTypes;

@AssertTrue(message = "WEB 플랫폼은 authorizationCode가 필수입니다")
private boolean isWebPlatformValid() {
if (platformType != PlatformType.WEB) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import com.dreamteam.alter.domain.terms.type.TermsStatus;
import com.dreamteam.alter.domain.terms.type.TermsType;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;

import java.util.Comparator;
import java.util.stream.Collectors;
import jakarta.persistence.LockModeType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
Expand Down Expand Up @@ -91,6 +95,40 @@ public Optional<Terms> findPublishedByTypeWithLock(TermsType type) {
);
}

@Override
public List<Terms> findLatestPublishedPerType() {
QTerms sub = new QTerms("sub");

List<Terms> result = queryFactory
.selectFrom(terms)
.where(
terms.status.eq(TermsStatus.PUBLISHED),
terms.effectiveAt.eq(
JPAExpressions
.select(sub.effectiveAt.max())
.from(sub)
.where(
sub.type.eq(terms.type),
sub.status.eq(TermsStatus.PUBLISHED)
)
)
)
.orderBy(terms.type.asc(), terms.effectiveAt.desc(), terms.id.desc())
.fetch();

// 동일 effective_at 중복 방어: 타입별 id 내림차순 기준 첫 번째 1건만 취함
return result.stream()
.collect(Collectors.toMap(
Terms::getType,
t -> t,
(existing, replacement) -> existing
))
.values()
.stream()
.sorted(Comparator.comparing(Terms::getType))
.toList();
}

private BooleanExpression notDeleted() {
return terms.status.ne(TermsStatus.DELETED);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dreamteam.alter.adapter.outbound.terms.persistence;

import com.dreamteam.alter.domain.terms.entity.UserTermsAgreement;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserTermsAgreementJpaRepository extends JpaRepository<UserTermsAgreement, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dreamteam.alter.adapter.outbound.terms.persistence;

import com.dreamteam.alter.domain.terms.entity.UserTermsAgreement;
import com.dreamteam.alter.domain.terms.port.outbound.UserTermsAgreementRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Repository
@RequiredArgsConstructor
@Transactional
public class UserTermsAgreementRepositoryImpl implements UserTermsAgreementRepository {

private final UserTermsAgreementJpaRepository userTermsAgreementJpaRepository;

@Override
public List<UserTermsAgreement> saveAll(List<UserTermsAgreement> agreements) {
return userTermsAgreementJpaRepository.saveAll(agreements);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.dreamteam.alter.application.terms.service;

import com.dreamteam.alter.common.exception.CustomException;
import com.dreamteam.alter.common.exception.ErrorCode;
import com.dreamteam.alter.domain.terms.entity.Terms;
import com.dreamteam.alter.domain.terms.port.outbound.TermsQueryRepository;
import com.dreamteam.alter.domain.terms.type.TermsType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Set;

@Component("termsAgreementValidator")
@RequiredArgsConstructor
public class TermsAgreementValidator {

private final TermsQueryRepository termsQueryRepository;

@Transactional(readOnly = true)
public List<Terms> validateAndResolve(Set<TermsType> agreedTermsTypes) {
List<Terms> latestPublishedTerms = termsQueryRepository.findLatestPublishedPerType();

boolean allRequiredAgreed = latestPublishedTerms.stream()
.filter(Terms::isRequired)
.allMatch(t -> agreedTermsTypes.contains(t.getType()));

if (!allRequiredAgreed) {
throw new CustomException(ErrorCode.REQUIRED_TERMS_NOT_AGREED);
}

return latestPublishedTerms.stream()
.filter(t -> agreedTermsTypes.contains(t.getType()))
.toList();
Comment thread
hodoon marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.dreamteam.alter.application.terms.usecase;

import com.dreamteam.alter.domain.terms.port.inbound.GetPublishedTermsListUseCase;
import com.dreamteam.alter.domain.terms.port.outbound.TermsQueryRepository;
import com.dreamteam.alter.domain.terms.result.GetPublishedTermsListResult;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service("getPublishedTermsList")
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class GetPublishedTermsList implements GetPublishedTermsListUseCase {

private final TermsQueryRepository termsQueryRepository;

@Override
public List<GetPublishedTermsListResult> execute() {
return termsQueryRepository.findLatestPublishedPerType()
.stream()
.map(GetPublishedTermsListResult::from)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.dreamteam.alter.common.exception.ErrorCode;
import com.dreamteam.alter.common.util.PasswordValidator;
import com.dreamteam.alter.domain.email.port.outbound.EmailVerificationSessionStoreRepository;
import com.dreamteam.alter.application.terms.service.TermsAgreementValidator;
import com.dreamteam.alter.domain.terms.entity.Terms;
import com.dreamteam.alter.domain.user.port.inbound.CreateUserUseCase;
import com.dreamteam.alter.domain.user.port.outbound.UserQueryRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -16,6 +18,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;

@Service("createUser")
@RequiredArgsConstructor
Expand All @@ -26,6 +29,7 @@ public class CreateUser implements CreateUserUseCase {
private final SignupSessionCacheRepository cacheRepository;
private final EmailVerificationSessionStoreRepository emailVerificationSessionStoreRepository;
private final CreateUserTx createUserTx;
private final TermsAgreementValidator termsAgreementValidator;

@Override
public GenerateTokenResponseDto execute(CreateUserRequestDto request) {
Expand All @@ -41,6 +45,9 @@ public GenerateTokenResponseDto execute(CreateUserRequestDto request) {
// 중복 확인
validateDuplication(request, contact, sessionIdKey);

// 약관 동의 검증
List<Terms> agreedTerms = termsAgreementValidator.validateAndResolve(request.getAgreedTermsTypes());

// 비밀번호 형식 검증
if (!PasswordValidator.isValid(request.getPassword())) {
throw new CustomException(ErrorCode.INVALID_PASSWORD_FORMAT);
Expand All @@ -53,7 +60,8 @@ public GenerateTokenResponseDto execute(CreateUserRequestDto request) {
GenerateTokenResponseDto response = createUserTx.process(
request, contact, verifiedEmail,
request.getNotificationConsent(),
request.getNightNotificationConsent()
request.getNightNotificationConsent(),
agreedTerms
);

// 회원가입 세션 삭제
Expand Down
Loading
Loading