Skip to content

Commit ed7cf96

Browse files
authored
Merge pull request #40 from Hsu-Connect/feat/#39
[#39] Feat: 내 커리어 수정 API 구현
2 parents ab13180 + 8dc28d0 commit ed7cf96

8 files changed

Lines changed: 185 additions & 1 deletion

File tree

src/main/java/hansung/hansung_connect/common/exception/code/status/ErrorStatus.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ public enum ErrorStatus implements BaseErrorCode {
3838
CAREER_END_YM_FORBIDDEN_WHEN_EMPLOYED(HttpStatus.BAD_REQUEST, "CAREER4006", "재직중이면 종료 연월을 입력할 수 없습니다."),
3939
CAREER_END_YM_REQUIRED_WHEN_NOT_EMPLOYED(HttpStatus.BAD_REQUEST, "CAREER4007", "재직중이 아니면 종료 연월이 필요합니다."),
4040
CAREER_END_YM_BEFORE_START(HttpStatus.BAD_REQUEST, "CAREER4008", "종료 연월은 시작 연월보다 앞설 수 없습니다."),
41-
CAREER_BULK_EMPTY(HttpStatus.BAD_REQUEST, "CAREER4009", "추가할 커리어 항목이 비어 있습니다.");
41+
CAREER_BULK_EMPTY(HttpStatus.BAD_REQUEST, "CAREER4009", "추가할 커리어 항목이 비어 있습니다."),
42+
CAREER_NOT_FOUND(HttpStatus.BAD_REQUEST, "CAREER4007", "해당 커리어를 찾을 수 없습니다."),
43+
CAREER_FORBIDDEN(HttpStatus.FORBIDDEN, "CAREER4008", "해당 커리어에 대한 수정 권한이 없습니다.");
4244

4345

4446
private final HttpStatus httpStatus;

src/main/java/hansung/hansung_connect/domain/career/controller/CareerController.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import io.swagger.v3.oas.annotations.Operation;
99
import io.swagger.v3.oas.annotations.tags.Tag;
1010
import lombok.RequiredArgsConstructor;
11+
import org.springframework.web.bind.annotation.PathVariable;
1112
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.PutMapping;
1214
import org.springframework.web.bind.annotation.RequestBody;
1315
import org.springframework.web.bind.annotation.RestController;
1416

@@ -61,5 +63,32 @@ public ApiResponse<CareerResponseDTO.BulkCreateResponseDTO> createCareers(
6163
@RequestBody BatchCreateRequestDTO requestDTO) {
6264
return ApiResponse.onSuccess(careerCommandService.createCareers(requestDTO));
6365
}
66+
67+
@Operation(
68+
summary = "내 커리어 수정",
69+
description = """
70+
기존 커리어 정보를 수정(전체 대체)하는 API입니다.
71+
<br><br>
72+
요청 규칙
73+
- careerId: 수정할 커리어의 ID (PathVariable로 전달)
74+
- 요청 본문은 생성(Create) 요청과 동일하며, 전달된 값으로 전부 교체됩니다.
75+
- isEmployed(재직 여부)가 true이면 endYm은 null이어야 합니다.
76+
- isEmployed(재직 여부)가 false이면 endYm은 필수입니다.
77+
- 연월은 "2024-04" 또는 "2024.04" 형태 모두 허용합니다.
78+
79+
JobType ENUM
80+
- PERMANENT (정규직)
81+
- TEMPORARY (계약직)
82+
- INTERN (인턴)
83+
- FREELANCER (프리랜서)
84+
"""
85+
)
86+
@PutMapping("/users/me/careers/{careerId}")
87+
public ApiResponse<CareerResponseDTO.UpdateResponseDTO> updateCareer(
88+
@PathVariable Long careerId,
89+
@RequestBody CareerRequestDTO.UpdateRequestDTO request) {
90+
return ApiResponse.onSuccess(careerCommandService.updateCareer(careerId, request));
91+
}
92+
6493
}
6594

src/main/java/hansung/hansung_connect/domain/career/converter/CareerConverter.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import hansung.hansung_connect.domain.career.dto.CareerRequestDTO;
44
import hansung.hansung_connect.domain.career.dto.CareerRequestDTO.CreateRequestDTO;
5+
import hansung.hansung_connect.domain.career.dto.CareerRequestDTO.UpdateRequestDTO;
56
import hansung.hansung_connect.domain.career.dto.CareerResponseDTO;
67
import hansung.hansung_connect.domain.career.dto.CareerResponseDTO.CreateResponseDTO;
8+
import hansung.hansung_connect.domain.career.dto.CareerResponseDTO.UpdateResponseDTO;
79
import hansung.hansung_connect.domain.career.entity.Career;
810
import hansung.hansung_connect.domain.user.entity.User;
911
import java.time.YearMonth;
@@ -39,6 +41,30 @@ public CreateResponseDTO toResponseDTO(Career career) {
3941
.build();
4042
}
4143

44+
public void applyUpdate(Career career, UpdateRequestDTO dto) {
45+
// 엔티티에 update(...) 메서드가 없으면 아래 주석 참고해서 추가하세요.
46+
career.update(
47+
dto.getCompanyName(),
48+
dto.getPosition(),
49+
dto.getJobType(),
50+
dto.isEmployed(),
51+
parseYm(dto.getStartYm()),
52+
parseYmNullable(dto.getEndYm())
53+
);
54+
}
55+
56+
public UpdateResponseDTO toUpdateResponseDTO(Career career) {
57+
return UpdateResponseDTO.builder()
58+
.id(career.getId())
59+
.companyName(career.getCompanyName())
60+
.position(career.getPosition())
61+
.jobType(career.getJobType())
62+
.employed(career.isEmployed())
63+
.startYm(formatYm(career.getStartYm()))
64+
.endYm(formatYmNullable(career.getEndYm()))
65+
.build();
66+
}
67+
4268
// ===== 배치 변환 =====
4369
public List<Career> toEntities(List<CareerRequestDTO.CreateRequestDTO> items, User user) {
4470
return items.stream()

src/main/java/hansung/hansung_connect/domain/career/dto/CareerRequestDTO.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ public static class BatchCreateRequestDTO {
4343
private List<CreateRequestDTO> items;
4444
}
4545

46+
@Getter
47+
@NoArgsConstructor
48+
@Schema(name = "CareerUpdateRequest", title = "커리어 수정 요청(전체 대체)")
49+
public static class UpdateRequestDTO {
50+
51+
@Schema(description = "회사명", example = "한성대학교", required = true)
52+
private String companyName;
53+
54+
@Schema(description = "직무명", example = "백엔드 개발자", required = true)
55+
private String position;
56+
57+
@Schema(description = "재직 형태", example = "PERMANENT", required = true)
58+
private JobType jobType;
59+
60+
@Schema(description = "근무 시작 연월 (yyyy-MM 또는 yyyy.MM)", example = "2024-04", required = true)
61+
private String startYm;
62+
63+
@Schema(description = "근무 종료 연월 (재직중이면 null)", example = "2024-08")
64+
private String endYm;
65+
66+
@Schema(description = "재직 여부", example = "true", required = true)
67+
private boolean employed;
68+
}
69+
4670
}
4771

4872

src/main/java/hansung/hansung_connect/domain/career/dto/CareerResponseDTO.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,34 @@ public static class BulkCreateResponseDTO {
5252
@Schema(description = "생성된 개수", example = "3")
5353
private int createdCount;
5454
}
55+
56+
@Getter
57+
@Builder
58+
@NoArgsConstructor
59+
@AllArgsConstructor
60+
@Schema(name = "CareerUpdateResponse", title = "커리어 수정 응답")
61+
public static class UpdateResponseDTO {
62+
63+
@Schema(description = "커리어 ID", example = "7")
64+
private Long id;
65+
66+
@Schema(description = "회사명", example = "한성대학교")
67+
private String companyName;
68+
69+
@Schema(description = "직무명", example = "백엔드 개발자")
70+
private String position;
71+
72+
@Schema(description = "재직 형태", example = "PERMANENT")
73+
private JobType jobType;
74+
75+
@Schema(description = "재직 여부", example = "true")
76+
private boolean employed;
77+
78+
@Schema(description = "근무 시작 연월", example = "2024-04")
79+
private String startYm;
80+
81+
@Schema(description = "근무 종료 연월 (재직중이면 null)", example = "2024-08")
82+
private String endYm;
83+
}
5584
}
5685

