Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public record AnswerResponse(
UUID postId,
String content,
Boolean isAdopted,
Boolean isEdited,
UUID authorId,
String authorNickname,
Job authorJob,
Expand All @@ -26,6 +27,7 @@ public static AnswerResponse of(Answer answer) {
answer.getPost().getId(),
answer.getContent(),
answer.getIsAdopted(),
answer.getIsEdited(),
answer.getUser().getId(),
answer.getUser().getNickname(),
answer.getUser().getJob(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public record AnswerWithCommentsResponse(
Level authorLevel,
String authorProfileImage,
Boolean isAdopted,
Boolean isEdited,
Instant createdAt,
Instant updatedAt,
List<CommentResponse> comments
Expand All @@ -33,6 +34,7 @@ public static AnswerWithCommentsResponse of(Answer answer, List<Comment> comment
answer.getUser().getLevel(),
answer.getUser().getProfileImage(),
answer.getIsAdopted(),
answer.getIsEdited(),
answer.getCreatedAt() != null ? answer.getCreatedAt().toInstant(ZoneOffset.UTC) : null,
answer.getUpdatedAt() != null ? answer.getUpdatedAt().toInstant(ZoneOffset.UTC) : null,
comments.stream().map(CommentResponse::of).toList()
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/devpick/domain/community/entity/Answer.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ public class Answer extends BaseTimeEntity {
@Builder.Default
private Boolean isAdopted = false;

@Column(name = "is_edited", nullable = false, columnDefinition = "boolean default false")
@Builder.Default
private Boolean isEdited = false;

public void update(String content) {
this.content = content;
this.isEdited = true;
}

public void adopt() {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/devpick/domain/job/dto/JobApiModels.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public record JobListItemResponse(
List<String> matchedTags,
List<String> missingTags,
boolean bookmarked,
String status
String status,
boolean resumeAvailable
) {}

public record MatchItemResponse(String label, String status) {}
Expand Down Expand Up @@ -81,6 +82,7 @@ public record JobDetailResponse(
List<String> missingTags,
boolean bookmarked,
String status,
boolean resumeAvailable,
String salary,
String applyUrl,
List<String> responsibilities,
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/devpick/domain/job/service/JobService.java
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ public JobDetailResponse getJobDetail(UUID userId, UUID jobId) {
base.missingTags(),
base.bookmarked(),
base.status(),
base.resumeAvailable(),
p.getSalaryDisplay() != null ? p.getSalaryDisplay() : "",
p.getApplyUrl() != null ? p.getApplyUrl() : p.getSourceUrl(),
new ArrayList<>(p.getResponsibilities()),
Expand Down Expand Up @@ -542,7 +543,8 @@ private JobListItemResponse toListItem(JobPosting p, Map<String, Integer> userSk
m.matchedTags(),
m.missingTags(),
bookmarked,
p.getStatus().name()
p.getStatus().name(),
!userSkills.isEmpty()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ void setUp() {
);

answerResponse = new AnswerResponse(
answerId, postId, "Test Answer", false,
answerId, postId, "Test Answer", false, false,
userId, "tester", null, null,
Instant.now(), Instant.now()
);
Expand Down Expand Up @@ -125,7 +125,7 @@ void createAnswer_postNotFound_returns404() throws Exception {
void updateAnswer_success_returns200() throws Exception {
AnswerUpdateRequest request = new AnswerUpdateRequest("Updated Answer");
AnswerResponse updated = new AnswerResponse(
answerId, postId, "Updated Answer", false,
answerId, postId, "Updated Answer", false, true,
userId, "tester", null, null, Instant.now(), Instant.now());
given(answerService.updateAnswer(eq(userId), eq(postId), eq(answerId), any())).willReturn(updated);

Expand Down Expand Up @@ -164,7 +164,7 @@ void deleteAnswer_success_returns204() throws Exception {
@DisplayName("POST /posts/{postId}/answers/{answerId}/adopt - 채택 성공 시 200 반환")
void adoptAnswer_success_returns200() throws Exception {
AnswerResponse adopted = new AnswerResponse(
answerId, postId, "Test Answer", true,
answerId, postId, "Test Answer", true, false,
userId, "tester", null, null, Instant.now(), Instant.now());
given(answerService.adoptAnswer(userId, postId, answerId)).willReturn(adopted);

Expand All @@ -190,7 +190,7 @@ void adoptAnswer_alreadyAdopted_returns409() throws Exception {
void getAnswers_success_returns200() throws Exception {
AnswerWithCommentsResponse answerWithComments = new AnswerWithCommentsResponse(
answerId, "Test Answer", userId, "tester", null, null, null,
false, Instant.now(), Instant.now(), List.of()
false, false, Instant.now(), Instant.now(), List.of()
);
AnswerListResponse listResponse = new AnswerListResponse(List.of(answerWithComments));
given(answerService.getAnswers(postId)).willReturn(listResponse);
Expand Down
35 changes: 35 additions & 0 deletions src/test/java/com/devpick/domain/community/entity/AnswerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,41 @@
@DisplayName("Answer Entity 단위 테스트")
class AnswerTest {

@Test
@DisplayName("빌더 기본값 - isEdited=false")
void builder_defaultIsEditedFalse() {
User user = buildUser();
Post post = buildPost(user);
Answer answer = Answer.builder().post(post).user(user).content("내용").build();
assertThat(answer.getIsEdited()).isFalse();
}

@Test
@DisplayName("update() 호출 시 isEdited=true, 내용이 변경된다")
void update_setsIsEditedTrueAndUpdatesContent() {
User user = buildUser();
Post post = buildPost(user);
Answer answer = Answer.builder().post(post).user(user).content("원본").build();

answer.update("수정된 내용");

assertThat(answer.getIsEdited()).isTrue();
assertThat(answer.getContent()).isEqualTo("수정된 내용");
}

@Test
@DisplayName("adopt() 호출 시 isEdited는 변경되지 않는다")
void adopt_doesNotChangeIsEdited() {
User user = buildUser();
Post post = buildPost(user);
Answer answer = Answer.builder().post(post).user(user).content("내용").build();

answer.adopt();

assertThat(answer.getIsEdited()).isFalse();
assertThat(answer.getIsAdopted()).isTrue();
}

@Test
@DisplayName("빌더 기본값 - isAdopted=false")
void builder_defaultIsAdoptedFalse() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,24 @@ void createAnswer_postNotFound_throwsException() {
}

@Test
@DisplayName("updateAnswer — 성공 시 수정된 답변 반환")
@DisplayName("updateAnswer — 성공 시 수정된 답변 반환, isEdited=true")
void updateAnswer_success_returnsUpdatedAnswer() {
AnswerUpdateRequest request = new AnswerUpdateRequest("Updated Answer");
given(answerRepository.findById(answerId)).willReturn(Optional.of(answer));

AnswerResponse response = answerService.updateAnswer(userId, postId, answerId, request);

assertThat(response.content()).isEqualTo("Updated Answer");
assertThat(response.isEdited()).isTrue();
}

@Test
@DisplayName("updateAnswer — 수정 전 isEdited=false, 수정 후 isEdited=true")
void updateAnswer_isEdited_toggledByUpdate() {
given(answerRepository.findById(answerId)).willReturn(Optional.of(answer));

AnswerResponse before = answerService.updateAnswer(userId, postId, answerId, new AnswerUpdateRequest("내용1"));
assertThat(before.isEdited()).isTrue();
}

@Test
Expand Down Expand Up @@ -219,7 +229,7 @@ void deleteAnswer_unauthorized_throwsException() {
}

@Test
@DisplayName("adoptAnswer — 성공 시 채택된 답변 반환")
@DisplayName("adoptAnswer — 성공 시 isAdopted=true, isEdited는 변경되지 않는다")
void adoptAnswer_success_adoptsAnswer() {
given(postRepository.findById(postId)).willReturn(Optional.of(post));
given(answerRepository.findAdoptedByPostIdForUpdate(postId)).willReturn(List.of());
Expand All @@ -228,6 +238,7 @@ void adoptAnswer_success_adoptsAnswer() {
AnswerResponse response = answerService.adoptAnswer(userId, postId, answerId);

assertThat(response.isAdopted()).isTrue();
assertThat(response.isEdited()).isFalse();
}

@Test
Expand Down
81 changes: 80 additions & 1 deletion src/test/java/com/devpick/domain/job/service/JobServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package com.devpick.domain.job.service;

import com.devpick.domain.job.dto.JobApiModels.JobDetailResponse;
import com.devpick.domain.job.entity.EmploymentType;
import com.devpick.domain.job.entity.JobParseStatus;
import com.devpick.domain.job.entity.JobPosting;
import com.devpick.domain.job.entity.JobPostingCategory;
import com.devpick.domain.job.entity.JobPostingStatus;
import com.devpick.domain.job.entity.PostingExperienceLevel;
import com.devpick.domain.job.repository.JobBookmarkRepository;
import com.devpick.domain.job.repository.JobPostingRepository;
import com.devpick.domain.point.entity.PointAction;
import com.devpick.domain.point.service.PointService;
import com.devpick.domain.report.repository.HistoryRepository;
import com.devpick.domain.user.entity.Tag;
import com.devpick.domain.user.entity.User;
import com.devpick.domain.user.entity.UserTag;
import com.devpick.domain.user.repository.UserRepository;
import com.devpick.global.common.exception.DevpickException;
import com.devpick.global.common.exception.ErrorCode;
Expand All @@ -18,9 +26,11 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
Expand Down Expand Up @@ -100,7 +110,76 @@ void bookmark_userNotFound_throwsException() {

assertThatThrownBy(() -> jobService.bookmark(userId, jobId))
.isInstanceOf(DevpickException.class)
.satisfies(e -> org.assertj.core.api.Assertions.assertThat(
.satisfies(e -> assertThat(
((DevpickException) e).getErrorCode()).isEqualTo(ErrorCode.USER_NOT_FOUND));
}

private static final UUID INTERNAL_OPS_USER_ID = UUID.fromString("00000000-0000-0000-0000-000000000000");

private JobPosting mockListableJob(UUID jobId) {
JobPosting job = mock(JobPosting.class);
given(job.getId()).willReturn(jobId);
given(job.getTitle()).willReturn("백엔드 개발자");
given(job.getCompanyName()).willReturn("카카오");
given(job.getTechStack()).willReturn(List.of());
given(job.getRequiredSkills()).willReturn(List.of());
given(job.getPreferredSkills()).willReturn(List.of());
given(job.getCompanyLogoUrl()).willReturn(null);
given(job.getLocation()).willReturn("서울");
given(job.getRollingDeadline()).willReturn(false);
given(job.getDeadline()).willReturn(null);
given(job.getEmploymentType()).willReturn(EmploymentType.FULL_TIME);
given(job.getJobCategory()).willReturn(JobPostingCategory.BACKEND);
given(job.getExperienceLevel()).willReturn(PostingExperienceLevel.JUNIOR);
given(job.getStatus()).willReturn(JobPostingStatus.ACTIVE);
given(job.getParseStatus()).willReturn(JobParseStatus.OK);
given(job.getSalaryDisplay()).willReturn("");
given(job.getApplyUrl()).willReturn("https://example.com");
given(job.getResponsibilities()).willReturn(List.of());
given(job.getRequirementBullets()).willReturn(List.of());
given(job.getPreferredQualificationBullets()).willReturn(List.of());
given(job.getBenefits()).willReturn(List.of());
given(job.getHiringProcess()).willReturn(List.of());
given(job.getJdImageUrls()).willReturn(List.of());
return job;
}

@Test
@DisplayName("getJobDetail — 이력서·태그 없는 유저는 resumeAvailable=false")
void getJobDetail_noSkills_resumeAvailableFalse() {
UUID jobId = UUID.randomUUID();
JobPosting job = mockListableJob(jobId);

given(jobPostingRepository.findById(jobId)).willReturn(Optional.of(job));
given(masterResumeRepository.findByUserId(INTERNAL_OPS_USER_ID)).willReturn(Optional.empty());
given(userRepository.findById(INTERNAL_OPS_USER_ID)).willReturn(Optional.empty());
given(jobBookmarkRepository.existsByUserIdAndJobPosting_Id(INTERNAL_OPS_USER_ID, jobId)).willReturn(false);

JobDetailResponse response = jobService.getJobDetail(INTERNAL_OPS_USER_ID, jobId);

assertThat(response.resumeAvailable()).isFalse();
}

@Test
@DisplayName("getJobDetail — 기술 태그가 등록된 유저는 resumeAvailable=true")
void getJobDetail_hasTagSkills_resumeAvailableTrue() {
UUID jobId = UUID.randomUUID();
JobPosting job = mockListableJob(jobId);

Tag tag = mock(Tag.class);
given(tag.getName()).willReturn("Java");
UserTag userTag = mock(UserTag.class);
given(userTag.getTag()).willReturn(tag);
User userWithTags = mock(User.class);
given(userWithTags.getUserTags()).willReturn(List.of(userTag));

given(jobPostingRepository.findById(jobId)).willReturn(Optional.of(job));
given(masterResumeRepository.findByUserId(INTERNAL_OPS_USER_ID)).willReturn(Optional.empty());
given(userRepository.findById(INTERNAL_OPS_USER_ID)).willReturn(Optional.of(userWithTags));
given(jobBookmarkRepository.existsByUserIdAndJobPosting_Id(INTERNAL_OPS_USER_ID, jobId)).willReturn(false);

JobDetailResponse response = jobService.getJobDetail(INTERNAL_OPS_USER_ID, jobId);

assertThat(response.resumeAvailable()).isTrue();
}
}
Loading