From 7432298de4d4900147d87818196cd82e4d42de02 Mon Sep 17 00:00:00 2001 From: kymin Date: Sat, 29 Aug 2026 21:46:51 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=95=BD=EA=B4=80=20=EB=8F=99?= =?UTF-8?q?=EC=9D=98=20=EC=97=AC=EB=B6=80=20=EA=B4=80=EB=A6=AC=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/entity/MemberAgreement.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java b/routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java new file mode 100644 index 0000000..4181083 --- /dev/null +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java @@ -0,0 +1,56 @@ +package org.sopt.routee.member.internal.entity; + +import java.time.Instant; + +import org.sopt.routee.entity.BaseEntity; + +import io.hypersistence.utils.hibernate.id.Tsid; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@Table(name = "member_agreement", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_member_agreement_member_id", + columnNames = {"member_id"}) + }) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberAgreement extends BaseEntity { + @Id + @Tsid + private Long id; + + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "member_id", nullable = false, updatable = false) + private Member member; + + @Column(name = "service_terms_agreed_at") + private Instant serviceTermsAgreedAt; + + @Column(name = "privacy_policy_agreed_at") + private Instant privacyPolicyAgreedAt; + + @Column(name = "location_service_terms_agreed_at") + private Instant locationServiceTermsAgreedAt; + + @Column(name = "over14_confirmed_at") + private Instant over14ConfirmedAt; + + @Column(name = "marketing_consent_agreed_at") + private Instant marketingConsentAgreedAt; +} From b3f40fe1a5d1f50d3bfe58e80d14927fd556c51d Mon Sep 17 00:00:00 2001 From: kymin Date: Mon, 31 Aug 2026 02:52:20 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EC=95=BD=EA=B4=80=20=EB=8F=99=EC=9D=98?= =?UTF-8?q?=20=EC=97=AC=EB=B6=80=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/internal/code/ErrorCode.java | 1 + .../internal/controller/MemberController.java | 5 ++-- .../controller/MemberControllerDocs.java | 21 +++++++++++++-- .../dto/request/AgreementRequest.java | 22 +++++++++++++++ .../dto/request/RegisterRequest.java | 12 ++++++--- ...RequiredAgreementNotAcceptedException.java | 11 ++++++++ .../member/internal/mapper/MemberMapper.java | 14 ++++++++++ .../repository/MemberAgreementRepository.java | 16 +++++++++++ .../internal/service/MemberService.java | 27 ++++++++++++++++++- .../service/dto/command/AgreementCommand.java | 10 +++++++ .../service/dto/command/RegisterCommand.java | 6 ++++- 11 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/AgreementRequest.java create mode 100644 routee-member/src/main/java/org/sopt/routee/member/internal/exception/RequiredAgreementNotAcceptedException.java create mode 100644 routee-member/src/main/java/org/sopt/routee/member/internal/repository/MemberAgreementRepository.java create mode 100644 routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/AgreementCommand.java diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/code/ErrorCode.java b/routee-member/src/main/java/org/sopt/routee/member/internal/code/ErrorCode.java index a434d0c..b183173 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/code/ErrorCode.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/code/ErrorCode.java @@ -12,6 +12,7 @@ public enum ErrorCode implements ErrorResultCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자 정보가 존재하지 않습니다."), ALREADY_REGISTERED_MEMBER(HttpStatus.CONFLICT, "이미 가입된 회원입니다."), + REQUIRED_AGREEMENT_NOT_ACCEPTED(HttpStatus.BAD_REQUEST, "필수 약관에 모두 동의해야 합니다."), INVALID_TIME_ZONE(HttpStatus.BAD_REQUEST, "Time-Zone 헤더 값이 올바르지 않습니다."), UNSUPPORTED_IMAGE_FILE_EXTENSION(HttpStatus.BAD_REQUEST, "지원하지 않는 이미지 파일 확장자입니다."); diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberController.java b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberController.java index 81842b3..5c0ae98 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberController.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberController.java @@ -53,9 +53,10 @@ public class MemberController implements MemberControllerDocs { @PostMapping(path = "/member/register") public ResponseEntity> register( - @Valid @RequestBody RegisterRequest request + @Valid @RequestBody RegisterRequest request, + @RequestHeader(name = "Time-Zone") ZoneId timeZone ) { - memberService.register(request.toCommand()); + memberService.register(request.toCommand(timeZone)); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.MEMBER_REGISTER)); diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberControllerDocs.java b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberControllerDocs.java index 635f013..475508e 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberControllerDocs.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/MemberControllerDocs.java @@ -38,7 +38,9 @@ public interface MemberControllerDocs { @Operation( summary = "소셜 회원가입", - description = "OIDC ID 토큰과 닉네임으로 회원가입합니다. 완료 후 POST /auth/login으로 토큰을 발급받으세요." + description = "OIDC ID 토큰, 닉네임, 약관 동의 여부로 회원가입합니다. 필수 약관(서비스 이용약관, 개인정보 처리방침, " + + "위치기반 서비스 이용약관, 만 14세 이상 확인)에 모두 동의해야 하며, 동의 시점은 Time-Zone 헤더 기준으로 저장됩니다. " + + "완료 후 POST /auth/login으로 토큰을 발급받으세요." ) @ApiResponses({ @ApiResponse(responseCode = "201", description = "회원가입 성공"), @@ -47,6 +49,12 @@ public interface MemberControllerDocs { examples = { @ExampleObject(name = "INVALID_NICKNAME_FORMAT", value = "{\"status\":400,\"code\":\"INVALID_INPUT_VALUE\",\"message\":\"닉네임은 한글, 영어, 숫자만 사용하여 2자 이상 12자 이하로 입력해야 합니다.\"}"), + @ExampleObject(name = "REQUIRED_AGREEMENT_NOT_ACCEPTED", + value = "{\"status\":400,\"code\":\"REQUIRED_AGREEMENT_NOT_ACCEPTED\",\"message\":\"필수 약관에 모두 동의해야 합니다.\"}"), + @ExampleObject(name = "MISSING_REQUEST_HEADER", + value = "{\"status\":400,\"code\":\"MISSING_REQUEST_HEADER\",\"message\":\"필수 요청 헤더가 누락되었습니다.\"}"), + @ExampleObject(name = "INVALID_HEADER", + value = "{\"status\":400,\"code\":\"INVALID_HEADER\",\"message\":\"헤더값이 올바르지 않습니다.\"}"), @ExampleObject(name = "INVALID_REQUEST_BODY", value = "{\"status\":400,\"code\":\"INVALID_REQUEST_BODY\",\"message\":\"요청 바디를 읽을 수 없습니다.\"}") })), @@ -65,7 +73,16 @@ public interface MemberControllerDocs { examples = @ExampleObject(name = "ALREADY_REGISTERED_MEMBER", value = "{\"status\":409,\"code\":\"ALREADY_REGISTERED_MEMBER\",\"message\":\"이미 가입된 회원입니다.\"}"))) }) - ResponseEntity> register(@Valid @RequestBody RegisterRequest request); + ResponseEntity> register( + @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, + content = @Content(schema = @Schema(implementation = RegisterRequest.class), + examples = @ExampleObject(value = "{\"provider\":\"APPLE\",\"idToken\":\"eyJ...\",\"nickname\":\"루티\"," + + "\"agreements\":{\"serviceTerms\":true,\"privacyPolicy\":true,\"locationServiceTerms\":true," + + "\"over14\":true,\"marketingConsent\":false}}"))) + @Valid @RequestBody RegisterRequest request, + @Parameter(description = "IANA Time Zone ID", example = "Asia/Seoul", required = true) + @RequestHeader("Time-Zone") ZoneId timeZone + ); @Operation(summary = "회원 탈퇴", description = "인증된 회원의 정보를 삭제하고, 보유한 액세스/리프레시 토큰을 무효화합니다.") @SecurityRequirement(name = "bearerAuth") diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/AgreementRequest.java b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/AgreementRequest.java new file mode 100644 index 0000000..6e3cbc8 --- /dev/null +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/AgreementRequest.java @@ -0,0 +1,22 @@ +package org.sopt.routee.member.internal.controller.dto.request; + +import org.sopt.routee.member.internal.service.dto.command.AgreementCommand; + +import jakarta.validation.constraints.NotNull; + +public record AgreementRequest( + @NotNull // 서비스 이용 약관 + Boolean serviceTerms, + @NotNull // 개인정보 처리 방침 + Boolean privacyPolicy, + @NotNull // 위치기반 서비스 이용약관 + Boolean locationServiceTerms, + @NotNull // 만 14세 이상 여부 + Boolean over14, + @NotNull // 마케팅 활용 및 광고성 정보 수신 동의 + Boolean marketingConsent +) { + public AgreementCommand toCommand() { + return new AgreementCommand(serviceTerms, privacyPolicy, locationServiceTerms, over14, marketingConsent); + } +} diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java index 84a9942..6c2e0bc 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java @@ -1,8 +1,11 @@ package org.sopt.routee.member.internal.controller.dto.request; +import java.time.ZoneId; + import org.sopt.routee.external.api.type.OAuthProvider; import org.sopt.routee.member.internal.service.dto.command.RegisterCommand; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; @@ -14,9 +17,12 @@ public record RegisterRequest( String idToken, @NotBlank @Pattern(regexp = "^[가-힣a-zA-Z0-9]{2,12}$", message = "닉네임은 한글, 영어, 숫자만 사용하여 2자 이상 12자 이하로 입력해야 합니다.") - String nickname + String nickname, + @NotNull + @Valid + AgreementRequest agreements ) { - public RegisterCommand toCommand(){ - return new RegisterCommand(provider, idToken, nickname); + public RegisterCommand toCommand(ZoneId timeZone){ + return new RegisterCommand(provider, idToken, nickname, agreements.toCommand(), timeZone); } } diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/exception/RequiredAgreementNotAcceptedException.java b/routee-member/src/main/java/org/sopt/routee/member/internal/exception/RequiredAgreementNotAcceptedException.java new file mode 100644 index 0000000..c3c861d --- /dev/null +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/exception/RequiredAgreementNotAcceptedException.java @@ -0,0 +1,11 @@ +package org.sopt.routee.member.internal.exception; + +import org.sopt.routee.exception.BaseException; +import org.sopt.routee.member.internal.code.ErrorCode; + +public class RequiredAgreementNotAcceptedException extends BaseException { + + public RequiredAgreementNotAcceptedException() { + super(ErrorCode.REQUIRED_AGREEMENT_NOT_ACCEPTED); + } +} diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java b/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java index 06daad5..f3e243a 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java @@ -1,11 +1,13 @@ package org.sopt.routee.member.internal.mapper; +import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.List; import org.sopt.routee.activity.api.result.MonthlyActivityDailySummaryResult; +import org.sopt.routee.member.internal.service.dto.command.AgreementCommand; import org.sopt.routee.member.internal.service.dto.command.RegisterCommand; import org.sopt.routee.member.internal.service.dto.result.ActivitySummaryResult; import org.sopt.routee.member.internal.service.dto.result.DailySummary; @@ -16,6 +18,7 @@ import org.sopt.routee.member.api.result.TokenClaimsResult; import org.sopt.routee.member.api.type.MemberRole; import org.sopt.routee.member.internal.entity.Member; +import org.sopt.routee.member.internal.entity.MemberAgreement; import org.sopt.routee.util.TimeZoneUtils; import lombok.AccessLevel; @@ -34,6 +37,17 @@ public static Member toEntity(RegisterCommand command, String oauthId) { .build(); } + public static MemberAgreement toAgreementEntity(Member member, AgreementCommand agreement, Instant agreedAt) { + return MemberAgreement.builder() + .member(member) + .serviceTermsAgreedAt(agreement.serviceTerms() ? agreedAt : null) + .privacyPolicyAgreedAt(agreement.privacyPolicy() ? agreedAt : null) + .locationServiceTermsAgreedAt(agreement.locationServiceTerms() ? agreedAt : null) + .over14ConfirmedAt(agreement.over14() ? agreedAt : null) + .marketingConsentAgreedAt(agreement.marketingConsent() ? agreedAt : null) + .build(); + } + public static TokenClaimsResult toTokenClaimsResult(Member member) { return new TokenClaimsResult(member.getId(), member.getRole().name()); } diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/repository/MemberAgreementRepository.java b/routee-member/src/main/java/org/sopt/routee/member/internal/repository/MemberAgreementRepository.java new file mode 100644 index 0000000..4bc99d2 --- /dev/null +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/repository/MemberAgreementRepository.java @@ -0,0 +1,16 @@ +package org.sopt.routee.member.internal.repository; + +import java.util.Optional; + +import org.sopt.routee.member.internal.entity.MemberAgreement; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberAgreementRepository extends JpaRepository { + + Optional findByMember_Id(Long memberId); + + boolean existsByMember_Id(Long memberId); + + void deleteByMember_Id(Long memberId); + +} diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java b/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java index f9e2e1e..5465f9c 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java @@ -1,5 +1,7 @@ package org.sopt.routee.member.internal.service; +import java.time.Instant; +import java.time.LocalDateTime; import java.time.YearMonth; import java.time.ZoneId; import java.util.List; @@ -19,6 +21,7 @@ import org.sopt.routee.external.api.type.OAuthProvider; import org.sopt.routee.external.api.port.OidcVerifyPort; import org.sopt.routee.member.api.event.MemberWithdrawnEvent; +import org.sopt.routee.member.internal.service.dto.command.AgreementCommand; import org.sopt.routee.member.internal.service.dto.command.ProfileImageUploadUrlCommand; import org.sopt.routee.member.internal.service.dto.command.RegisterCommand; import org.sopt.routee.member.internal.service.dto.command.UpdateNicknameCommand; @@ -33,10 +36,13 @@ import org.sopt.routee.member.internal.entity.Member; import org.sopt.routee.member.internal.exception.AlreadyRegisteredMemberException; import org.sopt.routee.member.internal.exception.MemberNotFoundException; +import org.sopt.routee.member.internal.exception.RequiredAgreementNotAcceptedException; import org.sopt.routee.member.internal.exception.UnsupportedImageFileExtensionException; import org.sopt.routee.member.internal.mapper.MemberMapper; +import org.sopt.routee.member.internal.repository.MemberAgreementRepository; import org.sopt.routee.member.internal.repository.MemberRepository; import org.sopt.routee.member.internal.service.validator.ProfileImageFileNameValidator; +import org.sopt.routee.util.TimeZoneUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -53,6 +59,7 @@ public class MemberService { private final OidcVerifyPort oidcVerifyPort; private final ActivityUseCase activityUseCase; private final MemberRepository memberRepository; + private final MemberAgreementRepository memberAgreementRepository; private final ApplicationEventPublisher applicationEventPublisher; private final FileUploadPresignPort fileUploadPresignPort; private final FileImageAccessUrlPort fileImageAccessUrlPort; @@ -75,12 +82,29 @@ public boolean existsById(long memberId) { @Transactional public void register(RegisterCommand command) { + validateRequiredAgreements(command.agreement()); + String oauthId = oidcVerifyPort.extractSubject(command.provider(), command.idToken()); if (memberRepository.existsByOauthIdAndOauthProvider(oauthId, command.provider())) { throw new AlreadyRegisteredMemberException(); } - memberRepository.save(MemberMapper.toEntity(command, oauthId)); + + Member savedMember = memberRepository.save(MemberMapper.toEntity(command, oauthId)); + + Instant agreedAt = TimeZoneUtils.toUtcInstantTime(LocalDateTime.now(command.timeZone()), command.timeZone()); + memberAgreementRepository.save(MemberMapper.toAgreementEntity(savedMember, command.agreement(), agreedAt)); + } + + private void validateRequiredAgreements(AgreementCommand agreement) { + boolean allRequiredAccepted = agreement.serviceTerms() + && agreement.privacyPolicy() + && agreement.locationServiceTerms() + && agreement.over14(); + + if (!allRequiredAccepted) { + throw new RequiredAgreementNotAcceptedException(); + } } public void withdraw(long memberId, String accessTokenHash, String refreshTokenHash) { @@ -88,6 +112,7 @@ public void withdraw(long memberId, String accessTokenHash, String refreshTokenH Member member = memberRepository.findById(memberId) .orElseThrow(MemberNotFoundException::new); + memberAgreementRepository.deleteByMember_Id(memberId); memberRepository.delete(member); activityUseCase.deleteForMemberWithdrawal(memberId); diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/AgreementCommand.java b/routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/AgreementCommand.java new file mode 100644 index 0000000..602549d --- /dev/null +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/AgreementCommand.java @@ -0,0 +1,10 @@ +package org.sopt.routee.member.internal.service.dto.command; + +public record AgreementCommand( + boolean serviceTerms, + boolean privacyPolicy, + boolean locationServiceTerms, + boolean over14, + boolean marketingConsent +) { +} diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/RegisterCommand.java b/routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/RegisterCommand.java index b433fb2..278ab78 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/RegisterCommand.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/service/dto/command/RegisterCommand.java @@ -1,10 +1,14 @@ package org.sopt.routee.member.internal.service.dto.command; +import java.time.ZoneId; + import org.sopt.routee.external.api.type.OAuthProvider; public record RegisterCommand( OAuthProvider provider, String idToken, - String nickname + String nickname, + AgreementCommand agreement, + ZoneId timeZone ) { } From 25852180ba93c1cac19dee367099de134d80e0a4 Mon Sep 17 00:00:00 2001 From: kymin Date: Mon, 31 Aug 2026 13:49:09 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/controller/dto/request/RegisterRequest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java index ce54f64..8f49b0c 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/controller/dto/request/RegisterRequest.java @@ -17,9 +17,12 @@ public record RegisterRequest( String idToken, @NotBlank @Pattern(regexp = "^(?=.{1,12}$)[가-힣a-zA-Z0-9]+(?: [가-힣a-zA-Z0-9]+)*$", message = "닉네임은 한글, 영어, 숫자와 공백을 사용하여 1자 이상 12자 이하로 입력해야 하며, 공백은 연속될 수 없습니다.") - String nickname + String nickname, + @NotNull + @Valid + AgreementRequest agreements ) { - public RegisterCommand toCommand(ZoneId timeZone){ + public RegisterCommand toCommand(ZoneId timeZone) { return new RegisterCommand(provider, idToken, nickname, agreements.toCommand(), timeZone); } } From 6f13cca258e346575989da097261667fc06aa194 Mon Sep 17 00:00:00 2001 From: kymin Date: Mon, 31 Aug 2026 13:55:31 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=ED=95=AD=EB=AA=A9=EB=B3=84=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=8C=80=20=EB=B3=84=EB=8F=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/internal/entity/MemberAgreement.java | 16 ++++++++++++++++ .../member/internal/mapper/MemberMapper.java | 9 ++++++++- .../member/internal/service/MemberService.java | 3 ++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java b/routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java index 4181083..ef027bf 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/entity/MemberAgreement.java @@ -1,6 +1,7 @@ package org.sopt.routee.member.internal.entity; import java.time.Instant; +import java.time.ZoneId; import org.sopt.routee.entity.BaseEntity; @@ -42,15 +43,30 @@ public class MemberAgreement extends BaseEntity { @Column(name = "service_terms_agreed_at") private Instant serviceTermsAgreedAt; + @Column(name = "service_terms_agreed_zone") + private ZoneId serviceTermsAgreedZone; + @Column(name = "privacy_policy_agreed_at") private Instant privacyPolicyAgreedAt; + @Column(name = "privacy_policy_agreed_zone") + private ZoneId privacyPolicyAgreedZone; + @Column(name = "location_service_terms_agreed_at") private Instant locationServiceTermsAgreedAt; + @Column(name = "location_service_terms_agreed_zone") + private ZoneId locationServiceTermsAgreedZone; + @Column(name = "over14_confirmed_at") private Instant over14ConfirmedAt; + @Column(name = "over14_confirmed_zone") + private ZoneId over14ConfirmedZone; + @Column(name = "marketing_consent_agreed_at") private Instant marketingConsentAgreedAt; + + @Column(name = "marketing_consent_agreed_zone") + private ZoneId marketingConsentAgreedZone; } diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java b/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java index f3e243a..50661e8 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/mapper/MemberMapper.java @@ -37,14 +37,21 @@ public static Member toEntity(RegisterCommand command, String oauthId) { .build(); } - public static MemberAgreement toAgreementEntity(Member member, AgreementCommand agreement, Instant agreedAt) { + public static MemberAgreement toAgreementEntity( + Member member, AgreementCommand agreement, Instant agreedAt, ZoneId agreedZone + ) { return MemberAgreement.builder() .member(member) .serviceTermsAgreedAt(agreement.serviceTerms() ? agreedAt : null) + .serviceTermsAgreedZone(agreement.serviceTerms() ? agreedZone : null) .privacyPolicyAgreedAt(agreement.privacyPolicy() ? agreedAt : null) + .privacyPolicyAgreedZone(agreement.privacyPolicy() ? agreedZone : null) .locationServiceTermsAgreedAt(agreement.locationServiceTerms() ? agreedAt : null) + .locationServiceTermsAgreedZone(agreement.locationServiceTerms() ? agreedZone : null) .over14ConfirmedAt(agreement.over14() ? agreedAt : null) + .over14ConfirmedZone(agreement.over14() ? agreedZone : null) .marketingConsentAgreedAt(agreement.marketingConsent() ? agreedAt : null) + .marketingConsentAgreedZone(agreement.marketingConsent() ? agreedZone : null) .build(); } diff --git a/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java b/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java index ced21c2..fd2f499 100644 --- a/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java +++ b/routee-member/src/main/java/org/sopt/routee/member/internal/service/MemberService.java @@ -93,7 +93,8 @@ public void register(RegisterCommand command) { Member savedMember = memberRepository.save(MemberMapper.toEntity(command, oauthId)); Instant agreedAt = TimeZoneUtils.toUtcInstantTime(LocalDateTime.now(command.timeZone()), command.timeZone()); - memberAgreementRepository.save(MemberMapper.toAgreementEntity(savedMember, command.agreement(), agreedAt)); + memberAgreementRepository.save( + MemberMapper.toAgreementEntity(savedMember, command.agreement(), agreedAt, command.timeZone())); } private void validateRequiredAgreements(AgreementCommand agreement) {