diff --git a/src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java b/src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java new file mode 100644 index 000000000..57035537d --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/controller/AdminHostUniversityController.java @@ -0,0 +1,71 @@ +package com.example.solidconnection.admin.university.controller; + +import com.example.solidconnection.admin.university.dto.AdminHostUniversityCreateRequest; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityDetailResponse; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityResponse; +import com.example.solidconnection.admin.university.dto.AdminHostUniversitySearchCondition; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityUpdateRequest; +import com.example.solidconnection.admin.university.service.AdminHostUniversityService; +import com.example.solidconnection.common.response.PageResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RequestMapping("/admin/host-universities") +@RestController +public class AdminHostUniversityController { + + private final AdminHostUniversityService adminHostUniversityService; + + @GetMapping + public ResponseEntity> getHostUniversities( + AdminHostUniversitySearchCondition condition, + @PageableDefault(size = 20) Pageable pageable + ) { + return ResponseEntity.ok(PageResponse.of(adminHostUniversityService.getHostUniversities(condition, pageable))); + } + + @GetMapping("/{host-university-id}") + public ResponseEntity getHostUniversity( + @PathVariable("host-university-id") Long hostUniversityId + ) { + AdminHostUniversityDetailResponse response = adminHostUniversityService.getHostUniversity(hostUniversityId); + return ResponseEntity.ok(response); + } + + @PostMapping + public ResponseEntity createHostUniversity( + @Valid @RequestBody AdminHostUniversityCreateRequest request + ) { + AdminHostUniversityDetailResponse response = adminHostUniversityService.createHostUniversity(request); + return ResponseEntity.ok(response); + } + + @PutMapping("/{host-university-id}") + public ResponseEntity updateHostUniversity( + @PathVariable("host-university-id") Long hostUniversityId, + @Valid @RequestBody AdminHostUniversityUpdateRequest request + ) { + AdminHostUniversityDetailResponse response = adminHostUniversityService.updateHostUniversity(hostUniversityId, request); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{host-university-id}") + public ResponseEntity deleteHostUniversity( + @PathVariable("host-university-id") Long hostUniversityId + ) { + adminHostUniversityService.deleteHostUniversity(hostUniversityId); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java new file mode 100644 index 000000000..6b77061b6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityCreateRequest.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.admin.university.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminHostUniversityCreateRequest( + @NotBlank(message = "한글 대학명은 필수입니다") + @Size(max = 100, message = "한글 대학명은 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "영문 대학명은 필수입니다") + @Size(max = 100, message = "영문 대학명은 100자 이하여야 합니다") + String englishName, + + @NotBlank(message = "표시 대학명은 필수입니다") + @Size(max = 100, message = "표시 대학명은 100자 이하여야 합니다") + String formatName, + + @Size(max = 500, message = "홈페이지 URL은 500자 이하여야 합니다") + String homepageUrl, + + @Size(max = 500, message = "영어 강좌 URL은 500자 이하여야 합니다") + String englishCourseUrl, + + @Size(max = 500, message = "숙소 URL은 500자 이하여야 합니다") + String accommodationUrl, + + @NotBlank(message = "로고 이미지 URL은 필수입니다") + @Size(max = 500, message = "로고 이미지 URL은 500자 이하여야 합니다") + String logoImageUrl, + + @NotBlank(message = "배경 이미지 URL은 필수입니다") + @Size(max = 500, message = "배경 이미지 URL은 500자 이하여야 합니다") + String backgroundImageUrl, + + @Size(max = 1000, message = "상세 정보는 1000자 이하여야 합니다") + String detailsForLocal, + + @NotBlank(message = "국가 코드는 필수입니다") + String countryCode, + + @NotBlank(message = "지역 코드는 필수입니다") + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityDetailResponse.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityDetailResponse.java new file mode 100644 index 000000000..1630f5066 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityDetailResponse.java @@ -0,0 +1,40 @@ +package com.example.solidconnection.admin.university.dto; + +import com.example.solidconnection.university.domain.HostUniversity; + +public record AdminHostUniversityDetailResponse( + Long id, + String koreanName, + String englishName, + String formatName, + String homepageUrl, + String englishCourseUrl, + String accommodationUrl, + String logoImageUrl, + String backgroundImageUrl, + String detailsForLocal, + String countryCode, + String countryKoreanName, + String regionCode, + String regionKoreanName +) { + + public static AdminHostUniversityDetailResponse from(HostUniversity hostUniversity) { + return new AdminHostUniversityDetailResponse( + hostUniversity.getId(), + hostUniversity.getKoreanName(), + hostUniversity.getEnglishName(), + hostUniversity.getFormatName(), + hostUniversity.getHomepageUrl(), + hostUniversity.getEnglishCourseUrl(), + hostUniversity.getAccommodationUrl(), + hostUniversity.getLogoImageUrl(), + hostUniversity.getBackgroundImageUrl(), + hostUniversity.getDetailsForLocal(), + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getCode() : null, + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getKoreanName() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getCode() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getKoreanName() : null + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityResponse.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityResponse.java new file mode 100644 index 000000000..12975c0a5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityResponse.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.admin.university.dto; + +import com.example.solidconnection.university.domain.HostUniversity; + +public record AdminHostUniversityResponse( + Long id, + String koreanName, + String englishName, + String formatName, + String logoImageUrl, + String countryCode, + String countryKoreanName, + String regionCode, + String regionKoreanName +) { + + public static AdminHostUniversityResponse from(HostUniversity hostUniversity) { + return new AdminHostUniversityResponse( + hostUniversity.getId(), + hostUniversity.getKoreanName(), + hostUniversity.getEnglishName(), + hostUniversity.getFormatName(), + hostUniversity.getLogoImageUrl(), + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getCode() : null, + hostUniversity.getCountry() != null ? hostUniversity.getCountry().getKoreanName() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getCode() : null, + hostUniversity.getRegion() != null ? hostUniversity.getRegion().getKoreanName() : null + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversitySearchCondition.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversitySearchCondition.java new file mode 100644 index 000000000..cbf13ec56 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversitySearchCondition.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.admin.university.dto; + +public record AdminHostUniversitySearchCondition( + String keyword, + String countryCode, + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java new file mode 100644 index 000000000..cb2e64a74 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/dto/AdminHostUniversityUpdateRequest.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.admin.university.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminHostUniversityUpdateRequest( + @NotBlank(message = "한글 대학명은 필수입니다") + @Size(max = 100, message = "한글 대학명은 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "영문 대학명은 필수입니다") + @Size(max = 100, message = "영문 대학명은 100자 이하여야 합니다") + String englishName, + + @NotBlank(message = "표시 대학명은 필수입니다") + @Size(max = 100, message = "표시 대학명은 100자 이하여야 합니다") + String formatName, + + @Size(max = 500, message = "홈페이지 URL은 500자 이하여야 합니다") + String homepageUrl, + + @Size(max = 500, message = "영어 강좌 URL은 500자 이하여야 합니다") + String englishCourseUrl, + + @Size(max = 500, message = "숙소 URL은 500자 이하여야 합니다") + String accommodationUrl, + + @NotBlank(message = "로고 이미지 URL은 필수입니다") + @Size(max = 500, message = "로고 이미지 URL은 500자 이하여야 합니다") + String logoImageUrl, + + @NotBlank(message = "배경 이미지 URL은 필수입니다") + @Size(max = 500, message = "배경 이미지 URL은 500자 이하여야 합니다") + String backgroundImageUrl, + + @Size(max = 1000, message = "상세 정보는 1000자 이하여야 합니다") + String detailsForLocal, + + @NotBlank(message = "국가 코드는 필수입니다") + String countryCode, + + @NotBlank(message = "지역 코드는 필수입니다") + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java b/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java new file mode 100644 index 000000000..c03e9f526 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java @@ -0,0 +1,152 @@ +package com.example.solidconnection.admin.university.service; + +import static com.example.solidconnection.common.exception.ErrorCode.COUNTRY_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS; +import static com.example.solidconnection.common.exception.ErrorCode.HOST_UNIVERSITY_HAS_REFERENCES; +import static com.example.solidconnection.common.exception.ErrorCode.REGION_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_NOT_FOUND; + +import com.example.solidconnection.admin.university.dto.AdminHostUniversityCreateRequest; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityDetailResponse; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityResponse; +import com.example.solidconnection.admin.university.dto.AdminHostUniversitySearchCondition; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityUpdateRequest; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.location.country.domain.Country; +import com.example.solidconnection.location.country.repository.CountryRepository; +import com.example.solidconnection.location.region.domain.Region; +import com.example.solidconnection.location.region.repository.RegionRepository; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.HostUniversityRepository; +import com.example.solidconnection.university.repository.UnivApplyInfoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AdminHostUniversityService { + + private final HostUniversityRepository hostUniversityRepository; + private final CountryRepository countryRepository; + private final RegionRepository regionRepository; + private final UnivApplyInfoRepository univApplyInfoRepository; + + @Transactional(readOnly = true) + public Page getHostUniversities( + AdminHostUniversitySearchCondition condition, + Pageable pageable + ) { + Page hostUniversityPage = hostUniversityRepository.findAllBySearchCondition( + condition.keyword(), + condition.countryCode(), + condition.regionCode(), + pageable + ); + return hostUniversityPage.map(AdminHostUniversityResponse::from); + } + + @Transactional(readOnly = true) + public AdminHostUniversityDetailResponse getHostUniversity(Long id) { + HostUniversity hostUniversity = hostUniversityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + return AdminHostUniversityDetailResponse.from(hostUniversity); + } + + @Transactional + public AdminHostUniversityDetailResponse createHostUniversity(AdminHostUniversityCreateRequest request) { + validateKoreanNameNotExists(request.koreanName()); + + Country country = findCountryByCode(request.countryCode()); + Region region = findRegionByCode(request.regionCode()); + + HostUniversity hostUniversity = new HostUniversity( + null, + request.koreanName(), + request.englishName(), + request.formatName(), + request.homepageUrl(), + request.englishCourseUrl(), + request.accommodationUrl(), + request.logoImageUrl(), + request.backgroundImageUrl(), + request.detailsForLocal(), + country, + region + ); + + HostUniversity savedHostUniversity = hostUniversityRepository.save(hostUniversity); + return AdminHostUniversityDetailResponse.from(savedHostUniversity); + } + + private void validateKoreanNameNotExists(String koreanName) { + hostUniversityRepository.findByKoreanName(koreanName) + .ifPresent(existingUniversity -> { + throw new CustomException(HOST_UNIVERSITY_ALREADY_EXISTS); + }); + } + + @Transactional + public AdminHostUniversityDetailResponse updateHostUniversity(Long id, AdminHostUniversityUpdateRequest request) { + HostUniversity hostUniversity = hostUniversityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + + validateKoreanNameNotDuplicated(request.koreanName(), id); + + Country country = findCountryByCode(request.countryCode()); + Region region = findRegionByCode(request.regionCode()); + + hostUniversity.update( + request.koreanName(), + request.englishName(), + request.formatName(), + request.homepageUrl(), + request.englishCourseUrl(), + request.accommodationUrl(), + request.logoImageUrl(), + request.backgroundImageUrl(), + request.detailsForLocal(), + country, + region + ); + + return AdminHostUniversityDetailResponse.from(hostUniversity); + } + + private void validateKoreanNameNotDuplicated(String koreanName, Long excludeId) { + hostUniversityRepository.findByKoreanName(koreanName) + .ifPresent(existingUniversity -> { + if (!existingUniversity.getId().equals(excludeId)) { + throw new CustomException(HOST_UNIVERSITY_ALREADY_EXISTS); + } + }); + } + + private Country findCountryByCode(String countryCode) { + return countryRepository.findByCode(countryCode) + .orElseThrow(() -> new CustomException(COUNTRY_NOT_FOUND)); + } + + private Region findRegionByCode(String regionCode) { + return regionRepository.findById(regionCode) + .orElseThrow(() -> new CustomException(REGION_NOT_FOUND)); + } + + @Transactional + public void deleteHostUniversity(Long id) { + HostUniversity hostUniversity = hostUniversityRepository.findById(id) + .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); + + validateNoReferences(id); + + hostUniversityRepository.delete(hostUniversity); + } + + private void validateNoReferences(Long hostUniversityId) { + if (univApplyInfoRepository.existsByUniversityId(hostUniversityId)) { + throw new CustomException(HOST_UNIVERSITY_HAS_REFERENCES); + } + } +} diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index d00ce52b3..83aeaf2aa 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -41,6 +41,9 @@ public enum ErrorCode { REGION_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "지역을 찾을 수 없습니다."), REGION_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 지역을 찾을 수 없습니다."), REGION_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 지역입니다."), + HOST_UNIVERSITY_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 파견 대학입니다."), + HOST_UNIVERSITY_HAS_REFERENCES(HttpStatus.CONFLICT.value(), "해당 파견 대학을 참조하는 대학 지원 정보가 존재합니다."), + COUNTRY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "국가를 찾을 수 없습니다."), COUNTRY_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 국가를 찾을 수 없습니다."), GPA_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 학점입니다."), LANGUAGE_TEST_SCORE_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 어학성적입니다."), diff --git a/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java b/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java index 8b997a2de..7aaf8ff56 100644 --- a/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java +++ b/src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java @@ -2,12 +2,15 @@ import com.example.solidconnection.location.country.domain.Country; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface CountryRepository extends JpaRepository { + Optional findByCode(String code); + List findAllByKoreanNameIn(List koreanNames); @Query(""" diff --git a/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java b/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java index 3d817b45f..7ae461f67 100644 --- a/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java +++ b/src/main/java/com/example/solidconnection/university/domain/HostUniversity.java @@ -56,4 +56,30 @@ public class HostUniversity extends BaseEntity { @ManyToOne private Region region; + + public void update( + String koreanName, + String englishName, + String formatName, + String homepageUrl, + String englishCourseUrl, + String accommodationUrl, + String logoImageUrl, + String backgroundImageUrl, + String detailsForLocal, + Country country, + Region region + ) { + this.koreanName = koreanName; + this.englishName = englishName; + this.formatName = formatName; + this.homepageUrl = homepageUrl; + this.englishCourseUrl = englishCourseUrl; + this.accommodationUrl = accommodationUrl; + this.logoImageUrl = logoImageUrl; + this.backgroundImageUrl = backgroundImageUrl; + this.detailsForLocal = detailsForLocal; + this.country = country; + this.region = region; + } } diff --git a/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java b/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java index 09a2ea390..3fa80629a 100644 --- a/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java @@ -4,12 +4,16 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.repository.custom.HostUniversityFilterRepository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface HostUniversityRepository extends JpaRepository { +public interface HostUniversityRepository extends JpaRepository, HostUniversityFilterRepository { default HostUniversity getHostUniversityById(Long id) { return findById(id) .orElseThrow(() -> new CustomException(UNIVERSITY_NOT_FOUND)); } + + Optional findByKoreanName(String koreanName); } diff --git a/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java b/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java index 1cc25ee35..e0b71f8a9 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UnivApplyInfoRepository.java @@ -66,4 +66,6 @@ default UnivApplyInfo getUnivApplyInfoById(Long id) { WHERE uai.id IN :ids """) List findAllByIds(@Param("ids") List ids); + + boolean existsByUniversityId(Long universityId); } diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java new file mode 100644 index 000000000..1e6cbc01f --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.university.domain.HostUniversity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface HostUniversityFilterRepository { + + Page findAllBySearchCondition( + String keyword, + String countryCode, + String regionCode, + Pageable pageable + ); +} diff --git a/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java new file mode 100644 index 000000000..e53ff4f2c --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepositoryImpl.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.university.repository.custom; + +import com.example.solidconnection.location.country.domain.QCountry; +import com.example.solidconnection.location.region.domain.QRegion; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.domain.QHostUniversity; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +@Repository +public class HostUniversityFilterRepositoryImpl implements HostUniversityFilterRepository { + + private final JPAQueryFactory queryFactory; + + @Autowired + public HostUniversityFilterRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page findAllBySearchCondition( + String keyword, + String countryCode, + String regionCode, + Pageable pageable + ) { + QHostUniversity hostUniversity = QHostUniversity.hostUniversity; + QCountry country = QCountry.country; + QRegion region = QRegion.region; + + List content = queryFactory + .selectFrom(hostUniversity) + .leftJoin(hostUniversity.country, country).fetchJoin() + .leftJoin(hostUniversity.region, region).fetchJoin() + .where( + keywordContains(hostUniversity, keyword), + countryCodeEq(country, countryCode), + regionCodeEq(region, regionCode) + ) + .orderBy(hostUniversity.id.asc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(hostUniversity.count()) + .from(hostUniversity) + .leftJoin(hostUniversity.country, country) + .leftJoin(hostUniversity.region, region) + .where( + keywordContains(hostUniversity, keyword), + countryCodeEq(country, countryCode), + regionCodeEq(region, regionCode) + ); + + return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); + } + + private BooleanExpression keywordContains(QHostUniversity hostUniversity, String keyword) { + if (keyword == null || keyword.isBlank()) { + return null; + } + return hostUniversity.koreanName.contains(keyword) + .or(hostUniversity.englishName.containsIgnoreCase(keyword)); + } + + private BooleanExpression countryCodeEq(QCountry country, String countryCode) { + if (countryCode == null || countryCode.isBlank()) { + return null; + } + return country.code.eq(countryCode); + } + + private BooleanExpression regionCodeEq(QRegion region, String regionCode) { + if (regionCode == null || regionCode.isBlank()) { + return null; + } + return region.code.eq(regionCode); + } +} diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java new file mode 100644 index 000000000..620f18a4d --- /dev/null +++ b/src/test/java/com/example/solidconnection/admin/service/AdminHostUniversityServiceTest.java @@ -0,0 +1,401 @@ +package com.example.solidconnection.admin.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import com.example.solidconnection.admin.university.dto.AdminHostUniversityCreateRequest; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityDetailResponse; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityResponse; +import com.example.solidconnection.admin.university.dto.AdminHostUniversitySearchCondition; +import com.example.solidconnection.admin.university.dto.AdminHostUniversityUpdateRequest; +import com.example.solidconnection.admin.university.service.AdminHostUniversityService; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.location.country.domain.Country; +import com.example.solidconnection.location.country.fixture.CountryFixture; +import com.example.solidconnection.location.region.domain.Region; +import com.example.solidconnection.location.region.fixture.RegionFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.university.domain.HostUniversity; +import com.example.solidconnection.university.fixture.UnivApplyInfoFixtureBuilder; +import com.example.solidconnection.university.fixture.UniversityFixture; +import com.example.solidconnection.university.repository.HostUniversityRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +@TestContainerSpringBootTest +@DisplayName("파견 대학 관리 서비스 테스트") +class AdminHostUniversityServiceTest { + + @Autowired + private AdminHostUniversityService adminHostUniversityService; + + @Autowired + private HostUniversityRepository hostUniversityRepository; + + @Autowired + private UniversityFixture universityFixture; + + @Autowired + private CountryFixture countryFixture; + + @Autowired + private RegionFixture regionFixture; + + @Autowired + private UnivApplyInfoFixtureBuilder univApplyInfoFixtureBuilder; + + @Nested + class 목록_조회 { + + @Test + void 대학이_없으면_빈_목록을_반환한다() { + // given + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).isEmpty(); + assertThat(response.getTotalElements()).isZero(); + } + + @Test + void 키워드로_한글명을_검색한다() { + // given + universityFixture.괌_대학(); + HostUniversity target = universityFixture.메이지_대학(); + + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition("메이지", null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(1); + assertThat(response.getContent().get(0).koreanName()).isEqualTo(target.getKoreanName()); + } + + @Test + void 키워드로_영문명을_검색한다() { + // given + universityFixture.괌_대학(); + HostUniversity target = universityFixture.메이지_대학(); + + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition("Meiji", null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(1); + assertThat(response.getContent().get(0).englishName()).isEqualTo(target.getEnglishName()); + } + + @Test + void 국가_코드로_필터링한다() { + // given + universityFixture.괌_대학(); + universityFixture.네바다주립_대학_라스베이거스(); + universityFixture.메이지_대학(); + + Country usa = countryFixture.미국(); + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, usa.getCode(), null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(2); + assertThat(response.getContent()) + .extracting(r -> r.countryCode()) + .containsOnly(usa.getCode()); + } + + @Test + void 지역_코드로_필터링한다() { + // given + universityFixture.괌_대학(); + universityFixture.서던덴마크_대학(); + universityFixture.그라츠_대학(); + + Region europe = regionFixture.유럽(); + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, null, europe.getCode()); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 20)); + + // then + assertThat(response.getContent()).hasSize(2); + assertThat(response.getContent()) + .extracting(r -> r.regionCode()) + .containsOnly(europe.getCode()); + } + + @Test + void 페이징이_정상_작동한다() { + // given + universityFixture.괌_대학(); + universityFixture.네바다주립_대학_라스베이거스(); + universityFixture.메이지_대학(); + + AdminHostUniversitySearchCondition condition = new AdminHostUniversitySearchCondition(null, null, null); + + // when + Page response = adminHostUniversityService.getHostUniversities( + condition, PageRequest.of(0, 2)); + + // then + assertThat(response.getContent()).hasSize(2); + assertThat(response.getTotalElements()).isEqualTo(3); + assertThat(response.getTotalPages()).isEqualTo(2); + assertThat(response.hasNext()).isTrue(); + } + } + + @Nested + class 상세_조회 { + + @Test + void 존재하는_대학을_조회하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.getHostUniversity(university.getId()); + + // then + assertThat(response.id()).isEqualTo(university.getId()); + assertThat(response.koreanName()).isEqualTo(university.getKoreanName()); + assertThat(response.englishName()).isEqualTo(university.getEnglishName()); + } + + @Test + void 존재하지_않는_대학을_조회하면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> adminHostUniversityService.getHostUniversity(999L)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.UNIVERSITY_NOT_FOUND.getMessage()); + } + } + + @Nested + class 생성 { + + @Test + void 유효한_정보로_대학을_생성하면_성공한다() { + // given + Country country = countryFixture.미국(); + Region region = regionFixture.영미권(); + + AdminHostUniversityCreateRequest request = new AdminHostUniversityCreateRequest( + "테스트 대학", + "Test University", + "테스트 대학", + "https://homepage.com", + "https://english-course.com", + "https://accommodation.com", + "https://logo.com/image.png", + "https://background.com/image.png", + "상세 정보", + country.getCode(), + region.getCode() + ); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.createHostUniversity(request); + + // then + assertThat(response.koreanName()).isEqualTo(request.koreanName()); + assertThat(response.englishName()).isEqualTo(request.englishName()); + + HostUniversity savedUniversity = hostUniversityRepository.findById(response.id()).orElseThrow(); + assertThat(savedUniversity.getKoreanName()).isEqualTo(request.koreanName()); + } + + @Test + void 이미_존재하는_한글명으로_생성하면_예외_응답을_반환한다() { + // given + HostUniversity existing = universityFixture.괌_대학(); + Country country = countryFixture.미국(); + Region region = regionFixture.영미권(); + + AdminHostUniversityCreateRequest request = new AdminHostUniversityCreateRequest( + existing.getKoreanName(), + "New English Name", + "표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + country.getCode(), + region.getCode() + ); + + // when & then + assertThatCode(() -> adminHostUniversityService.createHostUniversity(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); + } + } + + @Nested + class 수정 { + + @Test + void 유효한_정보로_대학을_수정하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + Country country = countryFixture.일본(); + Region region = regionFixture.아시아(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + "수정된 대학명", + "Updated University", + "수정된 표시명", + "https://new-homepage.com", + null, null, + "https://new-logo.com/image.png", + "https://new-background.com/image.png", + "수정된 상세 정보", + country.getCode(), + region.getCode() + ); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.updateHostUniversity( + university.getId(), request); + + // then + assertThat(response.koreanName()).isEqualTo(request.koreanName()); + assertThat(response.countryCode()).isEqualTo(country.getCode()); + + HostUniversity updatedUniversity = hostUniversityRepository.findById(university.getId()).orElseThrow(); + assertThat(updatedUniversity.getKoreanName()).isEqualTo(request.koreanName()); + } + + @Test + void 존재하지_않는_대학을_수정하면_예외_응답을_반환한다() { + // given + Country country = countryFixture.미국(); + Region region = regionFixture.영미권(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + "수정된 대학명", + "Updated University", + "수정된 표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + country.getCode(), + region.getCode() + ); + + // when & then + assertThatCode(() -> adminHostUniversityService.updateHostUniversity(999L, request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.UNIVERSITY_NOT_FOUND.getMessage()); + } + + @Test + void 다른_대학의_한글명으로_수정하면_예외_응답을_반환한다() { + // given + HostUniversity university1 = universityFixture.괌_대학(); + HostUniversity university2 = universityFixture.메이지_대학(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + university2.getKoreanName(), + "Updated University", + "수정된 표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + university1.getCountry().getCode(), + university1.getRegion().getCode() + ); + + // when & then + assertThatCode(() -> adminHostUniversityService.updateHostUniversity(university1.getId(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 같은_대학의_한글명으로_수정하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + + AdminHostUniversityUpdateRequest request = new AdminHostUniversityUpdateRequest( + university.getKoreanName(), + "Updated English Name", + "수정된 표시명", + null, null, null, + "https://logo.com/image.png", + "https://background.com/image.png", + null, + university.getCountry().getCode(), + university.getRegion().getCode() + ); + + // when + AdminHostUniversityDetailResponse response = adminHostUniversityService.updateHostUniversity( + university.getId(), request); + + // then + assertThat(response.koreanName()).isEqualTo(university.getKoreanName()); + assertThat(response.englishName()).isEqualTo(request.englishName()); + } + } + + @Nested + class 삭제 { + + @Test + void 존재하는_대학을_삭제하면_성공한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + + // when + adminHostUniversityService.deleteHostUniversity(university.getId()); + + // then + assertThat(hostUniversityRepository.findById(university.getId())).isEmpty(); + } + + @Test + void 존재하지_않는_대학을_삭제하면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> adminHostUniversityService.deleteHostUniversity(999L)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.UNIVERSITY_NOT_FOUND.getMessage()); + } + + @Test + void 참조하는_대학_지원_정보가_있으면_예외_응답을_반환한다() { + // given + HostUniversity university = universityFixture.괌_대학(); + univApplyInfoFixtureBuilder.univApplyInfo() + .termId(1L) + .koreanName("괌 대학 지원 정보") + .university(university) + .create(); + + // when & then + assertThatCode(() -> adminHostUniversityService.deleteHostUniversity(university.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.HOST_UNIVERSITY_HAS_REFERENCES.getMessage()); + } + } +}