src/main/java/hansung/hansung_connect/domain/career/entity/Career.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,18 @@ public class Career extends BaseEntity {
5656
@ManyToOne
5757
@JoinColumn(name = "user_id")
5858
private User user;
59+
60+
public void update(String companyName,
61+
String position,
62+
JobType jobType,
63+
boolean employed,
64+
YearMonth startYm,
65+
YearMonth endYm) {
66+
this.companyName = companyName;
67+
this.position = position;
68+
this.jobType = jobType;
69+
this.isEmployed = employed;
70+
this.startYm = startYm;
71+
this.endYm = endYm;
72+
}
5973
}

src/main/java/hansung/hansung_connect/domain/career/service/CareerCommandService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ public interface CareerCommandService {
88
CareerResponseDTO.CreateResponseDTO createCareer(CareerRequestDTO.CreateRequestDTO requestDTO);
99

1010
CareerResponseDTO.BulkCreateResponseDTO createCareers(BatchCreateRequestDTO requestDTO);
11+
12+
CareerResponseDTO.UpdateResponseDTO updateCareer(Long careerId, CareerRequestDTO.UpdateRequestDTO requestDTO);
13+
1114
}
1215

src/main/java/hansung/hansung_connect/domain/career/service/CareerCommandServiceImpl.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_END_YM_BEFORE_START;
66
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_END_YM_FORBIDDEN_WHEN_EMPLOYED;
77
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_END_YM_REQUIRED_WHEN_NOT_EMPLOYED;
8+
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_FORBIDDEN;
89
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_INVALID_YEARMONTH_FORMAT;
910
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_JOBTYPE_REQUIRED;
11+
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_NOT_FOUND;
1012
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_POSITION_REQUIRED;
1113
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.CAREER_START_YM_REQUIRED;
1214
import static hansung.hansung_connect.common.exception.code.status.ErrorStatus.USER_NOT_FOUND;
@@ -66,6 +68,61 @@ public CareerResponseDTO.BulkCreateResponseDTO createCareers(BatchCreateRequestD
6668
return careerConverter.toBulkCreateResponseDTO(saved);
6769
}
6870

