Skip to content

Commit 3a7e3c9

Browse files
authored
feat: 어드민 국가 crud 추가 (#656)
* feat: 국가 관련 어드민 crud 추가 * test: 국가 관련 어드민 crud 테스트 추가
1 parent bb37cd6 commit 3a7e3c9

9 files changed

Lines changed: 462 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ out/
4040
### Claude Code ###
4141
.claude/settings.local.json
4242

43+
### Serena ###
44+
.serena/
45+
4346
### YML ###
4447
application-secret.yml
4548
application-prod.yml
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.example.solidconnection.admin.location.country.controller;
2+
3+
import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest;
4+
import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse;
5+
import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest;
6+
import com.example.solidconnection.admin.location.country.service.AdminCountryService;
7+
import jakarta.validation.Valid;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.DeleteMapping;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.PathVariable;
14+
import org.springframework.web.bind.annotation.PostMapping;
15+
import org.springframework.web.bind.annotation.PutMapping;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RestController;
19+
20+
import java.util.List;
21+
22+
@RequiredArgsConstructor
23+
@RequestMapping("/admin/countries")
24+
@RestController
25+
public class AdminCountryController {
26+
27+
private final AdminCountryService adminCountryService;
28+
29+
@GetMapping
30+
public ResponseEntity<List<AdminCountryResponse>> getAllCountries() {
31+
List<AdminCountryResponse> responses = adminCountryService.getAllCountries();
32+
return ResponseEntity.ok(responses);
33+
}
34+
35+
@PostMapping
36+
public ResponseEntity<AdminCountryResponse> createCountry(
37+
@Valid @RequestBody AdminCountryCreateRequest request
38+
) {
39+
AdminCountryResponse response = adminCountryService.createCountry(request);
40+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
41+
}
42+
43+
@PutMapping("/{code}")
44+
public ResponseEntity<AdminCountryResponse> updateCountry(
45+
@PathVariable String code,
46+
@Valid @RequestBody AdminCountryUpdateRequest request
47+
) {
48+
AdminCountryResponse response = adminCountryService.updateCountry(code, request);
49+
return ResponseEntity.ok(response);
50+
}
51+
52+
@DeleteMapping("/{code}")
53+
public ResponseEntity<Void> deleteCountry(
54+
@PathVariable String code
55+
) {
56+
adminCountryService.deleteCountry(code);
57+
return ResponseEntity.noContent().build();
58+
}
59+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.example.solidconnection.admin.location.country.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
6+
public record AdminCountryCreateRequest(
7+
@NotBlank(message = "국가 코드는 필수입니다")
8+
@Size(min = 2, max = 2, message = "국가 코드는 2자여야 합니다")
9+
String code,
10+
11+
@NotBlank(message = "한글 국가명은 필수입니다")
12+
@Size(min = 1, max = 100, message = "한글 국가명은 1자 이상 100자 이하여야 합니다")
13+
String koreanName,
14+
15+
@NotBlank(message = "지역 코드는 필수입니다")
16+
@Size(min = 1, max = 10, message = "지역 코드는 1자 이상 10자 이하여야 합니다")
17+
String regionCode
18+
) {
19+
20+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.example.solidconnection.admin.location.country.dto;
2+
3+
import com.example.solidconnection.location.country.domain.Country;
4+
5+
public record AdminCountryResponse(
6+
String code,
7+
String koreanName,
8+
String regionCode
9+
) {
10+
11+
public static AdminCountryResponse from(Country country) {
12+
return new AdminCountryResponse(
13+
country.getCode(),
14+
country.getKoreanName(),
15+
country.getRegionCode()
16+
);
17+
}
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.example.solidconnection.admin.location.country.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.Size;
5+
6+
public record AdminCountryUpdateRequest(
7+
@NotBlank(message = "한글 국가명은 필수입니다")
8+
@Size(min = 1, max = 100, message = "한글 국가명은 1자 이상 100자 이하여야 합니다")
9+
String koreanName,
10+
11+
@NotBlank(message = "지역 코드는 필수입니다")
12+
@Size(min = 1, max = 10, message = "지역 코드는 1자 이상 10자 이하여야 합니다")
13+
String regionCode
14+
) {
15+
16+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.example.solidconnection.admin.location.country.service;
2+
3+
import com.example.solidconnection.admin.location.country.dto.AdminCountryCreateRequest;
4+
import com.example.solidconnection.admin.location.country.dto.AdminCountryResponse;
5+
import com.example.solidconnection.admin.location.country.dto.AdminCountryUpdateRequest;
6+
import com.example.solidconnection.common.exception.CustomException;
7+
import com.example.solidconnection.common.exception.ErrorCode;
8+
import com.example.solidconnection.location.country.domain.Country;
9+
import com.example.solidconnection.location.country.repository.CountryRepository;
10+
import com.example.solidconnection.location.region.repository.RegionRepository;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.stereotype.Service;
13+
import org.springframework.transaction.annotation.Transactional;
14+
15+
import java.util.List;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
public class AdminCountryService {
20+
21+
private final CountryRepository countryRepository;
22+
private final RegionRepository regionRepository;
23+
24+
@Transactional(readOnly = true)
25+
public List<AdminCountryResponse> getAllCountries() {
26+
return countryRepository.findAll()
27+
.stream()
28+
.map(AdminCountryResponse::from)
29+
.toList();
30+
}
31+
32+
@Transactional
33+
public AdminCountryResponse createCountry(AdminCountryCreateRequest request) {
34+
validateCodeNotExists(request.code());
35+
validateKoreanNameNotExists(request.koreanName());
36+
validateRegionCodeExists(request.regionCode());
37+
38+
Country country = new Country(request.code(), request.koreanName(), request.regionCode());
39+
Country savedCountry = countryRepository.save(country);
40+
41+
return AdminCountryResponse.from(savedCountry);
42+
}
43+
44+
private void validateCodeNotExists(String code) {
45+
countryRepository.findByCode(code)
46+
.ifPresent(country -> {
47+
throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS);
48+
});
49+
}
50+
51+
private void validateKoreanNameNotExists(String koreanName) {
52+
countryRepository.findAllByKoreanNameIn(List.of(koreanName))
53+
.stream()
54+
.findFirst()
55+
.ifPresent(country -> {
56+
throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS);
57+
});
58+
}
59+
60+
private void validateRegionCodeExists(String regionCode) {
61+
if (regionCode != null) {
62+
regionRepository.findById(regionCode)
63+
.orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND));
64+
}
65+
}
66+
67+
@Transactional
68+
public AdminCountryResponse updateCountry(String code, AdminCountryUpdateRequest request) {
69+
Country country = countryRepository.findByCode(code)
70+
.orElseThrow(() -> new CustomException(ErrorCode.COUNTRY_NOT_FOUND));
71+
72+
validateKoreanNameNotDuplicated(request.koreanName(), code);
73+
validateRegionCodeExists(request.regionCode());
74+
75+
country.updateKoreanName(request.koreanName());
76+
country.updateRegionCode(request.regionCode());
77+
78+
return AdminCountryResponse.from(country);
79+
}
80+
81+
private void validateKoreanNameNotDuplicated(String koreanName, String excludeCode) {
82+
countryRepository.findAllByKoreanNameIn(List.of(koreanName))
83+
.stream()
84+
.findFirst()
85+
.ifPresent(existingCountry -> {
86+
if (!existingCountry.getCode().equals(excludeCode)) {
87+
throw new CustomException(ErrorCode.COUNTRY_ALREADY_EXISTS);
88+
}
89+
});
90+
}
91+
92+
@Transactional
93+
public void deleteCountry(String code) {
94+
Country country = countryRepository.findByCode(code)
95+
.orElseThrow(() -> new CustomException(ErrorCode.COUNTRY_NOT_FOUND));
96+
97+
countryRepository.delete(country);
98+
}
99+
}

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public enum ErrorCode {
4141
REGION_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "지역을 찾을 수 없습니다."),
4242
REGION_NOT_FOUND_BY_KOREAN_NAME(HttpStatus.NOT_FOUND.value(), "이름에 해당하는 지역을 찾을 수 없습니다."),
4343
REGION_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 지역입니다."),
44+
COUNTRY_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 국가입니다."),
4445
HOST_UNIVERSITY_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 존재하는 파견 대학입니다."),
4546
HOST_UNIVERSITY_HAS_REFERENCES(HttpStatus.CONFLICT.value(), "해당 파견 대학을 참조하는 대학 지원 정보가 존재합니다."),
4647
COUNTRY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "국가를 찾을 수 없습니다."),

src/main/java/com/example/solidconnection/location/country/domain/Country.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,12 @@ public Country(String code, String koreanName, String regionCode) {
2929
this.koreanName = koreanName;
3030
this.regionCode = regionCode;
3131
}
32+
33+
public void updateKoreanName(String koreanName) {
34+
this.koreanName = koreanName;
35+
}
36+
37+
public void updateRegionCode(String regionCode) {
38+
this.regionCode = regionCode;
39+
}
3240
}

0 commit comments

Comments
 (0)