From 23eb58ccf487668933f49f5e65250c4bc93c88d8 Mon Sep 17 00:00:00 2001 From: jjuuuunnii <129721531+jjuuuunnii@users.noreply.github.com> Date: Thu, 5 Jun 2025 15:52:47 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Feature:=20=EC=96=B4=EB=93=9C?= =?UTF-8?q?=EB=AF=BC=20=EC=BB=A4=EB=A6=AC=EC=96=B4=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=204.6=20API=20DTO=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#320)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- http/career/CarrerControllerHttpRequest.http | 62 +++++++++- .../request/UpdateAdminsCareerRequestDto.java | 64 ++++++++++ .../ReadAdminsCareersDetailsResponseDto.java | 117 ++++++++++++++++++ ...ReadAdminsCareersOverviewsResponseDto.java | 106 ++++++++++++++++ .../service/CreateAdminsCareerService.java | 5 +- .../service/DeleteAdminsCareerService.java | 42 +++++++ .../ReadAdminsCareersDetailsService.java | 58 +++++++++ .../ReadAdminsCareersOverviewsService.java | 111 +++++++++++++++++ .../service/UpdateAdminsCareerService.java | 85 +++++++++++++ .../usecase/DeleteAdminsCareerUseCase.java | 11 ++ .../ReadAdminsCareersDetailsUseCase.java | 12 ++ .../ReadAdminsCareersOverviewsUseCase.java | 16 +++ .../usecase/UpdateAdminsCareerUseCase.java | 17 +++ .../CareerAdminsCommandV1Controller.java | 39 +++++- .../query/CareerAdminQueryV1Controller.java | 54 ++++++++ .../inglo/giggle/career/domain/Career.java | 32 +++++ .../giggle/career/domain/CareerImage.java | 12 +- .../repository/CareerImageRepository.java | 6 + .../career/repository/CareerRepository.java | 2 + .../impl/CareerImageRepositoryImpl.java | 15 +++ .../repository/impl/CareerRepositoryImpl.java | 5 + .../mysql/CareerImageJpaRepository.java | 2 + ...nerJobPostingUserOverviewsResponseDto.java | 20 ++- .../service/UpdateOwnerJobPostingService.java | 1 - 24 files changed, 885 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/inglo/giggle/career/application/dto/request/UpdateAdminsCareerRequestDto.java create mode 100644 src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersDetailsResponseDto.java create mode 100644 src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersOverviewsResponseDto.java create mode 100644 src/main/java/com/inglo/giggle/career/application/service/DeleteAdminsCareerService.java create mode 100644 src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersDetailsService.java create mode 100644 src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersOverviewsService.java create mode 100644 src/main/java/com/inglo/giggle/career/application/service/UpdateAdminsCareerService.java create mode 100644 src/main/java/com/inglo/giggle/career/application/usecase/DeleteAdminsCareerUseCase.java create mode 100644 src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersDetailsUseCase.java create mode 100644 src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersOverviewsUseCase.java create mode 100644 src/main/java/com/inglo/giggle/career/application/usecase/UpdateAdminsCareerUseCase.java create mode 100644 src/main/java/com/inglo/giggle/career/controller/query/CareerAdminQueryV1Controller.java diff --git a/http/career/CarrerControllerHttpRequest.http b/http/career/CarrerControllerHttpRequest.http index adbbb891..6f78960f 100644 --- a/http/career/CarrerControllerHttpRequest.http +++ b/http/career/CarrerControllerHttpRequest.http @@ -32,7 +32,7 @@ Authorization: Bearer {{access_token}} ### 14.5 (어드민) 커리어 추가하기 -### +//@no-log POST http://localhost:8080/v1/admins/careers Content-Type: multipart/form-data; boundary=boundary Authorization: Bearer {{access_token}} @@ -77,3 +77,63 @@ Authorization: Bearer {{access_token}} +### 14.7 (어드민) 커리어 리스트 조회 +//@no-log +GET http://localhost:8080/v1/admins/careers/overviews? + page=1& + size=10& + sorting=TRENDING +Content-Type: application/json +Authorization: Bearer {{access_token}} + + + +### 14.8 (어드민) 커리어 디테일 조회 +//@no-log +GET http://localhost:8080/v1/admins/careers/1/details +Content-Type: application/json +Authorization: Bearer {{access_token}} + + +### 14.9 (어드민) 커리어 수정하기 +//@no-log +PUT http://localhost:8080/v1/admins/careers/1 +Content-Type: multipart/form-data; boundary=boundary +Authorization: Bearer {{access_token}} + +--boundary +Content-Disposition: form-data; name="image"; filename="image.png" +Content-Type: image/png + +< /Users/jjuuuunnii/Desktop/image.png + +--boundary +Content-Disposition: form-data; name="body" +Content-Type: application/json + +{ + "title": "승주니랑 동겸이의 스프링 여행", + "career_category": "PROGRAM", + "host_name": "한국글로벌협회", + "organizer_name": "서울청년센터", + "address": "서울 강남구", + "recruitment_start_date": "2025-06-01", + "recruitment_end_date": "2025-06-30", + "reward": 500000, + "visa": ["D_2_2", "D_4_1", "F_2"], + "recruitment_number": 9, + "education": "BACHELOR", + "preferred_conditions": "외국어 능통자 우대", + "details": "해외 파견 인턴십 프로그램으로, 글로벌 기업에서 실무 경험을 쌓을 수 있는 기회입니다.", + "application_url": "https://apply.globalinternship.kr", + "deleted_image_ids": [1] +} + +--boundary + + + +### 14.10 (어드민) 커리어 삭제하기 +//@no-log +DELETE http://localhost:8080/v1/admins/careers/2 +Authorization: Bearer {{access_token}} \ No newline at end of file diff --git a/src/main/java/com/inglo/giggle/career/application/dto/request/UpdateAdminsCareerRequestDto.java b/src/main/java/com/inglo/giggle/career/application/dto/request/UpdateAdminsCareerRequestDto.java new file mode 100644 index 00000000..b8aaadf1 --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/dto/request/UpdateAdminsCareerRequestDto.java @@ -0,0 +1,64 @@ +package com.inglo.giggle.career.application.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.inglo.giggle.career.domain.type.ECareerCategory; +import com.inglo.giggle.core.type.EEducationLevel; +import com.inglo.giggle.core.type.EVisa; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.List; +import java.util.Set; + +public record UpdateAdminsCareerRequestDto( + + @NotBlank(message = "title은 필수 입력 값입니다.") + @JsonProperty("title") + String title, + + @JsonProperty("career_category") + ECareerCategory careerCategory, + + @JsonProperty("host_name") + String hostName, + + @JsonProperty("organizer_name") + String organizerName, + + @JsonProperty("address") + String address, + + @JsonProperty("recruitment_start_date") + String recruitmentStartDate, // ISO 8601 형식: "yyyy-MM-dd" + + @JsonProperty("recruitment_end_date") + String recruitmentEndDate, + + @JsonProperty("reward") + Integer reward, + + @JsonProperty("visa") + Set visa, + + @JsonProperty("recruitment_number") + Integer recruitmentNumber, + + @NotNull + @JsonProperty("education") + EEducationLevel education, + + @JsonProperty("preferred_conditions") + String preferredConditions, + + @JsonProperty("details") + String details, + + @NotBlank + @JsonProperty("application_url") + String applicationUrl, + + @JsonProperty("delete_img_ids") + List deleteImgIds + +) { +} diff --git a/src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersDetailsResponseDto.java b/src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersDetailsResponseDto.java new file mode 100644 index 00000000..de3348e3 --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersDetailsResponseDto.java @@ -0,0 +1,117 @@ +package com.inglo.giggle.career.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.inglo.giggle.career.domain.type.ECareerCategory; +import com.inglo.giggle.core.dto.SelfValidating; +import com.inglo.giggle.core.type.EEducationLevel; +import com.inglo.giggle.core.type.EVisa; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; +import java.util.Set; + +@Getter +public class ReadAdminsCareersDetailsResponseDto extends SelfValidating { + + @JsonProperty("imgs") + private final List imgs; + + @JsonProperty("title") + private final String title; + + @JsonProperty("career_category") + private final ECareerCategory careerCategory; // nullable + + @JsonProperty("host_name") + private final String hostName; + + @JsonProperty("organizer_name") + private final String organizerName; + + @JsonProperty("address") + private final String address; + + @JsonProperty("recruitment_start_date") + private final String recruitmentStartDate; + + @JsonProperty("recruitment_end_date") + private final String recruitmentEndDate; + + @JsonProperty("reward") + private final Integer reward; + + @JsonProperty("visa") + private final Set visa; + + @JsonProperty("recruitment_number") + private final Integer recruitmentNumber; + + @JsonProperty("education") + private final EEducationLevel education; + + @JsonProperty("preferred_conditions") + private final String preferredConditions; + + @JsonProperty("details") + private final String details; + + @NotNull + @JsonProperty("application_url") + private final String applicationUrl; + + @Builder + public ReadAdminsCareersDetailsResponseDto( + List imgs, + String title, + ECareerCategory careerCategory, + String hostName, + String organizerName, + String address, + String recruitmentStartDate, + String recruitmentEndDate, + Integer reward, + Set visa, + Integer recruitmentNumber, + EEducationLevel education, + String preferredConditions, + String details, + String applicationUrl + ) { + this.imgs = imgs; + this.title = title; + this.careerCategory = careerCategory; + this.hostName = hostName; + this.organizerName = organizerName; + this.address = address; + this.recruitmentStartDate = recruitmentStartDate; + this.recruitmentEndDate = recruitmentEndDate; + this.reward = reward; + this.visa = visa; + this.recruitmentNumber = recruitmentNumber; + this.education = education; + this.preferredConditions = preferredConditions; + this.details = details; + this.applicationUrl = applicationUrl; + this.validateSelf(); + } + + @Getter + public static class imgs extends SelfValidating { + + @JsonProperty("id") + @NotNull(message = "이미지 ID는 필수입니다.") + private final Long id; + + @JsonProperty("urls") + @NotNull(message = "이미지 URL은 필수입니다.") + private final String url; + + @Builder + public imgs(Long id, String url) { + this.id = id; + this.url = url; + } + } +} diff --git a/src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersOverviewsResponseDto.java b/src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersOverviewsResponseDto.java new file mode 100644 index 00000000..aa43407e --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/dto/response/ReadAdminsCareersOverviewsResponseDto.java @@ -0,0 +1,106 @@ +package com.inglo.giggle.career.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.inglo.giggle.career.domain.type.ECareerCategory; +import com.inglo.giggle.career.domain.type.ERecruitmentStatus; +import com.inglo.giggle.core.dto.SelfValidating; +import com.inglo.giggle.core.type.EVisa; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +@Getter +public class ReadAdminsCareersOverviewsResponseDto extends SelfValidating { + + @NotNull + @JsonProperty("career_list") + private final List careerList; + + @NotNull + @JsonProperty("has_next") + private final Boolean hasNext; + + @Builder + public ReadAdminsCareersOverviewsResponseDto(List careerList, Boolean hasNext) { + this.careerList = careerList; + this.hasNext = hasNext; + this.validateSelf(); + } + + @Getter + public static class CareerOverviewDto { + + @NotNull + @JsonProperty("id") + private final Long id; + + @NotNull + @JsonProperty("title") + private final String title; + + @NotNull + @JsonProperty("career_category") + private final ECareerCategory careerCategory; + + @NotNull + @JsonProperty("visa") + private final List visa; + + @NotNull + @JsonProperty("host_name") + private final String hostName; + + @NotNull + @JsonProperty("organizer_name") + private final String organizerName; + + @NotNull + @JsonProperty("left_days") + private final String leftDays; + + @NotNull + @JsonProperty("status") + private final ERecruitmentStatus status; + + @NotNull + @JsonProperty("recruitment_start_date") + private final String recruitmentStartDate; + + @NotNull + @JsonProperty("recruitment_end_date") + private final String recruitmentEndDate; + + @NotNull + @JsonProperty("created_at") + private final String createdAt; + + @Builder + public CareerOverviewDto( + Long id, + String title, + ECareerCategory careerCategory, + List visa, + String hostName, + String organizerName, + String leftDays, + ERecruitmentStatus status, + String recruitmentStartDate, + String recruitmentEndDate, + String createdAt + ) { + this.id = id; + this.title = title; + this.careerCategory = careerCategory; + this.visa = visa; + this.hostName = hostName; + this.organizerName = organizerName; + this.leftDays = leftDays; + this.status = status; + this.recruitmentStartDate = recruitmentStartDate; + this.recruitmentEndDate = recruitmentEndDate; + this.createdAt = createdAt; + } + } +} diff --git a/src/main/java/com/inglo/giggle/career/application/service/CreateAdminsCareerService.java b/src/main/java/com/inglo/giggle/career/application/service/CreateAdminsCareerService.java index b230df73..4f4fe83e 100644 --- a/src/main/java/com/inglo/giggle/career/application/service/CreateAdminsCareerService.java +++ b/src/main/java/com/inglo/giggle/career/application/service/CreateAdminsCareerService.java @@ -52,15 +52,18 @@ public void execute(List images, CreateAdminsCareerRequestDto req List imageUrls; if (images != null && !images.isEmpty()) { + UUID serialId = UUID.randomUUID(); + imageUrls = images.stream().map(image -> s3Util.uploadImageFile( image, - UUID.randomUUID().toString(), + serialId.toString(), EImageType.CAREER_IMG )).toList(); List careerImages = imageUrls.stream() .map(url -> CareerImage.builder() .career(career) + .serialId(serialId) .imgUrl(url) .build()) .toList(); diff --git a/src/main/java/com/inglo/giggle/career/application/service/DeleteAdminsCareerService.java b/src/main/java/com/inglo/giggle/career/application/service/DeleteAdminsCareerService.java new file mode 100644 index 00000000..c0db2e29 --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/service/DeleteAdminsCareerService.java @@ -0,0 +1,42 @@ +package com.inglo.giggle.career.application.service; + +import com.inglo.giggle.career.application.usecase.DeleteAdminsCareerUseCase; +import com.inglo.giggle.career.domain.Career; +import com.inglo.giggle.career.domain.CareerImage; +import com.inglo.giggle.career.repository.CareerImageRepository; +import com.inglo.giggle.career.repository.CareerRepository; +import com.inglo.giggle.core.type.EImageType; +import com.inglo.giggle.core.utility.S3Util; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class DeleteAdminsCareerService implements DeleteAdminsCareerUseCase { + + private final CareerRepository careerRepository; + private final CareerImageRepository careerImageRepository; + + private final S3Util s3Util; + + @Override + @Transactional + public void execute(Long careerId) { + List careerImages = careerImageRepository.findAllByCareerId(careerId); + + careerImages.forEach( careerImage -> { + s3Util.deleteFile( + careerImage.getImgUrl(), + EImageType.CAREER_IMG, + careerImage.getSerialId().toString() + ); + careerImageRepository.delete(careerImage); + }); + + Career career = careerRepository.findByIdOrElseThrow(careerId); + careerRepository.delete(career); + } +} diff --git a/src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersDetailsService.java b/src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersDetailsService.java new file mode 100644 index 00000000..7de82758 --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersDetailsService.java @@ -0,0 +1,58 @@ +package com.inglo.giggle.career.application.service; + +import com.inglo.giggle.career.application.dto.response.ReadAdminsCareersDetailsResponseDto; +import com.inglo.giggle.career.application.usecase.ReadAdminsCareersDetailsUseCase; +import com.inglo.giggle.career.domain.Career; +import com.inglo.giggle.career.domain.CareerImage; +import com.inglo.giggle.career.repository.CareerImageRepository; +import com.inglo.giggle.career.repository.CareerRepository; +import com.inglo.giggle.core.utility.DateTimeUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ReadAdminsCareersDetailsService implements ReadAdminsCareersDetailsUseCase { + + private final CareerRepository careerRepository; + private final CareerImageRepository careerImageRepository; + + @Override + @Transactional(readOnly = true) + public ReadAdminsCareersDetailsResponseDto execute(Long careerId) { + + List careerImages = careerImageRepository.findByCareerId(careerId); + Career career = careerRepository.findByIdOrElseThrow(careerId); + + List imgs = careerImages.stream().map(careerImage -> + ReadAdminsCareersDetailsResponseDto.imgs.builder() + .id(careerImage.getId()) + .url(careerImage.getImgUrl()) + .build() + ).toList(); + + return ReadAdminsCareersDetailsResponseDto.builder() + .imgs(imgs) + .title(career.getTitle()) + .careerCategory(career.getCategory()) + .hostName(career.getHost()) + .organizerName(career.getOrganizer()) + .recruitmentStartDate(DateTimeUtil.convertLocalDateToString(career.getStartDate())) + .recruitmentEndDate(DateTimeUtil.convertLocalDateToString(career.getEndDate())) + .reward(career.getReward()) + .visa(new HashSet<>(career.getVisa())) + .recruitmentNumber(career.getRecruitmentNumber()) + .education(career.getEducationLevel()) + .preferredConditions(career.getPreferredConditions()) + .details(career.getDetails()) + .address(career.getAddress()) + .applicationUrl(career.getApplicationUrl()) + .build(); + } +} diff --git a/src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersOverviewsService.java b/src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersOverviewsService.java new file mode 100644 index 00000000..9e0e9055 --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/service/ReadAdminsCareersOverviewsService.java @@ -0,0 +1,111 @@ +package com.inglo.giggle.career.application.service; + +import com.inglo.giggle.career.application.dto.response.ReadAdminsCareersOverviewsResponseDto; +import com.inglo.giggle.career.application.usecase.ReadAdminsCareersOverviewsUseCase; +import com.inglo.giggle.career.domain.Career; +import com.inglo.giggle.career.domain.type.ECareerCategory; +import com.inglo.giggle.career.domain.type.ERecruitmentStatus; +import com.inglo.giggle.career.repository.CareerRepository; +import com.inglo.giggle.core.utility.EnumParseUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReadAdminsCareersOverviewsService implements ReadAdminsCareersOverviewsUseCase { + + // 북마크 많은 순 + private static final String TRENDING = "POPULAR"; + + // 최신순 + private static final String RECENT = "RECENT"; + + private static final String END = "end"; + + private final CareerRepository careerRepository; + + @Override + @Transactional(readOnly = true) + public ReadAdminsCareersOverviewsResponseDto execute( + int page, + int size, + String search, + String sorting, + String category + ) { + Pageable pageable = PageRequest.of(page - 1, size); + List categories; + if(category == null || category.isBlank()) { + categories = List.of( + ECareerCategory.ACTIVITY, + ECareerCategory.PROGRAM, + ECareerCategory.CONTEST, + ECareerCategory.CLUB + ); + } else { + categories = EnumParseUtil.parseEnums(category, ECareerCategory.class); + } + + Page careerPage = switch (sorting.toUpperCase()) { + case TRENDING -> careerRepository.findCareersOrderByBookMarks(search, categories, pageable); + case RECENT -> careerRepository.findCareersOrderByCreatedAt(search, categories, pageable); + default -> careerRepository.findCareersOrderByBookMarks(search, categories, pageable); + }; + + List dtos = careerPage.getContent().stream() + .map(this::toDto) + .toList(); + + return ReadAdminsCareersOverviewsResponseDto.builder() + .careerList(dtos) + .hasNext(careerPage.hasNext()) + .build(); + } + + private ReadAdminsCareersOverviewsResponseDto.CareerOverviewDto toDto(Career career) { + return ReadAdminsCareersOverviewsResponseDto.CareerOverviewDto.builder() + .id(career.getId()) + .title(career.getTitle()) + .careerCategory(career.getCategory()) + .visa(new ArrayList<>(career.getVisa())) + .hostName(career.getHost()) + .organizerName(career.getOrganizer()) + .leftDays(calculateLeftDays(career.getStartDate(), career.getEndDate())) + .status(calculateRecruitmentStatus(career.getStartDate(), career.getEndDate())) + .recruitmentStartDate(career.getStartDate().toString()) + .recruitmentEndDate(career.getEndDate().toString()) + .createdAt(career.getCreatedAt().toString()) + .build(); + } + + private String calculateLeftDays(LocalDate startDate, LocalDate endDate) { + LocalDate today = LocalDate.now(); + Long days; + if (today.isBefore(startDate)) { + days = ChronoUnit.DAYS.between(LocalDate.now(), startDate); + } + else if (today.isEqual(startDate) || today.isAfter(startDate)) { + days = ChronoUnit.DAYS.between(LocalDate.now(), endDate); + } + else { + days = null; + } + return days != null ? days + " days left" : END; + } + + private ERecruitmentStatus calculateRecruitmentStatus(LocalDate start, LocalDate end) { + LocalDate today = LocalDate.now(); + if (today.isBefore(start)) return ERecruitmentStatus.PRE_RECRUITMENT; + if (!today.isAfter(end)) return ERecruitmentStatus.RECRUITMENT; + return ERecruitmentStatus.CLOSED; + } +} diff --git a/src/main/java/com/inglo/giggle/career/application/service/UpdateAdminsCareerService.java b/src/main/java/com/inglo/giggle/career/application/service/UpdateAdminsCareerService.java new file mode 100644 index 00000000..bcd404e4 --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/service/UpdateAdminsCareerService.java @@ -0,0 +1,85 @@ +package com.inglo.giggle.career.application.service; + +import com.inglo.giggle.career.application.dto.request.UpdateAdminsCareerRequestDto; +import com.inglo.giggle.career.application.usecase.UpdateAdminsCareerUseCase; +import com.inglo.giggle.career.domain.Career; +import com.inglo.giggle.career.domain.CareerImage; +import com.inglo.giggle.career.repository.CareerImageRepository; +import com.inglo.giggle.career.repository.CareerRepository; +import com.inglo.giggle.core.type.EImageType; +import com.inglo.giggle.core.utility.DateTimeUtil; +import com.inglo.giggle.core.utility.S3Util; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UpdateAdminsCareerService implements UpdateAdminsCareerUseCase { + + private final CareerRepository careerRepository; + private final CareerImageRepository careerImageRepository; + + private final S3Util s3Util; + + @Override + @Transactional + public void execute(Long careerId, List images, UpdateAdminsCareerRequestDto requestDto) { + + Career career = careerRepository.findByIdOrElseThrow(careerId); + career.updateSelf( + requestDto.title(), + requestDto.hostName(), + requestDto.organizerName(), + requestDto.address(), + DateTimeUtil.convertStringToLocalDate(requestDto.recruitmentStartDate()), + DateTimeUtil.convertStringToLocalDate(requestDto.recruitmentEndDate()), + requestDto.reward(), + requestDto.visa(), + requestDto.recruitmentNumber(), + requestDto.education(), + requestDto.preferredConditions(), + requestDto.careerCategory(), + requestDto.details(), + requestDto.applicationUrl() + ); + + if(requestDto.deleteImgIds() != null && !requestDto.deleteImgIds().isEmpty()) { + careerImageRepository.findAllById(requestDto.deleteImgIds()) + .forEach(careerImage -> { + s3Util.deleteFile( + careerImage.getImgUrl(), + EImageType.CAREER_IMG, + careerImage.getSerialId().toString() + ); + careerImageRepository.delete(careerImage); + }); + } + + if(images != null && !images.isEmpty()) { + UUID serialId = UUID.randomUUID(); + + List imageUrls = images.stream().map(image -> s3Util.uploadImageFile( + image, + serialId.toString(), + EImageType.CAREER_IMG + )).toList(); + + List careerImages = imageUrls.stream() + .map(url -> CareerImage.builder() + .imgUrl(url) + .serialId(serialId) + .career(career) + .build()) + .toList(); + + careerImageRepository.saveAll(careerImages); + } + + careerRepository.save(career); + } +} diff --git a/src/main/java/com/inglo/giggle/career/application/usecase/DeleteAdminsCareerUseCase.java b/src/main/java/com/inglo/giggle/career/application/usecase/DeleteAdminsCareerUseCase.java new file mode 100644 index 00000000..f198b55a --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/usecase/DeleteAdminsCareerUseCase.java @@ -0,0 +1,11 @@ +package com.inglo.giggle.career.application.usecase; + +import com.inglo.giggle.core.annotation.bean.UseCase; + +@UseCase +public interface DeleteAdminsCareerUseCase { + + void execute( + Long careerId + ); +} diff --git a/src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersDetailsUseCase.java b/src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersDetailsUseCase.java new file mode 100644 index 00000000..64877b2d --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersDetailsUseCase.java @@ -0,0 +1,12 @@ +package com.inglo.giggle.career.application.usecase; + +import com.inglo.giggle.career.application.dto.response.ReadAdminsCareersDetailsResponseDto; +import com.inglo.giggle.core.annotation.bean.UseCase; + +@UseCase +public interface ReadAdminsCareersDetailsUseCase { + + ReadAdminsCareersDetailsResponseDto execute( + Long careerId + ); +} diff --git a/src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersOverviewsUseCase.java b/src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersOverviewsUseCase.java new file mode 100644 index 00000000..cd06936f --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/usecase/ReadAdminsCareersOverviewsUseCase.java @@ -0,0 +1,16 @@ +package com.inglo.giggle.career.application.usecase; + +import com.inglo.giggle.career.application.dto.response.ReadAdminsCareersOverviewsResponseDto; +import com.inglo.giggle.core.annotation.bean.UseCase; + +@UseCase +public interface ReadAdminsCareersOverviewsUseCase { + + ReadAdminsCareersOverviewsResponseDto execute( + int page, + int size, + String search, + String sorting, + String category + ); +} diff --git a/src/main/java/com/inglo/giggle/career/application/usecase/UpdateAdminsCareerUseCase.java b/src/main/java/com/inglo/giggle/career/application/usecase/UpdateAdminsCareerUseCase.java new file mode 100644 index 00000000..ee55480d --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/application/usecase/UpdateAdminsCareerUseCase.java @@ -0,0 +1,17 @@ +package com.inglo.giggle.career.application.usecase; + +import com.inglo.giggle.career.application.dto.request.UpdateAdminsCareerRequestDto; +import com.inglo.giggle.core.annotation.bean.UseCase; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@UseCase +public interface UpdateAdminsCareerUseCase { + + void execute( + Long careerId, + List images, + UpdateAdminsCareerRequestDto requestDto + ); +} diff --git a/src/main/java/com/inglo/giggle/career/controller/command/CareerAdminsCommandV1Controller.java b/src/main/java/com/inglo/giggle/career/controller/command/CareerAdminsCommandV1Controller.java index a8cffd59..084051df 100644 --- a/src/main/java/com/inglo/giggle/career/controller/command/CareerAdminsCommandV1Controller.java +++ b/src/main/java/com/inglo/giggle/career/controller/command/CareerAdminsCommandV1Controller.java @@ -1,15 +1,15 @@ package com.inglo.giggle.career.controller.command; import com.inglo.giggle.career.application.dto.request.CreateAdminsCareerRequestDto; +import com.inglo.giggle.career.application.dto.request.UpdateAdminsCareerRequestDto; import com.inglo.giggle.career.application.usecase.CreateAdminsCareerUseCase; +import com.inglo.giggle.career.application.usecase.DeleteAdminsCareerUseCase; +import com.inglo.giggle.career.application.usecase.UpdateAdminsCareerUseCase; import com.inglo.giggle.core.dto.ResponseDto; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -20,6 +20,8 @@ public class CareerAdminsCommandV1Controller { private final CreateAdminsCareerUseCase createAdminsCareerUseCase; + private final UpdateAdminsCareerUseCase updateAdminsCareerUseCase; + private final DeleteAdminsCareerUseCase deleteAdminsCareerUseCase; /** * 14.5 (관리자) 커리어 추가히기 @@ -36,4 +38,33 @@ public ResponseDto createCareer( return ResponseDto.created(null); } + + /** + * 14.9 (관리자) 커리어 수정하기 + */ + @PutMapping(value = "/admins/careers/{careerId}", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public ResponseDto updateCareer( + @PathVariable(value = "careerId") Long careerId, + @RequestPart(value = "image", required = false) List images, + @Valid @RequestPart(value = "body") UpdateAdminsCareerRequestDto requestDto + ) { + updateAdminsCareerUseCase.execute( + careerId, + images, + requestDto + ); + + return ResponseDto.ok(null); + } + + /** + * 14.10 (관리자) 커리어 삭제하기 + */ + @DeleteMapping("/admins/careers/{careerId}") + public ResponseDto deleteCareer( + @PathVariable(value = "careerId") Long careerId + ) { + deleteAdminsCareerUseCase.execute(careerId); + return ResponseDto.ok(null); + } } diff --git a/src/main/java/com/inglo/giggle/career/controller/query/CareerAdminQueryV1Controller.java b/src/main/java/com/inglo/giggle/career/controller/query/CareerAdminQueryV1Controller.java new file mode 100644 index 00000000..3d51beb0 --- /dev/null +++ b/src/main/java/com/inglo/giggle/career/controller/query/CareerAdminQueryV1Controller.java @@ -0,0 +1,54 @@ +package com.inglo.giggle.career.controller.query; + +import com.inglo.giggle.career.application.dto.response.ReadAdminsCareersDetailsResponseDto; +import com.inglo.giggle.career.application.dto.response.ReadAdminsCareersOverviewsResponseDto; +import com.inglo.giggle.career.application.usecase.ReadAdminsCareersDetailsUseCase; +import com.inglo.giggle.career.application.usecase.ReadAdminsCareersOverviewsUseCase; +import com.inglo.giggle.core.dto.ResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1") +public class CareerAdminQueryV1Controller { + + private final ReadAdminsCareersOverviewsUseCase readAdminsCareersOverviewsUseCase; + private final ReadAdminsCareersDetailsUseCase readAdminsCareersDetailsUseCase; + + /** + * 14.7 (어드민) 커리어 리스트 조회 + */ + @GetMapping("/admins/careers/overviews") + public ResponseDto readAdminsCareersOverviews( + @RequestParam(value = "page") Integer page, + @RequestParam(value = "size") Integer size, + @RequestParam(value = "search", required = false) String search, + @RequestParam(value = "sorting", required = false) String sorting, + @RequestParam(value = "category", required = false) String category + ) { + return ResponseDto.ok( + readAdminsCareersOverviewsUseCase.execute( + page, + size, + search, + sorting, + category + ) + ); + } + + /** + * 14.8 (어드민) 커리어 디테일 조회 + */ + @GetMapping("/admins/careers/{career-id}/details") + public ResponseDto readAdminsCareersDetails( + @PathVariable(value = "career-id") Long careerId + ) { + return ResponseDto.ok( + readAdminsCareersDetailsUseCase.execute( + careerId + ) + ); + } +} diff --git a/src/main/java/com/inglo/giggle/career/domain/Career.java b/src/main/java/com/inglo/giggle/career/domain/Career.java index ad34d923..c070d489 100644 --- a/src/main/java/com/inglo/giggle/career/domain/Career.java +++ b/src/main/java/com/inglo/giggle/career/domain/Career.java @@ -132,4 +132,36 @@ public Career( this.details = details; this.applicationUrl = applicationUrl; } + + public void updateSelf( + String title, + String host, + String organizer, + String address, + LocalDate startDate, + LocalDate endDate, + Integer reward, + Set visa, + Integer recruitmentNumber, + EEducationLevel educationLevel, + String preferredConditions, + ECareerCategory category, + String details, + String applicationUrl + ) { + this.title = title; + this.host = host; + this.organizer = organizer; + this.address = address; + this.startDate = startDate; + this.endDate = endDate; + this.reward = reward; + this.visa = visa; + this.recruitmentNumber = recruitmentNumber; + this.educationLevel = educationLevel; + this.preferredConditions = preferredConditions; + this.category = category; + this.details = details; + this.applicationUrl = applicationUrl; + } } diff --git a/src/main/java/com/inglo/giggle/career/domain/CareerImage.java b/src/main/java/com/inglo/giggle/career/domain/CareerImage.java index 8657e7e8..7e3a75ff 100644 --- a/src/main/java/com/inglo/giggle/career/domain/CareerImage.java +++ b/src/main/java/com/inglo/giggle/career/domain/CareerImage.java @@ -9,6 +9,8 @@ import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; +import java.util.UUID; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -31,6 +33,9 @@ public class CareerImage extends BaseEntity { @Column(name = "img_url", nullable = false) private String imgUrl; + @Column(name = "serial_id", nullable = false) + private UUID serialId; + /* -------------------------------------------- */ /* Many To One Mapping ------------------------ */ /* -------------------------------------------- */ @@ -42,8 +47,13 @@ public class CareerImage extends BaseEntity { /* Methods ------------------------------------ */ /* -------------------------------------------- */ @Builder - public CareerImage(String imgUrl, Career career) { + public CareerImage( + String imgUrl, + UUID serialId, + Career career + ) { this.imgUrl = imgUrl; + this.serialId = serialId; this.career = career; } } \ No newline at end of file diff --git a/src/main/java/com/inglo/giggle/career/repository/CareerImageRepository.java b/src/main/java/com/inglo/giggle/career/repository/CareerImageRepository.java index 87fc7e2f..510d1d08 100644 --- a/src/main/java/com/inglo/giggle/career/repository/CareerImageRepository.java +++ b/src/main/java/com/inglo/giggle/career/repository/CareerImageRepository.java @@ -8,4 +8,10 @@ public interface CareerImageRepository { List findByCareerId(Long careerId); void saveAll(List careerImages); + + List findAllById(List careerImageIds); + + void delete(CareerImage careerImage); + + List findAllByCareerId(Long careerId); } diff --git a/src/main/java/com/inglo/giggle/career/repository/CareerRepository.java b/src/main/java/com/inglo/giggle/career/repository/CareerRepository.java index ad7cb9c8..e9c9f82c 100644 --- a/src/main/java/com/inglo/giggle/career/repository/CareerRepository.java +++ b/src/main/java/com/inglo/giggle/career/repository/CareerRepository.java @@ -17,4 +17,6 @@ public interface CareerRepository { Career findByIdOrElseThrow(Long careerId); void save(Career career); + + void delete(Career career); } diff --git a/src/main/java/com/inglo/giggle/career/repository/impl/CareerImageRepositoryImpl.java b/src/main/java/com/inglo/giggle/career/repository/impl/CareerImageRepositoryImpl.java index eff9eb56..c9d22c84 100644 --- a/src/main/java/com/inglo/giggle/career/repository/impl/CareerImageRepositoryImpl.java +++ b/src/main/java/com/inglo/giggle/career/repository/impl/CareerImageRepositoryImpl.java @@ -23,4 +23,19 @@ public List findByCareerId(Long careerId) { public void saveAll(List careerImages) { careerImageJpaRepository.saveAll(careerImages); } + + @Override + public List findAllById(List careerImageIds) { + return careerImageJpaRepository.findAllById(careerImageIds); + } + + @Override + public void delete(CareerImage careerImage) { + careerImageJpaRepository.delete(careerImage); + } + + @Override + public List findAllByCareerId(Long careerId) { + return careerImageJpaRepository.findAllByCareerId(careerId); + } } diff --git a/src/main/java/com/inglo/giggle/career/repository/impl/CareerRepositoryImpl.java b/src/main/java/com/inglo/giggle/career/repository/impl/CareerRepositoryImpl.java index 07e14f2a..10f94690 100644 --- a/src/main/java/com/inglo/giggle/career/repository/impl/CareerRepositoryImpl.java +++ b/src/main/java/com/inglo/giggle/career/repository/impl/CareerRepositoryImpl.java @@ -113,6 +113,11 @@ public void save(Career career) { careerJpaRepository.save(career); } + @Override + public void delete(Career career) { + careerJpaRepository.delete(career); + } + private BooleanExpression applyFilters(QCareer career, String search, List categories) { BooleanExpression predicate = career.deletedAt.isNull(); diff --git a/src/main/java/com/inglo/giggle/career/repository/mysql/CareerImageJpaRepository.java b/src/main/java/com/inglo/giggle/career/repository/mysql/CareerImageJpaRepository.java index 474516f5..a6f56aa9 100644 --- a/src/main/java/com/inglo/giggle/career/repository/mysql/CareerImageJpaRepository.java +++ b/src/main/java/com/inglo/giggle/career/repository/mysql/CareerImageJpaRepository.java @@ -7,4 +7,6 @@ public interface CareerImageJpaRepository extends JpaRepository { List findByCareerId(Long careerId); + + List findAllByCareerId(Long careerId); } diff --git a/src/main/java/com/inglo/giggle/posting/application/dto/response/ReadOwnersJobPostingUserOwnerJobPostingUserOverviewsResponseDto.java b/src/main/java/com/inglo/giggle/posting/application/dto/response/ReadOwnersJobPostingUserOwnerJobPostingUserOverviewsResponseDto.java index 47d46e47..2941591e 100644 --- a/src/main/java/com/inglo/giggle/posting/application/dto/response/ReadOwnersJobPostingUserOwnerJobPostingUserOverviewsResponseDto.java +++ b/src/main/java/com/inglo/giggle/posting/application/dto/response/ReadOwnersJobPostingUserOwnerJobPostingUserOverviewsResponseDto.java @@ -10,6 +10,7 @@ import java.time.LocalDate; import java.util.List; +import java.util.UUID; @Getter public class ReadOwnersJobPostingUserOwnerJobPostingUserOverviewsResponseDto extends SelfValidating { @@ -46,6 +47,10 @@ public static class ApplicantOverviewDto extends SelfValidating