Skip to content
Merged
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
798bacb
#170 [feat] 마이페이지 홈화면 API 구현
seongmin0229 Nov 29, 2025
ca83d8f
#170 [feat] 개인정보 수정 페이지 조회 API
seongmin0229 Nov 29, 2025
6b1a58a
#170 [feat] 개인정보 수정 API 구현, 테스트 코드 추가
seongmin0229 Dec 1, 2025
a44999d
#170 [feat] SignupRequest 수정, 닉네임 검증 API 구현
seongmin0229 Dec 3, 2025
876cfa1
#170 [feat] 닉네임 검증 기능 재사용할 수 있도록 수정
seongmin0229 Dec 3, 2025
ea1df52
#170 [feat] 취득, 취득 예정 API 추가 및 수정
seongmin0229 Dec 6, 2025
eb627be
#170 [feat] 대학교 변경 API 추가, 닉네임 검증 API 화이트 리스트에 반영
seongmin0229 Dec 23, 2025
c6752f2
# 170 [feat] 학과 변경 API 추가
seongmin0229 Dec 28, 2025
2754125
#164 [fix] 계열별/직무별 자격증 Top3 조회 구현
woals2840 Dec 30, 2025
85f0d76
#164 [fix] 자격증 조회 수정
woals2840 Dec 30, 2025
3477234
#170 [feat] 마이페이지 홈화면 API 구현
seongmin0229 Nov 29, 2025
edc7fbb
#170 [feat] 개인정보 수정 페이지 조회 API
seongmin0229 Nov 29, 2025
b3a494b
#170 [feat] 개인정보 수정 API 구현, 테스트 코드 추가
seongmin0229 Dec 1, 2025
65c89f7
#170 [feat] SignupRequest 수정, 닉네임 검증 API 구현
seongmin0229 Dec 3, 2025
5316e1a
#170 [feat] 닉네임 검증 기능 재사용할 수 있도록 수정
seongmin0229 Dec 3, 2025
43f3016
#170 [feat] 취득, 취득 예정 API 추가 및 수정
seongmin0229 Dec 6, 2025
4cd9d13
#170 [feat] 대학교 변경 API 추가, 닉네임 검증 API 화이트 리스트에 반영
seongmin0229 Dec 23, 2025
4058d84
# 170 [feat] 학과 변경 API 추가
seongmin0229 Dec 28, 2025
3fd0166
Merge remote-tracking branch 'origin/feat/#170' into feat/#170
seongmin0229 Dec 30, 2025
9de7f45
#164 [fix] 계열별/직무별 자격증 Top3 조회 구현
woals2840 Dec 30, 2025
55eb838
#164 [fix] 자격증 조회 수정
woals2840 Dec 30, 2025
becfb70
Merge remote-tracking branch 'origin/fix/#164' into fix/#164
seongmin0229 Dec 30, 2025
5569a68
#170 [feat] 닉네임 검증 로직 변경
seongmin0229 Jan 1, 2026
dd5f9cf
#170 [feat] 닉네임 검증 메서드 분리
seongmin0229 Jan 1, 2026
6c3ea00
#170 [feat] 닉네임 검증 API jwt 필터 예외 처리
seongmin0229 Jan 3, 2026
a0f8adf
#170 [fix] 에러 코드 수정
seongmin0229 Jan 3, 2026
c08a34a
#164 [fix] 코드래빗 반영
woals2840 Jan 6, 2026
5499a47
Merge pull request #172 from cerdeuk/fix/#164
woals2840 Jan 6, 2026
405ddbe
#170 [fix] 에러 코드 수정
seongmin0229 Jan 6, 2026
916c3fe
#170 [feat] 마이페이지 홈화면 API 구현
seongmin0229 Nov 29, 2025
5674896
#170 [feat] 개인정보 수정 페이지 조회 API
seongmin0229 Nov 29, 2025
da58c65
#170 [feat] 개인정보 수정 API 구현, 테스트 코드 추가
seongmin0229 Dec 1, 2025
77afe8d
#170 [feat] SignupRequest 수정, 닉네임 검증 API 구현
seongmin0229 Dec 3, 2025
1f26c5d
#170 [feat] 닉네임 검증 기능 재사용할 수 있도록 수정
seongmin0229 Dec 3, 2025
e4843a9
#170 [feat] 취득, 취득 예정 API 추가 및 수정
seongmin0229 Dec 6, 2025
5595795
#170 [feat] 대학교 변경 API 추가, 닉네임 검증 API 화이트 리스트에 반영
seongmin0229 Dec 23, 2025
53b5697
# 170 [feat] 학과 변경 API 추가
seongmin0229 Dec 28, 2025
8384bbd
#170 [feat] 마이페이지 홈화면 API 구현
seongmin0229 Nov 29, 2025
3b3670c
#170 [feat] 개인정보 수정 페이지 조회 API
seongmin0229 Nov 29, 2025
5b17bbc
#170 [feat] 개인정보 수정 API 구현, 테스트 코드 추가
seongmin0229 Dec 1, 2025
6834098
#170 [feat] SignupRequest 수정, 닉네임 검증 API 구현
seongmin0229 Dec 3, 2025
0b1ca31
#170 [feat] 닉네임 검증 기능 재사용할 수 있도록 수정
seongmin0229 Dec 3, 2025
3c3dfe4
#170 [feat] 대학교 변경 API 추가, 닉네임 검증 API 화이트 리스트에 반영
seongmin0229 Dec 23, 2025
fb28e5c
# 170 [feat] 학과 변경 API 추가
seongmin0229 Dec 28, 2025
7a96b96
#170 [feat] 닉네임 검증 로직 변경
seongmin0229 Jan 1, 2026
98f17b8
#170 [feat] 닉네임 검증 메서드 분리
seongmin0229 Jan 1, 2026
a31eb8d
#170 [feat] 닉네임 검증 API jwt 필터 예외 처리
seongmin0229 Jan 3, 2026
3774ad0
#170 [fix] 에러 코드 수정
seongmin0229 Jan 3, 2026
a5538fd
#170 [fix] 에러 코드 수정
seongmin0229 Jan 6, 2026
aef719f
#170 [feat] rebase
seongmin0229 Jan 6, 2026
e8f6309
Merge remote-tracking branch 'origin/feat/#170' into feat/#170
seongmin0229 Jan 6, 2026
d3d140a
#170 [fix] 코드래빗 리뷰 반영
seongmin0229 Jan 8, 2026
078d2ed
#170 [fix] 코드래빗 리뷰 반영2
seongmin0229 Jan 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ dependencies {
implementation 'io.micrometer:micrometer-registry-prometheus'


// ahocorasick(아호코라식) 의존성 추가
implementation 'org.ahocorasick:ahocorasick:0.6.3'
}