71+
// 커리어 수정(전체 대체)
72+
@Override
73+
public CareerResponseDTO.UpdateResponseDTO updateCareer(Long careerId,
74+
CareerRequestDTO.UpdateRequestDTO requestDTO) {
75+
validateBusiness(requestDTO);
76+
77+
// TODO: SecurityContext로 대체
78+
Long currentUserId = 1L;
79+
User user = userRepository.findById(currentUserId)
80+
.orElseThrow(() -> new GeneralException(USER_NOT_FOUND));
81+
82+
// 커리어 조회, 소유자 검증
83+
Career career = careerRepository.findById(careerId)
84+
.orElseThrow(() -> new GeneralException(CAREER_NOT_FOUND));
85+
86+
if (!career.getUser().getId().equals(user.getId())) {
87+
throw new GeneralException(CAREER_FORBIDDEN); // 본인 소유 아님
88+
}
89+
90+
careerConverter.applyUpdate(career, requestDTO);
91+
92+
return careerConverter.toUpdateResponseDTO(career);
93+
}
94+
95+
// ====== 비즈니스 검증 (Update 전용) ======
96+
private void validateBusiness(CareerRequestDTO.UpdateRequestDTO dto) {
97+
if (dto.getCompanyName() == null || dto.getCompanyName().isBlank()) {
98+
throw new GeneralException(CAREER_COMPANY_REQUIRED);
99+
}
100+
if (dto.getPosition() == null || dto.getPosition().isBlank()) {
101+
throw new GeneralException(CAREER_POSITION_REQUIRED);
102+
}
103+
if (dto.getJobType() == null) {
104+
throw new GeneralException(CAREER_JOBTYPE_REQUIRED);
105+
}
106+
if (dto.getStartYm() == null || dto.getStartYm().isBlank()) {
107+
throw new GeneralException(CAREER_START_YM_REQUIRED);
108+
}
109+
110+
YearMonth start = parseYm(dto.getStartYm());
111+
YearMonth end = dto.isEmployed() ? null : parseYmNullable(dto.getEndYm());
112+
113+
if (dto.isEmployed() && dto.getEndYm() != null) {
114+
throw new GeneralException(CAREER_END_YM_FORBIDDEN_WHEN_EMPLOYED);
115+
}
116+
if (!dto.isEmployed()) {
117+
if (end == null) {
118+
throw new GeneralException(CAREER_END_YM_REQUIRED_WHEN_NOT_EMPLOYED);
119+
}
120+
if (end.isBefore(start)) {
121+
throw new GeneralException(CAREER_END_YM_BEFORE_START);
122+
}
123+
}
124+
}
125+
69126
// ===== 도메인 규칙 검증 (단건/배치 공용) =====
70127
private void validateBusiness(CareerRequestDTO.CreateRequestDTO dto) {
71128
if (dto.getCompanyName() == null || dto.getCompanyName().isBlank()) {

0 commit comments

Comments
 (0)