Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ out/
### Claude Code ###
.claude/settings.local.json

### Serena ###
.serena/

### YML ###
application-secret.yml
application-prod.yml
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import문 순서 관련해서 한 번 리포매팅 해주세요 !


@RequiredArgsConstructor
@RequestMapping("/admin/countries")
@RestController
public class AdminCountryController {

private final AdminCountryService adminCountryService;

@GetMapping
public ResponseEntity<List<AdminCountryResponse>> getAllCountries() {
List<AdminCountryResponse> responses = adminCountryService.getAllCountries();
return ResponseEntity.ok(responses);
}

@PostMapping
public ResponseEntity<AdminCountryResponse> createCountry(
@Valid @RequestBody AdminCountryCreateRequest request
) {
AdminCountryResponse response = adminCountryService.createCountry(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@PutMapping("/{code}")
public ResponseEntity<AdminCountryResponse> updateCountry(
@PathVariable String code,
@Valid @RequestBody AdminCountryUpdateRequest request
) {
AdminCountryResponse response = adminCountryService.updateCountry(code, request);
return ResponseEntity.ok(response);
}

@DeleteMapping("/{code}")
public ResponseEntity<Void> deleteCountry(
@PathVariable String code
) {
adminCountryService.deleteCountry(code);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -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
) {

}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
@@ -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
) {

}
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마찬가지로 리포매팅 해주세요 !


@Service
@RequiredArgsConstructor
public class AdminCountryService {

private final CountryRepository countryRepository;
private final RegionRepository regionRepository;

@Transactional(readOnly = true)
public List<AdminCountryResponse> 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 단일 조회임에도 findXXXIn 을 사용하고 있습니다. findByKoreanName 메서드를 사용하는 것이 좋을 거 같아요 !

});
}

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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 마찬가지로 단일 조회 메서드를 사용하면 좋을 거 같아요 AdminRegionService와 일관성 있기도 하고요 !

.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);
}
Comment on lines +92 to +98
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Country를 참조하는 엔티티 찾기
echo "=== Country 참조 엔티티 검색 ==="
rg -n 'private Country|@ManyToOne.*Country|@JoinColumn.*country' --type=java -g '!*Test*' | head -20

# 2. AdminCountryService의 전체 구조 확인
echo -e "\n=== AdminCountryService 구조 ==="
rg -n 'class AdminCountryService|public void|private' src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java | head -30

# 3. 예외 처리 패턴 확인
echo -e "\n=== DataIntegrityViolationException 처리 ==="
rg -n 'DataIntegrityViolationException|catch.*Exception' --type=java -g '!*Test*' | grep -i country

# 4. HOST_UNIVERSITY_HAS_REFERENCES 사용 위치
echo -e "\n=== HOST_UNIVERSITY_HAS_REFERENCES 검색 ==="
rg -n 'HOST_UNIVERSITY_HAS_REFERENCES' --type=java

Repository: solid-connection/solid-connect-server

Length of output: 1845


🏁 Script executed:

#!/bin/bash
# 1. HostUniversity 엔티티의 Country 관계 설정 확인
echo "=== HostUniversity 엔티티 구조 ==="
sed -n '50,70p' src/main/java/com/example/solidconnection/university/domain/HostUniversity.java

# 2. AdminHostUniversityService의 deleteHostUniversity 메서드 구현
echo -e "\n=== AdminHostUniversityService deleteHostUniversity 메서드 ==="
rg -n 'public void deleteHostUniversity|private void validateHostUniversity' -A 15 src/main/java/com/example/solidconnection/admin/university/service/AdminHostUniversityService.java | head -50

# 3. Country 엔티티의 정의와 cascade 설정
echo -e "\n=== Country 엔티티 정의 ==="
cat -n src/main/java/com/example/solidconnection/common/domain/Country.java

# 4. UnivApplyInfo 또는 다른 엔티티에서 Country 참조 여부 확인
echo -e "\n=== UnivApplyInfo와 Country 관계 ==="
rg -n 'Country' src/main/java/com/example/solidconnection/university/domain/UnivApplyInfo.java | head -10

Repository: solid-connection/solid-connect-server

Length of output: 1457


