Repository navigation
feat: region 관련 관리 기능 추가 #561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
6e8dc15
92305a0
329a7cc
fa6dda5
eefc764
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package com.example.solidconnection.admin.location.region.controller; | ||
|
|
||
| import com.example.solidconnection.admin.location.region.dto.AdminRegionCreateRequest; | ||
| import com.example.solidconnection.admin.location.region.dto.AdminRegionResponse; | ||
| import com.example.solidconnection.admin.location.region.dto.AdminRegionUpdateRequest; | ||
| import com.example.solidconnection.admin.location.region.service.AdminRegionService; | ||
| import jakarta.validation.Valid; | ||
| import java.util.List; | ||
| 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; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @RequestMapping("/admin/regions") | ||
| @RestController | ||
| public class AdminRegionController { | ||
|
|
||
| private final AdminRegionService adminRegionService; | ||
|
|
||
| @GetMapping | ||
| public ResponseEntity<List<AdminRegionResponse>> getAllRegions() { | ||
| List<AdminRegionResponse> responses = adminRegionService.getAllRegions(); | ||
| return ResponseEntity.ok(responses); | ||
| } | ||
|
|
||
| @PostMapping | ||
| public ResponseEntity<AdminRegionResponse> createRegion( | ||
| @Valid @RequestBody AdminRegionCreateRequest request | ||
| ) { | ||
| AdminRegionResponse response = adminRegionService.createRegion(request); | ||
| return ResponseEntity.status(HttpStatus.CREATED).body(response); | ||
| } | ||
|
|
||
| @PutMapping("/{code}") | ||
| public ResponseEntity<AdminRegionResponse> updateRegion( | ||
| @PathVariable String code, | ||
| @Valid @RequestBody AdminRegionUpdateRequest request | ||
| ) { | ||
| AdminRegionResponse response = adminRegionService.updateRegion(code, request); | ||
| return ResponseEntity.ok(response); | ||
| } | ||
|
|
||
| @DeleteMapping("/{code}") | ||
| public ResponseEntity<Void> deleteRegion( | ||
| @PathVariable String code | ||
| ) { | ||
| adminRegionService.deleteRegion(code); | ||
| return ResponseEntity.noContent().build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.example.solidconnection.admin.location.region.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Size; | ||
|
|
||
| public record AdminRegionCreateRequest( | ||
| @NotBlank(message = "지역 코드는 필수입니다") | ||
| @Size(min = 1, max = 10, message = "지역 코드는 1자 이상 10자 이하여야 합니다") | ||
| String code, | ||
|
|
||
| @NotBlank(message = "한글 지역명은 필수입니다") | ||
| @Size(min = 1, max = 100, message = "한글 지역명은 1자 이상 100자 이하여야 합니다") | ||
| String koreanName | ||
| ) { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.example.solidconnection.admin.location.region.dto; | ||
|
|
||
| import com.example.solidconnection.location.region.domain.Region; | ||
|
|
||
| public record AdminRegionResponse( | ||
| String code | ||
| ) { | ||
|
|
||
| public static AdminRegionResponse from(Region region) { | ||
| return new AdminRegionResponse( | ||
| region.getCode() | ||
| ); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package com.example.solidconnection.admin.location.region.service; | ||
|
|
||
| import com.example.solidconnection.admin.location.region.dto.AdminRegionCreateRequest; | ||
| import com.example.solidconnection.admin.location.region.dto.AdminRegionResponse; | ||
| import com.example.solidconnection.admin.location.region.dto.AdminRegionUpdateRequest; | ||
| import com.example.solidconnection.common.exception.CustomException; | ||
| import com.example.solidconnection.common.exception.ErrorCode; | ||
| import com.example.solidconnection.location.region.domain.Region; | ||
| import com.example.solidconnection.location.region.repository.RegionRepository; | ||
| import java.util.List; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AdminRegionService { | ||
|
|
||
| private final RegionRepository regionRepository; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public List<AdminRegionResponse> getAllRegions() { | ||
| return regionRepository.findAll() | ||
| .stream() | ||
| .map(AdminRegionResponse::from) | ||
| .toList(); | ||
| } | ||
|
|
||
| @Transactional | ||
| public AdminRegionResponse createRegion(AdminRegionCreateRequest request) { | ||
| regionRepository.findById(request.code()) | ||
| .ifPresent(region -> { | ||
| throw new CustomException(ErrorCode.REGION_ALREADY_EXISTS); | ||
| }); | ||
|
|
||
| regionRepository.findByKoreanName(request.koreanName()) | ||
| .ifPresent(region -> { | ||
| throw new CustomException(ErrorCode.REGION_ALREADY_EXISTS); | ||
| }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희는 |
||
|
|
||
| Region region = new Region(request.code(), request.koreanName()); | ||
| Region savedRegion = regionRepository.save(region); | ||
|
|
||
| return AdminRegionResponse.from(savedRegion); | ||
| } | ||
|
Comment on lines
+29
to
+52
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 중복 체크 로직에 경쟁 조건(race condition) 위험이 있습니다. 현재 구조의 문제점:
위 체크와 저장 사이에 다른 트랜잭션이 동일한 code나 koreanName을 가진 Region을 생성할 수 있습니다. 이는 동시성 환경에서 중복 데이터가 저장될 수 있는 위험을 초래합니다. 권장 해결 방안:
@Transactional
public AdminRegionResponse createRegion(AdminRegionCreateRequest request) {
- regionRepository.findById(request.code())
- .ifPresent(region -> {
- throw new CustomException(ErrorCode.REGION_ALREADY_EXISTS);
- });
-
- regionRepository.findByKoreanName(request.koreanName())
- .ifPresent(region -> {
- throw new CustomException(ErrorCode.REGION_ALREADY_EXISTS);
- });
-
- Region region = new Region(request.code(), request.koreanName());
- Region savedRegion = regionRepository.save(region);
-
- return AdminRegionResponse.from(savedRegion);
+ try {
+ Region region = new Region(request.code(), request.koreanName());
+ Region savedRegion = regionRepository.save(region);
+ return AdminRegionResponse.from(savedRegion);
+ } catch (DataIntegrityViolationException e) {
+ throw new CustomException(ErrorCode.REGION_ALREADY_EXISTS);
+ }
}이 방식은 데이터베이스의 ACID 속성을 활용하여 원자적으로 중복을 방지합니다.
🤖 Prompt for AI Agents |
||
|
|
||
| @Transactional | ||
| public AdminRegionResponse updateRegion(String code, AdminRegionUpdateRequest request) { | ||
| Region region = regionRepository.findById(code) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND)); | ||
|
|
||
| regionRepository.findByKoreanName(request.koreanName()) | ||
| .ifPresent(existingRegion -> { | ||
| if (!existingRegion.getCode().equals(code)) { | ||
| throw new CustomException(ErrorCode.REGION_ALREADY_EXISTS); | ||
| } | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 업데이트 시 중복 이름 체크도 경쟁 조건에 노출되어 있습니다. createRegion과 동일한 문제로, 중복 체크와 저장 사이에 다른 트랜잭션이 동일한 koreanName을 사용할 수 있습니다. 데이터베이스 unique 제약 조건을 추가하고 예외 처리로 전환하는 것을 권장합니다. 🤖 Prompt for AI Agents |
||
|
|
||
| Region updatedRegion = new Region(region.getCode(), request.koreanName()); | ||
| Region savedRegion = regionRepository.save(updatedRegion); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| return AdminRegionResponse.from(savedRegion); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void deleteRegion(String code) { | ||
| Region region = regionRepository.findById(code) | ||
| .orElseThrow(() -> new CustomException(ErrorCode.REGION_NOT_FOUND)); | ||
|
|
||
| regionRepository.delete(region); | ||
| } | ||
|
Gyuhyeok99 marked this conversation as resolved.
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
응답 코드를 지정하는 방향으로 작성해주었네요
여담으로 예전에 RESTful API 설계 원칙에 맞게 응답 코드를 지정하자고 얘기가 나왔던 것으로 기억하는데, 여유 생기면 한 번 진행해봐야겠네요