Skip to content

Commit 040c860

Browse files
authored
Merge pull request #18 from rnqhstmd/main
[volume-1] 회원가입, 내 정보 조회, 포인트 조회, 포인트 충전 구현
2 parents d5ec397 + be2f54f commit 040c860

29 files changed

Lines changed: 2104 additions & 0 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.loopers.application.point;
2+
3+
import com.loopers.domain.point.Point;
4+
import com.loopers.domain.point.PointService;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
@Component
10+
@RequiredArgsConstructor
11+
public class PointFacade {
12+
13+
private final PointService pointService;
14+
15+
@Transactional
16+
public void createPointForUser(String userId) {
17+
pointService.createPoint(userId);
18+
}
19+
20+
@Transactional(readOnly = true)
21+
public Point getPoint(String userId) {
22+
return pointService.getPoint(userId);
23+
}
24+
25+
@Transactional
26+
public void chargePoint(String userId, Long amount) {
27+
pointService.chargePoint(userId, amount);
28+
}
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.loopers.application.user;
2+
3+
4+
import com.loopers.application.point.PointFacade;
5+
import com.loopers.domain.user.Gender;
6+
import com.loopers.domain.user.User;
7+
import com.loopers.domain.user.UserService;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
@Component
13+
@RequiredArgsConstructor
14+
public class UserFacade {
15+
16+
private final UserService userService;
17+
private final PointFacade pointFacade;
18+
19+
@Transactional
20+
public UserInfo signUp(String userId, String email, String birthDate, Gender gender) {
21+
User user = userService.signUp(userId, email, birthDate, gender);
22+
pointFacade.createPointForUser(userId);
23+
return UserInfo.from(user);
24+
}
25+
26+
public UserInfo getUserInfo(String userId) {
27+
User user = userService.getUserByUserId(userId);
28+
return UserInfo.from(user);
29+
}
30+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.loopers.application.user;
2+
3+
4+
import com.loopers.domain.user.Gender;
5+
import com.loopers.domain.user.User;
6+
7+
public record UserInfo(
8+
String userId,
9+
String email,
10+
String birthDate,
11+
Gender gender
12+
) {
13+
public static UserInfo from(User user) {
14+
return new UserInfo(
15+
user.getUserIdValue(),
16+
user.getEmailValue(),
17+
user.getBirthDateValue(),
18+
user.getGender()
19+
);
20+
}
21+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.loopers.domain.point;
2+
3+
4+
import com.loopers.domain.BaseEntity;
5+
import com.loopers.support.error.CoreException;
6+
import com.loopers.support.error.ErrorType;
7+
import jakarta.persistence.Column;
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.Table;
10+
import lombok.AccessLevel;
11+
import lombok.Getter;
12+
import lombok.NoArgsConstructor;
13+
14+
@Entity
15+
@Getter
16+
@Table(name = "points")
17+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
18+
public class Point extends BaseEntity {
19+
20+
@Column(name = "user_id", unique = true, nullable = false, length = 10)
21+
private String userId;
22+
23+
@Column(name = "amount", nullable = false)
24+
private Long amount;
25+
26+
private Point(String userId, Long amount) {
27+
validateUserId(userId);
28+
validateAmount(amount);
29+
this.userId = userId;
30+
this.amount = amount;
31+
}
32+
33+
public static Point create(String userId) {
34+
return new Point(userId, 0L);
35+
}
36+
37+
public static Point create(String userId, Long initialAmount) {
38+
return new Point(userId, initialAmount);
39+
}
40+
41+
public void charge(Long amount) {
42+
if (amount == null) {
43+
throw new CoreException(ErrorType.BAD_REQUEST, "포인트 금액은 필수입니다.");
44+
}
45+
if (amount <= 0) {
46+
throw new CoreException(ErrorType.BAD_REQUEST, "충전 금액은 0보다 커야 합니다.");
47+
}
48+
this.amount += amount;
49+
}
50+
51+
private void validateUserId(String userId) {
52+
if (userId == null || userId.isBlank()) {
53+
throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID는 비어있을 수 없습니다.");
54+
}
55+
}
56+
57+
private void validateAmount(Long amount) {
58+
if (amount == null) {
59+
throw new CoreException(ErrorType.BAD_REQUEST, "포인트 금액은 필수입니다.");
60+
}
61+
if (amount < 0) {
62+
throw new CoreException(ErrorType.BAD_REQUEST, "포인트 금액은 0 이상이어야 합니다.");
63+
}
64+
}
65+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.loopers.domain.point;
2+
3+
import java.util.Optional;
4+
5+
public interface PointRepository {
6+
Point save(Point point);
7+
Optional<Point> findByUserId(String userId);
8+
boolean existsByUserId(String userId);
9+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.loopers.domain.point;
2+
3+
import com.loopers.support.error.CoreException;
4+
import com.loopers.support.error.ErrorType;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
@Service
10+
@RequiredArgsConstructor
11+
@Transactional(readOnly = true)
12+
public class PointService {
13+
14+
private final PointRepository pointRepository;
15+
16+
@Transactional
17+
public Point createPoint(String userId) {
18+
if (pointRepository.existsByUserId(userId)) {
19+
throw new CoreException(ErrorType.CONFLICT, "이미 포인트가 존재하는 사용자입니다.");
20+
}
21+
Point point = Point.create(userId);
22+
return pointRepository.save(point);
23+
}
24+
25+
public Point getPoint(String userId) {
26+
return pointRepository.findByUserId(userId)
27+
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "포인트를 찾을 수 없습니다."));
28+
}
29+
30+
public Long getPointAmount(String userId) {
31+
Point point = getPoint(userId); // 여기서 예외 발생 가능
32+
return point.getAmount();
33+
}
34+
35+
@Transactional
36+
public void chargePoint(String userId, Long amount) {
37+
Point point = pointRepository.findByUserId(userId)
38+
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "포인트를 찾을 수 없습니다."));
39+
point.charge(amount);
40+
pointRepository.save(point);
41+
}
42+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.loopers.domain.user;
2+
3+
import com.loopers.support.error.CoreException;
4+
import com.loopers.support.error.ErrorType;
5+
import jakarta.persistence.Embeddable;
6+
import lombok.AccessLevel;
7+
import lombok.EqualsAndHashCode;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
import java.time.LocalDate;
12+
import java.time.format.DateTimeFormatter;
13+
import java.time.format.DateTimeParseException;
14+
15+
@Getter
16+
@Embeddable
17+
@EqualsAndHashCode
18+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
19+
public class BirthDate {
20+
21+
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
22+
23+
private String value;
24+
25+
public BirthDate(String value) {
26+
validate(value);
27+
this.value = value;
28+
}
29+
30+
private void validate(String value) {
31+
if (value == null || value.isBlank()) {
32+
throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 비어있을 수 없습니다.");
33+
}
34+
try {
35+
LocalDate.parse(value, FORMATTER);
36+
} catch (DateTimeParseException e) {
37+
throw new CoreException(ErrorType.BAD_REQUEST, "생년월일은 yyyy-MM-dd 형식이어야 합니다.");
38+
}
39+
}
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.loopers.domain.user;
2+
3+
import com.loopers.support.error.CoreException;
4+
import com.loopers.support.error.ErrorType;
5+
import jakarta.persistence.Embeddable;
6+
import lombok.AccessLevel;
7+
import lombok.EqualsAndHashCode;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
import java.util.regex.Pattern;
12+
13+
@Getter
14+
@Embeddable
15+
@EqualsAndHashCode
16+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
17+
public class Email {
18+
19+
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$");
20+
21+
private String value;
22+
23+
public Email(String value) {
24+
validate(value);
25+
this.value = value;
26+
}
27+
28+
private void validate(String value) {
29+
if (value == null || value.isBlank()) {
30+
throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 비어있을 수 없습니다.");
31+
}
32+
if (!EMAIL_PATTERN.matcher(value).matches()) {
33+
throw new CoreException(ErrorType.BAD_REQUEST, "이메일은 xx@yy.zz 형식이어야 합니다.");
34+
}
35+
}
36+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.loopers.domain.user;
2+
3+
public enum Gender {
4+
MALE, FEMALE, OTHER
5+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.loopers.domain.user;
2+
3+
4+
import com.loopers.domain.BaseEntity;
5+
import com.loopers.support.error.CoreException;
6+
import com.loopers.support.error.ErrorType;
7+
import jakarta.persistence.*;
8+
import lombok.AccessLevel;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
import java.util.regex.Pattern;
13+
14+
15+
@Entity
16+
@Getter
17+
@Table(name = "users")
18+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
19+
public class User extends BaseEntity {
20+
21+
private static final Pattern USER_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]{1,10}$");
22+
23+
@Column(name = "user_id", unique = true, nullable = false, length = 10)
24+
private String userId;
25+
26+
@Embedded
27+
@AttributeOverride(name = "value", column = @Column(name = "email", nullable = false, length = 100))
28+
private Email email;
29+
30+
@Embedded
31+
@AttributeOverride(name = "value", column = @Column(name = "birth_date", nullable = false, length = 10))
32+
private BirthDate birthDate;
33+
34+
@Enumerated(EnumType.STRING)
35+
@Column(name = "gender", nullable = false, length = 10)
36+
private Gender gender;
37+
38+
private User(String userId, Email email, BirthDate birthDate, Gender gender) {
39+
validateUserId(userId);
40+
validateRequiredFields(userId, email, birthDate, gender);
41+
this.userId = userId;
42+
this.email = email;
43+
this.birthDate = birthDate;
44+
this.gender = gender;
45+
}
46+
47+
public static User create(String userId, String email, String birthDate, Gender gender) {
48+
if (gender == null) {
49+
throw new CoreException(ErrorType.BAD_REQUEST, "성별은 필수입니다.");
50+
}
51+
return new User(
52+
userId,
53+
new Email(email),
54+
new BirthDate(birthDate),
55+
gender
56+
);
57+
}
58+
59+
private void validateUserId(String userId) {
60+
if (userId == null || userId.isBlank()) {
61+
throw new CoreException(ErrorType.BAD_REQUEST, "ID는 비어있을 수 없습니다.");
62+
}
63+
if (!USER_ID_PATTERN.matcher(userId).matches()) {
64+
throw new CoreException(ErrorType.BAD_REQUEST, "ID는 영문 및 숫자 10자 이내여야 합니다.");
65+
}
66+
}
67+
68+
private void validateRequiredFields(String userId, Email email, BirthDate birthDate, Gender gender) {
69+
if (userId == null || email == null || birthDate == null || gender == null) {
70+
throw new CoreException(ErrorType.BAD_REQUEST, "필수 필드가 누락되었습니다.");
71+
}
72+
}
73+
74+
public String getUserIdValue() {
75+
return userId;
76+
}
77+
78+
public String getEmailValue() {
79+
return email.getValue();
80+
}
81+
82+
public String getBirthDateValue() {
83+
return birthDate.getValue();
84+
}
85+
}

0 commit comments

Comments
 (0)