🏁 Script executed:

#!/bin/bash
# 1. Country 엔티티 파일 위치 찾기
echo "=== Country 엔티티 파일 검색 ==="
find . -name 'Country.java' -type f | grep -v target | head -5

# 2. CountryRepository 메서드 확인
echo -e "\n=== CountryRepository 구조 ==="
cat -n src/main/java/com/example/solidconnection/common/repository/CountryRepository.java

# 3. HostUniversityRepository에서 Country 참조 확인 메서드
echo -e "\n=== HostUniversityRepository 메서드 ==="
rg -n 'existsByCountry|countByCountry|findByCountry' src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java

# 4. AdminCountryService 전체 코드
echo -e "\n=== AdminCountryService 전체 ==="
cat -n src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java

Repository: solid-connection/solid-connect-server

Length of output: 5027


🏁 Script executed:

#!/bin/bash
# 1. 모든 Repository 파일 찾기
echo "=== Repository 파일 위치 ==="
find . -name '*Repository.java' -path '*/repository/*' -type f | grep -E 'Country|HostUniversity'

# 2. Country 엔티티 구조
echo -e "\n=== Country 엔티티 ==="
cat -n src/main/java/com/example/solidconnection/location/country/domain/Country.java

# 3. HostUniversityRepository 내용
echo -e "\n=== HostUniversityRepository 메서드들 ==="
rg -n 'interface HostUniversityRepository|boolean exists|List<' src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java

Repository: solid-connection/solid-connect-server

Length of output: 1971


🏁 Script executed:

#!/bin/bash
# 1. HostUniversityRepository 전체 내용
echo "=== HostUniversityRepository 전체 ==="
cat -n src/main/java/com/example/solidconnection/university/repository/HostUniversityRepository.java

# 2. CountryRepository 전체 내용
echo -e "\n=== CountryRepository 전체 ==="
cat -n src/main/java/com/example/solidconnection/location/country/repository/CountryRepository.java

# 3. HostUniversityFilterRepository에서 Country 관련 메서드
echo -e "\n=== HostUniversityFilterRepository ==="
rg -n 'Country|country' src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java

Repository: solid-connection/solid-connect-server

Length of output: 2275


🏁 Script executed:

#!/bin/bash
# HostUniversityFilterRepository 전체 내용
echo "=== HostUniversityFilterRepository 전체 ==="
cat -n src/main/java/com/example/solidconnection/university/repository/custom/HostUniversityFilterRepository.java

Repository: solid-connection/solid-connect-server

Length of output: 690


Country 삭제 시 참조 무결성 검증을 추가하세요.

HostUniversity가 Country를 참조하고 있으므로, 삭제 전 다음 사항을 처리해야 합니다:

  1. HostUniversityRepository에 참조 확인 메서드 추가
    현재는 Country를 참조하는 HostUniversity를 확인할 방법이 없습니다.

  2. AdminCountryService에 validateNoReferences 메서드 구현
    AdminHostUniversityService의 삭제 로직을 참고하여 동일한 패턴을 적용하세요. 해당 Country를 참조하는 HostUniversity가 존재하면 예외를 발생시켜야 합니다.

  3. deleteCountry 메서드 수정
    실제 삭제 전에 validateNoReferences()를 호출하여 참조 여부를 검증하세요. 검증 없이 진행하면 DataIntegrityViolationException이 발생할 수 있습니다.

🤖 Prompt for AI Agents
In
`@src/main/java/com/example/solidconnection/admin/location/country/service/AdminCountryService.java`
around lines 92 - 98, Add referential-integrity checks before deleting a
Country: add a method in HostUniversityRepository like
existsByCountry_Code(String code) (or similar) to detect any HostUniversity
referencing the country; implement a validateNoReferences(String code) helper in
AdminCountryService that uses that repository method and throws the same
CustomException (with an appropriate ErrorCode, e.g.,
ErrorCode.COUNTRY_STILL_REFERENCED) following the pattern used in
AdminHostUniversityService; finally, call validateNoReferences(code) at the
start of deleteCountry(String code) before fetching/deleting the Country to
prevent DataIntegrityViolationException.

}
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "국가를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading
Loading