Skip to content
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

프로젝트 저장 유효성 검사 완료 및 테스트 코드 일부 작성 #71

Merged
merged 47 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
88cf32e
feat: 공통 상수 관리 클래스 생성
Sehee-Lee-01 Feb 23, 2024
693b90b
feat: 공통 정규식 관리 클래스 생성
Sehee-Lee-01 Feb 23, 2024
3ee6502
feat: 프로젝트 상수 관리 클래스 생성
Sehee-Lee-01 Feb 23, 2024
b4b3eda
feat: 공통 정규식 상수 적용(URI)
Sehee-Lee-01 Feb 23, 2024
413a173
feat: 프로젝트 도메인 request dto 유효성 검사 적용
Sehee-Lee-01 Feb 23, 2024
88781ca
feat: 프로젝트 도메인 상수 적용
Sehee-Lee-01 Feb 23, 2024
a3ae300
Merge branch 'dev' into feat/#27-validate-project
Sehee-Lee-01 Feb 26, 2024
7e7d9bb
feat: 길이 제약 없는 Text 데이터 공통 제한 길이 설정
Sehee-Lee-01 Feb 26, 2024
c2f184f
feat: 프로젝트 생성 관련 유효성 검사 클래스 생성
Sehee-Lee-01 Feb 26, 2024
e9e8eb1
feat: 프로젝트 관련 클래스 생성자에 유효성 검사 적용
Sehee-Lee-01 Feb 26, 2024
e1b6acb
comment: 불필요한 주석 삭제
Sehee-Lee-01 Feb 26, 2024
4cfe9b0
chore: init.sql 변경사항 적용
Sehee-Lee-01 Feb 26, 2024
986a732
Revert "chore: init.sql 변경사항 적용"
Sehee-Lee-01 Feb 26, 2024
87c352c
chore: init.sql 멤버 닉네임 null로 설정 및 프로젝트 관련 글자 제한 수 수정
Sehee-Lee-01 Feb 26, 2024
156db91
refactor: 저장 후 저장 결과 리턴하도록 변경
Sehee-Lee-01 Feb 26, 2024
911447f
test: 프로젝트 도메인 관련 테스트 클래스 생성
Sehee-Lee-01 Feb 26, 2024
1b0f35e
feat: 레이아웃 이미지 클래스 유효설 검사 클래스 구현
Sehee-Lee-01 Feb 26, 2024
418b6cc
refactor: 글자수 제한 수정 및 List 형태 갯수 제한 상수 설정
Sehee-Lee-01 Feb 26, 2024
0c2b4f9
refactor: 프로젝트 저장 시 저장된 프로젝트 정보 반환하는 것으로 수정
Sehee-Lee-01 Feb 26, 2024
2b68614
fix: validateBlank를 적용한 것을 validateNotBlank로 변경 및 멤버 도메인 생성자 유효성 검사 로…
Sehee-Lee-01 Feb 26, 2024
fc4ffe2
fix: 프로젝트 기간이 null인 경우 고려하여 응답 dto 수정
Sehee-Lee-01 Feb 26, 2024
e5bf6d8
feat: 컬렉션 리스트 null과 empty를 동시에 확인하는 기능 추가
Sehee-Lee-01 Feb 26, 2024
3dfce0f
refactor: 프로젝트 서비스 코드 간소화 및 기능 응집성 높이기
Sehee-Lee-01 Feb 26, 2024
9a60d50
refactor: 변수명 변경
Sehee-Lee-01 Feb 26, 2024
fbadabb
refactor: 프로젝트 스킬 유효성 검사 클래스 생성
Sehee-Lee-01 Feb 26, 2024
3dddfc2
comment: 멤버 등록을 안하는 경우 메모
Sehee-Lee-01 Feb 26, 2024
6b46d12
comment: 불필요한 주석 제거
Sehee-Lee-01 Feb 26, 2024
e9949ed
Merge branch 'feat/#27-validate-project' of https://github.com/side-p…
Sehee-Lee-01 Feb 26, 2024
58aa4b0
feat: 프로젝트 기간 날짜 정보만 담도록 수정
Sehee-Lee-01 Feb 26, 2024
c72ba9e
refactor: 프로젝트 기간 연-월로 수정
Sehee-Lee-01 Feb 27, 2024
1585c99
Merge branch 'dev' into feat/#27-validate-project
Sehee-Lee-01 Feb 27, 2024
053a161
style: 메서드 간 간격 조정
Sehee-Lee-01 Feb 27, 2024
5bdfdf4
test: ProjectSkillServiceTest 클래스 생성
Sehee-Lee-01 Feb 27, 2024
1120fc2
comment: 불필요한 주석 삭제
Sehee-Lee-01 Feb 27, 2024
750f7de
refactor: 프로젝트 연관 도메인 서비스에서 프로젝트 null 확인 코드 추가
Sehee-Lee-01 Feb 27, 2024
d673a5e
test: FileServiceTest 파일 목록 저장 기능 테스트 코드 작성
Sehee-Lee-01 Feb 27, 2024
4d361c6
merge: 프로젝트 상세조회 기능 리팩토링 브랜치와 merge
Sehee-Lee-01 Feb 27, 2024
4f1a22c
refactor: 멤버와 기술스택 개수 상수 수정
Sehee-Lee-01 Feb 27, 2024
ba84c1b
refactor: 유효성 검사 메시지 수정
Sehee-Lee-01 Feb 27, 2024
7feab3a
refactor: 유효성 검사 로직 수정
Sehee-Lee-01 Feb 27, 2024
4fc1dfe
rename: 패키지 이름 수정
Sehee-Lee-01 Feb 27, 2024
4f66d9e
test: 레이아웃 이미지 저장 테스트 코드 작성
Sehee-Lee-01 Feb 27, 2024
b92d292
test: 프로젝트 스킬 저장 테스트 코드 작성
Sehee-Lee-01 Feb 27, 2024
efe24f6
test: 프로젝트 멤버 저장 테스트 코드 작성
Sehee-Lee-01 Feb 27, 2024
396ea3d
test: 테스트 코드에 사용되는 리스트 길이 수정
Sehee-Lee-01 Feb 27, 2024
ac1ff3a
Merge branch 'dev' into test/#40-test-create-project
Sehee-Lee-01 Feb 27, 2024
f20d355
refactor: 오타 수정
Sehee-Lee-01 Feb 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryDSL에서 만들어주는 generated 폴더는 .gitignore에 추가해도 될 것 같긴 하네용!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 제가 테스트 코드 작성하면서 QClass 관련 파일들 .gitignore에 추가할게요!

Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class QProject extends EntityPathBase<Project> {

public final StringPath description = createString("description");

public final DateTimePath<java.time.LocalDateTime> endDate = createDateTime("endDate", java.time.LocalDateTime.class);
public final ComparablePath<java.time.YearMonth> endDate = createComparable("endDate", java.time.YearMonth.class);

public final StringPath githubUrl = createString("githubUrl");

Expand All @@ -44,7 +44,7 @@ public class QProject extends EntityPathBase<Project> {

public final NumberPath<Long> ownerId = createNumber("ownerId", Long.class);

public final DateTimePath<java.time.LocalDateTime> startDate = createDateTime("startDate", java.time.LocalDateTime.class);
public final ComparablePath<java.time.YearMonth> startDate = createComparable("startDate", java.time.YearMonth.class);

public final StringPath subName = createString("subName");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package sixgaezzang.sidepeek.common.util;

public class CommonConstant {
public static final long MIN_ID = 1;
public static final int MAX_TEXT_LENGTH = 21_844;
}
6 changes: 6 additions & 0 deletions src/main/java/sixgaezzang/sidepeek/common/util/Regex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package sixgaezzang.sidepeek.common.util;

public class Regex {
public static final String URL_REGEXP =
"^(https?)://([^:/\\s]+)(:([^/]*))?((/[^\\s/]+)*)?/?([^#\\s?]*)(\\?([^#\\s]*))?(#(\\w*))?$";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import static io.micrometer.common.util.StringUtils.isBlank;
import static io.micrometer.common.util.StringUtils.isNotBlank;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_TEXT_LENGTH;
import static sixgaezzang.sidepeek.common.util.Regex.URL_REGEXP;
import static sixgaezzang.sidepeek.users.domain.Password.PASSWORD_REGXP;

import java.util.Collection;
import java.util.Objects;
import java.util.regex.Pattern;
import lombok.NoArgsConstructor;
import org.springframework.util.Assert;
Expand All @@ -16,8 +20,7 @@ public final class ValidationUtils {
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+[.][0-9A-Za-z]+$");

private static final Pattern URI_PATTERN = Pattern.compile(
"^(https?)://([^:/\\s]+)(:([^/]*))?((/[^\\s/]+)*)?/?([^#\\s?]*)(\\?([^#\\s]*))?(#(\\w*))?$");
private static final Pattern URI_PATTERN = Pattern.compile(URL_REGEXP);

public static void validateEmail(String input, String message) {
pattern(input, EMAIL_PATTERN, message);
Expand All @@ -35,6 +38,10 @@ public static void validateMaxLength(String input, int maxLength, String message
Assert.isTrue(input.length() <= maxLength, message);
}

public static void validateTextLength(String input, String message) {
Assert.isTrue(input.length() <= MAX_TEXT_LENGTH, message);
}

public static void validateNotBlank(String input, String message) {
Assert.isTrue(isNotBlank(input), message);
}
Expand All @@ -43,6 +50,14 @@ public static void validateBlank(String input, String message) {
Assert.isTrue(isBlank(input), message);
}

public static <T> void validateNotNullAndEmpty(Collection<T> input, String message) {
Assert.isTrue(isNotNullOrEmpty(input), message);
}

public static <T> boolean isNotNullOrEmpty(Collection<T> input) {
return Objects.nonNull(input) && !input.isEmpty();
}

private static void pattern(String input, Pattern pattern, String message) {
Assert.isTrue(pattern.matcher(input).matches(), message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.net.URI;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -26,14 +27,16 @@ public class ProjectController {
private final ProjectService projectService;

@PostMapping
public ResponseEntity<Void> save(@RequestBody ProjectSaveRequest request) {
Long id = projectService.save(request);
public ResponseEntity<ProjectResponse> save(
@Valid @RequestBody ProjectSaveRequest request
) {
ProjectResponse response = projectService.save(request);
URI uri = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/projects/{id}")
.buildAndExpand(id).toUri();
.buildAndExpand(response.id()).toUri();

return ResponseEntity.created(uri)
.build();
.body(response);
}

@GetMapping("/{id}")
Expand Down
77 changes: 60 additions & 17 deletions src/main/java/sixgaezzang/sidepeek/projects/domain/Project.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
package sixgaezzang.sidepeek.projects.domain;

import static sixgaezzang.sidepeek.projects.util.ProjectConstant.MAX_OVERVIEW_LENGTH;
import static sixgaezzang.sidepeek.projects.util.ProjectConstant.MAX_PROJECT_NAME_LENGTH;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateDeployUrl;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateDescription;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateDuration;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateGithubUrl;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateName;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateOverview;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateOwnerId;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateSubName;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateThumbnailUrl;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateTroubleshooting;

import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
import java.time.YearMonth;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLRestriction;
import sixgaezzang.sidepeek.common.domain.BaseTimeEntity;
import sixgaezzang.sidepeek.projects.util.converter.YearMonthDateAttributeConverter;

@Entity
@Table(name = "project")
Expand All @@ -25,20 +41,22 @@ public class Project extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name", nullable = false, length = 300)
@Column(name = "name", nullable = false, length = MAX_PROJECT_NAME_LENGTH)
private String name;

@Column(name = "sub_name", length = 300)
@Column(name = "sub_name", length = MAX_PROJECT_NAME_LENGTH)
private String subName;

@Column(name = "overview", nullable = false, length = 1000)
@Column(name = "overview", nullable = false, length = MAX_OVERVIEW_LENGTH)
private String overview;

@Column(name = "start_date", columnDefinition = "TIMESTAMP")
private LocalDateTime startDate;
@Column(name = "start_date", columnDefinition = "DATE")
@Convert(converter = YearMonthDateAttributeConverter.class)
Sehee-Lee-01 marked this conversation as resolved.
Show resolved Hide resolved
private YearMonth startDate;

@Column(name = "end_date", columnDefinition = "TIMESTAMP")
private LocalDateTime endDate;
@Column(name = "end_date", columnDefinition = "DATE")
@Convert(converter = YearMonthDateAttributeConverter.class)
private YearMonth endDate;

@Column(name = "thumbnail_url", columnDefinition = "TEXT")
private String thumbnailUrl;
Expand All @@ -49,8 +67,8 @@ public class Project extends BaseTimeEntity {
@Column(name = "github_url", columnDefinition = "TEXT")
private String githubUrl;

@Column(name = "owner_id", columnDefinition = "BIGINT")
private Long ownerId;
@Column(name = "owner_id", columnDefinition = "BIGINT", nullable = false)
private Long ownerId; // TODO: User로 설정하는 것이 좋을까요? 놓는다면 [accessToken id 일치 확인 + 유저 존재 확인(추가 발생)] 해야합니다!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전 개인적으로 저희가 ORM을 사용하고 있기 때문에 객체를 사용하는 것이 좋을 것 같다곤 생각합니당..! 객체지향 vs 효율(복잡성) 차이일 것 같긴합니당!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 개인적으로 ownerId라는 컬럼이 프론트단에서 프로젝트의 유저인지 확인하는 용도로만 쓰이기 때문에 User 객체보다는 ownerId를 그대로 유지하는 것이 좋다고 생각해요..! 혹시 프로젝트 게시글 작성자에 대한 추가 정보가 요구되는 일이 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가 정보를 요구하는 일은 없습니다!
생각해보니 로그인을 하면 보통 유저가 삭제된 상태는 아닐테니 우선 요 부분은 효율성을 위해서 Id로 남겨두고 나중에 필요하면 유저로 바꾸는 것으로 하겠습니당! 다들 감사합니다ㅠㅠㅠ


@Column(name = "description", nullable = false, columnDefinition = "TEXT")
private String description;
Expand All @@ -68,21 +86,28 @@ public class Project extends BaseTimeEntity {
private LocalDateTime deletedAt;

@Builder
public Project(String name, String subName, String overview, LocalDateTime startDate,
LocalDateTime endDate, Long ownerId,
String thumbnailUrl, String deployUrl, String githubUrl, String description,
String troubleshooting) {
public Project(String name, String subName, String overview, YearMonth startDate, YearMonth endDate, Long ownerId,
String thumbnailUrl, String deployUrl, String githubUrl, String description,
String troubleshooting) {
validateConstructorRequiredArguments(name, overview, githubUrl, description, ownerId);
validateConstructorOptionArguments(subName, thumbnailUrl, deployUrl, troubleshooting, startDate, endDate);

// Required
this.name = name;
this.subName = subName;
this.overview = overview;
this.githubUrl = githubUrl;
this.description = description;
this.ownerId = ownerId;

// Option
this.subName = subName;
this.startDate = startDate;
this.endDate = endDate;
this.ownerId = ownerId;
this.thumbnailUrl = thumbnailUrl;
this.deployUrl = deployUrl;
this.githubUrl = githubUrl;
this.description = description;
this.troubleshooting = troubleshooting;

// Etc
Sehee-Lee-01 marked this conversation as resolved.
Show resolved Hide resolved
this.likeCount = 0L;
this.viewCount = 0L;
}
Expand All @@ -91,4 +116,22 @@ public void increaseViewCount() {
this.viewCount++;
}

private void validateConstructorRequiredArguments(String name, String overview, String githubUrl,
String description, Long ownerId) {
validateName(name);
validateOverview(overview);
validateGithubUrl(githubUrl);
validateDescription(description);
validateOwnerId(ownerId);
}

private void validateConstructorOptionArguments(String subName, String thumbnailUrl, String deployUrl,
String troubleshooting, YearMonth startDate, YearMonth endDate) {
validateSubName(subName);
validateThumbnailUrl(thumbnailUrl);
validateDeployUrl(deployUrl);
validateTroubleshooting(troubleshooting);
validateDuration(startDate, endDate);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package sixgaezzang.sidepeek.projects.domain;

import static sixgaezzang.sidepeek.projects.util.ProjectConstant.MAX_CATEGORY_LENGTH;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -13,7 +15,9 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sixgaezzang.sidepeek.projects.util.validation.ProjectValidator;
import sixgaezzang.sidepeek.skill.domain.Skill;
import sixgaezzang.sidepeek.skill.util.validation.SkillValidator;

@Entity
@Table(name = "project_skill")
Expand All @@ -33,14 +37,21 @@ public class ProjectSkill {
@JoinColumn(name = "skill_id")
private Skill skill;

@Column(name = "category", nullable = false, length = 50)
@Column(name = "category", nullable = false, length = MAX_CATEGORY_LENGTH)
private String category;

@Builder
public ProjectSkill(Project project, Skill skill, String category) {
validateConstructorArguments(project, skill, category);
this.project = project;
this.skill = skill;
this.category = category;
}

private void validateConstructorArguments(Project project, Skill skill, String category) {
ProjectValidator.validateProject(project);
SkillValidator.validateSkill(skill);
SkillValidator.validateCategory(category);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package sixgaezzang.sidepeek.projects.domain.file;

import io.jsonwebtoken.lang.Assert;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand All @@ -15,7 +16,9 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sixgaezzang.sidepeek.common.util.ValidationUtils;
import sixgaezzang.sidepeek.projects.domain.Project;
import sixgaezzang.sidepeek.projects.util.validation.ProjectValidator;

@Entity
@Table(name = "files")
Expand All @@ -40,9 +43,16 @@ public class File {

@Builder
public File(Project project, FileType type, String url) {
validateConstructorArguments(project, type, url);
this.project = project;
this.type = type;
this.url = url;
}

private void validateConstructorArguments(Project project, FileType type, String url) {
ProjectValidator.validateProject(project);
Assert.notNull(type, "type을 입력해주세요.");
ValidationUtils.validateURI(url, "프로젝트 레이아웃 이미지 URL 형식이 올바르지 않습니다.");
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package sixgaezzang.sidepeek.projects.domain.member;

import static sixgaezzang.sidepeek.projects.util.ProjectConstant.MAX_ROLE_LENGTH;
import static sixgaezzang.sidepeek.users.domain.User.MAX_NICKNAME_LENGTH;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -9,11 +12,14 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sixgaezzang.sidepeek.projects.domain.Project;
import sixgaezzang.sidepeek.projects.util.validation.MemberValidator;
import sixgaezzang.sidepeek.projects.util.validation.ProjectValidator;
import sixgaezzang.sidepeek.users.domain.User;

@Entity
Expand All @@ -34,18 +40,40 @@ public class Member {
@JoinColumn(name = "project_id")
private Project project;

@Column(name = "role", nullable = false, length = 15)
@Column(name = "role", nullable = false, length = MAX_ROLE_LENGTH)
private String role;

@Column(name = "nickname", nullable = false, length = 20)
@Column(name = "nickname", length = MAX_NICKNAME_LENGTH)
private String nickname;

@Builder
public Member(User user, Project project, String role, String nickname) {
this.user = user;
validateConstructorRequiredArguments(project, role);
validateConstructorMemberInfoArguments(user, nickname);

// Required
this.project = project;
this.role = role;
this.user = user;
this.nickname = nickname;
}

private void validateConstructorMemberInfoArguments(User user, String nickname) {
if (Objects.nonNull(user)) { // 회원인 경우
MemberValidator.validateFellowMemberUser(user);
return;
}
if (Objects.nonNull(nickname)) { // 비회원인 경우
MemberValidator.validateNonFellowMemberNickname(nickname);
return;
}

throw new IllegalArgumentException("회원인 멤버는 유저 Id를, 비회원인 멤버는 닉네임을 입력해주세요.");
}

private void validateConstructorRequiredArguments(Project project, String role) {
ProjectValidator.validateProject(project);
MemberValidator.validateRole(role);
}

}
Loading