Repository navigation
[FEAT] 유저 엔티티 구현 #18
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
[FEAT] 유저 엔티티 구현 #18
Changes from all commits
4eca5a0
502cd72
ca09338
ef81da0
742fa31
e4568ec
7089299
67dcb6c
71d57b6
66a48cc
3eba5e2
4f0bee3
956942d
311491d
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 |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package com.mr.domain.user.entity; | ||
|
|
||
| import com.mr.domain.user.exception.InstrumentErrorStatus; | ||
| import com.mr.global.apipayload.exception.GeneralException; | ||
| import com.mr.global.entity.BaseTimeEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table(name = "instrument") | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class Instrument extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| @Column(name = "instrument_id") | ||
| private Long instrumentId; | ||
|
|
||
| @Column(name = "code", nullable = false, unique = true, length = 20) | ||
| private String code; | ||
|
|
||
| @Column(name = "name", nullable = false, length = 50) | ||
| private String name; | ||
|
|
||
| @Column(name = "is_active", nullable = false) | ||
| private boolean active; | ||
|
|
||
| @Builder(access = AccessLevel.PRIVATE) | ||
| private Instrument(String code, String name, boolean active) { | ||
| validateCode(code); | ||
| validateName(name); | ||
| this.code = code; | ||
| this.name = name; | ||
| this.active = active; | ||
| } | ||
|
|
||
| public static Instrument create(String code, String name) { | ||
| return Instrument.builder() | ||
| .code(code) | ||
| .name(name) | ||
| .active(true) | ||
| .build(); | ||
| } | ||
|
|
||
| private static void validateCode(String code) { | ||
| if (code == null) { | ||
| throw new GeneralException(InstrumentErrorStatus.CODE_REQUIRED); | ||
| } | ||
| } | ||
|
|
||
| private static void validateName(String name) { | ||
| if (name == null) { | ||
| throw new GeneralException(InstrumentErrorStatus.NAME_REQUIRED); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package com.mr.domain.user.entity; | ||
|
|
||
| import com.mr.domain.user.entity.enums.TheoryLevel; | ||
| import com.mr.domain.user.exception.StudentErrorStatus; | ||
| import com.mr.global.apipayload.exception.GeneralException; | ||
| import com.mr.global.entity.BaseTimeEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.OneToOne; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table(name = "student") | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class Student extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| @Column(name = "student_id") | ||
| private Long studentId; | ||
|
|
||
| @OneToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false, unique = true) | ||
| private User user; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "theory_level", nullable = false, length = 20) | ||
| private TheoryLevel theoryLevel; | ||
|
|
||
| @Builder(access = AccessLevel.PRIVATE) | ||
| private Student(User user, TheoryLevel theoryLevel) { | ||
| validateUser(user); | ||
| validateTheoryLevel(theoryLevel); | ||
| this.user = user; | ||
| this.theoryLevel = theoryLevel; | ||
| } | ||
|
|
||
| public static Student create(User user, TheoryLevel theoryLevel) { | ||
| return Student.builder() | ||
| .user(user) | ||
| .theoryLevel(theoryLevel) | ||
| .build(); | ||
| } | ||
|
|
||
| public void updateTheoryLevel(TheoryLevel theoryLevel) { | ||
| validateTheoryLevel(theoryLevel); | ||
| this.theoryLevel = theoryLevel; | ||
| } | ||
|
|
||
| private static void validateUser(User user) { | ||
| if (user == null) { | ||
| throw new GeneralException(StudentErrorStatus.USER_REQUIRED); | ||
| } | ||
| } | ||
|
|
||
| private static void validateTheoryLevel(TheoryLevel theoryLevel) { | ||
| if (theoryLevel == null) { | ||
| throw new GeneralException(StudentErrorStatus.THEORY_LEVEL_REQUIRED); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package com.mr.domain.user.entity; | ||
|
|
||
| import com.mr.domain.user.exception.StudentInstrumentErrorStatus; | ||
| import com.mr.global.apipayload.exception.GeneralException; | ||
| import com.mr.global.entity.BaseTimeEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table( | ||
| name = "student_instrument", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(name = "uk_student_instrument_student_id_instrument_id", | ||
| columnNames = {"student_id", "instrument_id"}) | ||
| } | ||
| ) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class StudentInstrument extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| @Column(name = "student_instrument_id") | ||
| private Long studentInstrumentId; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "student_id", nullable = false) | ||
| private Student student; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "instrument_id", nullable = false) | ||
| private Instrument instrument; | ||
|
|
||
| @Column(name = "is_primary", nullable = false) | ||
| private boolean primary; | ||
|
|
||
| @Builder(access = AccessLevel.PRIVATE) | ||
| private StudentInstrument(Student student, Instrument instrument, boolean primary) { | ||
| validateStudent(student); | ||
| validateInstrument(instrument); | ||
| this.student = student; | ||
| this.instrument = instrument; | ||
| this.primary = primary; | ||
| } | ||
|
|
||
| public static StudentInstrument createPrimary(Student student, Instrument instrument) { | ||
| return StudentInstrument.builder() | ||
| .student(student) | ||
| .instrument(instrument) | ||
| .primary(true) | ||
| .build(); | ||
| } | ||
|
|
||
| public static StudentInstrument createSecondary(Student student, Instrument instrument) { | ||
| return StudentInstrument.builder() | ||
| .student(student) | ||
| .instrument(instrument) | ||
| .primary(false) | ||
| .build(); | ||
| } | ||
|
|
||
| public void markAsPrimary() { | ||
| this.primary = true; | ||
| } | ||
|
|
||
| public void unmarkAsPrimary() { | ||
| this.primary = false; | ||
| } | ||
|
|
||
| private static void validateStudent(Student student) { | ||
| if (student == null) { | ||
| throw new GeneralException(StudentInstrumentErrorStatus.STUDENT_REQUIRED); | ||
| } | ||
| } | ||
|
|
||
| private static void validateInstrument(Instrument instrument) { | ||
| if (instrument == null) { | ||
| throw new GeneralException(StudentInstrumentErrorStatus.INSTRUMENT_REQUIRED); | ||
| } | ||
| } | ||
| } |
|
Contributor
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. 닉네임 유효성 검증 로직함수 만드시는게 좋을 것 같습니다!
Contributor
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. 예외처리도 해주세요
Contributor
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.
public void updateNickname(String nickname) {
validateNickname(nickname);
this.nickname = nickname;
}
private static void validateNickname(String nickname) {
if (nickname == null || nickname.isBlank()) {
throw new GeneralException(UserErrorStatus.NICKNAME_REQUIRED);
}
if (nickname.length() > NICKNAME_MAX_LENGTH) {
throw new GeneralException(UserErrorStatus.NICKNAME_TOO_LONG);
}
}예외 타입은
Contributor
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. IllegalArgumentException 대신 GeneralException 활용하신 것 좋습니다~! 다만 저희 대면 회의 때 정책으로 가능한 닉네임 양식 정해뒀었던 걸로 기억해요~! 지금은 null이나 빈 값을 방어하고, 최댓값을 방어하고 있는데, (1) 한글인 경우 자모음만 들어오는 것 방어 + (2) 한글/영어/숫자 외 문자 방어 + (3) 최소 글자수(2글자) 제한을 같이 검증하는 방안으로 수정해주실 수 있으신가요?! 회의록에서 긁어왔습니당...ㅎㅎ
Contributor
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. 넵 반영했습니다! private static final Pattern NICKNAME_PATTERN =
Pattern.compile("^[가-힣a-zA-Z0-9]{2,10}$");한글 완성형(가-힣)만 허용해서 자음/모음 단독 입력은 유니코드 범위가 달라 자동으로 막히고 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package com.mr.domain.user.entity; | ||
|
|
||
| import com.mr.domain.user.exception.UserErrorStatus; | ||
| import com.mr.global.apipayload.exception.GeneralException; | ||
| import com.mr.global.entity.BaseTimeEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.validation.constraints.Size; | ||
| import java.util.regex.Pattern; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table(name = "users") | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class User extends BaseTimeEntity { | ||
|
|
||
| private static final int NICKNAME_MIN_LENGTH = 2; | ||
| private static final int NICKNAME_MAX_LENGTH = 10; | ||
| private static final Pattern NICKNAME_PATTERN = | ||
| Pattern.compile("^[가-힣a-zA-Z0-9]{" + NICKNAME_MIN_LENGTH + "," + NICKNAME_MAX_LENGTH + "}$"); | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| @Column(name = "user_id") | ||
| private Long userId; | ||
|
|
||
| @Size( | ||
| min = NICKNAME_MIN_LENGTH, | ||
| max = NICKNAME_MAX_LENGTH, | ||
| message = "닉네임은 2자 이상 10자 이하로 입력해야 합니다." | ||
| ) | ||
| @Column(name = "nickname", unique = true, length = NICKNAME_MAX_LENGTH) | ||
| private String nickname; | ||
|
|
||
| @Column(name = "profile_img_url", nullable = false, length = 255) | ||
| private String profileImgUrl; | ||
|
|
||
| @Builder(access = AccessLevel.PRIVATE) | ||
| private User(String profileImgUrl) { | ||
| this.profileImgUrl = profileImgUrl; | ||
| } | ||
|
|
||
| public static User createFromOAuth(String profileImgUrl) { | ||
| return User.builder() | ||
| .profileImgUrl(profileImgUrl) | ||
| .build(); | ||
| } | ||
|
Comment on lines
+46
to
+55
Contributor
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.
public boolean isOnboardingCompleted() {
return nickname != null;
}다만 실제 검사를 인증/인가 로직, 필터 또는 각 API의 공통 사용자 조회 로직 중 어디에서 처리할지는 전체 로그인 흐름과 연관된 부분이라, 해당 로직을 추후 구현할 예정인지 궁금합니다. 이 부분은 인증 쪽 담당하시는 예원님과 함께 정책을 정해보면 좋을 것 같습니다! 백엔드 전체 회의 때 안건으로 올려도 좋을 것 같고...!
Contributor
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. 말씀하신 대로 isOnboardingCompleted()는 추가했습니다. public boolean isOnboardingCompleted() {
return nickname != null;
}다만 이걸 필터/인터셉터에서 전역으로 막을지 각 API에서 개별 검사할지는
Contributor
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. 좋습니당! |
||
|
|
||
| public boolean isOnboardingCompleted() { | ||
| return nickname != null; | ||
| } | ||
|
|
||
| public void updateNickname(String nickname) { | ||
| String trimmedNickname = nickname == null ? null : nickname.trim(); | ||
| validateNickname(trimmedNickname); | ||
| this.nickname = trimmedNickname; | ||
| } | ||
|
|
||
| private static void validateNickname(String nickname) { | ||
| if (nickname == null || nickname.isBlank()) { | ||
| throw new GeneralException(UserErrorStatus.NICKNAME_REQUIRED); | ||
| } | ||
| if (!NICKNAME_PATTERN.matcher(nickname).matches()) { | ||
| throw new GeneralException(UserErrorStatus.NICKNAME_INVALID_FORMAT); | ||
| } | ||
| } | ||
|
Comment on lines
+61
to
+74
Contributor
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. 현재는 updateNickname()에만 validateNickname()을 거치고 있는데, 유저 생성 시 닉네임을 함께 전달받을 때를 고려해 생성자 내부 또는 닉네임 할당 시 항상 validateNickname()을 거치도록 처리하는 것은 어떨지 의견 여쭙습니다!
Contributor
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. 확인해보니 현재 |
||
| } | ||
|
ownue marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.mr.domain.user.entity.enums; | ||
|
|
||
| public enum TheoryLevel { | ||
| BEGINNER, | ||
| INTERMEDIATE, | ||
| ADVANCED | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.mr.domain.user.exception; | ||
|
|
||
| import com.mr.global.apipayload.code.BaseCode; | ||
| import org.springframework.http.HttpStatus; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum InstrumentErrorStatus implements BaseCode { | ||
|
|
||
| CODE_REQUIRED(HttpStatus.BAD_REQUEST, "INSTRUMENT_400_01", "code는 필수입니다."), | ||
| NAME_REQUIRED(HttpStatus.BAD_REQUEST, "INSTRUMENT_400_02", "name은 필수입니다."); | ||
|
|
||
| private final HttpStatus status; | ||
| private final String code; | ||
| private final String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.mr.domain.user.exception; | ||
|
|
||
| import com.mr.global.apipayload.code.BaseCode; | ||
| import org.springframework.http.HttpStatus; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum StudentErrorStatus implements BaseCode { | ||
|
|
||
| USER_REQUIRED(HttpStatus.BAD_REQUEST, "STUDENT_400_01", "user는 필수입니다."), | ||
| THEORY_LEVEL_REQUIRED(HttpStatus.BAD_REQUEST, "STUDENT_400_02", "theoryLevel은 필수입니다."); | ||
|
|
||
| private final HttpStatus status; | ||
| private final String code; | ||
| private final String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.mr.domain.user.exception; | ||
|
|
||
| import com.mr.global.apipayload.code.BaseCode; | ||
| import org.springframework.http.HttpStatus; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum StudentInstrumentErrorStatus implements BaseCode { | ||
|
|
||
| STUDENT_REQUIRED(HttpStatus.BAD_REQUEST, "STUDENT_INSTRUMENT_400_01", "student는 필수입니다."), | ||
| INSTRUMENT_REQUIRED(HttpStatus.BAD_REQUEST, "STUDENT_INSTRUMENT_400_02", "instrument는 필수입니다."); | ||
|
|
||
| private final HttpStatus status; | ||
| private final String code; | ||
| private final String message; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.mr.domain.user.exception; | ||
|
|
||
| import com.mr.global.apipayload.code.BaseCode; | ||
| import org.springframework.http.HttpStatus; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @AllArgsConstructor | ||
| public enum UserErrorStatus implements BaseCode { | ||
|
|
||
| NICKNAME_REQUIRED(HttpStatus.BAD_REQUEST, "USER_400_01", "닉네임은 필수입니다."), | ||
| NICKNAME_INVALID_FORMAT(HttpStatus.BAD_REQUEST, "USER_400_02", "닉네임은 한글, 영어, 숫자 2~10자로 입력해야 합니다."); | ||
|
|
||
| private final HttpStatus status; | ||
| private final String code; | ||
| private final String message; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.