Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ public SuccessResponse<PaginationResponse<CityResponse>> getCities(
@PostMapping("/cities")
public SuccessResponse<Void> createCity(@Valid @RequestBody CityCreateRequest request) {
adminLocationModifier.createCity(request);

return SuccessResponse.of("도시가 추가되었습니다.");
}

Expand All @@ -70,15 +69,13 @@ public SuccessResponse<Void> updateCity(
@Valid @RequestBody CityUpdateRequest request
) {
adminLocationModifier.updateCity(cityId, request);

return SuccessResponse.of("도시 정보가 수정되었습니다.");
}

@AdminAccess
@DeleteMapping("/cities/{cityId}")
public SuccessResponse<Void> deleteCity(@PathVariable Long cityId) {
adminLocationModifier.deleteCity(cityId);

return SuccessResponse.of("도시가 삭제되었습니다.");
}

Expand All @@ -91,4 +88,4 @@ public SuccessResponse<Void> updateCityPriority(
adminLocationModifier.updateCityPriority(cityId, priority);
return SuccessResponse.of("우선순위가 업데이트되었습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.souzip.application.admin;

import com.souzip.application.admin.provided.AdminLocationModifier;
import com.souzip.application.admin.required.CityCommandPort;
import com.souzip.domain.city.application.command.*;
import com.souzip.domain.city.application.port.CityManagementPort;
import com.souzip.domain.city.entity.CityCreateRequest;
import com.souzip.domain.city.entity.CityUpdateRequest;
import lombok.RequiredArgsConstructor;
Expand All @@ -13,25 +14,37 @@
@Service
public class AdminLocationModifyService implements AdminLocationModifier {

private final CityCommandPort cityCommandPort;
private final CityManagementPort cityManagementPort;

@Override
public void createCity(CityCreateRequest request) {
cityCommandPort.createCity(request);
cityManagementPort.createCity(new CreateCityCommand(
request.nameEn(),
request.nameKr(),
request.coordinate().getLatitude().doubleValue(),

Choose a reason for hiding this comment

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

P1 Badge 좌표 null 값을 변환하기 전에 유효성 검증하라

request.coordinate().getLatitude().doubleValue()를 바로 호출하면 coordinate 내부 필드가 비어 있는 요청(예: {"coordinate":{...}}에서 latitude/longitude 누락)에서 NullPointerException이 발생해 500으로 떨어집니다. CityCreateRequest/CityUpdateRequestcoordinate 객체 존재만 검증하고 내부 숫자 null은 막지 못하므로, 여기서 null 체크를 추가하거나 Coordinate 필드에 @NotNull 제약을 걸어 4xx로 일관되게 처리해야 합니다.

Useful? React with 👍 / 👎.

request.coordinate().getLongitude().doubleValue(),
request.countryId()
));
Comment on lines +21 to +27
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Coordinates are converted BigDecimal -> double -> BigDecimal across the admin write path (Coordinate is BigDecimal, commands are Double, entity is BigDecimal). This can introduce precision loss and is unnecessary work. Prefer keeping coordinates as BigDecimal (or Coordinate) in the command layer so the value can be passed through without lossy conversion.

Copilot uses AI. Check for mistakes.
}

@Override
public void updateCity(Long cityId, CityUpdateRequest request) {
cityCommandPort.updateCity(cityId, request);
cityManagementPort.updateCity(new UpdateCityCommand(
cityId,
request.nameEn(),
request.nameKr(),
request.coordinate().getLatitude().doubleValue(),
request.coordinate().getLongitude().doubleValue()
));
Comment on lines +32 to +38
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

Same issue as createCity(...): CityUpdateRequest coordinates are converted BigDecimal -> double -> BigDecimal, risking precision loss and adding avoidable conversions. Consider changing the command (and service) to accept BigDecimal/Coordinate so update passes validated values through directly.

Copilot uses AI. Check for mistakes.
}

@Override
public void deleteCity(Long cityId) {
cityCommandPort.deleteCity(cityId);
cityManagementPort.deleteCity(new DeleteCityCommand(cityId));
}

@Override
public void updateCityPriority(Long cityId, Integer priority) {
cityCommandPort.updateCityPriority(cityId, priority);
cityManagementPort.updateCityPriority(new UpdateCityPriorityCommand(cityId, priority));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.souzip.application.admin;

import com.souzip.application.admin.provided.AdminLocationFinder;
import com.souzip.application.admin.required.CityQueryPort;
import com.souzip.application.admin.required.CountryQueryPort;
import com.souzip.domain.city.entity.City;
import com.souzip.domain.city.repository.CityRepository;
import com.souzip.domain.country.entity.Country;
import com.souzip.domain.country.repository.CountryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -18,16 +18,22 @@
@Service
public class AdminLocationQueryService implements AdminLocationFinder {

private final CityQueryPort cityQueryPort;
private final CountryQueryPort countryQueryPort;
private final CityRepository cityRepository;
private final CountryRepository countryRepository;

@Override
public Page<City> getCities(Long countryId, String keyword, Pageable pageable) {
return cityQueryPort.getCities(countryId, keyword, pageable);
if (keyword == null || keyword.isBlank()) {
return cityRepository.findByCountryIdWithPaging(countryId, pageable);
}
return cityRepository.searchByKeyword(countryId, keyword, pageable);

Choose a reason for hiding this comment

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

P2 Badge 검색어를 trim 후 조회하도록 수정하라

현재는 공백 포함 키워드가 그대로 searchByKeyword로 전달되어, 사용자가 "서울 "처럼 앞뒤 공백을 포함해 입력하면 실제 데이터가 있어도 0건이 반환될 수 있습니다. 동일 도메인의 CityAdminQueryService는 조회 전에 keyword.trim()을 적용하고 있어 동작이 불일치하므로, 여기서도 trim 후 검색해야 검색 정확도 저하를 막을 수 있습니다.

Useful? React with 👍 / 👎.

}
Comment on lines 25 to 30
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

keyword is passed to cityRepository.searchByKeyword(...) without trimming, so inputs like " Seoul " will likely return no results. Note CityAdminQueryService trims the keyword before calling the same repository method. Consider applying keyword.trim() here as well to keep admin search behavior consistent.

Copilot uses AI. Check for mistakes.

@Override
public List<Country> getCountries(String keyword) {
return countryQueryPort.getCountries(keyword);
if (keyword == null || keyword.isBlank()) {
return countryRepository.findAllByOrderByNameKrAsc();
}
return countryRepository.findByKeywordOrderByNameKrAsc(keyword);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ public interface AdminLocationFinder {
Page<City> getCities(Long countryId, String keyword, Pageable pageable);

List<Country> getCountries(String keyword);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ public interface AdminLocationModifier {
void deleteCity(Long cityId);

void updateCityPriority(Long cityId, Integer priority);
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,95 +1,100 @@
package com.souzip.domain.city.application.command;
package com.souzip.domain.city.application.command;

import com.souzip.domain.city.application.port.CityManagementPort;
import com.souzip.domain.city.entity.City;
import com.souzip.domain.city.event.CityCreatedEvent;
import com.souzip.domain.city.event.CityDeletedEvent;
import com.souzip.domain.city.event.CityPriorityUpdatedEvent;
import com.souzip.domain.city.repository.CityRepository;
import com.souzip.domain.city.service.CityPriorityDomainService;
import com.souzip.domain.country.entity.Country;
import com.souzip.domain.country.repository.CountryRepository;
import com.souzip.global.exception.BusinessException;
import com.souzip.global.exception.ErrorCode;
import java.math.BigDecimal;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.souzip.domain.city.application.port.CityManagementPort;
import com.souzip.domain.city.entity.City;
import com.souzip.domain.city.event.CityCreatedEvent;
Comment on lines +1 to +5
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

This file appears to have an extra indentation prefix on the package/import lines and the rest of the code (leading spaces at the start of each line). It’s inconsistent with the rest of the Java sources and tends to create noisy diffs and formatting issues in IDEs/linters. Please reformat so package starts at column 0 and indentation is standard.

Copilot uses AI. Check for mistakes.
import com.souzip.domain.city.event.CityDeletedEvent;
import com.souzip.domain.city.event.CityPriorityUpdatedEvent;
import com.souzip.domain.city.repository.CityRepository;
import com.souzip.domain.city.service.CityPriorityDomainService;
import com.souzip.domain.country.entity.Country;
import com.souzip.domain.country.repository.CountryRepository;
import com.souzip.global.exception.BusinessException;
import com.souzip.global.exception.ErrorCode;
import java.math.BigDecimal;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class CityCommandService implements CityManagementPort {
@RequiredArgsConstructor
@Service
public class CityCommandService implements CityManagementPort {

private final CityRepository cityRepository;
private final CountryRepository countryRepository;
private final ApplicationEventPublisher eventPublisher;
private final CityPriorityDomainService cityPriorityDomainService;
private final CityRepository cityRepository;
private final CountryRepository countryRepository;
private final ApplicationEventPublisher eventPublisher;
private final CityPriorityDomainService cityPriorityDomainService;

@Transactional
@Override
public void createCity(CreateCityCommand command) {
Country country = findCountryById(command.countryId());
City city = City.create(
command.nameEn(),
command.nameKr(),
BigDecimal.valueOf(command.latitude()),
BigDecimal.valueOf(command.longitude()),
country
);
cityRepository.save(city);
@Transactional
@Override
public void createCity(CreateCityCommand command) {
Country country = findCountryById(command.countryId());
City city = City.create(
command.nameEn(),
command.nameKr(),
BigDecimal.valueOf(command.latitude()),
BigDecimal.valueOf(command.longitude()),
country
);
cityRepository.save(city);

eventPublisher.publishEvent(CityCreatedEvent.of(
city.getId(),
country.getId()
));
}
eventPublisher.publishEvent(CityCreatedEvent.of(
city.getId(),
country.getId()
));
}

@Transactional
@Override
public void updateCity(UpdateCityCommand command) {
City city = findCityById(command.cityId());
city.updateName(command.nameEn(), command.nameKr());
}
@Transactional
@Override
public void updateCity(UpdateCityCommand command) {
City city = findCityById(command.cityId());
city.update(
command.nameEn(),
command.nameKr(),
BigDecimal.valueOf(command.latitude()),
BigDecimal.valueOf(command.longitude())
);
Comment on lines +50 to +57
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

CityCommandService converts boxed Double coordinates via BigDecimal.valueOf(command.latitude()) / ...longitude(). If either value is null this will throw an NPE at runtime. Add a null-validation step (or switch the command fields to non-null types like BigDecimal / primitive double) before converting.

Copilot uses AI. Check for mistakes.
}

@Transactional
@Override
public void deleteCity(DeleteCityCommand command) {
City city = findCityById(command.cityId());
cityRepository.delete(city);
@Transactional
@Override
public void deleteCity(DeleteCityCommand command) {
City city = findCityById(command.cityId());
cityRepository.delete(city);

eventPublisher.publishEvent(CityDeletedEvent.of(city.getId()));
}
eventPublisher.publishEvent(CityDeletedEvent.of(city.getId()));
}

@Transactional
@Override
public void updateCityPriority(UpdateCityPriorityCommand command) {
City city = findCityByIdWithLock(command.cityId());
Integer oldPriority = city.getPriority();
Long countryId = city.getCountry().getId();
@Transactional
@Override
public void updateCityPriority(UpdateCityPriorityCommand command) {
City city = findCityByIdWithLock(command.cityId());
Integer oldPriority = city.getPriority();
Long countryId = city.getCountry().getId();

cityPriorityDomainService.adjustPriorities(city.getId(), oldPriority, command.newPriority(), countryId);
city.updatePriority(command.newPriority());
cityPriorityDomainService.adjustPriorities(city.getId(), oldPriority, command.newPriority(), countryId);
city.updatePriority(command.newPriority());

eventPublisher.publishEvent(CityPriorityUpdatedEvent.of(
city.getId(),
oldPriority,
command.newPriority()
));
}
eventPublisher.publishEvent(CityPriorityUpdatedEvent.of(
city.getId(),
oldPriority,
command.newPriority()
));
}

private City findCityByIdWithLock(Long cityId) {
return cityRepository.findByIdWithLock(cityId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "도시를 찾을 수 없습니다."));
}
private City findCityByIdWithLock(Long cityId) {
return cityRepository.findByIdWithLock(cityId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "도시를 찾을 수 없습니다."));
}

private City findCityById(Long cityId) {
return cityRepository.findById(cityId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "도시를 찾을 수 없습니다."));
}
private City findCityById(Long cityId) {
return cityRepository.findById(cityId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "도시를 찾을 수 없습니다."));
}

private Country findCountryById(Long countryId) {
return countryRepository.findById(countryId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "나라를 찾을 수 없습니다."));
private Country findCountryById(Long countryId) {
return countryRepository.findById(countryId)
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "나라를 찾을 수 없습니다."));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
public record UpdateCityCommand(
Long cityId,
String nameEn,
String nameKr
String nameKr,
Double latitude,
Double longitude
Comment on lines 3 to +8
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

UpdateCityCommand uses boxed Double for coordinates. This makes latitude/longitude nullable (risking NPEs where BigDecimal.valueOf(...) is used) and forces lossy BigDecimal -> double -> BigDecimal conversions in the admin flow. Prefer BigDecimal (or Coordinate) for these fields, or at least use primitive double if null is not allowed.

Copilot uses AI. Check for mistakes.
) {
}
4 changes: 3 additions & 1 deletion src/main/java/com/souzip/domain/city/entity/City.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ public void updatePriority(Integer priority) {
this.priority = priority;
}

public void updateName(String nameEn, String nameKr) {
public void update(String nameEn, String nameKr, BigDecimal latitude, BigDecimal longitude) {
this.nameEn = nameEn;
this.nameKr = nameKr;
this.latitude = latitude;
this.longitude = longitude;
}
Comment on lines +65 to 70
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

update(...) assigns latitude/longitude directly even though the columns are nullable = false. Since these parameters are nullable, this can lead to invalid entity state and a later persistence failure. Consider validating non-null (and ideally range, consistent with Coordinate) before assignment, or accept a validated value object like Coordinate instead of raw BigDecimals.

Copilot uses AI. Check for mistakes.

private void validatePriority(Integer priority) {
Expand Down
Loading
Loading