From 267492b5e19f5e700fff7d119599c290014f8352 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:13:49 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EA=B5=AD=EA=B0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=96=B4=EB=93=9C=EB=AF=BC=20crud=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminCountryController.java | 59 +++++++++++ .../dto/AdminCountryCreateRequest.java | 20 ++++ .../country/dto/AdminCountryResponse.java | 18 ++++ .../dto/AdminCountryUpdateRequest.java | 16 +++ .../country/service/AdminCountryService.java | 99 +++++++++++++++++++ .../common/exception/ErrorCode.java | 1 + .../location/country/domain/Country.java | 8 ++ 7 files changed, 221 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/admin/location/country/controller/AdminCountryController.java create mode 100644 src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryCreateRequest.java create mode 100644 src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryResponse.java create mode 100644 src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryUpdateRequest.java create mode 100644 src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java diff --git a/src/main/java/com/example/solidconnection/admin/location/country/controller/AdminCountryController.java b/src/main/java/com/example/solidconnection/admin/location/country/controller/AdminCountryController.java new file mode 100644 index 000000000..6b4a39e21 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/controller/AdminCountryController.java @@ -0,0 +1,59 @@ +package com.example.solidconnection.admin.location.country.controller; + +import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest; +import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse; +import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest; +import com.example.solidconnection.admin.location.country.service.AdminCountryService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +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; + +import java.util.List; + +@RequiredArgsConstructor +@RequestMapping("/admin/countries") +@RestController +public class AdminCountryController { + + private final AdminCountryService adminCountryService; + + @GetMapping + public ResponseEntity> getAllCountries() { + List responses = adminCountryService.getAllCountries(); + return ResponseEntity.ok(responses); + } + + @PostMapping + public ResponseEntity createCountry( + @Valid @RequestBody AdminCountryCreateRequest request + ) { + AdminCountryResponse response = adminCountryService.createCountry(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @PutMapping("/{code}") + public ResponseEntity updateCountry( + @PathVariable String code, + @Valid @RequestBody AdminCountryUpdateRequest request + ) { + AdminCountryResponse response = adminCountryService.updateCountry(code, request); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{code}") + public ResponseEntity deleteCountry( + @PathVariable String code + ) { + adminCountryService.deleteCountry(code); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryCreateRequest.java b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryCreateRequest.java new file mode 100644 index 000000000..0c5fe8b40 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryCreateRequest.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.admin.location.country.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminCountryCreateRequest( + @NotBlank(message = "국가 코드는 필수입니다") + @Size(min = 2, max = 2, message = "국가 코드는 2자여야 합니다") + String code, + + @NotBlank(message = "한글 국가명은 필수입니다") + @Size(min = 1, max = 100, message = "한글 국가명은 1자 이상 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "지역 코드는 필수입니다") + @Size(min = 1, max = 10, message = "지역 코드는 1자 이상 10자 이하여야 합니다") + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryResponse.java b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryResponse.java new file mode 100644 index 000000000..19bc69eac --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryResponse.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.admin.location.country.dto; + +import com.example.solidconnection.location.country.domain.Country; + +public record AdminCountryResponse( + String code, + String koreanName, + String regionCode +) { + + public static AdminCountryResponse from(Country country) { + return new AdminCountryResponse( + country.getCode(), + country.getKoreanName(), + country.getRegionCode() + ); + } +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryUpdateRequest.java new file mode 100644 index 000000000..a9c3c7a43 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/dto/AdminCountryUpdateRequest.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.admin.location.country.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record AdminCountryUpdateRequest( + @NotBlank(message = "한글 국가명은 필수입니다") + @Size(min = 1, max = 100, message = "한글 국가명은 1자 이상 100자 이하여야 합니다") + String koreanName, + + @NotBlank(message = "지역 코드는 필수입니다") + @Size(min = 1, max = 10, message = "지역 코드는 1자 이상 10자 이하여야 합니다") + String regionCode +) { + +} diff --git a/src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java b/src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java new file mode 100644 index 000000000..279fcfc97 --- /dev/null +++ b/src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java @@ -0,0 +1,99 @@ +package com.example.solidconnection.admin.location.country.service; + +import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest; +import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse; +import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest; +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.repository.CountryRepository; +import com.example.solidconnection.location.region.repository.RegionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AdminCountryService { + + private final CountryRepository countryRepository; + private final RegionRepository regionRepository; + + @Transactional(readOnly = true) + public List getAllCountries() { + return countryRepository.findAll() + .stream() + .map(AdminCountryResponse::from) + .toList(); + } + + @Transactional + public AdminCountryResponse createCountry(AdminCountryCreateRequest request) { + validateCodeNotExists(request.code()); + validateKoreanNameNotExists(request.koreanName()); + validateRegionCodeExists(request.regionCode()); + + Country country = new Country(request.code(), request.koreanName(), request.regionCode()); + Country savedCountry = countryRepository.save(country); + + return AdminCountryResponse.from(savedCountry); + } + + private void validateCodeNotExists(String code) { + countryRepository.findByCode(code) + .ifPresent(country -> { + throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS); + }); + } + + private void validateKoreanNameNotExists(String koreanName) { + countryRepository.findAllByKoreanNameIn(List.of(koreanName)) + .stream() + .findFirst() + .ifPresent(country -> { + throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS); + }); + } + + private void validateRegionCodeExists(String regionCode) { + if (regionCode != null) { + regionRepository.findById(regionCode) + .orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND)); + } + } + + @Transactional + public AdminCountryResponse updateCountry(String code, AdminCountryUpdateRequest request) { + Country country = countryRepository.findByCode(code) + .orElseThrow(() -> new CustomException(ErrorCode.COUNTRY_NOT_FOUND)); + + validateKoreanNameNotDuplicated(request.koreanName(), code); + validateRegionCodeExists(request.regionCode()); + + country.updateKoreanName(request.koreanName()); + country.updateRegionCode(request.regionCode()); + + return AdminCountryResponse.from(country); + } + + private void validateKoreanNameNotDuplicated(String koreanName, String excludeCode) { + countryRepository.findAllByKoreanNameIn(List.of(koreanName)) + .stream() + .findFirst() + .ifPresent(existingCountry -> { + if (!existingCountry.getCode().equals(excludeCode)) { + throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS); + } + }); + } + + @Transactional + public void deleteCountry(String code) { + Country country = countryRepository.findByCode(code) + .orElseThrow(() -> new CustomException(ErrorCode.COUNTRY_NOT_FOUND)); + + countryRepository.delete(country); + } +} 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 48e11d846..84a6a8d00 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,7 @@ 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(), "이미 존재하는 지역입니다."), + COUNTRY_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(), "국가를 찾을 수 없습니다."), diff --git a/src/main/java/com/example/solidconnection/location/country/domain/Country.java b/src/main/java/com/example/solidconnection/location/country/domain/Country.java index f9a487eda..67e295540 100644 --- a/src/main/java/com/example/solidconnection/location/country/domain/Country.java +++ b/src/main/java/com/example/solidconnection/location/country/domain/Country.java @@ -29,4 +29,12 @@ public Country(String code, String koreanName, String regionCode) { this.koreanName = koreanName; this.regionCode = regionCode; } + + public void updateKoreanName(String koreanName) { + this.koreanName = koreanName; + } + + public void updateRegionCode(String regionCode) { + this.regionCode = regionCode; + } } From ffc623ac7b59668f0cf499f1f7b72999b73c98ca Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:48:07 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=EA=B5=AD=EA=B0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=96=B4=EB=93=9C=EB=AF=BC=20crud=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .../service/AdminCountryServiceTest.java | 238 ++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/admin/location/country/service/AdminCountryServiceTest.java diff --git a/.gitignore b/.gitignore index 7a58382b4..f6e780891 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ out/ ### Claude Code ### .claude/settings.local.json +### Serena ### +.serena/ + ### YML ### application-secret.yml application-prod.yml diff --git a/src/test/java/com/example/solidconnection/admin/location/country/service/AdminCountryServiceTest.java b/src/test/java/com/example/solidconnection/admin/location/country/service/AdminCountryServiceTest.java new file mode 100644 index 000000000..e9784cbcd --- /dev/null +++ b/src/test/java/com/example/solidconnection/admin/location/country/service/AdminCountryServiceTest.java @@ -0,0 +1,238 @@ +package com.example.solidconnection.admin.location.country.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest; +import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse; +import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest; +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.country.repository.CountryRepository; +import com.example.solidconnection.location.region.domain.Region; +import com.example.solidconnection.location.region.fixture.RegionFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.List; +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; + +@TestContainerSpringBootTest +@DisplayName("국가 관련 관리자 서비스 테스트") +class AdminCountryServiceTest { + + @Autowired + private AdminCountryService adminCountryService; + + @Autowired + private CountryRepository countryRepository; + + @Autowired + private CountryFixture countryFixture; + + @Autowired + private RegionFixture regionFixture; + + @Nested + class 전체_국가_조회 { + + @Test + void 국가가_없으면_빈_목록을_반환한다() { + // when + List responses = adminCountryService.getAllCountries(); + + // then + assertThat(responses).isEqualTo(List.of()); + } + + @Test + void 저장된_모든_국가를_조회한다() { + // given + Country country1 = countryFixture.미국(); + Country country2 = countryFixture.캐나다(); + Country country3 = countryFixture.일본(); + + // when + List responses = adminCountryService.getAllCountries(); + + // then + assertThat(responses) + .hasSize(3) + .extracting(AdminCountryResponse::code) + .containsExactlyInAnyOrder( + country1.getCode(), + country2.getCode(), + country3.getCode() + ); + } + } + + @Nested + class 국가_생성 { + + @Test + void 유효한_정보로_국가를_생성하면_성공한다() { + // given + Region region = regionFixture.아시아(); + AdminCountryCreateRequest request = new AdminCountryCreateRequest("KR", "대한민국", region.getCode()); + + // when + AdminCountryResponse response = adminCountryService.createCountry(request); + + // then + assertThat(response.code()).isEqualTo("KR"); + assertThat(response.koreanName()).isEqualTo("대한민국"); + assertThat(response.regionCode()).isEqualTo(region.getCode()); + + // 데이터베이스에 저장되었는지 확인 + Country savedCountry = countryRepository.findByCode(request.code()).orElseThrow(); + assertAll( + () -> assertThat(savedCountry.getKoreanName()).isEqualTo(request.koreanName()), + () -> assertThat(savedCountry.getRegionCode()).isEqualTo(request.regionCode()) + ); + } + + @Test + void 이미_존재하는_코드로_국가를_생성하면_예외_응답을_반환한다() { + // given + Country country = countryFixture.미국(); + + AdminCountryCreateRequest request = new AdminCountryCreateRequest("US", "새로운 미국", country.getRegionCode()); + + // when & then + assertThatCode(() -> adminCountryService.createCountry(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 이미_존재하는_한글명으로_국가를_생성하면_예외_응답을_반환한다() { + // given + countryFixture.일본(); + Region region = regionFixture.아시아(); + + AdminCountryCreateRequest request = new AdminCountryCreateRequest("NEW_CODE", "일본", region.getCode()); + + // when & then + assertThatCode(() -> adminCountryService.createCountry(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 존재하지_않는_지역_코드로_국가를_생성하면_예외_응답을_반환한다() { + // given + AdminCountryCreateRequest request = new AdminCountryCreateRequest("KR", "대한민국", "NOT_EXIST_REGION"); + + // when & then + assertThatCode(() -> adminCountryService.createCountry(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.REGION_NOT_FOUND.getMessage()); + } + } + + @Nested + class 국가_수정 { + + @Test + void 유효한_정보로_국가를_수정하면_성공한다() { + // given + Country country = countryFixture.미국(); + Region newRegion = regionFixture.유럽(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest("미합중국", newRegion.getCode()); + + // when + AdminCountryResponse response = adminCountryService.updateCountry(country.getCode(), request); + + // then + Country updatedCountry = countryRepository.findByCode(country.getCode()).orElseThrow(); + assertAll( + () -> assertThat(response.code()).isEqualTo(country.getCode()), + () -> assertThat(updatedCountry.getKoreanName()).isEqualTo(request.koreanName()), + () -> assertThat(updatedCountry.getRegionCode()).isEqualTo(request.regionCode()) + ); + } + + @Test + void 존재하지_않는_국가_코드로_수정하면_예외_응답을_반환한다() { + // given + Region region = regionFixture.아시아(); + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest("대한민국", region.getCode()); + + // when & then + assertThatCode(() -> adminCountryService.updateCountry("NOT_EXIST", request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_NOT_FOUND.getMessage()); + } + + @Test + void 다른_국가의_한글명으로_수정하면_예외_응답을_반환한다() { + // given + Country country1 = countryFixture.미국(); + Country country2 = countryFixture.캐나다(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest(country2.getKoreanName(), country1.getRegionCode()); + + // when & then + assertThatCode(() -> adminCountryService.updateCountry(country1.getCode(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_ALREADY_EXISTS.getMessage()); + } + + @Test + void 같은_국가의_한글명으로_수정하면_성공한다() { + // given + Country country = countryFixture.일본(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest(country.getKoreanName(), country.getRegionCode()); + + // when + AdminCountryResponse response = adminCountryService.updateCountry(country.getCode(), request); + + // then + assertThat(response.code()).isEqualTo(country.getCode()); + } + + @Test + void 존재하지_않는_지역_코드로_수정하면_예외_응답을_반환한다() { + // given + Country country = countryFixture.미국(); + + AdminCountryUpdateRequest request = new AdminCountryUpdateRequest("미합중국", "NOT_EXIST_REGION"); + + // when & then + assertThatCode(() -> adminCountryService.updateCountry(country.getCode(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.REGION_NOT_FOUND.getMessage()); + } + } + + @Nested + class 국가_삭제 { + + @Test + void 존재하는_국가를_삭제하면_성공한다() { + // given + Country country = countryFixture.미국(); + + // when + adminCountryService.deleteCountry(country.getCode()); + + // then + assertThat(countryRepository.findByCode(country.getCode())).isEmpty(); + } + + @Test + void 존재하지_않는_국가를_삭제하면_예외_응답을_반환한다() { + // when & then + assertThatCode(() -> adminCountryService.deleteCountry("NOT_EXIST")) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.COUNTRY_NOT_FOUND.getMessage()); + } + } +}