// Querydsl 빌드 옵션 설정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,19 +25,19 @@
public class AcquisitionController {
private final AcquisitionService acquisitionService;

@PostMapping("/{certificationId}")
@PostMapping
@Operation(summary = "취득한 자격증 추가 API", description = "취득한 자격증을 추가합니다")
public ResponseEntity<SuccessResponse<?>> addAcquisition(
public ResponseEntity<SuccessResponse<Boolean>> 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<SuccessResponse<GetAcquisitionDetailResponse>> getAcquisition(
@Parameter(description = "acquisition Id", example = "1")
Expand All @@ -46,7 +47,18 @@ public ResponseEntity<SuccessResponse<GetAcquisitionDetailResponse>> getAcquisit
return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, getAcquisitionDetailResponse));
}

@DeleteMapping("/{acquisitionId}")
@PatchMapping(value = "/{acquisitionId}")
@Operation(summary = "취득한 자격증 정보 수정 API", description = "취득한 자격증 정보를 수정합니다.")
public ResponseEntity<SuccessResponse<Void>> 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<SuccessResponse<Void>> deleteAcquisition(
@AuthenticationPrincipal Long userId,
Expand All @@ -62,7 +74,7 @@ public ResponseEntity<SuccessResponse<Void>> deleteAcquisition(
public ResponseEntity<SuccessResponse<GetAcquisitionListResponse>> getAllAcquisitions(
@AuthenticationPrincipal Long userId
) {
List<GetAcquisitionListDetailResponse> getAcquisitionResponses = acquisitionService.getAcquisitionList(userId);
List<GetAcquisitionResponse> getAcquisitionResponses = acquisitionService.getAcquisitionList(userId);
GetAcquisitionListResponse getAcquisitionListResponse = GetAcquisitionListResponse.of(getAcquisitionResponses);

return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, getAcquisitionListResponse));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.certi_server.domain.acquisition.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

public record CreateAcquisitionRequest(
@NotNull(message = "자격증 id는 필수 값입니다.") Long certificationId
) {
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public record GetAcquisitionDetailResponse(
List<String> 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()
Expand All @@ -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();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import java.util.List;

public record GetAcquisitionListResponse(
List<GetAcquisitionListDetailResponse> acquisitionListDetailResponses
List<GetAcquisitionResponse> acquisitionListDetailResponses
) {
public static GetAcquisitionListResponse of(List<GetAcquisitionListDetailResponse> priorCertificaitonResponseList) {
public static GetAcquisitionListResponse of(List<GetAcquisitionResponse> priorCertificaitonResponseList) {
return new GetAcquisitionListResponse(priorCertificaitonResponseList);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> tags
Long acquisitionId,
String cardFrontImageUrl,
int index,
String name,
List<String> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -44,11 +46,23 @@ 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) {
this.certification = certification;
this.smallCardType = smallCardType;
this.cardType = cardType;
this.user = user;
this.acquisitionDate = LocalDate.now();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

생성자 설계의 일관성을 개선하세요.

생성자가 grade 파라미터는 받지만 acquisitionDate는 받지 않고 LocalDate.now()로 하드코딩하고 있습니다. 이는 다음과 같은 문제를 야기할 수 있습니다:

  • 과거 날짜로 취득 정보를 생성해야 하는 경우 대응이 불가능합니다
  • changeAcquisition 메서드에서는 acquisitionDate를 변경할 수 있지만, 생성 시점에는 지정할 수 없어 일관성이 떨어집니다
🔎 생성자에 acquisitionDate 파라미터 추가
 @Builder
-public Acquisition(Certification certification, CardType cardType, User user, SmallCardType smallCardType, String grade) {
+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.acquisitionDate = LocalDate.now();
+    this.grade = grade;
+    this.acquisitionDate = acquisitionDate != null ? acquisitionDate : LocalDate.now();
 }
🤖 Prompt for AI Agents
In
@src/main/java/org/sopt/certi_server/domain/acquisition/entity/Acquisition.java
around lines 56-62, The Acquisition constructor currently accepts a grade but
always sets acquisitionDate = LocalDate.now(), preventing creation of
acquisitions with specific dates and causing inconsistency with
changeAcquisition; modify the constructor (public Acquisition(Certification
certification, CardType cardType, User user, SmallCardType smallCardType, String
grade)) to accept a LocalDate acquisitionDate parameter and assign it to the
acquisitionDate field, or add an overloaded constructor that takes
acquisitionDate while keeping the existing one delegating to the new constructor
with LocalDate.now() so callers can opt to supply an explicit date.


public void changeAcquisition(LocalDate acquisitionDate, String grade) {
this.acquisitionDate = acquisitionDate;
this.grade = grade;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,8 +40,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);

//중복 여부 확인
Expand Down Expand Up @@ -91,12 +92,12 @@ public GetAcquisitionDetailResponse getAcquisitionDetail(final Long acquisitionI
return GetAcquisitionDetailResponse.from(acquisition);
}

public List<GetAcquisitionListDetailResponse> getAcquisitionList(final Long userId) {
public List<GetAcquisitionResponse> getAcquisitionList(final Long userId) {
User user = userService.getUser(userId);
List<Acquisition> acquisitionList = acquisitionRepository.findByUserOrderByIdDesc(user);

return acquisitionList.stream()
.map(GetAcquisitionListDetailResponse::from)
.map(GetAcquisitionResponse::from)
.toList();
}

Expand All @@ -111,4 +112,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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -81,4 +82,24 @@ public ResponseEntity<SuccessResponse<CertificationRecommendationListResponse>>
return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certiRecommendListRes));
}

@GetMapping("/job")
@Operation(summary = "직무별 자격증 조회 API", description = "3순위 직무별 자격증을 조회합니다")
public ResponseEntity<SuccessResponse<?>> getTop3ByJob(
@AuthenticationPrincipal Long userId
){
List<CertificationRankResponse> certificationRankResponseList = certificationService.getCertificationJob(userId);
return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certificationRankResponseList));
}

@GetMapping("/track")
@Operation(summary = "계열별 자격증 조회 API", description = "3순위 계열별 자격증을 조회합니다")
public ResponseEntity<SuccessResponse<?>> getTop3ByTrack(
@AuthenticationPrincipal Long userId
) {
List<CertificationRankResponse> certificationRankResponseList = certificationService.getCertificationTrack(userId);
return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certificationRankResponseList));

}


}
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public record CertificationScoreDto(
String testType,
List<String> tags,
int recommendationScore,
boolean isFavorite) {
boolean isFavorite,
String description) {
public static CertificationScoreDto from(
Certification certification,
int recommendationScore,
Expand All @@ -24,7 +25,8 @@ public static CertificationScoreDto from(
certification.getTestType().getType(),
certification.getTags().stream().toList(),
recommendationScore,
isFavorite
isFavorite,
certification.getDescription()
);
}

Expand Down
Loading