-
Notifications
You must be signed in to change notification settings - Fork 36
[volume-1] 유저/포인트 도메인 구현 및 테스트 코드 작성 #35
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
Merged
sylee6529
merged 17 commits into
Loopers-dev-lab:sylee6529
from
sylee6529:feat/user-point-test
Nov 13, 2025
Merged
Changes from 16 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
41c4e50
feat: 회원 가입 및 조회 기능 구현
sylee6529 3a5db12
feat: 포인트 관리 기능 구현
sylee6529 06e1422
feat: 회원 API 기능 구현
sylee6529 8492cfe
feat: 포인트 조회 API 구현
sylee6529 624f6e7
feat: SecurityConfig 추가
sylee6529 c7385cd
feat: 에러 타입 추가
sylee6529 37a55a6
fix: 안 쓰는 example 코드 삭제
sylee6529 4fc9310
refactor: 도메인 모델 구조 개선
sylee6529 e2d9b27
refactor: 서비스 계층 메서드명 통일
sylee6529 694cf6b
refactor: 어플리케이션 계층 리팩토링
sylee6529 7931738
feat: API DTO에 validation 추가
sylee6529 8d25f25
refactor: API 컨트롤러 및 스펙 정리
sylee6529 ddc2def
test: 테스트 코드 업데이트
sylee6529 cea05c1
refactor: 회원 생성과 포인트 초기화는 트랜잭션으로 보장되어야 한다
sylee6529 436001f
refactor: 유저 비밀번호에 jsonignore 처리 추가
sylee6529 df455a5
refactor: 유저 생성자에 gender 검증 추가
sylee6529 d1c961c
refactor: 중복 회원 경쟁적 생성 시 유니크 제약조건 에러 처리
sylee6529 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
17 changes: 0 additions & 17 deletions
17
apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
This file was deleted.
Oops, something went wrong.
13 changes: 0 additions & 13 deletions
13
apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
This file was deleted.
Oops, something went wrong.
36 changes: 36 additions & 0 deletions
36
apps/commerce-api/src/main/java/com/loopers/application/members/MemberFacade.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.loopers.application.members; | ||
|
|
||
| import com.loopers.domain.members.Gender; | ||
| import com.loopers.domain.members.MemberModel; | ||
| import com.loopers.domain.members.MemberService; | ||
| import com.loopers.domain.points.PointService; | ||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class MemberFacade { | ||
| private final MemberService memberService; | ||
| private final PointService pointService; | ||
|
|
||
| @Transactional | ||
| public MemberInfo registerMember(String memberId, String email, String password, String birthDate, Gender gender) { | ||
| MemberModel member = memberService.registerMember(memberId, email, password, birthDate, gender); | ||
| pointService.initializeMemberPoints(memberId); | ||
| return MemberInfo.from(member); | ||
| } | ||
|
|
||
| public MemberInfo getMemberByMemberId(String memberId) { | ||
| MemberModel member = memberService.getMemberByMemberId(memberId); | ||
| if (member == null) { | ||
| throw new CoreException( | ||
| ErrorType.NOT_FOUND, | ||
| "존재하지 않는 회원입니다." | ||
| ); | ||
| } | ||
| return MemberInfo.from(member); | ||
| } | ||
| } |
24 changes: 24 additions & 0 deletions
24
apps/commerce-api/src/main/java/com/loopers/application/members/MemberInfo.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.loopers.application.members; | ||
|
|
||
| import com.loopers.domain.members.Gender; | ||
| import com.loopers.domain.members.MemberModel; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| public record MemberInfo( | ||
| Long id, | ||
| String memberId, | ||
| String email, | ||
| LocalDate birthDate, | ||
| Gender gender | ||
| ) { | ||
| public static MemberInfo from(MemberModel model) { | ||
| return new MemberInfo( | ||
| model.getId(), | ||
| model.getMemberId(), | ||
| model.getEmail(), | ||
| model.getBirthDate(), | ||
| model.getGender() | ||
| ); | ||
| } | ||
| } |
28 changes: 28 additions & 0 deletions
28
apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.loopers.config; | ||
|
|
||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
| import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
| import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; | ||
| import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||
| import org.springframework.security.web.SecurityFilterChain; | ||
|
|
||
| @Configuration | ||
| @EnableWebSecurity | ||
| public class SecurityConfig { | ||
|
|
||
| @Bean | ||
| public PasswordEncoder passwordEncoder() { | ||
| return new BCryptPasswordEncoder(); | ||
| } | ||
|
|
||
| @Bean | ||
| public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
| return http | ||
| .csrf(AbstractHttpConfigurer::disable) | ||
| .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()) | ||
| .build(); | ||
| } | ||
| } |
44 changes: 0 additions & 44 deletions
44
apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
This file was deleted.
Oops, something went wrong.
7 changes: 0 additions & 7 deletions
7
apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
This file was deleted.
Oops, something went wrong.
20 changes: 0 additions & 20 deletions
20
apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
This file was deleted.
Oops, something went wrong.
7 changes: 7 additions & 0 deletions
7
apps/commerce-api/src/main/java/com/loopers/domain/members/Gender.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.loopers.domain.members; | ||
|
|
||
| public enum Gender { | ||
| MALE, | ||
| FEMALE, | ||
| OTHER | ||
| } |
97 changes: 97 additions & 0 deletions
97
apps/commerce-api/src/main/java/com/loopers/domain/members/MemberModel.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.loopers.domain.members; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||
| import com.loopers.domain.BaseEntity; | ||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.Table; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.time.format.DateTimeParseException; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| @NoArgsConstructor | ||
| @Getter | ||
| @Entity | ||
| @Table(name = "members") | ||
| public class MemberModel extends BaseEntity { | ||
|
|
||
| private static final Pattern MEMBER_ID_REGEX = Pattern.compile("^[a-zA-Z0-9]{1,10}$"); | ||
| private static final Pattern EMAIL_REGEX = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$"); | ||
| private static final DateTimeFormatter BIRTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); | ||
|
|
||
| @Column(name = "member_id", nullable = false, unique = true, length = 10) | ||
| private String memberId; | ||
|
|
||
| @Column(name = "email", nullable = false) | ||
| private String email; | ||
|
|
||
| @Column(name = "birth_date", nullable = false) | ||
| private LocalDate birthDate; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "gender", nullable = false) | ||
| private Gender gender; | ||
|
|
||
| @Column(name = "password", nullable = false) | ||
| @JsonIgnore | ||
| private String password; | ||
|
|
||
| public MemberModel(String memberId, String email, String password, String birthDate, Gender gender) { | ||
| validateMemberId(memberId); | ||
| validateEmail(email); | ||
| validateGender(gender); | ||
|
|
||
| this.memberId = memberId; | ||
| this.email = email; | ||
| this.password = password; | ||
| this.birthDate = parseBirthDate(birthDate); | ||
| this.gender = gender; | ||
| } | ||
|
sylee6529 marked this conversation as resolved.
|
||
|
|
||
| private static void validateMemberId(String memberId) { | ||
| if (!MEMBER_ID_REGEX.matcher(memberId).matches()) { | ||
| throw new CoreException( | ||
| ErrorType.BAD_REQUEST, | ||
| "ID는 영문 및 숫자 10자 이내여야 합니다." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| private static void validateEmail(String email) { | ||
| if (!EMAIL_REGEX.matcher(email).matches()) { | ||
| throw new CoreException( | ||
| ErrorType.BAD_REQUEST, | ||
| "이메일은 xx@yy.zz 형식이어야 합니다." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| public static void validateGender(Gender gender) { | ||
| if (gender == null) { | ||
| throw new CoreException( | ||
| ErrorType.BAD_REQUEST, | ||
| "성별은 필수입니다." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| private static LocalDate parseBirthDate(String birthDate) { | ||
| try { | ||
| return LocalDate.parse(birthDate, BIRTH_DATE_FORMATTER); | ||
| } catch (DateTimeParseException e) { | ||
| throw new CoreException( | ||
| ErrorType.BAD_REQUEST, | ||
| "생년월일은 yyyy-MM-dd 형식이어야 합니다." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| } | ||
7 changes: 7 additions & 0 deletions
7
apps/commerce-api/src/main/java/com/loopers/domain/members/MemberRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.loopers.domain.members; | ||
|
|
||
| public interface MemberRepository { | ||
| MemberModel save(MemberModel member); | ||
| MemberModel findByMemberId(String memberId); | ||
| boolean existsByMemberId(String memberId); | ||
| } | ||
|
sylee6529 marked this conversation as resolved.
|
||
35 changes: 35 additions & 0 deletions
35
apps/commerce-api/src/main/java/com/loopers/domain/members/MemberService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.loopers.domain.members; | ||
|
|
||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class MemberService { | ||
|
|
||
| private final MemberRepository memberRepository; | ||
| private final PasswordEncoder passwordEncoder; | ||
|
|
||
| @Transactional | ||
| public MemberModel registerMember(String memberId, String email, String password, String birthDate, Gender gender) { | ||
| if (memberRepository.existsByMemberId(memberId)) { | ||
| throw new CoreException( | ||
| ErrorType.CONFLICT, | ||
| "이미 가입된 ID 입니다." | ||
| ); | ||
| } | ||
|
sylee6529 marked this conversation as resolved.
Outdated
|
||
|
|
||
| String encodedPassword = passwordEncoder.encode(password); | ||
| MemberModel member = new MemberModel(memberId, email, encodedPassword, birthDate, gender); | ||
| return memberRepository.save(member); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public MemberModel getMemberByMemberId(String memberId) { | ||
| return memberRepository.findByMemberId(memberId); | ||
| } | ||
| } | ||
40 changes: 40 additions & 0 deletions
40
apps/commerce-api/src/main/java/com/loopers/domain/points/PointModel.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.loopers.domain.points; | ||
|
|
||
| import com.loopers.domain.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.math.BigDecimal; | ||
|
|
||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Getter | ||
| @Entity | ||
| @Table(name = "point") | ||
| public class PointModel extends BaseEntity { | ||
|
|
||
| @Column(unique = true, nullable = false, length = 10) | ||
| private String memberId; | ||
|
|
||
| @Column(nullable = false) | ||
| private BigDecimal amount; | ||
|
|
||
| public static PointModel create(String memberId, BigDecimal amount) { | ||
| if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { | ||
| throw new IllegalArgumentException("포인트는 0 이상이어야 합니다"); | ||
| } | ||
|
|
||
| return new PointModel(memberId, amount); | ||
| } | ||
|
|
||
| public void addAmount(BigDecimal addAmount) { | ||
| if (addAmount.compareTo(BigDecimal.ZERO) <= 0) { | ||
| throw new IllegalArgumentException("추가하는 포인트는 0보다 커야 합니다"); | ||
| } | ||
| this.amount = this.amount.add(addAmount); | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
apps/commerce-api/src/main/java/com/loopers/domain/points/PointRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.loopers.domain.points; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface PointRepository { | ||
| Optional<PointModel> findByMemberId(String memberId); | ||
| PointModel save(PointModel pointModel); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.