Skip to content

Commit

Permalink
Merge pull request #119 from side-peek/feat/#113-check-tech-duplicate
Browse files Browse the repository at this point in the history
기술스택과 멤버 중복 검사 로직 구현
  • Loading branch information
Sehee-Lee-01 authored Mar 11, 2024
2 parents b6d6a4b + 8721bea commit 009f329
Show file tree
Hide file tree
Showing 19 changed files with 291 additions and 127 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sixgaezzang.sidepeek.common.dto.request;

import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.CATEGORY_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.CATEGORY_OVER_MAX_LENGTH;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.CATEGORY_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.CATEGORY_OVER_MAX_LENGTH;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_CATEGORY_LENGTH;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MIN_ID;
import static sixgaezzang.sidepeek.skill.exception.message.SkillErrorMessage.SKILL_ID_IS_NULL;
Expand All @@ -18,7 +18,7 @@
import sixgaezzang.sidepeek.users.domain.UserSkill;

@Schema(description = "기술 스택 저장 요청 정보")
public record UpdateUserSkillRequest(
public record SaveTechStackRequest(
@Schema(description = "기술 스택 Id", example = "1")
@Min(value = MIN_ID, message = "스킬 id는 " + MIN_ID + "보다 작을 수 없습니다.")
@NotNull(message = SKILL_ID_IS_NULL)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package sixgaezzang.sidepeek.common.exception.message;

import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_CATEGORY_LENGTH;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_TECH_STACK_COUNT;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_TEXT_LENGTH;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class CommonErrorMessage {

// Common
public static final String LOGIN_IS_REQUIRED = "로그인이 필요합니다.";

Expand All @@ -20,11 +19,4 @@ public final class CommonErrorMessage {
public static final String GITHUB_URL_IS_NULL = "Github URL을 입력해주세요.";
public static final String GITHUB_URL_OVER_MAX_LENGTH = "Github URL은 " + MAX_TEXT_LENGTH + "자 이하여야 합니다.";

// TechStack
public static final String TECH_STACKS_IS_NULL = "기술 스택들을 입력해주세요.";
public static final String TECH_STACKS_OVER_MAX_COUNT =
"기술 스택은 " + MAX_TECH_STACK_COUNT + "개 미만이어야 합니다.";
public static final String CATEGORY_IS_NULL = "기술 스택 카테고리를 입력해주세요.";
public static final String CATEGORY_OVER_MAX_LENGTH = "기술 스택 카테고리는 " + MAX_CATEGORY_LENGTH + "자 이하여야 합니다.";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package sixgaezzang.sidepeek.common.exception.message;

import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_CATEGORY_LENGTH;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_TECH_STACK_COUNT;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TechStackErrorMessage {

// Tech Stack List
public static final String TECH_STACKS_IS_NULL = "기술 스택들을 입력해주세요.";
public static final String TECH_STACKS_OVER_MAX_COUNT =
"기술 스택은 " + MAX_TECH_STACK_COUNT + "개 미만이어야 합니다.";
public static final String TECH_STACK_IS_DUPLICATED = "같은 카테고리 내에 같은 기술 스택이 있습니다.";

// Tech Stack
public static final String CATEGORY_IS_NULL = "기술 스택 카테고리를 입력해주세요.";
public static final String CATEGORY_OVER_MAX_LENGTH =
"기술 스택 카테고리는 " + MAX_CATEGORY_LENGTH + "자 이하여야 합니다.";

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package sixgaezzang.sidepeek.common.util.validation;

import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.CATEGORY_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.CATEGORY_OVER_MAX_LENGTH;
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.TECH_STACKS_OVER_MAX_COUNT;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.CATEGORY_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.CATEGORY_OVER_MAX_LENGTH;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.TECH_STACKS_OVER_MAX_COUNT;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.TECH_STACK_IS_DUPLICATED;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_CATEGORY_LENGTH;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_TECH_STACK_COUNT;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateCollection;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateDuplicate;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateMaxLength;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateNotBlank;
import static sixgaezzang.sidepeek.skill.exception.message.SkillErrorMessage.SKILL_ID_IS_NULL;
Expand All @@ -13,26 +16,25 @@
import java.util.List;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import sixgaezzang.sidepeek.common.dto.request.UpdateUserSkillRequest;
import sixgaezzang.sidepeek.common.dto.request.SaveTechStackRequest;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class TechStackValidator {

public static void validateTechStacks(List<UpdateUserSkillRequest> techStacks) {
public static void validateTechStacks(List<SaveTechStackRequest> techStacks) {
Assert.isTrue(techStacks.size() <= MAX_TECH_STACK_COUNT, TECH_STACKS_OVER_MAX_COUNT);

techStacks.forEach(TechStackValidator::validateTechStack);
validateCollection(techStacks, TechStackValidator::validateTechStack);
validateDuplicate(techStacks, TECH_STACK_IS_DUPLICATED);
}

public static void validateTechStack(UpdateUserSkillRequest techStack) {
public static void validateTechStack(SaveTechStackRequest techStack) {
Assert.notNull(techStack.skillId(), SKILL_ID_IS_NULL);
Assert.notNull(techStack.category(), CATEGORY_IS_NULL);
}

public static void validateCategory(String category) {
validateNotBlank(category, CATEGORY_IS_NULL);
validateMaxLength(category, MAX_CATEGORY_LENGTH,
CATEGORY_OVER_MAX_LENGTH);
validateMaxLength(category, MAX_CATEGORY_LENGTH, CATEGORY_OVER_MAX_LENGTH);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
import static sixgaezzang.sidepeek.users.domain.Password.PASSWORD_REGXP;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -72,6 +78,35 @@ public static void validateGithubUrl(String githubUrl) {
validateURI(githubUrl, GITHUB_URL_IS_INVALID);
}

public static <T, U> void validateInclude(
List<T> values, BiPredicate<T, U> condition, U requiredValue, String errorMessage
) {
values.stream().filter(value -> condition.test(value, requiredValue))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(errorMessage));
}

public static <T extends Collection<U>, U> void validateCollection(T values, Consumer<U> validator) {
values.forEach(validator);
}

public static <T> void validateDuplicate(List<T> values, String errorMessage) {
Set<T> set = new HashSet<>();
values.forEach(value -> {
Assert.isTrue(!set.contains(value), errorMessage);
set.add(value);
});
}

public static <T, U> void validateKeyDuplicate(List<T> values, Function<T, U> getKey, String errorMessage) {
Set<U> keySet = new HashSet<>();
values.forEach(value -> {
U key = getKey.apply(value);
Assert.isTrue(!keySet.contains(key), errorMessage);
keySet.add(getKey.apply(value));
});
}

public static <T> boolean isNotNullOrEmpty(Collection<T> input) {
return Objects.nonNull(input) && !input.isEmpty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.GITHUB_URL_IS_INVALID;
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.GITHUB_URL_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.GITHUB_URL_OVER_MAX_LENGTH;
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.TECH_STACKS_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.TECH_STACKS_OVER_MAX_COUNT;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.TECH_STACKS_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.TECH_STACKS_OVER_MAX_COUNT;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_TECH_STACK_COUNT;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MAX_TEXT_LENGTH;
import static sixgaezzang.sidepeek.common.util.CommonConstant.MIN_ID;
Expand Down Expand Up @@ -41,7 +41,7 @@
import java.time.YearMonth;
import java.util.List;
import org.hibernate.validator.constraints.URL;
import sixgaezzang.sidepeek.common.dto.request.UpdateUserSkillRequest;
import sixgaezzang.sidepeek.common.dto.request.SaveTechStackRequest;
import sixgaezzang.sidepeek.projects.domain.Project;

@Schema(description = "프로젝트 생성/수정 요청")
Expand Down Expand Up @@ -76,7 +76,7 @@ public record SaveProjectRequest(
@Schema(description = "프로젝트 기술 스택")
@Size(max = MAX_TECH_STACK_COUNT, message = TECH_STACKS_OVER_MAX_COUNT)
@NotEmpty(message = TECH_STACKS_IS_NULL)
List<UpdateUserSkillRequest> techStacks,
List<SaveTechStackRequest> techStacks,

// Option
@Schema(description = "프로젝트 부제목", example = "좋은 아이디어? 사이드픽에서 찾아봐!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public final class MemberErrorMessage {
public static final String MEMBER_OVER_MAX_COUNT = "멤버 수는 " + MAX_MEMBER_COUNT + "명 미만이어야 합니다.";
public static final String MEMBER_IS_EMPTY = "멤버 목록에는 최소 1명(작성자)을 포함해야합니다.";
public static final String MEMBER_NOT_INCLUDE_OWNER = "멤버 목록에 작성자를 필수로 포함해야합니다.";
public static final String MEMBER_IS_DUPLICATED = "같은 역할 내에 같은 멤버가 있습니다.";

// Member
public static final String ROLE_OVER_MAX_LENGTH =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package sixgaezzang.sidepeek.projects.service;

import static sixgaezzang.sidepeek.common.exception.message.CommonErrorMessage.TECH_STACKS_IS_NULL;
import static sixgaezzang.sidepeek.common.exception.message.TechStackErrorMessage.TECH_STACKS_IS_NULL;
import static sixgaezzang.sidepeek.common.util.validation.TechStackValidator.validateTechStacks;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateNotNullAndEmpty;
import static sixgaezzang.sidepeek.projects.util.validation.ProjectValidator.validateProject;
Expand All @@ -11,7 +11,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sixgaezzang.sidepeek.common.dto.request.UpdateUserSkillRequest;
import sixgaezzang.sidepeek.common.dto.request.SaveTechStackRequest;
import sixgaezzang.sidepeek.projects.domain.Project;
import sixgaezzang.sidepeek.projects.domain.ProjectSkill;
import sixgaezzang.sidepeek.projects.dto.response.ProjectSkillSummary;
Expand All @@ -32,7 +32,7 @@ public List<ProjectSkill> findAll(Project project) {
}

@Transactional
public List<ProjectSkillSummary> saveAll(Project project, List<UpdateUserSkillRequest> techStacks) {
public List<ProjectSkillSummary> saveAll(Project project, List<SaveTechStackRequest> techStacks) {
validateProject(project);
validateNotNullAndEmpty(techStacks, TECH_STACKS_IS_NULL);
validateTechStacks(techStacks);
Expand All @@ -49,7 +49,7 @@ public List<ProjectSkillSummary> saveAll(Project project, List<UpdateUserSkillRe
.toList();
}

private List<ProjectSkill> convertAllToEntity(Project project, List<UpdateUserSkillRequest> techStacks) {
private List<ProjectSkill> convertAllToEntity(Project project, List<SaveTechStackRequest> techStacks) {
return techStacks.stream().map(
techStack -> {
Skill skill = skillRepository.findById(techStack.skillId())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package sixgaezzang.sidepeek.projects.util.validation;

import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateCollection;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateInclude;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateKeyDuplicate;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateMaxLength;
import static sixgaezzang.sidepeek.common.util.validation.ValidationUtils.validateNotBlank;
import static sixgaezzang.sidepeek.projects.exception.message.MemberErrorMessage.MEMBER_IS_DUPLICATED;
import static sixgaezzang.sidepeek.projects.exception.message.MemberErrorMessage.MEMBER_IS_EMPTY;
import static sixgaezzang.sidepeek.projects.exception.message.MemberErrorMessage.MEMBER_NOT_INCLUDE_OWNER;
import static sixgaezzang.sidepeek.projects.exception.message.MemberErrorMessage.MEMBER_OVER_MAX_COUNT;
import static sixgaezzang.sidepeek.projects.exception.message.MemberErrorMessage.ROLE_IS_NULL;
import static sixgaezzang.sidepeek.projects.exception.message.MemberErrorMessage.ROLE_OVER_MAX_LENGTH;
import static sixgaezzang.sidepeek.projects.util.ProjectConstant.MAX_MEMBER_COUNT;
import static sixgaezzang.sidepeek.projects.util.ProjectConstant.MAX_ROLE_LENGTH;
import static sixgaezzang.sidepeek.users.util.validation.UserValidator.validateNickname;

import io.jsonwebtoken.lang.Assert;
import java.util.List;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import sixgaezzang.sidepeek.projects.dto.request.SaveMemberRequest;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
Expand All @@ -23,12 +29,19 @@ public final class MemberValidator {
public static void validateMembers(Long ownerId, List<SaveMemberRequest> members) {
Assert.notEmpty(members, MEMBER_IS_EMPTY);
Assert.isTrue(members.size() <= MAX_MEMBER_COUNT, MEMBER_OVER_MAX_COUNT);
members.stream().filter(member -> Objects.equals(member.userId(), ownerId))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(MEMBER_NOT_INCLUDE_OWNER));

validateInclude(members, (member, id) -> Objects.equals(member.userId(), id), ownerId,
MEMBER_NOT_INCLUDE_OWNER);
validateCollection(members, MemberValidator::validateSaveMemberRequest);
validateKeyDuplicate(members, (member) -> Pair.of(member.nickname(), member.role()), MEMBER_IS_DUPLICATED);
}

// Required
public static void validateSaveMemberRequest(SaveMemberRequest member) {
validateNickname(member.nickname());
validateRole(member.role());
}

public static void validateRole(String role) {
validateNotBlank(role, ROLE_IS_NULL);
validateMaxLength(role, MAX_ROLE_LENGTH, ROLE_OVER_MAX_LENGTH);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.List;
import sixgaezzang.sidepeek.common.dto.request.UpdateUserSkillRequest;
import sixgaezzang.sidepeek.common.dto.request.SaveTechStackRequest;

@Schema(description = "회원 프로필 수정 요청")
public record UpdateUserProfileRequest(
Expand Down Expand Up @@ -51,7 +51,7 @@ public record UpdateUserProfileRequest(
String blogUrl,

@Schema(description = "회원 기술 스택 목록")
List<UpdateUserSkillRequest> techStacks
List<SaveTechStackRequest> techStacks
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sixgaezzang.sidepeek.common.dto.request.UpdateUserSkillRequest;
import sixgaezzang.sidepeek.common.dto.request.SaveTechStackRequest;
import sixgaezzang.sidepeek.skill.domain.Skill;
import sixgaezzang.sidepeek.skill.repository.SkillRepository;
import sixgaezzang.sidepeek.users.domain.User;
Expand All @@ -36,7 +36,7 @@ public List<UserSkillSummary> findAllByUser(User user) {
.toList();
}

public List<UserSkillSummary> saveAll(User user, List<UpdateUserSkillRequest> techStacks) {
public List<UserSkillSummary> saveAll(User user, List<SaveTechStackRequest> techStacks) {
validateUser(user);
if (userSkillRepository.existsByUser(user)) {
userSkillRepository.deleteAllByUser(user);
Expand All @@ -55,7 +55,7 @@ public List<UserSkillSummary> saveAll(User user, List<UpdateUserSkillRequest> te
.toList();
}

private List<UserSkill> convertAllToEntity(User user, List<UpdateUserSkillRequest> techStacks) {
private List<UserSkill> convertAllToEntity(User user, List<SaveTechStackRequest> techStacks) {
return techStacks.stream()
.map(techStack -> {
Skill skill = skillRepository.findById(techStack.skillId())
Expand Down
Loading

0 comments on commit 009f329

Please sign in to comment.