Repository navigation
[Feat/#99] 회원가입 API 추가 (전화번호·약관 동의) #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cf20415
b1e0a1a
4d36d31
9fbb4c6
d66320f
2849edb
003e80a
3ec7c3b
98308ad
26831c6
70a8cf4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| package kr.ac.kookmin.stream.api.app.core.auth.response; | ||
|
|
||
| /** | ||
| * @param termsAgreementRequired 필수 약관에 아직 동의하지 않았으면 true. 앱은 이 값으로 약관 화면을 띄운다 | ||
| * @param signUpRequired 회원가입(전화번호·필수 약관 동의)을 마치지 않았으면 true. 앱은 이 값으로 회원가입 화면을 띄운다 | ||
| */ | ||
| public record OAuthLoginResponse( | ||
| String accessToken, | ||
| boolean termsAgreementRequired | ||
| String name, | ||
| boolean signUpRequired | ||
| ) {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package kr.ac.kookmin.stream.api.app.core.member; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import kr.ac.kookmin.stream.api.app.AppApiUser; | ||
| import kr.ac.kookmin.stream.api.app.core.member.request.MemberSignUpRequest; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; | ||
| import kr.ac.kookmin.stream.common.CommonErrorCode; | ||
| import kr.ac.kookmin.stream.member.domain.member.domain.MemberErrorCode; | ||
|
|
||
| /** | ||
| * 학생 앱 회원 API의 문서 명세. 구현은 {@link AppMemberController}가 맡는다. | ||
| */ | ||
| @Tag(name = "회원", description = "학생 앱 회원가입") | ||
| public interface AppMemberApi { | ||
|
|
||
| /** 회원가입. 로그인 응답의 signUpRequired가 true인 회원이 전화번호와 약관 동의 여부를 등록한다. */ | ||
| @Operation(summary = "회원가입", | ||
| description = """ | ||
| 로그인한 회원의 전화번호와 약관 동의 여부를 저장한다. | ||
| 전화번호는 하이픈을 넣어도 되고 빼도 된다. 필수 약관(PRIVACY_POLICY, TERMS_OF_SERVICE)에 모두 동의해야 한다.""") | ||
| @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) | ||
| @ApiErrorCode(type = MemberErrorCode.class, codes = { | ||
| "MEMBER_NOT_FOUND", | ||
| "REQUIRED_TERMS_NOT_AGREED", | ||
| "ALREADY_SIGNED_UP", | ||
| "PHONE_NUMBER_ALREADY_EXISTS" | ||
| }) | ||
| ApiResponse<Void> signUp(AppApiUser apiUser, MemberSignUpRequest request); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package kr.ac.kookmin.stream.api.app.core.member; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import kr.ac.kookmin.stream.api.app.AppApiUser; | ||
| import kr.ac.kookmin.stream.api.app.core.member.request.MemberSignUpRequest; | ||
| import kr.ac.kookmin.stream.api.common.dto.ApiResponse; | ||
| import kr.ac.kookmin.stream.member.domain.member.service.MemberService; | ||
| import lombok.RequiredArgsConstructor; | ||
| 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("/v1/app/members") | ||
| @RequiredArgsConstructor | ||
| public class AppMemberController implements AppMemberApi { | ||
|
|
||
| private final MemberService memberService; | ||
|
|
||
| @Override | ||
| @PostMapping("/sign-up") | ||
| public ApiResponse<Void> signUp( | ||
| AppApiUser apiUser, | ||
| @Valid @RequestBody MemberSignUpRequest request | ||
| ) { | ||
| memberService.signUp(apiUser.userId(), request.toCommand()); | ||
| return ApiResponse.success(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package kr.ac.kookmin.stream.api.app.core.member.request; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotEmpty; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Pattern; | ||
| import java.util.EnumMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import kr.ac.kookmin.stream.member.domain.member.domain.MemberSignUpCommand; | ||
| import kr.ac.kookmin.stream.member.domain.member.domain.TermType; | ||
|
|
||
| public record MemberSignUpRequest( | ||
| @NotBlank(message = "전화번호를 입력해 주세요.") | ||
| @Pattern(regexp = "^010-?\\d{4}-?\\d{4}$", message = "전화번호 형식이 올바르지 않습니다.") | ||
| String phoneNumber, | ||
|
|
||
| // 필수 약관 여부는 TermType이 정하므로 여기서는 동의 값을 강제하지 않는다 | ||
| @NotEmpty(message = "약관 동의 여부를 입력해 주세요.") | ||
| @Valid | ||
| List<@NotNull(message = "약관 동의 항목이 비어 있습니다.") TermAgreementRequest> termAgreements | ||
| ) { | ||
|
|
||
| public MemberSignUpCommand toCommand() { | ||
| validateTermTypesNotDuplicated(); | ||
|
|
||
| Map<TermType, Boolean> agreements = new EnumMap<>(TermType.class); | ||
| termAgreements.forEach(agreement -> agreements.put(agreement.termType(), agreement.agreed())); | ||
| return new MemberSignUpCommand(phoneNumber, agreements); | ||
| } | ||
|
|
||
| // 같은 약관이 두 번 오면 IllegalArgumentException → GlobalExceptionHandler가 INVALID_INPUT(400)으로 응답한다 | ||
| private void validateTermTypesNotDuplicated() { | ||
| long distinctTermTypeCount = termAgreements.stream() | ||
| .map(TermAgreementRequest::termType) | ||
| .distinct() | ||
| .count(); | ||
| if (distinctTermTypeCount != termAgreements.size()) { | ||
| throw new IllegalArgumentException("같은 약관의 동의 여부가 중복되었습니다."); | ||
| } | ||
| } | ||
|
|
||
| public record TermAgreementRequest( | ||
| @NotNull(message = "약관 종류를 입력해 주세요.") | ||
| TermType termType, | ||
|
|
||
| @NotNull(message = "약관 동의 여부를 입력해 주세요.") | ||
| Boolean agreed | ||
| ) {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package kr.ac.kookmin.stream.member.domain.member.domain; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * 회원가입 요청. 전화번호는 하이픈을 지운 숫자만 담는다. 중복 검사와 저장이 같은 형식을 쓰게 하기 위함이다. | ||
| */ | ||
| public record MemberSignUpCommand( | ||
| String phoneNumber, | ||
| Map<TermType, Boolean> termAgreements | ||
| ) { | ||
|
|
||
| public MemberSignUpCommand { | ||
| phoneNumber = phoneNumber.replace("-", ""); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,4 +28,19 @@ public static MemberTermAgreement of( | |
| ) { | ||
| return new MemberTermAgreement(id, memberId, termType, termVersion, agreed, agreedAt); | ||
| } | ||
|
|
||
| // 현재 약관 버전으로 동의 기록을 만든다. 동의하지 않았으면 동의 시각은 null이다 | ||
| public static MemberTermAgreement create(Long memberId, TermType termType, boolean agreed, LocalDateTime now) { | ||
| return new MemberTermAgreement(null, memberId, termType, termType.version(), agreed, agreed ? now : null); | ||
| } | ||
|
|
||
| // 이미 기록이 있는 약관은 새 행을 만들지 않고 현재 버전으로 덮어쓴다. 버전과 동의 여부가 그대로면 최초 동의 시각을 지키도록 건너뛴다 | ||
| public void update(boolean agreed, LocalDateTime now) { | ||
| if (termType.version().equals(this.termVersion) && this.agreed == agreed) { | ||
| return; | ||
| } | ||
| this.termVersion = termType.version(); | ||
| this.agreed = agreed; | ||
| this.agreedAt = agreed ? now : null; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주석처럼 이미 기록이 있는 약관은 새 행을 만들지 않고 현재 버전으로 덮어쓰게되는 경우, 동의 시각도 같이 덮어씌워져 최초 약관 동의 시각에 오류가 생길 수 있을 것 같습니다. termVersion이 동일한 경우에는 update를 스킵하는 로직을 두면 어떨까요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 70a8cf4 |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,9 @@ | ||
| package kr.ac.kookmin.stream.member.domain.member.service; | ||
|
|
||
| import java.util.Map; | ||
| import kr.ac.kookmin.stream.member.domain.member.domain.TermType; | ||
|
|
||
| public interface MemberTermService { | ||
| boolean hasAgreedRequiredTerms(Long memberId); | ||
| void agree(Long memberId, Map<TermType, Boolean> termAgreements); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 1172
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 9623
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 8976
🏁 Script executed:
Repository: billilge/stream-server
Length of output: 8976
앱의 로그인 응답 소비자를 새 필드로 전환하세요.
기존 응답 계약은 앱이
termsAgreementRequired로 약관 화면을 표시한다고 명시했습니다. 이 변경은 해당 필드를 제거하고signUpRequired를 반환합니다. 이전 계약을 사용하는 앱은 필수 가입 화면을 표시하지 못할 수 있습니다. 앱 전환이 완료되기 전까지 기존 필드의 호환성을 유지하세요.🤖 Prompt for AI Agents