diff --git a/build.gradle b/build.gradle index 9ae14a4..f0931d7 100644 --- a/build.gradle +++ b/build.gradle @@ -77,6 +77,8 @@ dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' + // ahocorasick(아호코라식) 의존성 추가 + implementation 'org.ahocorasick:ahocorasick:0.6.3' } // Querydsl 빌드 옵션 설정 diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/controller/AcquisitionController.java b/src/main/java/org/sopt/certi_server/domain/acquisition/controller/AcquisitionController.java index 9d74f04..c57b684 100644 --- a/src/main/java/org/sopt/certi_server/domain/acquisition/controller/AcquisitionController.java +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/controller/AcquisitionController.java @@ -4,10 +4,11 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.sopt.certi_server.domain.acquisition.dto.request.CreateAcquisitionRequest; +import org.sopt.certi_server.domain.acquisition.dto.request.PatchAcquisitionRequest; import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionDetailResponse; -import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionListDetailResponse; -import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionListResponse; import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionResponse; +import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionListResponse; import org.sopt.certi_server.domain.acquisition.service.AcquisitionService; import org.sopt.certi_server.global.error.code.SuccessCode; import org.sopt.certi_server.global.error.dto.SuccessResponse; @@ -24,19 +25,18 @@ public class AcquisitionController { private final AcquisitionService acquisitionService; - @PostMapping("/{certificationId}") + @PostMapping @Operation(summary = "취득한 자격증 추가 API", description = "취득한 자격증을 추가합니다") - public ResponseEntity> addAcquisition( + public ResponseEntity> addAcquisition( @AuthenticationPrincipal Long userId, - @Parameter(description = "certification Id", example = "1") - @PathVariable(name = "certificationId") Long certificationId - ) { - boolean isAcquired = acquisitionService.createAcquisition(userId, certificationId); + @RequestBody CreateAcquisitionRequest request + ) { + boolean isAcquired = acquisitionService.createAcquisition(userId, request); return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_CREATE, isAcquired)); } - @GetMapping("/{acquisitionId}") + @GetMapping(value = "/{acquisitionId}") @Operation(summary = "취득한 자격증 상세 조회 API", description = "취득한 자격증을 상세 조회합니다") public ResponseEntity> getAcquisition( @Parameter(description = "acquisition Id", example = "1") @@ -46,7 +46,18 @@ public ResponseEntity> getAcquisit return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, getAcquisitionDetailResponse)); } - @DeleteMapping("/{acquisitionId}") + @PatchMapping(value = "/{acquisitionId}") + @Operation(summary = "취득한 자격증 정보 수정 API", description = "취득한 자격증 정보를 수정합니다.") + public ResponseEntity> modifyAcquisition( + @AuthenticationPrincipal Long userId, + @RequestBody PatchAcquisitionRequest request, + @PathVariable(name = "acquisitionId") Long acquisitionId + ){ + acquisitionService.patchAcquisition(userId, acquisitionId, request); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_UPDATE)); + } + + @DeleteMapping(value = "/{acquisitionId}") @Operation(summary = "취득한 자격증 삭제 API", description = "취득한 자격증을 상세 조회합니다") public ResponseEntity> deleteAcquisition( @AuthenticationPrincipal Long userId, @@ -62,7 +73,7 @@ public ResponseEntity> deleteAcquisition( public ResponseEntity> getAllAcquisitions( @AuthenticationPrincipal Long userId ) { - List getAcquisitionResponses = acquisitionService.getAcquisitionList(userId); + List getAcquisitionResponses = acquisitionService.getAcquisitionList(userId); GetAcquisitionListResponse getAcquisitionListResponse = GetAcquisitionListResponse.of(getAcquisitionResponses); return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, getAcquisitionListResponse)); diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/request/CreateAcquisitionRequest.java b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/request/CreateAcquisitionRequest.java new file mode 100644 index 0000000..93408b2 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/request/CreateAcquisitionRequest.java @@ -0,0 +1,8 @@ +package org.sopt.certi_server.domain.acquisition.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record CreateAcquisitionRequest( + @NotNull(message = "자격증 id는 필수 값입니다.") Long certificationId +) { +} diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/request/PatchAcquisitionRequest.java b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/request/PatchAcquisitionRequest.java new file mode 100644 index 0000000..d02d40c --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/request/PatchAcquisitionRequest.java @@ -0,0 +1,16 @@ +package org.sopt.certi_server.domain.acquisition.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; + +public record PatchAcquisitionRequest( + + @NotNull(message = "취득 날짜 정보를 입력해주세요") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") + LocalDate acquisitionDate, + + String grade +) { +} diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionDetailResponse.java b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionDetailResponse.java index 7b21dcb..f576dd5 100644 --- a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionDetailResponse.java +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionDetailResponse.java @@ -20,7 +20,7 @@ public record GetAcquisitionDetailResponse( List tags, String description, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") - LocalDate createdAt + LocalDate acquisitionDate ) { public static GetAcquisitionDetailResponse from(Acquisition acquisition) { return GetAcquisitionDetailResponse.builder() @@ -29,7 +29,7 @@ public static GetAcquisitionDetailResponse from(Acquisition acquisition) { .cardBackImageUrl(acquisition.getCardType().getCardBackImageUrl()) .index(acquisition.getCardType().getIndex()) .name(acquisition.getCertification().getName()) - .createdAt(acquisition.getCreatedTime().toLocalDate()) + .acquisitionDate(acquisition.getAcquisitionDate()) .description(acquisition.getCertification().getDescription()) .tags(acquisition.getCertification().getTags()) .build(); diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionListDetailResponse.java b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionListDetailResponse.java deleted file mode 100644 index ef3553b..0000000 --- a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionListDetailResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.sopt.certi_server.domain.acquisition.dto.response; - -import java.time.LocalDate; -import java.util.List; - -import org.sopt.certi_server.domain.acquisition.entity.Acquisition; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import lombok.Builder; - -@Builder -public record GetAcquisitionListDetailResponse( - Long acquisitionId, - String cardFrontImageUrl, - int index, - String name, - List tags, - String description, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") - LocalDate createdAt -) { - public static GetAcquisitionListDetailResponse from(Acquisition acquisition) { - return GetAcquisitionListDetailResponse.builder() - .acquisitionId(acquisition.getId()) - .cardFrontImageUrl(acquisition.getCardType().getCardFrontImageUrl()) - .index(acquisition.getCardType().getIndex()) - .name(acquisition.getCertification().getName()) - .createdAt(acquisition.getCreatedTime().toLocalDate()) - .description(acquisition.getCertification().getDescription()) - .tags(acquisition.getCertification().getTags()) - .build(); - } -} diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionListResponse.java b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionListResponse.java index 675cbeb..10e5b24 100644 --- a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionListResponse.java +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionListResponse.java @@ -3,9 +3,9 @@ import java.util.List; public record GetAcquisitionListResponse( - List acquisitionListDetailResponses + List acquisitionListDetailResponses ) { - public static GetAcquisitionListResponse of(List priorCertificaitonResponseList) { + public static GetAcquisitionListResponse of(List priorCertificaitonResponseList) { return new GetAcquisitionListResponse(priorCertificaitonResponseList); } } diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionResponse.java b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionResponse.java index cb4b4a8..c3096e1 100644 --- a/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionResponse.java +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/dto/response/GetAcquisitionResponse.java @@ -1,32 +1,36 @@ package org.sopt.certi_server.domain.acquisition.dto.response; -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Builder; -import org.sopt.certi_server.domain.acquisition.entity.Acquisition; - import java.time.LocalDate; import java.util.List; +import org.sopt.certi_server.domain.acquisition.entity.Acquisition; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Builder; + @Builder public record GetAcquisitionResponse( - Long acquisitionId, - int index, - String name, - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") - LocalDate createdAt, - String cardFrontImageUrl, - String cardBackImageUrl, - List tags + Long acquisitionId, + String cardFrontImageUrl, + int index, + String name, + List tags, + String description, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") + LocalDate acquisitionDate, + String grade ) { - public static GetAcquisitionResponse from(Acquisition acquisition) { - return GetAcquisitionResponse.builder() - .acquisitionId(acquisition.getId()) - .index(acquisition.getCardType().getIndex()) - .name(acquisition.getCertification().getName()) - .createdAt(acquisition.getCreatedTime().toLocalDate()) - .cardFrontImageUrl(acquisition.getCardType().getCardFrontImageUrl()) - .cardBackImageUrl(acquisition.getCardType().getCardBackImageUrl()) - .tags(acquisition.getCertification().getTags()) - .build(); - } + public static GetAcquisitionResponse from(Acquisition acquisition) { + return GetAcquisitionResponse.builder() + .acquisitionId(acquisition.getId()) + .cardFrontImageUrl(acquisition.getCardType().getCardFrontImageUrl()) + .index(acquisition.getCardType().getIndex()) + .name(acquisition.getCertification().getName()) + .acquisitionDate(acquisition.getAcquisitionDate()) + .description(acquisition.getCertification().getDescription()) + .tags(acquisition.getCertification().getTags()) + .grade(acquisition.getGrade()) + .build(); + } } diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/entity/Acquisition.java b/src/main/java/org/sopt/certi_server/domain/acquisition/entity/Acquisition.java index 3798d82..1cb9290 100644 --- a/src/main/java/org/sopt/certi_server/domain/acquisition/entity/Acquisition.java +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/entity/Acquisition.java @@ -11,6 +11,8 @@ import org.sopt.certi_server.domain.user.entity.User; import org.sopt.certi_server.global.entity.BaseTimeEntity; +import java.time.LocalDate; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -44,11 +46,24 @@ public class Acquisition extends BaseTimeEntity { @JoinColumn(name = "user_id") private User user; + @Column(name = "grade") + private String grade; + + @Column(name = "acquisition_date") + private LocalDate acquisitionDate; + @Builder - public Acquisition(Certification certification, CardType cardType, User user, SmallCardType smallCardType) { + public Acquisition(Certification certification, CardType cardType, User user, SmallCardType smallCardType, String grade, LocalDate acquisitionDate) { this.certification = certification; this.smallCardType = smallCardType; this.cardType = cardType; this.user = user; + this.grade = grade; + this.acquisitionDate = acquisitionDate; + } + + public void changeAcquisition(LocalDate acquisitionDate, String grade) { + this.acquisitionDate = acquisitionDate; + this.grade = grade; } } diff --git a/src/main/java/org/sopt/certi_server/domain/acquisition/service/AcquisitionService.java b/src/main/java/org/sopt/certi_server/domain/acquisition/service/AcquisitionService.java index 742d001..fee3282 100644 --- a/src/main/java/org/sopt/certi_server/domain/acquisition/service/AcquisitionService.java +++ b/src/main/java/org/sopt/certi_server/domain/acquisition/service/AcquisitionService.java @@ -2,8 +2,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.sopt.certi_server.domain.acquisition.dto.request.CreateAcquisitionRequest; +import org.sopt.certi_server.domain.acquisition.dto.request.PatchAcquisitionRequest; import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionDetailResponse; -import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionListDetailResponse; import org.sopt.certi_server.domain.acquisition.dto.response.GetAcquisitionResponse; import org.sopt.certi_server.domain.acquisition.entity.Acquisition; import org.sopt.certi_server.domain.acquisition.entity.enums.CardType; @@ -21,6 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.util.List; import java.util.Objects; @@ -39,8 +41,8 @@ public class AcquisitionService { @Transactional - public boolean createAcquisition(final Long userId, final Long certificationId) { - Certification certification = certificationService.getCertification(certificationId); + public boolean createAcquisition(final Long userId, final CreateAcquisitionRequest request) { + Certification certification = certificationService.getCertification(request.certificationId()); User user = userService.getUser(userId); //중복 여부 확인 @@ -66,6 +68,7 @@ public boolean createAcquisition(final Long userId, final Long certificationId) .certification(certification) .cardType(cardType) .smallCardType(smallCardType) + .acquisitionDate(LocalDate.now()) .build(); acquisitionRepository.save(acquisition); @@ -91,12 +94,12 @@ public GetAcquisitionDetailResponse getAcquisitionDetail(final Long acquisitionI return GetAcquisitionDetailResponse.from(acquisition); } - public List getAcquisitionList(final Long userId) { + public List getAcquisitionList(final Long userId) { User user = userService.getUser(userId); List acquisitionList = acquisitionRepository.findByUserOrderByIdDesc(user); return acquisitionList.stream() - .map(GetAcquisitionListDetailResponse::from) + .map(GetAcquisitionResponse::from) .toList(); } @@ -111,4 +114,20 @@ public void deleteAcquisition(final Long userId, final Long acquisitionId) { acquisitionRepository.delete(findAcquisition); } + + @Transactional + public void patchAcquisition(Long userId, Long acquisitionId, PatchAcquisitionRequest request) { + + User user = userService.getUser(userId); + Acquisition acquisition = getAcquisition(acquisitionId); + + if (!Objects.equals(acquisition.getUser().getId(), user.getId())) { + throw new ForbiddenException(ErrorCode.ACCESS_DENIED); + } + + acquisition.changeAcquisition( + request.acquisitionDate(), + request.grade() + ); + } } diff --git a/src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java b/src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java index 61dd939..ede09c1 100644 --- a/src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java +++ b/src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.sopt.certi_server.domain.certification.dto.response.CertificationDetailResponse; import org.sopt.certi_server.domain.certification.dto.response.CertificationListResponse; +import org.sopt.certi_server.domain.certification.dto.response.CertificationRankResponse; import org.sopt.certi_server.domain.certification.dto.response.CertificationRecommendationListResponse; import org.sopt.certi_server.domain.certification.dto.response.CertificationSimple; import org.sopt.certi_server.domain.certification.service.CertificationService; @@ -81,4 +82,24 @@ public ResponseEntity> return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certiRecommendListRes)); } + @GetMapping("/job") + @Operation(summary = "직무별 자격증 조회 API", description = "3순위 직무별 자격증을 조회합니다") + public ResponseEntity> getTop3ByJob( + @AuthenticationPrincipal Long userId + ){ + List certificationRankResponseList = certificationService.getCertificationJob(userId); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certificationRankResponseList)); + } + + @GetMapping("/track") + @Operation(summary = "계열별 자격증 조회 API", description = "3순위 계열별 자격증을 조회합니다") + public ResponseEntity> getTop3ByTrack( + @AuthenticationPrincipal Long userId + ) { + List certificationRankResponseList = certificationService.getCertificationTrack(userId); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certificationRankResponseList)); + + } + + } diff --git a/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationRankResponse.java b/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationRankResponse.java new file mode 100644 index 0000000..5254738 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationRankResponse.java @@ -0,0 +1,19 @@ +package org.sopt.certi_server.domain.certification.dto.response; + +import org.sopt.certi_server.domain.certification.entity.Certification; + +public record CertificationRankResponse( + int rank, + String certificationName, + String certificationType +) { + public CertificationRankResponse(int rank, Certification certification) { + this( + rank, + certification.getName(), + certification.getCertificationType() != null + ? certification.getCertificationType().getKoreanName() + : null + ); + } +} diff --git a/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java b/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java index e81e726..a2a98c7 100644 --- a/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java +++ b/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java @@ -11,7 +11,8 @@ public record CertificationScoreDto( String testType, List tags, int recommendationScore, - boolean isFavorite) { + boolean isFavorite, + String description) { public static CertificationScoreDto from( Certification certification, int recommendationScore, @@ -24,7 +25,8 @@ public static CertificationScoreDto from( certification.getTestType().getType(), certification.getTags().stream().toList(), recommendationScore, - isFavorite + isFavorite, + certification.getDescription() ); } diff --git a/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationSimple.java b/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationSimple.java index 65ddc5d..592b86f 100644 --- a/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationSimple.java +++ b/src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationSimple.java @@ -16,6 +16,7 @@ public class CertificationSimple { private List tags; @JsonProperty(value = "isFavorite") private boolean favorite; + private String description; public CertificationSimple( Certification certification, @@ -29,5 +30,6 @@ public CertificationSimple( certification.getTestType().getType() : null; this.tags = certification.getTags(); this.favorite = favorite; + this.description = certification.getDescription(); } } diff --git a/src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java b/src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java index 0be5bc5..ab236af 100644 --- a/src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java +++ b/src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java @@ -17,15 +17,19 @@ import org.sopt.certi_server.domain.major.entity.Major; import org.sopt.certi_server.domain.major.repository.MajorRepository; import org.sopt.certi_server.domain.user.entity.User; +import org.sopt.certi_server.domain.user.entity.enums.TrackType; import org.sopt.certi_server.domain.user.service.UserService; import org.sopt.certi_server.global.error.code.ErrorCode; import org.sopt.certi_server.global.error.exception.NotFoundException; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -45,6 +49,7 @@ public class CertificationService { private final CertificationJobRepository certificationJobRepository; private final CertificationRepositoryCustomImpl certificationRepositoryCustomImpl; private final UserService userService; + private final FavoriteRepository favoriteRepository; @Cacheable( @@ -197,4 +202,40 @@ public CertificationListResponse getCertificationList(final Long userId, final b } + public List getCertificationJob(final Long userId){ + User user = userService.getUser(userId); + List jobList = userService.getUserJob(userId).jobList(); + if (jobList.isEmpty()) { + throw new NotFoundException(ErrorCode.JOB_NOT_FOUND); + } + String jobName = jobList.get(0); + Job job = jobRepository.findByName(jobName) + .orElseThrow(() -> new NotFoundException(ErrorCode.JOB_NOT_FOUND)); + + Pageable top3 = PageRequest.of(0, 3); + List certList = + favoriteRepository.findTopByJobOrderByFavoriteCount(job.getId(), top3); + + AtomicInteger rank = new AtomicInteger(1); + + return certList.stream() + .map(cert -> new CertificationRankResponse(rank.getAndIncrement(), cert)) + .toList(); + } + + public List getCertificationTrack(final Long userId){ + User user = userService.getUser(userId); + TrackType trackType = user.getTrack(); + + Pageable top3 = PageRequest.of(0, 3); + + List certificationList = favoriteRepository.findTopCertificationsByTrack(trackType, top3); + + AtomicInteger rank = new AtomicInteger(1); + + return certificationList.stream() + .map(c -> new CertificationRankResponse(rank.getAndIncrement(), c)) + .toList(); + } + } diff --git a/src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java b/src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java index ff41c55..514cd7e 100644 --- a/src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java +++ b/src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java @@ -3,8 +3,11 @@ import org.sopt.certi_server.domain.certification.entity.Certification; import org.sopt.certi_server.domain.favorite.entity.Favorite; import org.sopt.certi_server.domain.user.entity.User; +import org.sopt.certi_server.domain.user.entity.enums.TrackType; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -29,4 +32,34 @@ public interface FavoriteRepository extends JpaRepository { boolean existsByUserAndCertification(User user, Certification certification); void deleteAllByUser(User user); + + int countByUser(User user); + + @Query(""" + select distinct c + from Favorite f + join f.certification c + join CertificationJob cj on cj.certification = c + where cj.job.id = :jobId + group by c + order by count(f) desc, max(cj.weight) desc + """) + List findTopByJobOrderByFavoriteCount( + @Param("jobId") Long jobId, + Pageable pageable + ); + + @Query(""" + select c + from Favorite f + join f.certification c + join f.user u + where u.track = :track + group by c + order by count(f) desc + """) + List findTopCertificationsByTrack( + @Param("track") TrackType track, + Pageable pageable + ); } diff --git a/src/main/java/org/sopt/certi_server/domain/user/controller/UserController.java b/src/main/java/org/sopt/certi_server/domain/user/controller/UserController.java index 3d394f4..d18572c 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/controller/UserController.java +++ b/src/main/java/org/sopt/certi_server/domain/user/controller/UserController.java @@ -2,11 +2,17 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.sopt.certi_server.domain.user.dto.request.UpdateJobRequest; +import org.sopt.certi_server.domain.user.dto.request.*; +import org.sopt.certi_server.domain.user.dto.response.*; +import org.sopt.certi_server.domain.user.dto.request.UpdateUserRequest; import org.sopt.certi_server.domain.user.dto.response.GetJobResponse; +import org.sopt.certi_server.domain.user.dto.response.GetMyPageInfoResponse; import org.sopt.certi_server.domain.user.dto.response.GetUserResponse; +import org.sopt.certi_server.domain.user.dto.response.PersonalInformationResponse; import org.sopt.certi_server.domain.user.service.UserService; import org.sopt.certi_server.global.error.code.SuccessCode; import org.sopt.certi_server.global.error.dto.SuccessResponse; @@ -32,6 +38,35 @@ public ResponseEntity> getHomeUser( return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, getUserResponse)); } + @GetMapping(value = "/mypage") + @Operation(summary = "마이 페이지 홈 API", description = "마이페이지 홈 화면을 조회합니다.") + public ResponseEntity> getMyPageHomeInfo( + @AuthenticationPrincipal Long userId + ){ + + GetMyPageInfoResponse getMyPageInfoResponse = userService.getMyPageInfoResponse(userId); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, getMyPageInfoResponse)); + } + + @GetMapping(value = "/pinfo") + @Operation(summary = "개인정보 수정 페이지 조회 API", description = "개인정보 수정 페이지를 조회합니다.") + public ResponseEntity> getPersonalInformation( + @AuthenticationPrincipal Long userId + ){ + PersonalInformationResponse pInformationResponse = userService.getPersonalInformationResponse(userId); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, pInformationResponse)); + } + + @PutMapping(value = "/pinfo") + @Operation(summary = "개인정보 수정 API", description = "개인정보를 수정합니다.") + public ResponseEntity> putPersonalInformation( + @AuthenticationPrincipal Long userId, + @RequestBody UpdateUserRequest request + ){ + userService.updateUserInformation(userId, request); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_UPDATE)); + } + @GetMapping("/job") @Operation(summary = "희망직무 조회 API", description = "사용자의 희망직무를 조회합니다") public ResponseEntity> getUserJob( @@ -50,4 +85,33 @@ public ResponseEntity updateUserJob( userService.updateUserJob(userId, updateJobRequest.jobNameList()); return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_UPDATE)); } + + @GetMapping(value = "/validation") + @Operation(summary = "닉네임 검증 API", description = "닉네임이 중복이거나 욕설이 포함되어 있는지 검사합니다.") + public ResponseEntity> validateNickname( + @RequestParam(value = "keyword") String nickname + ){ + userService.validateNickname(nickname); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH)); + } + + @PutMapping(value = "/university") + @Operation(summary = "대학교 변경 API", description = "대학교 정보를 변경합니다.") + public ResponseEntity> updateUniversity( + @AuthenticationPrincipal Long userId, + @RequestBody UpdateUniversityRequest request + ){ + userService.changeUniversity(userId, request.universityName()); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_UPDATE)); + } + + @PutMapping(value = "/major") + @Operation(summary = "학과 변경 API", description = "학과 정보를 변경합니다.") + public ResponseEntity> updateMajor( + @AuthenticationPrincipal Long userId, + @RequestBody UpdateMajorRequest request + ){ + userService.changeMajor(userId, request.majorName()); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_UPDATE)); + } } diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/request/SignupRequest.java b/src/main/java/org/sopt/certi_server/domain/user/dto/request/SignupRequest.java index 4a9dceb..a08d569 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/dto/request/SignupRequest.java +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/request/SignupRequest.java @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import org.sopt.certi_server.domain.user.dto.response.OAuthUserInformation; +import org.sopt.certi_server.global.annotation.NoProfanity; import java.util.List; @@ -13,6 +14,7 @@ public record SignupRequest( @NotEmpty(message = "학년 정보는 필수입니다.") String grade, @NotEmpty(message = "계열 정보는 필수입니다.") String track, @NotEmpty(message = "전공 정보는 필수입니다.") String major, + @NotEmpty(message = "닉네임 정보는 필수입니다.") @Size(min = 1, max = 7, message = "닉네임은 최소 1자, 최대 7자까지 입력 해야 합니다.") @NoProfanity String nickname, @NotEmpty(message = "직무 정보는 필수입니다.") @Size(min = 1, max = 3, message = "직무는 최소 1개, 최대 3개까지 선택 가능합니다.") List jobs ) { } diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateMajorRequest.java b/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateMajorRequest.java new file mode 100644 index 0000000..ea18891 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateMajorRequest.java @@ -0,0 +1,4 @@ +package org.sopt.certi_server.domain.user.dto.request; + +public record UpdateMajorRequest(String majorName) { +} diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateUniversityRequest.java b/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateUniversityRequest.java new file mode 100644 index 0000000..a00b77e --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateUniversityRequest.java @@ -0,0 +1,4 @@ +package org.sopt.certi_server.domain.user.dto.request; + +public record UpdateUniversityRequest(String universityName) { +} diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateUserRequest.java b/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateUserRequest.java new file mode 100644 index 0000000..317722a --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/request/UpdateUserRequest.java @@ -0,0 +1,26 @@ +package org.sopt.certi_server.domain.user.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Past; +import lombok.Getter; +import org.sopt.certi_server.global.annotation.NoProfanity; + +import java.time.LocalDate; + +public record UpdateUserRequest( + @NoProfanity + String name, + @Email(message = "올바르지 않은 이메일 형식입니다.") + String email, + @NotEmpty(message = "닉네임은 공백이 될 수 없습니다.") + @NoProfanity + String nickName, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") + @Past(message = "생년월일은 과거 날짜여야 합니다.") + LocalDate birthDate +) { + +} diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/response/GetMyPageInfoResponse.java b/src/main/java/org/sopt/certi_server/domain/user/dto/response/GetMyPageInfoResponse.java new file mode 100644 index 0000000..56c9579 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/response/GetMyPageInfoResponse.java @@ -0,0 +1,29 @@ +package org.sopt.certi_server.domain.user.dto.response; + +import lombok.Builder; +import org.sopt.certi_server.domain.user.entity.User; + +import java.util.List; + +@Builder +public record GetMyPageInfoResponse( + String nickname, + String email, + GetJobResponse jobResponse, + int upCount, + int acCount, + int fCount +) { + public static GetMyPageInfoResponse from( + User user, GetJobResponse jobResponse, int upCount, int acCount, int fCount + ){ + return GetMyPageInfoResponse.builder() + .nickname(user.getNickname()) + .email(user.getEmail()) + .jobResponse(jobResponse) + .upCount(upCount) + .acCount(acCount) + .fCount(fCount) + .build(); + } +} diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/response/GetUserResponse.java b/src/main/java/org/sopt/certi_server/domain/user/dto/response/GetUserResponse.java index 1cf352d..bf556fd 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/dto/response/GetUserResponse.java +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/response/GetUserResponse.java @@ -6,14 +6,14 @@ @Builder public record GetUserResponse( - String name, + String nickname, String university, String major, int percentage ) { public static GetUserResponse from(User user, MajorImpl majorImpl, int percentage) { return GetUserResponse.builder() - .name(user.getNickname()) + .nickname(user.getNickname()) .university(user.getUniversity().getName()) .major(majorImpl.getName()) .percentage(percentage) diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/response/OAuthUserInformation.java b/src/main/java/org/sopt/certi_server/domain/user/dto/response/OAuthUserInformation.java index a254da0..541b134 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/dto/response/OAuthUserInformation.java +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/response/OAuthUserInformation.java @@ -8,7 +8,7 @@ public record OAuthUserInformation( Long socialId, SocialType socialType, String email, - @NotEmpty(message = "사용자 닉네임 정보는 필수입니다.") String nickname, + @NotEmpty(message = "사용자 닉네임 정보는 필수입니다.") String name, String profileImageUrl ) { public static OAuthUserInformation from( @@ -18,7 +18,7 @@ public static OAuthUserInformation from( information.id(), SocialType.KAKAO, information.kakaoAccount().email(), - information.kakaoAccount().profile().nickname(), + information.kakaoAccount().profile().name(), information.kakaoAccount().profile().profileImageUrl() ); } diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/response/PersonalInformationResponse.java b/src/main/java/org/sopt/certi_server/domain/user/dto/response/PersonalInformationResponse.java new file mode 100644 index 0000000..72cf237 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/response/PersonalInformationResponse.java @@ -0,0 +1,24 @@ +package org.sopt.certi_server.domain.user.dto.response; + +import lombok.Builder; +import org.sopt.certi_server.domain.user.entity.User; + +import java.time.LocalDate; + +@Builder +public record PersonalInformationResponse( + String nickName, + String name, + String email, + LocalDate birthDate +) { + + public static PersonalInformationResponse from(User user){ + return PersonalInformationResponse.builder() + .name(user.getName()) + .nickName(user.getNickname()) + .email(user.getEmail()) + .birthDate(user.getBirthDate()) + .build(); + } +} diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/response/UserInformation.java b/src/main/java/org/sopt/certi_server/domain/user/dto/response/UserInformation.java index d9c4d01..322ae27 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/dto/response/UserInformation.java +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/response/UserInformation.java @@ -5,7 +5,7 @@ public record UserInformation( String email, - String nickname, + String name, String profileImageUrl ) { public static UserInformation from( @@ -13,7 +13,7 @@ public static UserInformation from( ) { return new UserInformation( information.kakaoAccount().email(), - information.kakaoAccount().profile().nickname(), + information.kakaoAccount().profile().name(), information.kakaoAccount().profile().profileImageUrl() ); } diff --git a/src/main/java/org/sopt/certi_server/domain/user/dto/response/kakao/KakaoUserInformationResponse.java b/src/main/java/org/sopt/certi_server/domain/user/dto/response/kakao/KakaoUserInformationResponse.java index e859d34..ada4d37 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/dto/response/kakao/KakaoUserInformationResponse.java +++ b/src/main/java/org/sopt/certi_server/domain/user/dto/response/kakao/KakaoUserInformationResponse.java @@ -18,7 +18,7 @@ public record KakaoAccount( @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) public record KakaoProfile( - String nickname, + String name, String profileImageUrl ) { diff --git a/src/main/java/org/sopt/certi_server/domain/user/entity/User.java b/src/main/java/org/sopt/certi_server/domain/user/entity/User.java index 9d66f3f..925509a 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/entity/User.java +++ b/src/main/java/org/sopt/certi_server/domain/user/entity/User.java @@ -11,6 +11,8 @@ import org.sopt.certi_server.domain.user.entity.enums.TrackType; import org.sopt.certi_server.global.entity.BaseTimeEntity; +import java.time.LocalDate; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -37,9 +39,12 @@ public class User extends BaseTimeEntity { @JoinColumn(name = "major_impl_id") private MajorImpl major; - @Column(name = "nickname", nullable = false) + @Column(name = "nickname", nullable = false, unique = true) private String nickname; + @Column(name = "name") + private String name; + @Column(name = "email") private String email; @@ -53,6 +58,9 @@ public class User extends BaseTimeEntity { @Column(name = "social_id") private Long socialId; + @Column(name = "birth_date") + private LocalDate birthDate; + public User(String nickname, String email, String profileImageUrl) { this.nickname = nickname; @@ -61,7 +69,7 @@ public User(String nickname, String email, String profileImageUrl) { } @Builder - public User(Long id, University university, String track, String grade, MajorImpl major, String nickname, String email, + public User(Long id, University university, String track, String grade, MajorImpl major, String nickname, String name, String email, String profileImageUrl, SocialType socialType, Long socialId) { this.id = id; this.university = university; @@ -69,15 +77,25 @@ public User(Long id, University university, String track, String grade, MajorImp this.grade = Grade.from(grade); this.major = major; this.nickname = nickname; + this.name = name; this.email = email; this.profileImageUrl = profileImageUrl; this.socialType = socialType; this.socialId = socialId; } - public static User createUser(String nickname, String email, String profileImageUrl) { - return new User(nickname, email, profileImageUrl); + public void changeUser(String name, String nickname, String email, LocalDate birthDate){ + this.name = name; + this.nickname = nickname; + this.email = email; + this.birthDate = birthDate; } + public void changeUniversity(University university) { + this.university = university; + } + public void changeMajor(MajorImpl mi) { + this.major = mi; + } } diff --git a/src/main/java/org/sopt/certi_server/domain/user/repository/UserRepository.java b/src/main/java/org/sopt/certi_server/domain/user/repository/UserRepository.java index e0ddd6d..95593fd 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/repository/UserRepository.java +++ b/src/main/java/org/sopt/certi_server/domain/user/repository/UserRepository.java @@ -12,4 +12,6 @@ public interface UserRepository extends JpaRepository { Optional findBySocialTypeAndSocialId(SocialType socialType, Long socialId); Optional findByEmail(String email); + + boolean existsByNickname(String nickname); } diff --git a/src/main/java/org/sopt/certi_server/domain/user/service/AuthService.java b/src/main/java/org/sopt/certi_server/domain/user/service/AuthService.java index 78d438f..d925feb 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/service/AuthService.java +++ b/src/main/java/org/sopt/certi_server/domain/user/service/AuthService.java @@ -126,7 +126,8 @@ private User convertDtoToEntity(SignupRequest request) { return User.builder() .email(request.userInformation().email()) - .nickname(request.userInformation().nickname()) + .name(request.userInformation().name()) + .nickname(request.nickname()) .profileImageUrl(request.userInformation().profileImageUrl()) .track(request.track()) .grade(request.grade()) diff --git a/src/main/java/org/sopt/certi_server/domain/user/service/KakaoService.java b/src/main/java/org/sopt/certi_server/domain/user/service/KakaoService.java index 5e8f194..5bffad3 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/service/KakaoService.java +++ b/src/main/java/org/sopt/certi_server/domain/user/service/KakaoService.java @@ -54,7 +54,7 @@ public OAuthUserInformation getUserInfo(String code) { public OAuthUserInformation getUserInfoByAccessToken(String accessToken) { try { KakaoUserInformationResponse information = kakaoApiFeignClient.getInformation("Bearer " + accessToken); - log.info(information.kakaoAccount().profile().nickname()); + log.info(information.kakaoAccount().profile().name()); log.info(information.kakaoAccount().email()); return OAuthUserInformation.from(information); } catch (Exception e) { diff --git a/src/main/java/org/sopt/certi_server/domain/user/service/UserService.java b/src/main/java/org/sopt/certi_server/domain/user/service/UserService.java index fc4d5c8..b069e3c 100644 --- a/src/main/java/org/sopt/certi_server/domain/user/service/UserService.java +++ b/src/main/java/org/sopt/certi_server/domain/user/service/UserService.java @@ -4,24 +4,33 @@ import lombok.extern.slf4j.Slf4j; import org.sopt.certi_server.domain.acquisition.repository.AcquisitionRepository; import org.sopt.certi_server.domain.activity.repository.ActivityRepository; +import org.sopt.certi_server.domain.favorite.repository.FavoriteRepository; import org.sopt.certi_server.domain.job.entity.Job; import org.sopt.certi_server.domain.job.repository.JobRepository; import org.sopt.certi_server.domain.major.entity.MajorImpl; import org.sopt.certi_server.domain.major.repository.MajorImplRepository; +import org.sopt.certi_server.domain.user.dto.request.UpdateUserRequest; import org.sopt.certi_server.domain.user.dto.response.GetJobResponse; +import org.sopt.certi_server.domain.user.dto.response.GetMyPageInfoResponse; import org.sopt.certi_server.domain.user.dto.response.GetUserResponse; +import org.sopt.certi_server.domain.user.dto.response.PersonalInformationResponse; +import org.sopt.certi_server.domain.user.entity.University; import org.sopt.certi_server.domain.user.entity.User; import org.sopt.certi_server.domain.user.entity.UserJob; import org.sopt.certi_server.domain.user.repository.CareerRepository; +import org.sopt.certi_server.domain.user.repository.UniversityRepository; import org.sopt.certi_server.domain.user.repository.UserJobRepository; -import org.sopt.certi_server.domain.user.repository.UserMajorImplRepository; import org.sopt.certi_server.domain.user.repository.UserRepository; +import org.sopt.certi_server.domain.userprecertification.repository.UserPreCertificationRepository; import org.sopt.certi_server.global.error.code.ErrorCode; +import org.sopt.certi_server.global.error.exception.InvalidNicknameException; import org.sopt.certi_server.global.error.exception.NotFoundException; +import org.sopt.certi_server.global.valid.ProfanityFilter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; @Service @RequiredArgsConstructor @@ -30,13 +39,16 @@ public class UserService { private final UserRepository userRepository; - private final UserMajorImplRepository userMajorImplRepository; private final MajorImplRepository majorImplRepository; private final UserJobRepository userJobRepository; private final JobRepository jobRepository; private final AcquisitionRepository acquisitionRepository; + private final UserPreCertificationRepository userPreCertificationRepository; + private final FavoriteRepository favoriteRepository; private final CareerRepository careerRepository; private final ActivityRepository activityRepository; + private final ProfanityFilter profanityFilter; + private final UniversityRepository universityRepository; public User getUser(final Long userId) { return userRepository.findById(userId).orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND)); @@ -105,4 +117,97 @@ public int calculateResumeProgress(final User user) { return 96; } + public GetMyPageInfoResponse getMyPageInfoResponse(final Long userId){ + + + // user 정보(닉네임, 이메일) + User user = getUser(userId); + + // 직무 정보 + GetJobResponse jobResponse = getUserJob(userId); + + // 취득 예정, 취득, 즐겨찾기 자격증 개수 + int upCount = userPreCertificationRepository.countByUser(user); + int acCount = acquisitionRepository.countByUser(user); + int fCount = favoriteRepository.countByUser(user); + + return GetMyPageInfoResponse.from(user, jobResponse, upCount, acCount, fCount); + + } + + public PersonalInformationResponse getPersonalInformationResponse(final Long userId){ + User user = getUser(userId); + return PersonalInformationResponse.from(user); + } + + @Transactional + public void updateUserInformation(final Long userId, final UpdateUserRequest request) { + User user = getUser(userId); + + validateNickname(userId, request.nickName()); + + user.changeUser( + request.name(), + request.nickName(), + request.email(), + request.birthDate() + ); + } + + public void validateNickname(final Long userId, String nickname) { + + User user = getUser(userId); + if(!Objects.equals(user.getNickname(), nickname) && userRepository.existsByNickname(nickname)) { + throw new InvalidNicknameException(ErrorCode.NICKNAME_DUPLICATE); + } + + validateKeyword(nickname); + } + + public void validateNickname(String nickname) { + + if(userRepository.existsByNickname(nickname)) { + throw new InvalidNicknameException(ErrorCode.NICKNAME_DUPLICATE); + } + + validateKeyword(nickname); + } + + private void validateKeyword(String nickname) { + // 공백 검사 + if (nickname.isEmpty() || nickname.isBlank()){ + throw new InvalidNicknameException(ErrorCode.NICKNAME_EMPTY); + } + + // 길이 검사 + if(nickname.length() > 7){ + throw new InvalidNicknameException(ErrorCode.NICKNAME_TOO_LONG); + } + + // 욕설 검사 + if (profanityFilter.containsProfanity(nickname)){ + throw new InvalidNicknameException(ErrorCode.NICKNAME_CONTAINS_PROFANITY); + } + } + + + @Transactional + public void changeUniversity(final Long userId, final String universityName) { + User user = getUser(userId); + + University university = universityRepository.findByName(universityName) + .orElseThrow(() -> new NotFoundException(ErrorCode.UNIVERSITY_NOT_FOUND)); + + user.changeUniversity(university); + } + + @Transactional + public void changeMajor(final Long userId, final String majorName) { + User user = getUser(userId); + + MajorImpl mi = majorImplRepository.findMajorImplByName(majorName) + .orElseThrow(() -> new NotFoundException(ErrorCode.MAJOR_NOT_FOUND)); + + user.changeMajor(mi); + } } diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/controller/UserPreCertificationController.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/controller/UserPreCertificationController.java index 0a39691..880d8bd 100644 --- a/src/main/java/org/sopt/certi_server/domain/userprecertification/controller/UserPreCertificationController.java +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/controller/UserPreCertificationController.java @@ -5,6 +5,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.sopt.certi_server.domain.userprecertification.dto.request.CreateUserPreCertificationRequest; +import org.sopt.certi_server.domain.userprecertification.dto.request.PatchPreCertificationRequest; +import org.sopt.certi_server.domain.userprecertification.dto.response.PreCertificationSimple; import org.sopt.certi_server.domain.userprecertification.dto.response.PreCertificationSimpleListResponse; import org.sopt.certi_server.domain.userprecertification.service.UserPreCertificationService; import org.sopt.certi_server.global.error.code.SuccessCode; @@ -24,22 +27,33 @@ public class UserPreCertificationController { @GetMapping @Operation(summary = "취득예정 자격증 리스트 조회 API", description = "취득예정 자격증 리스트를 조회합니다") public ResponseEntity> getPreCertificationListData( - @AuthenticationPrincipal @NotNull(message = "인증되지 않은 사용자입니다.") Long userId + @AuthenticationPrincipal Long userId ) { return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, userPreCertificationService.getPreCertificationListDataByUserId(userId))); } - @PostMapping("/{certificationId}") + @PostMapping @Operation(summary = "취득예정 자격증 추가 API", description = "취득예정 자격증을 추가합니다") public ResponseEntity> addPreCertification( - @AuthenticationPrincipal @NotNull(message = "인증되지 않은 사용자입니다.") Long userId, + @AuthenticationPrincipal Long userId, @Parameter(description = "certification Id", example = "1") - @PathVariable(name = "certificationId") Long certificationId + @RequestBody CreateUserPreCertificationRequest request ) { - boolean isPreCertificated = userPreCertificationService.createNewPreCertification(userId, certificationId); + boolean isPreCertificated = userPreCertificationService.createNewPreCertification(userId, request); return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_CREATE, isPreCertificated)); } + @PatchMapping(value = "/{userPreCertificationId}") + @Operation(summary = "취득예정 정보를 수정 API", description = "취득예정 정보를 수정합니다.") + public ResponseEntity> modifyPreCertification( + @AuthenticationPrincipal Long userId, + @RequestBody PatchPreCertificationRequest request, + @PathVariable(name = "userPreCertificationId") Long userPreCertificationId + ){ + userPreCertificationService.patchPreCertification(userId, userPreCertificationId, request); + return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_UPDATE)); + } + @DeleteMapping(value = "/{certificationId}") @Operation(summary = "취득예정 자격증 삭제 API", description = "취득예정 자격증을 삭제합니다") public ResponseEntity> deletePreCertification( diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/request/CreateUserPreCertificationRequest.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/request/CreateUserPreCertificationRequest.java new file mode 100644 index 0000000..5957f05 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/request/CreateUserPreCertificationRequest.java @@ -0,0 +1,17 @@ +package org.sopt.certi_server.domain.userprecertification.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public record CreateUserPreCertificationRequest( + @NotNull(message = "자격증 id 정보는 필수입니다.") Long certificationId, + @NotBlank(message = "시험 장소(시/도)를 입력해주세요") String city, + @NotBlank(message = "시험 장소(시/군/구)를 입력해주세요") String state, + @NotNull(message = "시험 일정을 입력해주세요") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime testDate +) { +} diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/request/PatchPreCertificationRequest.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/request/PatchPreCertificationRequest.java new file mode 100644 index 0000000..694f6bf --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/request/PatchPreCertificationRequest.java @@ -0,0 +1,17 @@ +package org.sopt.certi_server.domain.userprecertification.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public record PatchPreCertificationRequest( + @NotNull(message = "날짜 정보를 입력해주세요") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime testDate, + + @NotBlank(message = "시험 장소(시/도)를 입력해주세요") String city, + @NotBlank(message = "시험 장소(시/군/구)를 입력해주세요") String state +) { +} diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/response/PreCertificationSimple.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/response/PreCertificationSimple.java index d81c2d0..9fe31cc 100644 --- a/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/response/PreCertificationSimple.java +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/dto/response/PreCertificationSimple.java @@ -4,6 +4,7 @@ import org.sopt.certi_server.domain.userprecertification.entity.UserPreCertification; import java.time.LocalDate; +import java.time.LocalDateTime; public record PreCertificationSimple( Long certificationId, @@ -11,10 +12,23 @@ public record PreCertificationSimple( String averagePeriod, @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy.MM.dd", timezone = "Asia/Seoul") LocalDate nearestTestDate, String agencyName, - int iconIndex + int iconIndex, + String city, + String state, + LocalDateTime testDate ) { public static PreCertificationSimple from(UserPreCertification upc) { - return new PreCertificationSimple(upc.getCertification().getId(), upc.getCertification().getName(), upc.getCertification().getAveragePeriod(), upc.getCertification().getNearestTestDate(), upc.getCertification().getAgency().getName(), upc.getIconType().getIndex()); + return new PreCertificationSimple( + upc.getCertification().getId(), + upc.getCertification().getName(), + upc.getCertification().getAveragePeriod(), + upc.getCertification().getNearestTestDate(), + upc.getCertification().getAgency().getName(), + upc.getIconType().getIndex(), + upc.getLocation().getCity(), + upc.getLocation().getState(), + upc.getTestDate() + ); } } diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/entity/Location.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/entity/Location.java new file mode 100644 index 0000000..23f6543 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/entity/Location.java @@ -0,0 +1,17 @@ +package org.sopt.certi_server.domain.userprecertification.entity; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Embeddable +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class Location { + private String city; + private String state; +} diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/entity/UserPreCertification.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/entity/UserPreCertification.java index e4aec30..f4a3b22 100644 --- a/src/main/java/org/sopt/certi_server/domain/userprecertification/entity/UserPreCertification.java +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/entity/UserPreCertification.java @@ -10,6 +10,8 @@ import org.sopt.certi_server.domain.userprecertification.entity.enums.IconType; import org.sopt.certi_server.global.entity.BaseTimeEntity; +import java.time.LocalDateTime; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -38,18 +40,33 @@ public class UserPreCertification extends BaseTimeEntity { @Enumerated(value = EnumType.STRING) private IconType iconType; + @Column(name = "test_date", nullable = false) + private LocalDateTime testDate; + + @Embedded + private Location location; + @Builder - public UserPreCertification(User user, Certification certification, IconType iconType) { + public UserPreCertification(User user, Certification certification, IconType iconType, Location location, LocalDateTime testDate) { this.user = user; this.certification = certification; this.iconType = iconType; + this.location = location; + this.testDate = testDate; } - public static UserPreCertification create(User user, Certification certification, IconType iconType) { + public static UserPreCertification create(User user, Certification certification, IconType iconType, Location location, LocalDateTime testDate) { return UserPreCertification.builder() .user(user) .certification(certification) .iconType(iconType) + .location(location) + .testDate(testDate) .build(); } + + public void changeUserPreCertification(Location location, LocalDateTime testDate){ + this.location = location; + this.testDate = testDate; + } } diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/repository/UserPreCertificationRepository.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/repository/UserPreCertificationRepository.java index 6c5b91d..ee7f502 100644 --- a/src/main/java/org/sopt/certi_server/domain/userprecertification/repository/UserPreCertificationRepository.java +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/repository/UserPreCertificationRepository.java @@ -41,4 +41,6 @@ List findByCertificationUserIn( @Param("certificationId") Long certificationId, @Param("users") List users ); + + int countByUser(User user); } diff --git a/src/main/java/org/sopt/certi_server/domain/userprecertification/service/UserPreCertificationService.java b/src/main/java/org/sopt/certi_server/domain/userprecertification/service/UserPreCertificationService.java index 08ea7c0..7b05676 100644 --- a/src/main/java/org/sopt/certi_server/domain/userprecertification/service/UserPreCertificationService.java +++ b/src/main/java/org/sopt/certi_server/domain/userprecertification/service/UserPreCertificationService.java @@ -7,8 +7,11 @@ import org.sopt.certi_server.domain.certification.service.CertificationService; import org.sopt.certi_server.domain.user.entity.User; import org.sopt.certi_server.domain.user.service.UserService; +import org.sopt.certi_server.domain.userprecertification.dto.request.CreateUserPreCertificationRequest; +import org.sopt.certi_server.domain.userprecertification.dto.request.PatchPreCertificationRequest; import org.sopt.certi_server.domain.userprecertification.dto.response.PreCertificationSimple; import org.sopt.certi_server.domain.userprecertification.dto.response.PreCertificationSimpleListResponse; +import org.sopt.certi_server.domain.userprecertification.entity.Location; import org.sopt.certi_server.domain.userprecertification.entity.UserPreCertification; import org.sopt.certi_server.domain.userprecertification.entity.enums.IconType; import org.sopt.certi_server.domain.userprecertification.repository.UserPreCertificationRepository; @@ -37,9 +40,9 @@ public PreCertificationSimpleListResponse getPreCertificationListDataByUserId(Lo } @Transactional - public boolean createNewPreCertification(final Long userId, final Long certificationId) { + public boolean createNewPreCertification(final Long userId, final CreateUserPreCertificationRequest request) { User user = userService.getUser(userId); - Certification certification = certificationService.getCertification(certificationId); + Certification certification = certificationService.getCertification(request.certificationId()); if(acquisitionRepository.existsByUserAndCertification(user, certification)){ log.info("이미 취득한 자격증에 대해 취득 예정 시도"); @@ -54,7 +57,16 @@ public boolean createNewPreCertification(final Long userId, final Long certifica .map(userPreCertification -> IconType.issueNextIconType(userPreCertification.getIconType().getIndex())) .orElseGet(IconType::issueRandomIconType); - userPreCertificationRepository.save(UserPreCertification.create(user, certification, iconType)); + userPreCertificationRepository.save(UserPreCertification.create( + user, + certification, + iconType, + Location.builder() + .city(request.city()) + .state(request.state()) + .build(), + request.testDate() + )); return true; } @@ -69,4 +81,23 @@ public void deletePreCertification(Long userId, Long certificationId) { userPreCertificationRepository.delete(userPreCertification); } + + @Transactional + public void patchPreCertification(Long userId, Long userPreCertificationId, PatchPreCertificationRequest request) { + + User user = userService.getUser(userId); + UserPreCertification upc = userPreCertificationRepository.findById(userPreCertificationId).orElseThrow( + () -> new NotFoundException(ErrorCode.PRECERTIFICATION_NOT_FOUND) + ); + + if(upc.getUser() != user) throw new ForbiddenException(ErrorCode.ACCESS_DENIED); + + upc.changeUserPreCertification( + Location.builder() + .city(request.city()) + .state(request.state()) + .build(), + request.testDate() + ); + } } diff --git a/src/main/java/org/sopt/certi_server/global/annotation/NoProfanity.java b/src/main/java/org/sopt/certi_server/global/annotation/NoProfanity.java new file mode 100644 index 0000000..1b7c51a --- /dev/null +++ b/src/main/java/org/sopt/certi_server/global/annotation/NoProfanity.java @@ -0,0 +1,17 @@ +package org.sopt.certi_server.global.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import org.sopt.certi_server.global.valid.ProfanityValidator; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = ProfanityValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface NoProfanity { + String message() default "비속어가 포함되어 있습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/org/sopt/certi_server/global/config/SecurityConfig.java b/src/main/java/org/sopt/certi_server/global/config/SecurityConfig.java index 88e4f3a..41ffff9 100644 --- a/src/main/java/org/sopt/certi_server/global/config/SecurityConfig.java +++ b/src/main/java/org/sopt/certi_server/global/config/SecurityConfig.java @@ -31,7 +31,8 @@ public class SecurityConfig { "/api/v1/admin/**", "/api/v1/university/**", "/api/v1/major/**", - "/actuator/**" + "/actuator/**", + "/api/v1/user/validation" }; diff --git a/src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java b/src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java index 234fbf9..bb4af89 100644 --- a/src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java +++ b/src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java @@ -17,6 +17,10 @@ public enum ErrorCode { TEST_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "E400007", "요청 테스트 타입이 올바르지 않습니다"), JOB_SIZE_ERROR(HttpStatus.BAD_REQUEST, "E400008", "희망 분야는 1~3개까지 선택가능합니다"), MISMATCH_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "E400009", "리프레시 토큰이 일치하지 않습니다"), + NICKNAME_TOO_LONG(HttpStatus.BAD_REQUEST, "E400010", "닉네임은 7자 이하만 가능합니다."), + NICKNAME_EMPTY(HttpStatus.BAD_REQUEST, "E400011", "닉네임은 공백일 수 없습니다."), + NICKNAME_DUPLICATE(HttpStatus.BAD_REQUEST, "E400012", "이미 존재하는 닉네임입니다."), + NICKNAME_CONTAINS_PROFANITY(HttpStatus.BAD_REQUEST, "E400013", "닉네임에 비속어를 포함할 수 없습니다."), /* 401 */ UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "E401001", "리소스 접근 권한이 없습니다."), @@ -31,21 +35,21 @@ public enum ErrorCode { DATA_NOT_FOUND(HttpStatus.NOT_FOUND, "E404001", "데이터가 존재하지 않습니다"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "E404002", "유저가 존재하지 않습니다"), MAJOR_NOT_FOUND(HttpStatus.NOT_FOUND, "E404003", "존재하지 않는 전공입니다."), - JOB_NOT_FOUND(HttpStatus.NOT_FOUND, "E40404", "존재하지 않는 직무입니다."), - GRADE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40405", "존재하지 않는 학년입니다."), - SOCIAL_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40406", "존재하지 않는 소셜 타입입니다."), - TRACK_NOT_FOUND(HttpStatus.NOT_FOUND, "E40407", "존재하지 않는 계열입니다."), - CERTIFICATION_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40408", "존재하지 않는 자격증 종류입니다."), - CERTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, " E40409", "자격증이 존재하지 않습니다"), - AGENCY_NOT_FOUND(HttpStatus.NOT_FOUND, "E40410", "존재하지 않는 인증기관입니다."), - CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "E40411", "존재하지 않는 카테고리입니다."), - UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND, "E40412", "존재하지 않는 대학교입니다"), - ACQUISITION_NOT_FOUND(HttpStatus.NOT_FOUND, "E40413", "존재하지 않는 취득 정보입니다."), - PRECERTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "E40414", "존재하지 않는 취득예정 정보입니다."), - REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E40415", "존재하지 않는 리프레시 토큰입니다"), - COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "E40416", "존재하지 않는 댓글입니다."), - MISSING_HANDLER(HttpStatus.NOT_FOUND, "E40417", "URL을 찾을 수 없습니다."), - COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "이미 취소된 좋아요입니다."), + JOB_NOT_FOUND(HttpStatus.NOT_FOUND, "E404004", "존재하지 않는 직무입니다."), + GRADE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404005", "존재하지 않는 학년입니다."), + SOCIAL_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404006", "존재하지 않는 소셜 타입입니다."), + TRACK_NOT_FOUND(HttpStatus.NOT_FOUND, "E404007", "존재하지 않는 계열입니다."), + CERTIFICATION_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404008", "존재하지 않는 자격증 종류입니다."), + CERTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "E404009", "자격증이 존재하지 않습니다"), + AGENCY_NOT_FOUND(HttpStatus.NOT_FOUND, "E404010", "존재하지 않는 인증기관입니다."), + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "E404011", "존재하지 않는 카테고리입니다."), + UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND, "E404012", "존재하지 않는 대학교입니다"), + ACQUISITION_NOT_FOUND(HttpStatus.NOT_FOUND, "E404013", "존재하지 않는 취득 정보입니다."), + PRECERTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "E404014", "존재하지 않는 취득예정 정보입니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E404015", "존재하지 않는 리프레시 토큰입니다"), + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "E404016", "존재하지 않는 댓글입니다."), + MISSING_HANDLER(HttpStatus.NOT_FOUND, "E404017", "URL을 찾을 수 없습니다."), + COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404018", "이미 취소된 좋아요입니다."), /* 409 CONFLICT */ diff --git a/src/main/java/org/sopt/certi_server/global/error/exception/InvalidNicknameException.java b/src/main/java/org/sopt/certi_server/global/error/exception/InvalidNicknameException.java new file mode 100644 index 0000000..43029e1 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/global/error/exception/InvalidNicknameException.java @@ -0,0 +1,9 @@ +package org.sopt.certi_server.global.error.exception; + +import org.sopt.certi_server.global.error.code.ErrorCode; + +public class InvalidNicknameException extends BusinessException{ + public InvalidNicknameException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/org/sopt/certi_server/global/filter/JwtAuthenticationFilter.java b/src/main/java/org/sopt/certi_server/global/filter/JwtAuthenticationFilter.java index 05417d1..3e47ca5 100644 --- a/src/main/java/org/sopt/certi_server/global/filter/JwtAuthenticationFilter.java +++ b/src/main/java/org/sopt/certi_server/global/filter/JwtAuthenticationFilter.java @@ -34,7 +34,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { "/api/v1/admin/**", "/api/v1/university/**", "/api/v1/major/**", - "/actuator/**" + "/actuator/**", + "/api/v1/user/validation" ); diff --git a/src/main/java/org/sopt/certi_server/global/valid/ProfanityFilter.java b/src/main/java/org/sopt/certi_server/global/valid/ProfanityFilter.java new file mode 100644 index 0000000..35abf11 --- /dev/null +++ b/src/main/java/org/sopt/certi_server/global/valid/ProfanityFilter.java @@ -0,0 +1,31 @@ +package org.sopt.certi_server.global.valid; + +import jakarta.annotation.PostConstruct; +import org.ahocorasick.trie.Trie; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class ProfanityFilter { + + private Trie trie; + + + @PostConstruct + public void init(){ + List badWords = List.of("병신", "시발", "개새끼", "fuck"); + + this.trie = Trie.builder() + .addKeywords(badWords) + .ignoreCase() + .ignoreOverlaps() + .build(); + } + + public boolean containsProfanity(String keyword){ + // 특수문자 제거 + String purifiedValue = keyword.replaceAll("[^가-힣a-zA-Z0-9]", ""); + return trie.containsMatch(purifiedValue); + } +} diff --git a/src/main/java/org/sopt/certi_server/global/valid/ProfanityValidator.java b/src/main/java/org/sopt/certi_server/global/valid/ProfanityValidator.java new file mode 100644 index 0000000..42ba03b --- /dev/null +++ b/src/main/java/org/sopt/certi_server/global/valid/ProfanityValidator.java @@ -0,0 +1,23 @@ +package org.sopt.certi_server.global.valid; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.sopt.certi_server.global.annotation.NoProfanity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ProfanityValidator implements ConstraintValidator { + + private final ProfanityFilter filter; + + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return !filter.containsProfanity(value); + } +} diff --git a/src/test/java/org/sopt/certi_server/domain/user/dto/request/UpdateUserRequestTest.java b/src/test/java/org/sopt/certi_server/domain/user/dto/request/UpdateUserRequestTest.java new file mode 100644 index 0000000..817863a --- /dev/null +++ b/src/test/java/org/sopt/certi_server/domain/user/dto/request/UpdateUserRequestTest.java @@ -0,0 +1,42 @@ +package org.sopt.certi_server.domain.user.dto.request; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDate; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class UpdateUserRequestTest { + + @Autowired + Validator validator; + + @Test + @DisplayName("비속어가 포함된 단어를 검증한다.") + void fail_profanity_nickname(){ + // Given + UpdateUserRequest request = new UpdateUserRequest( + "이성민", + "leesung2925@gmail.com", + "시발놈", + LocalDate.of(2000,2,29) + ); + + // When + Set> violations = validator.validate(request); + + // Then + assertThat(violations).isNotEmpty(); + assertThat(violations) + .extracting("message") + .containsExactly("비속어가 포함되어 있습니다."); + } +} diff --git a/src/test/java/org/sopt/certi_server/domain/user/service/UserServiceTest.java b/src/test/java/org/sopt/certi_server/domain/user/service/UserServiceTest.java new file mode 100644 index 0000000..dfe6560 --- /dev/null +++ b/src/test/java/org/sopt/certi_server/domain/user/service/UserServiceTest.java @@ -0,0 +1,243 @@ +package org.sopt.certi_server.domain.user.service; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.sopt.certi_server.domain.acquisition.entity.Acquisition; +import org.sopt.certi_server.domain.acquisition.entity.enums.CardType; +import org.sopt.certi_server.domain.acquisition.entity.enums.SmallCardType; +import org.sopt.certi_server.domain.acquisition.repository.AcquisitionRepository; +import org.sopt.certi_server.domain.certification.entity.Certification; +import org.sopt.certi_server.domain.certification.repository.CertificationRepository; +import org.sopt.certi_server.domain.comment.repository.CertificationCommentLikeRepository; +import org.sopt.certi_server.domain.comment.repository.CertificationCommentRepository; +import org.sopt.certi_server.domain.job.entity.Job; +import org.sopt.certi_server.domain.job.repository.JobRepository; +import org.sopt.certi_server.domain.major.entity.MajorImpl; +import org.sopt.certi_server.domain.major.repository.MajorImplRepository; +import org.sopt.certi_server.domain.user.dto.request.UpdateUserRequest; +import org.sopt.certi_server.domain.user.dto.response.GetMyPageInfoResponse; +import org.sopt.certi_server.domain.user.dto.response.PersonalInformationResponse; +import org.sopt.certi_server.domain.user.entity.University; +import org.sopt.certi_server.domain.user.entity.User; +import org.sopt.certi_server.domain.user.entity.UserJob; +import org.sopt.certi_server.domain.user.repository.UniversityRepository; +import org.sopt.certi_server.domain.user.repository.UserJobRepository; +import org.sopt.certi_server.domain.user.repository.UserRepository; +import org.sopt.certi_server.domain.userprecertification.repository.UserPreCertificationRepository; +import org.sopt.certi_server.global.error.code.ErrorCode; +import org.sopt.certi_server.global.error.exception.InvalidNicknameException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@SpringBootTest +@Transactional +class UserServiceTest { + + @Autowired + UserService userService; + + @Autowired + private UserRepository userRepository; + @Autowired + private CertificationRepository certificationRepository; + @Autowired + private CertificationCommentRepository certificationCommentRepository; + @Autowired + private CertificationCommentLikeRepository certificationCommentLikeRepository; + @Autowired + private AcquisitionRepository acquisitionRepository; + @Autowired + private UserPreCertificationRepository userPreCertificationRepository; + @Autowired + private UserJobRepository userJobRepository; + @Autowired + private JobRepository jobRepository; // Job 저장을 위해 추가 + @Autowired + private UniversityRepository universityRepository; + @Autowired + private MajorImplRepository majorImplRepository; + + private User testUser; + private User otherUser; + + private Job testJob; + private Certification testCertification; + private University testUniversity; + private MajorImpl testMajor; + @BeforeEach + void setUp(){ + + testUniversity = universityRepository.findById(1L).orElseThrow(); + testMajor = majorImplRepository.findMajorImplByName("전산학/컴퓨터공학").orElseThrow(); + testUser = userRepository.save( + User.builder() + .email("lee@gmail.com") + .nickname("이성민") + .track("공학계열") + .grade("1학년") + .major(testMajor) + .university(testUniversity) + .build() + ); + otherUser = userRepository.save( + User.builder() + .email("leee@gmail.com") + .nickname("이헝민") + .track("공학계열") + .grade("1학년") + .major(testMajor) + .university(testUniversity) + .build() + ); + testCertification = certificationRepository.findById(1L).orElseThrow(); + acquisitionRepository.save( + Acquisition.builder() + .user(testUser) + .certification(testCertification) + .cardType(CardType.THIRD) + .smallCardType(SmallCardType.THIRD) + .build() + ); + testJob = jobRepository.findByName("IT/인터넷").orElseThrow(); + userJobRepository.save(UserJob.createUserJob(testUser, testJob)); + } + + @Test + @DisplayName("[성공] 마이페이지 홈 내용을 유저의 현 상황에 맞게 조회를 성공한다.") + void get_my_page_home(){ + GetMyPageInfoResponse myPageInfoResponse = userService.getMyPageInfoResponse(testUser.getId()); + + Assertions.assertThat(myPageInfoResponse.nickname()).isEqualTo("이성민"); + Assertions.assertThat(myPageInfoResponse.email()).isEqualTo("lee@gmail.com"); + Assertions.assertThat(myPageInfoResponse.jobResponse().jobList()).contains("IT/인터넷"); + Assertions.assertThat(myPageInfoResponse.acCount()).isEqualTo(1); + Assertions.assertThat(myPageInfoResponse.upCount()).isEqualTo(0); + Assertions.assertThat(myPageInfoResponse.fCount()).isEqualTo(0); + } + + + @Nested + @DisplayName("개인 정보 수정") + class UpdateUserInformation{ + + @Test + @DisplayName("[성공] 개인정보 수정을 성공한다") + void update_user_information(){ + // Given + UpdateUserRequest request = new UpdateUserRequest( + "이성민", + "leesung2925@gmail.com", + "이뿡빵", + LocalDate.of(2000, 2, 29) + ); + + // When + userService.updateUserInformation(testUser.getId(), request); + + // Then + PersonalInformationResponse personalInformationResponse = userService.getPersonalInformationResponse(testUser.getId()); + + Assertions.assertThat(personalInformationResponse.name()).isEqualTo("이성민"); + Assertions.assertThat(personalInformationResponse.email()).isEqualTo("leesung2925@gmail.com"); + Assertions.assertThat(personalInformationResponse.birthDate()).isEqualTo(LocalDate.of(2000, 2, 29)); + Assertions.assertThat(personalInformationResponse.nickName()).isEqualTo("이뿡빵"); + } + + + } + + @Nested + @DisplayName("닉네임 검사") + class ValidateNickname{ + @Test + @DisplayName("[성공] 닉네임에 욕설이 포함되어 있다.") + void nickname_contains_profanity(){ + + // Given + String nickname = "시발이성민"; + + // When + + // Then + Assertions.assertThatThrownBy( + () -> userService.validateNickname(testUser.getId(), nickname) + ) + .isInstanceOf(InvalidNicknameException.class) + .hasMessageContaining(ErrorCode.NICKNAME_CONTAINS_PROFANITY.getMessage()); + } + + @Test + @DisplayName("[성공] 닉네임이 공백으로 이루어져있다.") + void nickname_only_blank(){ + + // Given + String nickname = " "; + + // When + + // Then + Assertions.assertThatThrownBy( + () -> userService.validateNickname(testUser.getId(), nickname) + ) + .isInstanceOf(InvalidNicknameException.class) + .hasMessageContaining(ErrorCode.NICKNAME_EMPTY.getMessage()); + } + + @Test + @DisplayName("[성공] 닉네임의 길이가 너무 길다") + void nickname_too_long(){ + + // Given + String nickname = "안녕하세요저는이성민입니다"; + + // When + + // Then + Assertions.assertThatThrownBy( + () -> userService.validateNickname(testUser.getId(), nickname) + ) + .isInstanceOf(InvalidNicknameException.class) + .hasMessageContaining(ErrorCode.NICKNAME_TOO_LONG.getMessage()); + } + + @Test + @DisplayName("[성공] 중복되는 닉네임이 존재한다") + void nickname_is_duplicated(){ + + // Given + String nickname = "이헝민"; + + // When + + // Then + Assertions.assertThatThrownBy( + () -> userService.validateNickname(testUser.getId(), nickname) + ) + .isInstanceOf(InvalidNicknameException.class) + .hasMessageContaining(ErrorCode.NICKNAME_DUPLICATE.getMessage()); + } + + @Test + @DisplayName("[성공] 내 현재 닉네임과 같다") + void nickname_is_duplicated_with_current_nickname(){ + + // Given + String nickname = "이성민"; + + // When + + // Then + Assertions.assertThatCode(() -> userService.validateNickname(testUser.getId(), nickname)) + .doesNotThrowAnyException(); + + } + } + + +}