From c6d06e817b01bd5854c6706f0e4833342298bc49 Mon Sep 17 00:00:00 2001 From: hyeonda02 Date: Tue, 22 Jul 2025 19:48:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Feat=20:=20=ED=99=88=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EA=B8=B0=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grafana/docker-compose 2.yml | 38 ++ grafana/prometheus-config 2.yaml | 8 + .../controller/CareerDetailController.java | 14 + .../response/RecentCareerDetailResponse.java | 44 ++ .../converter/BaseCareerDetailConverter.java | 22 + .../repository/CareerDetailJpaRepository.java | 2 + .../repository/CareerDetailRepository.java | 3 + .../CareerDetailRepositoryImpl.java | 5 + .../service/BaseCareerDetailService.java | 5 + .../service/BaseCareerDetailServiceImpl.java | 22 + .../service/CareerServiceImplTest 2.java | 380 ++++++++++++++++++ 11 files changed, 543 insertions(+) create mode 100644 grafana/docker-compose 2.yml create mode 100644 grafana/prometheus-config 2.yaml create mode 100644 src/main/java/umc/kkijuk/server/detail/controller/response/RecentCareerDetailResponse.java create mode 100644 src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java diff --git a/grafana/docker-compose 2.yml b/grafana/docker-compose 2.yml new file mode 100644 index 00000000..6f796b0e --- /dev/null +++ b/grafana/docker-compose 2.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + ## monitoring + prometheus: + image: prom/prometheus:latest + container_name: kkijuk-prometheus + volumes: + - ./prometheus-config.yaml:/etc/prometheus/prometheus-config.yaml + command: + - '--config.file=/etc/prometheus/prometheus-config.yaml' + ports: + - "9090:9090" + networks: + - monitoring-network + + grafana: + image: grafana/grafana:latest + container_name: kkijuk-grafana +# set in instance +# environment: +# - GF_SECURITY_ADMIN_USER=local +# - GF_SECURITY_ADMIN_PASSWORD=local + volumes: + - ./datasources:/etc/grafana/provisioning/datasources + - ./dashboards:/etc/grafana/provisioning/dashboards + - ./dashboards:/var/lib/grafana/dashboards + ports: + - "3000:3000" + networks: + - monitoring-network + +networks: + monitoring-network: + driver: bridge + +volumes: + loki_data: \ No newline at end of file diff --git a/grafana/prometheus-config 2.yaml b/grafana/prometheus-config 2.yaml new file mode 100644 index 00000000..72b6e721 --- /dev/null +++ b/grafana/prometheus-config 2.yaml @@ -0,0 +1,8 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: prometheus + metrics_path: '/actuator/prometheus' + static_configs: + - targets: ['host.docker.internal:8088'] \ No newline at end of file diff --git a/src/main/java/umc/kkijuk/server/detail/controller/CareerDetailController.java b/src/main/java/umc/kkijuk/server/detail/controller/CareerDetailController.java index f99e60c3..157ca208 100644 --- a/src/main/java/umc/kkijuk/server/detail/controller/CareerDetailController.java +++ b/src/main/java/umc/kkijuk/server/detail/controller/CareerDetailController.java @@ -5,12 +5,15 @@ import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; +import umc.kkijuk.server.career.controller.response.FindDetailResponse; import umc.kkijuk.server.common.LoginUser; import umc.kkijuk.server.detail.controller.response.BaseCareerDetailResponse; import umc.kkijuk.server.detail.controller.response.CareerDetailResponse; +import umc.kkijuk.server.detail.controller.response.RecentCareerDetailResponse; import umc.kkijuk.server.detail.dto.CareerDetailReqDto; import umc.kkijuk.server.detail.dto.CareerDetailUpdateReqDto; import umc.kkijuk.server.detail.service.BaseCareerDetailService; @@ -78,4 +81,15 @@ public CareerDetailResponse update( } + @GetMapping("/dashboard") + @Operation(summary = "홈화면 활동 기록 조회", description = "홈 화면에서 가장 최근에 기록한 활동기록 3개를 조회합니다. ") + public CareerDetailResponse> board(@RequestHeader("Authorization") String token){ + Member requestMember = loginUser.extractMemberId(token); + return CareerDetailResponse.success( + HttpStatus.OK, + "최근에 작성한 활동 기록 3개를 성공적으로 조회했습니다.", + careerDetailService.boardDetail(requestMember) + ); + } + } diff --git a/src/main/java/umc/kkijuk/server/detail/controller/response/RecentCareerDetailResponse.java b/src/main/java/umc/kkijuk/server/detail/controller/response/RecentCareerDetailResponse.java new file mode 100644 index 00000000..eeac3772 --- /dev/null +++ b/src/main/java/umc/kkijuk/server/detail/controller/response/RecentCareerDetailResponse.java @@ -0,0 +1,44 @@ +package umc.kkijuk.server.detail.controller.response; + +import java.time.LocalDate; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import umc.kkijuk.server.career.controller.response.CategoryResponse; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@Getter +@Setter +@Builder +@AllArgsConstructor +@Schema(description = "최근 활동 기록 응답 DTO") +public class RecentCareerDetailResponse { + + @Schema(description = "활동 기록 ID", example = "123") + private Long detailId; + @Schema(description = "활동 기록 제목", example = "끼적 백엔드") + private String detailTitle; + @Schema(description = "활동 기록 내용", example = "끼적 api 개발~..") + private String detailContent; + @Schema(description = "활동 기록 시작일", example = "2025-05-01") + private LocalDate detailStartDate; + @Schema(description = "활동 기록 종료일", example = "2025-06-30") + private LocalDate detailEndDate; + @Schema(description = "활동 기록에 연결된 태그 목록") + private List tags; + + @Schema(description = "활동 ID", example = "77") + private Long careerId; + @Schema(description = "활동 제목", example = "대외활동 - 백엔드") + private String careerTitle; + @Schema(description = "활동 별칭", example = "스프링부트") + private String careerAlias; + @Schema(description = "활동 카테고리 정보 (id, 설명, name)") + private CategoryResponse category; +} + diff --git a/src/main/java/umc/kkijuk/server/detail/dto/converter/BaseCareerDetailConverter.java b/src/main/java/umc/kkijuk/server/detail/dto/converter/BaseCareerDetailConverter.java index e7e8daf5..ad37642e 100644 --- a/src/main/java/umc/kkijuk/server/detail/dto/converter/BaseCareerDetailConverter.java +++ b/src/main/java/umc/kkijuk/server/detail/dto/converter/BaseCareerDetailConverter.java @@ -1,5 +1,10 @@ package umc.kkijuk.server.detail.dto.converter; +import java.util.stream.Collectors; +import umc.kkijuk.server.career.controller.response.CategoryResponse; +import umc.kkijuk.server.career.domain.BaseCareer; +import umc.kkijuk.server.detail.controller.response.RecentCareerDetailResponse; +import umc.kkijuk.server.detail.controller.response.TagResponse; import umc.kkijuk.server.detail.domain.BaseCareerDetail; import umc.kkijuk.server.detail.domain.CareerType; import umc.kkijuk.server.detail.dto.CareerDetailReqDto; @@ -24,5 +29,22 @@ public static BaseCareerDetail toBaseCareerDetail(Member requestMember, CareerDe } + public static RecentCareerDetailResponse toResponse(BaseCareerDetail detail, BaseCareer baseCareer, CareerType type) { + return new RecentCareerDetailResponse( + detail.getId(), + detail.getTitle(), + detail.getContent(), + detail.getStartDate(), + detail.getEndDate(), + detail.getCareerTagList().stream() + .map(tag -> new TagResponse(tag.getTag())) + .collect(Collectors.toList()), + baseCareer.getId(), + baseCareer.getName(), + baseCareer.getAlias(), + new CategoryResponse(type.getId(), type.getDescription(), type.name()) + ); + } + } diff --git a/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailJpaRepository.java b/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailJpaRepository.java index d458fca6..080709ad 100644 --- a/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailJpaRepository.java +++ b/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailJpaRepository.java @@ -65,4 +65,6 @@ List findByCareerIdAndCareerType(@Param("careerType") CareerTy "JOIN FETCH ct.tag t " + "WHERE EXISTS (SELECT 1 FROM CareerDetailTag ct2 WHERE ct2.baseCareerDetail=bcd AND ct2.tag.id=:tagId)" ) List findByTag(@Param("tagId") Long tagId); + + List findTop3ByMemberIdOrderByCreatedAtDesc(Long id); } diff --git a/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepository.java b/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepository.java index dcce867f..966599c4 100644 --- a/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepository.java +++ b/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepository.java @@ -18,4 +18,7 @@ public interface CareerDetailRepository { List findByMemberIdAndKeyword(Long id, String keyword); List findByTag(Long id); + + + List findTop3ByMemberIdOrderByCreatedAtDesc(Long id); } diff --git a/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepositoryImpl.java b/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepositoryImpl.java index 37041fe4..4bed713b 100644 --- a/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepositoryImpl.java +++ b/src/main/java/umc/kkijuk/server/detail/repository/CareerDetailRepositoryImpl.java @@ -41,4 +41,9 @@ public List findByMemberIdAndKeyword(Long id, String keyword) public List findByTag(Long id) { return careerDetailJpaRepository.findByTag(id); } + + @Override + public List findTop3ByMemberIdOrderByCreatedAtDesc(Long id) { + return careerDetailJpaRepository.findTop3ByMemberIdOrderByCreatedAtDesc(id); + } } diff --git a/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailService.java b/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailService.java index 30ef2c6d..4d578fa0 100644 --- a/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailService.java +++ b/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailService.java @@ -1,6 +1,9 @@ package umc.kkijuk.server.detail.service; +import java.util.List; +import umc.kkijuk.server.career.controller.response.FindDetailResponse; import umc.kkijuk.server.detail.controller.response.BaseCareerDetailResponse; +import umc.kkijuk.server.detail.controller.response.RecentCareerDetailResponse; import umc.kkijuk.server.detail.dto.CareerDetailReqDto; import umc.kkijuk.server.detail.dto.CareerDetailUpdateReqDto; import umc.kkijuk.server.member.domain.Member; @@ -11,4 +14,6 @@ public interface BaseCareerDetailService { void deleteDetail(Member requestMember, Long careerId, Long detailId); BaseCareerDetailResponse updateDetail(Member requestMember, CareerDetailUpdateReqDto request, Long careerId, Long detailId); + + List boardDetail(Member requestMember); } \ No newline at end of file diff --git a/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailServiceImpl.java b/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailServiceImpl.java index df489245..285dca32 100644 --- a/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailServiceImpl.java +++ b/src/main/java/umc/kkijuk/server/detail/service/BaseCareerDetailServiceImpl.java @@ -1,14 +1,20 @@ package umc.kkijuk.server.detail.service; +import java.time.LocalDate; +import java.util.Objects; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import umc.kkijuk.server.career.controller.response.CategoryResponse; +import umc.kkijuk.server.career.controller.response.FindDetailResponse; import umc.kkijuk.server.career.domain.*; import umc.kkijuk.server.career.repository.*; import umc.kkijuk.server.common.domian.exception.*; import umc.kkijuk.server.detail.controller.response.BaseCareerDetailResponse; +import umc.kkijuk.server.detail.controller.response.RecentCareerDetailResponse; +import umc.kkijuk.server.detail.controller.response.TagResponse; import umc.kkijuk.server.detail.domain.*; import umc.kkijuk.server.detail.domain.mapping.CareerDetailTag; import umc.kkijuk.server.detail.dto.CareerDetailReqDto; @@ -92,6 +98,22 @@ public BaseCareerDetailResponse updateDetail(Member requestMember, CareerDetailU return new BaseCareerDetailResponse(careerDetailRepository.save(baseCareerDetail)); } + @Override + public List boardDetail(Member requestMember) { + List details = careerDetailRepository.findTop3ByMemberIdOrderByCreatedAtDesc(requestMember.getId()); + + return details.stream() + .map(detail -> { + CareerType type = detail.getCareerType(); + Long careerId = detail.getCareerId(); + BaseCareer baseCareer = findBaseCareerByType(type, careerId); + if (baseCareer == null) return null; + return BaseCareerDetailConverter.toResponse(detail, baseCareer, type); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + private void validateOwner(BaseCareer career, Member requestMember) { if (!career.getMemberId().equals(requestMember.getId())) { throw new OwnerMismatchException(); diff --git a/src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java b/src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java new file mode 100644 index 00000000..da7ddd1b --- /dev/null +++ b/src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java @@ -0,0 +1,380 @@ +package umc.kkijuk.server.unitTest.career.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import umc.kkijuk.server.career.controller.response.*; +import umc.kkijuk.server.career.domain.*; +import umc.kkijuk.server.career.dto.*; +import umc.kkijuk.server.career.repository.*; +import umc.kkijuk.server.career.service.CareerServiceImpl; +import umc.kkijuk.server.common.service.RecordUpdateManager; +import umc.kkijuk.server.member.domain.Member; +import umc.kkijuk.server.record.domain.Record; +import umc.kkijuk.server.record.repository.RecordRepository; + +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + + +@ExtendWith(MockitoExtension.class) +class CareerServiceImplTest { + + @InjectMocks + private CareerServiceImpl careerService; + + @Mock + private ActivityRepository activityRepository; + + @Mock + private CircleRepository circleRepository; + + @Mock + private CompetitionRepository competitionRepository; + + @Mock + private EduCareerRepository eduCareerRepository; + + @Mock + private EmploymentRepository employmentRepository; + + @Mock + private ProjectRepository projectRepository; + + @Mock + private CareerEtcRepository etcRepository; + + @Mock + private RecordRepository recordRepository; + + @Mock + private RecordUpdateManager recordUpdateManager; + + @Test + @DisplayName("[CreateCareer] Activity를 생성하면 활동이 저장되고 Record의 타임스탬프가 갱신되어야 한다.") + void createActivity_shouldSaveActivityAndUpdateRecord() { + Member member = Member.builder().id(1L).build(); + ActivityReqDto reqDto = ActivityReqDto.builder() + .name("동아리 활동").unknown(false) + .enddate(LocalDate.of(2023, 12, 31)).build(); + Record mockRecord = Record.builder().memberId(member.getId()).build(); + + when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); + when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + ActivityResponse response = careerService.createActivity(member, reqDto); + + assertNotNull(response); + assertEquals("동아리 활동", response.getName()); + verify(activityRepository).save(any(Activity.class)); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[CreateCareer] Circle을 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") + void createCircle_shouldSaveCircleAndUpdateRecord() { + Member member = Member.builder().id(1L).build(); + CircleReqDto reqDto = CircleReqDto.builder() + .name("소모임").unknown(false) + .enddate(LocalDate.of(2023, 10, 10)).build(); + Record mockRecord = Record.builder().memberId(member.getId()).build(); + + when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); + when(circleRepository.save(any(Circle.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + CircleResponse response = careerService.createCircle(member, reqDto); + + assertNotNull(response); + assertEquals("소모임", response.getName()); + verify(circleRepository).save(any(Circle.class)); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[CreateCareer] Competition을 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") + void createCompetition_shouldSaveCompetitionAndUpdateRecord() { + Member member = Member.builder().id(1L).build(); + CompetitionReqDto reqDto = CompetitionReqDto.builder() + .name("공모전").unknown(false) + .enddate(LocalDate.of(2024, 1, 1)).build(); + Record mockRecord = Record.builder().memberId(member.getId()).build(); + + when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); + when(competitionRepository.save(any(Competition.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + CompetitionResponse response = careerService.createCompetition(member, reqDto); + + assertNotNull(response); + assertEquals("공모전", response.getName()); + verify(competitionRepository).save(any(Competition.class)); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[CreateCareer] EduCareer를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") + void createEduCareer_shouldSaveEduCareerAndUpdateRecord() { + Member member = Member.builder().id(1L).build(); + EduCareerReqDto reqDto = EduCareerReqDto.builder() + .name("교육 이력") + .unknown(false) + .enddate(LocalDate.of(2022, 5, 15)) + .time(120) + .build(); + + Record mockRecord = Record.builder().memberId(member.getId()).build(); + + when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); + when(eduCareerRepository.save(any(EduCareer.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + EduCareerResponse response = careerService.crateEduCareer(member, reqDto); + + assertNotNull(response); + assertEquals("교육 이력", response.getName()); + verify(eduCareerRepository).save(any(EduCareer.class)); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[CreateCareer] Employment를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") + void createEmployment_shouldSaveEmploymentAndUpdateRecord() { + Member member = Member.builder().id(1L).build(); + EmploymentReqDto reqDto = EmploymentReqDto.builder() + .name("직장").unknown(false) + .enddate(LocalDate.of(2021, 8, 1)).build(); + Record mockRecord = Record.builder().memberId(member.getId()).build(); + + when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); + when(employmentRepository.save(any(Employment.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + EmploymentResponse response = careerService.createEmployment(member, reqDto); + + assertNotNull(response); + assertEquals("직장", response.getName()); + verify(employmentRepository).save(any(Employment.class)); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[CreateCareer] Project를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") + void createProject_shouldSaveProjectAndUpdateRecord() { + Member member = Member.builder().id(1L).build(); + ProjectReqDto reqDto = ProjectReqDto.builder() + .name("사이드 프로젝트").unknown(false) + .enddate(LocalDate.of(2024, 12, 1)).build(); + Record mockRecord = Record.builder().memberId(member.getId()).build(); + + when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); + when(projectRepository.save(any(Project.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + ProjectResponse response = careerService.createProject(member, reqDto); + + assertNotNull(response); + assertEquals("사이드 프로젝트", response.getName()); + verify(projectRepository).save(any(Project.class)); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[CreateCareer] Etc를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") + void createEtc_shouldSaveEtcAndUpdateRecord() { + Member member = Member.builder().id(1L).build(); + EtcReqDto reqDto = EtcReqDto.builder() + .name("기타 활동").unknown(false) + .enddate(LocalDate.of(2020, 2, 2)).build(); + Record mockRecord = Record.builder().memberId(member.getId()).build(); + + when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); + when(etcRepository.save(any(CareerEtc.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + EtcResponse response = careerService.createEtc(member, reqDto); + + assertNotNull(response); + assertEquals("기타 활동", response.getName()); + verify(etcRepository).save(any(CareerEtc.class)); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[DeleteCareer] Activity를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") + void deleteActivity_shouldRemoveActivityAndUpdateRecord() { + // given + Long memberId = 1L; + Long activityId = 100L; + + Member member = Member.builder().id(memberId).build(); + Activity activity = Activity.builder().memberId(memberId).build(); + setId(activity, activityId); + Record mockRecord = Record.builder().memberId(memberId).build(); + + when(activityRepository.findById(activityId)).thenReturn(Optional.of(activity)); + when(recordRepository.findByMemberId(memberId)).thenReturn(mockRecord); + + // when + careerService.deleteActivity(member, activityId); + + // then + verify(activityRepository).delete(activity); + verify(recordUpdateManager).updateRecordTimestamp(mockRecord); + } + + @Test + @DisplayName("[DeleteCareer] Circle을 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") + void deleteCircle_shouldRemoveCircleAndUpdateRecord() { + // given + Long memberId = 1L; + Long circleId = 101L; + + Member member = Member.builder().id(memberId).build(); + Circle circle = Circle.builder().memberId(memberId).build(); + setId(circle, circleId); + Record record = Record.builder().memberId(memberId).build(); + + when(circleRepository.findById(circleId)).thenReturn(Optional.of(circle)); + when(recordRepository.findByMemberId(memberId)).thenReturn(record); + + // when + careerService.deleteCircle(member, circleId); + + // then + verify(circleRepository).delete(circle); + verify(recordUpdateManager).updateRecordTimestamp(record); + } + + @Test + @DisplayName("[DeleteCareer] Competition을 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") + void deleteCompetition_shouldRemoveCompetitionAndUpdateRecord() { + // given + Long memberId = 1L; + Long compId = 102L; + + Member member = Member.builder().id(memberId).build(); + Competition competition = Competition.builder().memberId(memberId).build(); + setId(competition, compId); + Record record = Record.builder().memberId(memberId).build(); + + when(competitionRepository.findById(compId)).thenReturn(Optional.of(competition)); + when(recordRepository.findByMemberId(memberId)).thenReturn(record); + + // when + careerService.deleteComp(member, compId); + + // then + verify(competitionRepository).delete(competition); + verify(recordUpdateManager).updateRecordTimestamp(record); + } + + @Test + @DisplayName("[DeleteCareer] EduCareer를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") + void deleteEduCareer_shouldRemoveEduCareerAndUpdateRecord() { + // given + Long memberId = 1L; + Long eduId = 103L; + + Member member = Member.builder().id(memberId).build(); + EduCareer edu = EduCareer.builder().memberId(memberId).build(); + setId(edu, eduId); + Record record = Record.builder().memberId(memberId).build(); + + when(eduCareerRepository.findById(eduId)).thenReturn(Optional.of(edu)); + when(recordRepository.findByMemberId(memberId)).thenReturn(record); + + // when + careerService.deleteEdu(member, eduId); + + // then + verify(eduCareerRepository).delete(edu); + verify(recordUpdateManager).updateRecordTimestamp(record); + } + + @Test + @DisplayName("[DeleteCareer] Employment를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") + void deleteEmployment_shouldRemoveEmploymentAndUpdateRecord() { + // given + Long memberId = 1L; + Long empId = 104L; + + Member member = Member.builder().id(memberId).build(); + Employment emp = Employment.builder().memberId(memberId).build(); + setId(emp, empId); + Record record = Record.builder().memberId(memberId).build(); + + when(employmentRepository.findById(empId)).thenReturn(Optional.of(emp)); + when(recordRepository.findByMemberId(memberId)).thenReturn(record); + + // when + careerService.deleteEmp(member, empId); + + // then + verify(employmentRepository).delete(emp); + verify(recordUpdateManager).updateRecordTimestamp(record); + } + + @Test + @DisplayName("[DeleteCareer] Project를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") + void deleteProject_shouldRemoveProjectAndUpdateRecord() { + // given + Long memberId = 1L; + Long projectId = 105L; + + Member member = Member.builder().id(memberId).build(); + Project project = Project.builder().memberId(memberId).build(); + setId(project, projectId); + Record record = Record.builder().memberId(memberId).build(); + + when(projectRepository.findById(projectId)).thenReturn(Optional.of(project)); + when(recordRepository.findByMemberId(memberId)).thenReturn(record); + + // when + careerService.deleteProject(member, projectId); + + // then + verify(projectRepository).delete(project); + verify(recordUpdateManager).updateRecordTimestamp(record); + } + + @Test + @DisplayName("[DeleteCareer] Etc를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") + void deleteEtc_shouldRemoveEtcAndUpdateRecord() { + // given + Long memberId = 1L; + Long etcId = 106L; + + Member member = Member.builder().id(memberId).build(); + CareerEtc etc = CareerEtc.builder().memberId(memberId).build(); + setId(etc, etcId); + Record record = Record.builder().memberId(memberId).build(); + + when(etcRepository.findById(etcId)).thenReturn(Optional.of(etc)); + when(recordRepository.findByMemberId(memberId)).thenReturn(record); + + // when + careerService.deleteEtc(member, etcId); + + // then + verify(etcRepository).delete(etc); + verify(recordUpdateManager).updateRecordTimestamp(record); + } + + private void setId(Object entity, Long id) { + try { + Field field = entity.getClass().getDeclaredField("id"); + field.setAccessible(true); + field.set(entity, id); + } catch (Exception e) { + throw new RuntimeException("ID 주입 실패", e); + } + } + + +} + From 40d9416a9421ded22dd9c7cd9f876feae19d94d2 Mon Sep 17 00:00:00 2001 From: hyeonda02 Date: Tue, 22 Jul 2025 20:30:34 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Feat=20:=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=20=EA=B8=B0=EB=A1=9D=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?-=20=ED=99=9C=EB=8F=99=20=EC=B0=BE=EA=B8=B0=20=EC=BD=A4?= =?UTF-8?q?=EB=B3=B4=EB=B0=95=EC=8A=A4=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grafana/docker-compose 2.yml | 38 -- grafana/prometheus-config 2.yaml | 8 - .../controller/CareerSearchController.java | 15 + .../response/CareerTitleResponse.java | 27 ++ .../career/service/CareerSearchService.java | 1 + .../service/CareerSearchServiceImpl.java | 27 ++ .../service/CareerServiceImplTest 2.java | 380 ------------------ 7 files changed, 70 insertions(+), 426 deletions(-) delete mode 100644 grafana/docker-compose 2.yml delete mode 100644 grafana/prometheus-config 2.yaml create mode 100644 src/main/java/umc/kkijuk/server/career/controller/response/CareerTitleResponse.java delete mode 100644 src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java diff --git a/grafana/docker-compose 2.yml b/grafana/docker-compose 2.yml deleted file mode 100644 index 6f796b0e..00000000 --- a/grafana/docker-compose 2.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: '3.8' - -services: - ## monitoring - prometheus: - image: prom/prometheus:latest - container_name: kkijuk-prometheus - volumes: - - ./prometheus-config.yaml:/etc/prometheus/prometheus-config.yaml - command: - - '--config.file=/etc/prometheus/prometheus-config.yaml' - ports: - - "9090:9090" - networks: - - monitoring-network - - grafana: - image: grafana/grafana:latest - container_name: kkijuk-grafana -# set in instance -# environment: -# - GF_SECURITY_ADMIN_USER=local -# - GF_SECURITY_ADMIN_PASSWORD=local - volumes: - - ./datasources:/etc/grafana/provisioning/datasources - - ./dashboards:/etc/grafana/provisioning/dashboards - - ./dashboards:/var/lib/grafana/dashboards - ports: - - "3000:3000" - networks: - - monitoring-network - -networks: - monitoring-network: - driver: bridge - -volumes: - loki_data: \ No newline at end of file diff --git a/grafana/prometheus-config 2.yaml b/grafana/prometheus-config 2.yaml deleted file mode 100644 index 72b6e721..00000000 --- a/grafana/prometheus-config 2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -global: - scrape_interval: 15s - -scrape_configs: - - job_name: prometheus - metrics_path: '/actuator/prometheus' - static_configs: - - targets: ['host.docker.internal:8088'] \ No newline at end of file diff --git a/src/main/java/umc/kkijuk/server/career/controller/CareerSearchController.java b/src/main/java/umc/kkijuk/server/career/controller/CareerSearchController.java index 5a291aa1..47a4cf75 100644 --- a/src/main/java/umc/kkijuk/server/career/controller/CareerSearchController.java +++ b/src/main/java/umc/kkijuk/server/career/controller/CareerSearchController.java @@ -139,4 +139,19 @@ public CareerResponse> findCareerForTimeline( } + @GetMapping("/list") + @Operation( + summary = "활동 리스트", + description = "새로운 활동 기록 추가 모달에 필요한 활동 정보들을 조회합니다.") + public CareerResponse> findCareerForNewDetail( + @RequestHeader("Authorization") String token + ){ + Member requestMember = loginUser.extractMemberId(token); + return CareerResponse.success( + CareerResponseMessage.CAREER_FINDALL_SUCCESS, + careerSearchService.findCareerForNewDetail(requestMember) + ); + + } + } diff --git a/src/main/java/umc/kkijuk/server/career/controller/response/CareerTitleResponse.java b/src/main/java/umc/kkijuk/server/career/controller/response/CareerTitleResponse.java new file mode 100644 index 00000000..04e00d70 --- /dev/null +++ b/src/main/java/umc/kkijuk/server/career/controller/response/CareerTitleResponse.java @@ -0,0 +1,27 @@ +package umc.kkijuk.server.career.controller.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@Getter +@Setter +@Builder +@AllArgsConstructor +@Schema(description = "활동 요약 정보 응답 DTO") +public class CareerTitleResponse { + + @Schema(description = "활동 ID", example = "42") + private Long careerId; + @Schema(description = "카테고리 정보 (id, 설명, name)") + private CategoryResponse category; + @Schema(description = "활동 제목", example = "끼적 백엔드 개발") + private String title; + @Schema(description = "활동 별칭", example = "Spring Boot API") + private String alias; +} diff --git a/src/main/java/umc/kkijuk/server/career/service/CareerSearchService.java b/src/main/java/umc/kkijuk/server/career/service/CareerSearchService.java index 9becdbdf..c69a9d82 100644 --- a/src/main/java/umc/kkijuk/server/career/service/CareerSearchService.java +++ b/src/main/java/umc/kkijuk/server/career/service/CareerSearchService.java @@ -20,4 +20,5 @@ public interface CareerSearchService { List findCareerWithKeyword(Member requestMember, String keyword, String sort); + List findCareerForNewDetail(Member requestMember); } diff --git a/src/main/java/umc/kkijuk/server/career/service/CareerSearchServiceImpl.java b/src/main/java/umc/kkijuk/server/career/service/CareerSearchServiceImpl.java index 4e61375a..5bc73e5a 100644 --- a/src/main/java/umc/kkijuk/server/career/service/CareerSearchServiceImpl.java +++ b/src/main/java/umc/kkijuk/server/career/service/CareerSearchServiceImpl.java @@ -305,7 +305,34 @@ public List findCareerWithKeyword(Member requestMember, Stri } + @Override + public List findCareerForNewDetail(Member requestMember) { + Long memberId = requestMember.getId(); + List careers = new ArrayList<>(); + + careers.addAll(activityRepository.findByMemberId(memberId)); + careers.addAll(projectJpaRepository.findByMemberId(memberId)); + careers.addAll(eduCareerJpaRepository.findByMemberId(memberId)); + careers.addAll(employmentJpaRepository.findByMemberId(memberId)); + careers.addAll(circleRepository.findByMemberId(memberId)); + careers.addAll(competitionJpaRepository.findByMemberId(memberId)); + careers.addAll(etcRepository.findByMemberId(memberId)); + return careers.stream() + .sorted(Comparator.comparing(BaseCareer::getStartdate).reversed()) + .map(career -> CareerTitleResponse.builder() + .careerId(career.getId()) + .title(career.getName()) + .alias(career.getAlias()) + .category(new CategoryResponse( + CareerType.fromClass(career).getId(), + CareerType.fromClass(career).getDescription(), + CareerType.fromClass(career).name() + )) + .build() + ) + .collect(Collectors.toList()); + } //타임라인 쪽에서 데이터 구별 하기 위함 diff --git a/src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java b/src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java deleted file mode 100644 index da7ddd1b..00000000 --- a/src/test/java/umc/kkijuk/server/unitTest/career/service/CareerServiceImplTest 2.java +++ /dev/null @@ -1,380 +0,0 @@ -package umc.kkijuk.server.unitTest.career.service; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import umc.kkijuk.server.career.controller.response.*; -import umc.kkijuk.server.career.domain.*; -import umc.kkijuk.server.career.dto.*; -import umc.kkijuk.server.career.repository.*; -import umc.kkijuk.server.career.service.CareerServiceImpl; -import umc.kkijuk.server.common.service.RecordUpdateManager; -import umc.kkijuk.server.member.domain.Member; -import umc.kkijuk.server.record.domain.Record; -import umc.kkijuk.server.record.repository.RecordRepository; - -import java.lang.reflect.Field; -import java.time.LocalDate; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - - - -@ExtendWith(MockitoExtension.class) -class CareerServiceImplTest { - - @InjectMocks - private CareerServiceImpl careerService; - - @Mock - private ActivityRepository activityRepository; - - @Mock - private CircleRepository circleRepository; - - @Mock - private CompetitionRepository competitionRepository; - - @Mock - private EduCareerRepository eduCareerRepository; - - @Mock - private EmploymentRepository employmentRepository; - - @Mock - private ProjectRepository projectRepository; - - @Mock - private CareerEtcRepository etcRepository; - - @Mock - private RecordRepository recordRepository; - - @Mock - private RecordUpdateManager recordUpdateManager; - - @Test - @DisplayName("[CreateCareer] Activity를 생성하면 활동이 저장되고 Record의 타임스탬프가 갱신되어야 한다.") - void createActivity_shouldSaveActivityAndUpdateRecord() { - Member member = Member.builder().id(1L).build(); - ActivityReqDto reqDto = ActivityReqDto.builder() - .name("동아리 활동").unknown(false) - .enddate(LocalDate.of(2023, 12, 31)).build(); - Record mockRecord = Record.builder().memberId(member.getId()).build(); - - when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); - when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - ActivityResponse response = careerService.createActivity(member, reqDto); - - assertNotNull(response); - assertEquals("동아리 활동", response.getName()); - verify(activityRepository).save(any(Activity.class)); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[CreateCareer] Circle을 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") - void createCircle_shouldSaveCircleAndUpdateRecord() { - Member member = Member.builder().id(1L).build(); - CircleReqDto reqDto = CircleReqDto.builder() - .name("소모임").unknown(false) - .enddate(LocalDate.of(2023, 10, 10)).build(); - Record mockRecord = Record.builder().memberId(member.getId()).build(); - - when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); - when(circleRepository.save(any(Circle.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - CircleResponse response = careerService.createCircle(member, reqDto); - - assertNotNull(response); - assertEquals("소모임", response.getName()); - verify(circleRepository).save(any(Circle.class)); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[CreateCareer] Competition을 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") - void createCompetition_shouldSaveCompetitionAndUpdateRecord() { - Member member = Member.builder().id(1L).build(); - CompetitionReqDto reqDto = CompetitionReqDto.builder() - .name("공모전").unknown(false) - .enddate(LocalDate.of(2024, 1, 1)).build(); - Record mockRecord = Record.builder().memberId(member.getId()).build(); - - when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); - when(competitionRepository.save(any(Competition.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - CompetitionResponse response = careerService.createCompetition(member, reqDto); - - assertNotNull(response); - assertEquals("공모전", response.getName()); - verify(competitionRepository).save(any(Competition.class)); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[CreateCareer] EduCareer를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") - void createEduCareer_shouldSaveEduCareerAndUpdateRecord() { - Member member = Member.builder().id(1L).build(); - EduCareerReqDto reqDto = EduCareerReqDto.builder() - .name("교육 이력") - .unknown(false) - .enddate(LocalDate.of(2022, 5, 15)) - .time(120) - .build(); - - Record mockRecord = Record.builder().memberId(member.getId()).build(); - - when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); - when(eduCareerRepository.save(any(EduCareer.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - EduCareerResponse response = careerService.crateEduCareer(member, reqDto); - - assertNotNull(response); - assertEquals("교육 이력", response.getName()); - verify(eduCareerRepository).save(any(EduCareer.class)); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[CreateCareer] Employment를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") - void createEmployment_shouldSaveEmploymentAndUpdateRecord() { - Member member = Member.builder().id(1L).build(); - EmploymentReqDto reqDto = EmploymentReqDto.builder() - .name("직장").unknown(false) - .enddate(LocalDate.of(2021, 8, 1)).build(); - Record mockRecord = Record.builder().memberId(member.getId()).build(); - - when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); - when(employmentRepository.save(any(Employment.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - EmploymentResponse response = careerService.createEmployment(member, reqDto); - - assertNotNull(response); - assertEquals("직장", response.getName()); - verify(employmentRepository).save(any(Employment.class)); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[CreateCareer] Project를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") - void createProject_shouldSaveProjectAndUpdateRecord() { - Member member = Member.builder().id(1L).build(); - ProjectReqDto reqDto = ProjectReqDto.builder() - .name("사이드 프로젝트").unknown(false) - .enddate(LocalDate.of(2024, 12, 1)).build(); - Record mockRecord = Record.builder().memberId(member.getId()).build(); - - when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); - when(projectRepository.save(any(Project.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - ProjectResponse response = careerService.createProject(member, reqDto); - - assertNotNull(response); - assertEquals("사이드 프로젝트", response.getName()); - verify(projectRepository).save(any(Project.class)); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[CreateCareer] Etc를 생성하면 저장되고 Record의 타임스탬프가 갱신되어야 한다.") - void createEtc_shouldSaveEtcAndUpdateRecord() { - Member member = Member.builder().id(1L).build(); - EtcReqDto reqDto = EtcReqDto.builder() - .name("기타 활동").unknown(false) - .enddate(LocalDate.of(2020, 2, 2)).build(); - Record mockRecord = Record.builder().memberId(member.getId()).build(); - - when(recordRepository.findByMemberId(member.getId())).thenReturn(mockRecord); - when(etcRepository.save(any(CareerEtc.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - EtcResponse response = careerService.createEtc(member, reqDto); - - assertNotNull(response); - assertEquals("기타 활동", response.getName()); - verify(etcRepository).save(any(CareerEtc.class)); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[DeleteCareer] Activity를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") - void deleteActivity_shouldRemoveActivityAndUpdateRecord() { - // given - Long memberId = 1L; - Long activityId = 100L; - - Member member = Member.builder().id(memberId).build(); - Activity activity = Activity.builder().memberId(memberId).build(); - setId(activity, activityId); - Record mockRecord = Record.builder().memberId(memberId).build(); - - when(activityRepository.findById(activityId)).thenReturn(Optional.of(activity)); - when(recordRepository.findByMemberId(memberId)).thenReturn(mockRecord); - - // when - careerService.deleteActivity(member, activityId); - - // then - verify(activityRepository).delete(activity); - verify(recordUpdateManager).updateRecordTimestamp(mockRecord); - } - - @Test - @DisplayName("[DeleteCareer] Circle을 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") - void deleteCircle_shouldRemoveCircleAndUpdateRecord() { - // given - Long memberId = 1L; - Long circleId = 101L; - - Member member = Member.builder().id(memberId).build(); - Circle circle = Circle.builder().memberId(memberId).build(); - setId(circle, circleId); - Record record = Record.builder().memberId(memberId).build(); - - when(circleRepository.findById(circleId)).thenReturn(Optional.of(circle)); - when(recordRepository.findByMemberId(memberId)).thenReturn(record); - - // when - careerService.deleteCircle(member, circleId); - - // then - verify(circleRepository).delete(circle); - verify(recordUpdateManager).updateRecordTimestamp(record); - } - - @Test - @DisplayName("[DeleteCareer] Competition을 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") - void deleteCompetition_shouldRemoveCompetitionAndUpdateRecord() { - // given - Long memberId = 1L; - Long compId = 102L; - - Member member = Member.builder().id(memberId).build(); - Competition competition = Competition.builder().memberId(memberId).build(); - setId(competition, compId); - Record record = Record.builder().memberId(memberId).build(); - - when(competitionRepository.findById(compId)).thenReturn(Optional.of(competition)); - when(recordRepository.findByMemberId(memberId)).thenReturn(record); - - // when - careerService.deleteComp(member, compId); - - // then - verify(competitionRepository).delete(competition); - verify(recordUpdateManager).updateRecordTimestamp(record); - } - - @Test - @DisplayName("[DeleteCareer] EduCareer를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") - void deleteEduCareer_shouldRemoveEduCareerAndUpdateRecord() { - // given - Long memberId = 1L; - Long eduId = 103L; - - Member member = Member.builder().id(memberId).build(); - EduCareer edu = EduCareer.builder().memberId(memberId).build(); - setId(edu, eduId); - Record record = Record.builder().memberId(memberId).build(); - - when(eduCareerRepository.findById(eduId)).thenReturn(Optional.of(edu)); - when(recordRepository.findByMemberId(memberId)).thenReturn(record); - - // when - careerService.deleteEdu(member, eduId); - - // then - verify(eduCareerRepository).delete(edu); - verify(recordUpdateManager).updateRecordTimestamp(record); - } - - @Test - @DisplayName("[DeleteCareer] Employment를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") - void deleteEmployment_shouldRemoveEmploymentAndUpdateRecord() { - // given - Long memberId = 1L; - Long empId = 104L; - - Member member = Member.builder().id(memberId).build(); - Employment emp = Employment.builder().memberId(memberId).build(); - setId(emp, empId); - Record record = Record.builder().memberId(memberId).build(); - - when(employmentRepository.findById(empId)).thenReturn(Optional.of(emp)); - when(recordRepository.findByMemberId(memberId)).thenReturn(record); - - // when - careerService.deleteEmp(member, empId); - - // then - verify(employmentRepository).delete(emp); - verify(recordUpdateManager).updateRecordTimestamp(record); - } - - @Test - @DisplayName("[DeleteCareer] Project를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") - void deleteProject_shouldRemoveProjectAndUpdateRecord() { - // given - Long memberId = 1L; - Long projectId = 105L; - - Member member = Member.builder().id(memberId).build(); - Project project = Project.builder().memberId(memberId).build(); - setId(project, projectId); - Record record = Record.builder().memberId(memberId).build(); - - when(projectRepository.findById(projectId)).thenReturn(Optional.of(project)); - when(recordRepository.findByMemberId(memberId)).thenReturn(record); - - // when - careerService.deleteProject(member, projectId); - - // then - verify(projectRepository).delete(project); - verify(recordUpdateManager).updateRecordTimestamp(record); - } - - @Test - @DisplayName("[DeleteCareer] Etc를 삭제하면 Repository에서 제거되고 Record의 타임스탬프가 갱신되어야 한다.") - void deleteEtc_shouldRemoveEtcAndUpdateRecord() { - // given - Long memberId = 1L; - Long etcId = 106L; - - Member member = Member.builder().id(memberId).build(); - CareerEtc etc = CareerEtc.builder().memberId(memberId).build(); - setId(etc, etcId); - Record record = Record.builder().memberId(memberId).build(); - - when(etcRepository.findById(etcId)).thenReturn(Optional.of(etc)); - when(recordRepository.findByMemberId(memberId)).thenReturn(record); - - // when - careerService.deleteEtc(member, etcId); - - // then - verify(etcRepository).delete(etc); - verify(recordUpdateManager).updateRecordTimestamp(record); - } - - private void setId(Object entity, Long id) { - try { - Field field = entity.getClass().getDeclaredField("id"); - field.setAccessible(true); - field.set(entity, id); - } catch (Exception e) { - throw new RuntimeException("ID 주입 실패", e); - } - } - - -} -