Skip to content

Commit bd37505

Browse files
authored
Merge pull request #40 from JobDri-Developer/feat/applicationdraft
[Feat] 임시저장 API 구현 (#23)
2 parents 40678ab + e9cc19b commit bd37505

9 files changed

Lines changed: 476 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.controller;
2+
3+
import com.jobdri.jobdri_api.domain.applicationdraft.dto.request.ApplicationDraftUpsertRequest;
4+
import com.jobdri.jobdri_api.domain.applicationdraft.dto.response.ApplicationDraftResponse;
5+
import com.jobdri.jobdri_api.domain.applicationdraft.dto.response.ApplicationDraftSaveResponse;
6+
import com.jobdri.jobdri_api.domain.applicationdraft.service.ApplicationDraftService;
7+
import com.jobdri.jobdri_api.domain.user.entity.User;
8+
import com.jobdri.jobdri_api.global.apiPayload.ApiResponse;
9+
import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode;
10+
import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException;
11+
import com.jobdri.jobdri_api.global.security.UserDetailsImpl;
12+
import io.swagger.v3.oas.annotations.Operation;
13+
import io.swagger.v3.oas.annotations.tags.Tag;
14+
import jakarta.validation.Valid;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
17+
import org.springframework.web.bind.annotation.DeleteMapping;
18+
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.PutMapping;
20+
import org.springframework.web.bind.annotation.RequestBody;
21+
import org.springframework.web.bind.annotation.RequestMapping;
22+
import org.springframework.web.bind.annotation.RestController;
23+
24+
@RestController
25+
@RequiredArgsConstructor
26+
@RequestMapping("/application-drafts")
27+
@Tag(name = "Application Draft", description = "지원 플로우 임시저장 API")
28+
public class ApplicationDraftController {
29+
30+
private final ApplicationDraftService applicationDraftService;
31+
32+
@Operation(
33+
summary = "내 임시저장 생성/수정",
34+
description = "로그인한 사용자의 지원 플로우 임시저장을 생성하거나 수정합니다. 사용자당 최근 임시저장 1개만 유지합니다."
35+
)
36+
@PutMapping("/me")
37+
public ApiResponse<ApplicationDraftSaveResponse> saveOrUpdateMyDraft(
38+
@AuthenticationPrincipal UserDetailsImpl userDetails,
39+
@Valid @RequestBody ApplicationDraftUpsertRequest request
40+
) {
41+
return ApiResponse.onSuccess(
42+
"임시저장되었습니다.",
43+
applicationDraftService.saveOrUpdate(getCurrentUser(userDetails), request)
44+
);
45+
}
46+
47+
@Operation(
48+
summary = "내 임시저장 조회",
49+
description = "로그인한 사용자의 최근 임시저장 1건을 조회합니다. 임시저장이 없으면 result를 null로 반환합니다."
50+
)
51+
@GetMapping("/me")
52+
public ApiResponse<ApplicationDraftResponse> getMyDraft(
53+
@AuthenticationPrincipal UserDetailsImpl userDetails
54+
) {
55+
return ApiResponse.onSuccess(
56+
null,
57+
applicationDraftService.getMyDraft(getCurrentUser(userDetails))
58+
);
59+
}
60+
61+
@Operation(
62+
summary = "내 임시저장 삭제",
63+
description = "로그인한 사용자의 임시저장을 삭제합니다. 임시저장이 없어도 성공 처리합니다."
64+
)
65+
@DeleteMapping("/me")
66+
public ApiResponse<Void> deleteMyDraft(
67+
@AuthenticationPrincipal UserDetailsImpl userDetails
68+
) {
69+
applicationDraftService.deleteMyDraft(getCurrentUser(userDetails));
70+
return ApiResponse.onSuccess("임시저장이 삭제되었습니다.");
71+
}
72+
73+
private User getCurrentUser(UserDetailsImpl userDetails) {
74+
if (userDetails == null) {
75+
throw new GeneralException(GeneralErrorCode.MISSING_AUTH_INFO);
76+
}
77+
return userDetails.getUser();
78+
}
79+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.dto.request;
2+
3+
import com.jobdri.jobdri_api.domain.applicationdraft.entity.ApplicationDraftStep;
4+
import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType;
5+
import jakarta.validation.constraints.NotNull;
6+
7+
import java.util.List;
8+
9+
public record ApplicationDraftUpsertRequest(
10+
@NotNull(message = "작성 단계는 필수입니다.")
11+
ApplicationDraftStep step,
12+
13+
@NotNull(message = "지원 유형은 필수입니다.")
14+
ApplyType type,
15+
16+
Long postingId,
17+
Long middleCategoryId,
18+
Long smallCategoryId,
19+
List<Long> selectedQuestionIds
20+
) {
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.dto.response;
2+
3+
import com.jobdri.jobdri_api.domain.applicationdraft.entity.ApplicationDraftStep;
4+
import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType;
5+
6+
import java.time.LocalDateTime;
7+
import java.util.List;
8+
9+
public record ApplicationDraftResponse(
10+
Long draftId,
11+
ApplicationDraftStep step,
12+
ApplyType type,
13+
Long postingId,
14+
Long middleCategoryId,
15+
Long smallCategoryId,
16+
List<Long> selectedQuestionIds,
17+
LocalDateTime savedAt
18+
) {
19+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.dto.response;
2+
3+
import com.jobdri.jobdri_api.domain.applicationdraft.entity.ApplicationDraftStep;
4+
5+
import java.time.LocalDateTime;
6+
7+
public record ApplicationDraftSaveResponse(
8+
Long draftId,
9+
ApplicationDraftStep step,
10+
LocalDateTime savedAt
11+
) {
12+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.entity;
2+
3+
import com.jobdri.jobdri_api.domain.mockapply.entity.ApplyType;
4+
import com.jobdri.jobdri_api.domain.user.entity.User;
5+
import jakarta.persistence.CollectionTable;
6+
import jakarta.persistence.Column;
7+
import jakarta.persistence.ElementCollection;
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.EnumType;
10+
import jakarta.persistence.Enumerated;
11+
import jakarta.persistence.FetchType;
12+
import jakarta.persistence.GeneratedValue;
13+
import jakarta.persistence.GenerationType;
14+
import jakarta.persistence.Id;
15+
import jakarta.persistence.JoinColumn;
16+
import jakarta.persistence.OneToOne;
17+
import jakarta.persistence.PrePersist;
18+
import jakarta.persistence.PreUpdate;
19+
import jakarta.persistence.Table;
20+
import lombok.AccessLevel;
21+
import lombok.AllArgsConstructor;
22+
import lombok.Builder;
23+
import lombok.Getter;
24+
import lombok.NoArgsConstructor;
25+
26+
import java.time.LocalDateTime;
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
30+
@Entity
31+
@Getter
32+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
33+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
34+
@Builder(access = AccessLevel.PRIVATE)
35+
@Table(name = "application_drafts")
36+
public class ApplicationDraft {
37+
38+
@Id
39+
@GeneratedValue(strategy = GenerationType.IDENTITY)
40+
private Long id;
41+
42+
@OneToOne(fetch = FetchType.LAZY, optional = false)
43+
@JoinColumn(name = "user_id", nullable = false, unique = true)
44+
private User user;
45+
46+
@Enumerated(EnumType.STRING)
47+
@Column(nullable = false)
48+
private ApplicationDraftStep step;
49+
50+
@Enumerated(EnumType.STRING)
51+
@Column(nullable = false)
52+
private ApplyType type;
53+
54+
@Column(name = "posting_id")
55+
private Long postingId;
56+
57+
@Column(name = "middle_category_id")
58+
private Long middleCategoryId;
59+
60+
@Column(name = "small_category_id")
61+
private Long smallCategoryId;
62+
63+
@Builder.Default
64+
@ElementCollection
65+
@CollectionTable(
66+
name = "application_draft_selected_questions",
67+
joinColumns = @JoinColumn(name = "application_draft_id")
68+
)
69+
@Column(name = "question_id", nullable = false)
70+
private List<Long> selectedQuestionIds = new ArrayList<>();
71+
72+
@Column(nullable = false, updatable = false)
73+
private LocalDateTime createdAt;
74+
75+
@Column(nullable = false)
76+
private LocalDateTime updatedAt;
77+
78+
public static ApplicationDraft create(
79+
User user,
80+
ApplicationDraftStep step,
81+
ApplyType type,
82+
Long postingId,
83+
Long middleCategoryId,
84+
Long smallCategoryId,
85+
List<Long> selectedQuestionIds
86+
) {
87+
return ApplicationDraft.builder()
88+
.user(user)
89+
.step(step)
90+
.type(type)
91+
.postingId(postingId)
92+
.middleCategoryId(middleCategoryId)
93+
.smallCategoryId(smallCategoryId)
94+
.selectedQuestionIds(normalizeQuestionIds(selectedQuestionIds))
95+
.build();
96+
}
97+
98+
public void update(
99+
ApplicationDraftStep step,
100+
ApplyType type,
101+
Long postingId,
102+
Long middleCategoryId,
103+
Long smallCategoryId,
104+
List<Long> selectedQuestionIds
105+
) {
106+
this.step = step;
107+
this.type = type;
108+
this.postingId = postingId;
109+
this.middleCategoryId = middleCategoryId;
110+
this.smallCategoryId = smallCategoryId;
111+
this.selectedQuestionIds.clear();
112+
this.selectedQuestionIds.addAll(normalizeQuestionIds(selectedQuestionIds));
113+
this.updatedAt = LocalDateTime.now();
114+
}
115+
116+
public LocalDateTime getSavedAt() {
117+
return updatedAt;
118+
}
119+
120+
@PrePersist
121+
void onCreate() {
122+
LocalDateTime now = LocalDateTime.now();
123+
this.createdAt = now;
124+
this.updatedAt = now;
125+
}
126+
127+
@PreUpdate
128+
void onUpdate() {
129+
if (this.updatedAt == null) {
130+
this.updatedAt = LocalDateTime.now();
131+
}
132+
}
133+
134+
private static List<Long> normalizeQuestionIds(List<Long> selectedQuestionIds) {
135+
if (selectedQuestionIds == null) {
136+
return new ArrayList<>();
137+
}
138+
return new ArrayList<>(selectedQuestionIds);
139+
}
140+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.entity;
2+
3+
public enum ApplicationDraftStep {
4+
TYPE_SELECT,
5+
COMPANY_SELECT,
6+
JOB_SELECT,
7+
QUESTION_SELECT,
8+
ANSWER_WRITE
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.repository;
2+
3+
import com.jobdri.jobdri_api.domain.applicationdraft.entity.ApplicationDraft;
4+
import com.jobdri.jobdri_api.domain.user.entity.User;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
import java.util.Optional;
8+
9+
public interface ApplicationDraftRepository extends JpaRepository<ApplicationDraft, Long> {
10+
Optional<ApplicationDraft> findByUser(User user);
11+
12+
void deleteByUser(User user);
13+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.jobdri.jobdri_api.domain.applicationdraft.service;
2+
3+
import com.jobdri.jobdri_api.domain.applicationdraft.dto.request.ApplicationDraftUpsertRequest;
4+
import com.jobdri.jobdri_api.domain.applicationdraft.dto.response.ApplicationDraftResponse;
5+
import com.jobdri.jobdri_api.domain.applicationdraft.dto.response.ApplicationDraftSaveResponse;
6+
import com.jobdri.jobdri_api.domain.applicationdraft.entity.ApplicationDraft;
7+
import com.jobdri.jobdri_api.domain.applicationdraft.repository.ApplicationDraftRepository;
8+
import com.jobdri.jobdri_api.domain.user.entity.User;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.transaction.annotation.Transactional;
12+
13+
import java.util.ArrayList;
14+
15+
@Service
16+
@RequiredArgsConstructor
17+
public class ApplicationDraftService {
18+
19+
private final ApplicationDraftRepository applicationDraftRepository;
20+
21+
@Transactional
22+
public ApplicationDraftSaveResponse saveOrUpdate(User user, ApplicationDraftUpsertRequest request) {
23+
ApplicationDraft draft = applicationDraftRepository.findByUser(user)
24+
.map(existingDraft -> {
25+
existingDraft.update(
26+
request.step(),
27+
request.type(),
28+
request.postingId(),
29+
request.middleCategoryId(),
30+
request.smallCategoryId(),
31+
request.selectedQuestionIds()
32+
);
33+
return existingDraft;
34+
})
35+
.orElseGet(() -> applicationDraftRepository.save(ApplicationDraft.create(
36+
user,
37+
request.step(),
38+
request.type(),
39+
request.postingId(),
40+
request.middleCategoryId(),
41+
request.smallCategoryId(),
42+
request.selectedQuestionIds()
43+
)));
44+
45+
return new ApplicationDraftSaveResponse(
46+
draft.getId(),
47+
draft.getStep(),
48+
draft.getSavedAt()
49+
);
50+
}
51+
52+
@Transactional(readOnly = true)
53+
public ApplicationDraftResponse getMyDraft(User user) {
54+
return applicationDraftRepository.findByUser(user)
55+
.map(this::toResponse)
56+
.orElse(null);
57+
}
58+
59+
@Transactional
60+
public void deleteMyDraft(User user) {
61+
applicationDraftRepository.deleteByUser(user);
62+
}
63+
64+
private ApplicationDraftResponse toResponse(ApplicationDraft draft) {
65+
return new ApplicationDraftResponse(
66+
draft.getId(),
67+
draft.getStep(),
68+
draft.getType(),
69+
draft.getPostingId(),
70+
draft.getMiddleCategoryId(),
71+
draft.getSmallCategoryId(),
72+
new ArrayList<>(draft.getSelectedQuestionIds()),
73+
draft.getSavedAt()
74+
);
75+
}
76+
}

0 commit comments

Comments
 (0)