✨ Feat: Admin 도메인 도입 및 Geocoding 기반 체육시설·매칭 관리 기능 확장#60
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Walkthrough관리자 전용 API 계층을 추가하고 시설 및 경기 관리 기능을 재구성합니다. 시설 생성 엔드포인트를 공개에서 관리자 전용으로 이동하고, 업데이트/삭제 기능을 추가합니다. 카카오 지오코딩 서비스를 통합하여 주소를 좌표로 변환하고, 주소 및 예약 시간 필드를 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant AdminController
participant AdminService
participant SportsFacilityService
participant KakaoGeocodingService
participant Database as Database/<br/>PostGIS
Client->>AdminController: POST /api/admin/facilities<br/>(FacilityCreateRequest)
AdminController->>AdminService: createFacility(request)
AdminService->>SportsFacilityService: create(request)
SportsFacilityService->>KakaoGeocodingService: toPoint(address)
KakaoGeocodingService->>KakaoGeocodingService: Call Kakao API<br/>Parse response
KakaoGeocodingService-->>SportsFacilityService: Point(x, y)<br/>SRID 4326
SportsFacilityService->>Database: Persist SportsFacility<br/>with address & location
Database-->>SportsFacilityService: FacilityResponse
SportsFacilityService-->>AdminService: FacilityResponse
AdminService-->>AdminController: FacilityResponse
AdminController-->>Client: 201 Created<br/>BaseResponse<FacilityResponse>
sequenceDiagram
participant Client
participant AdminController
participant AdminService
participant MatchService
participant Database
Client->>AdminController: POST /api/admin/matches<br/>(MatchCreateRequest)
AdminController->>AdminService: createMatch(adminId,<br/>MatchCreateRequest)
AdminService->>MatchService: createMatch(adminId,<br/>request)
MatchService->>Database: Persist MatchRoom<br/>with scheduledAt
Database-->>MatchService: MatchResponse
MatchService-->>AdminService: MatchResponse
AdminService-->>AdminController: MatchResponse
AdminController-->>Client: 201 Created<br/>BaseResponse<MatchResponse>
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java (1)
72-88:⚠️ Potential issue | 🟡 Minor캐시 무효화 누락 가능성
create,update,delete메서드가 시설 데이터를 변경하지만,getNear와getMarkers의 캐시(facilityNear,facilityMarkers)를 무효화하지 않습니다. 시설이 추가/수정/삭제되어도 캐시된 결과가 반환될 수 있습니다.♻️ 캐시 무효화 추가 예시
`@Override` +@CacheEvict(cacheNames = {"facilityNear", "facilityMarkers"}, allEntries = true) public FacilityResponse create(FacilityCreateRequest request) { // ... } `@Override` +@CacheEvict(cacheNames = {"facilityNear", "facilityMarkers"}, allEntries = true) public FacilityResponse update(Long facilityId, FacilityUpdateRequest request) { // ... } `@Override` +@CacheEvict(cacheNames = {"facilityNear", "facilityMarkers"}, allEntries = true) public void delete(Long facilityId) { // ... }
🧹 Nitpick comments (4)
src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java (2)
44-46: SRID 설정 중복
GeometryFactory생성자에서 이미 SRID 4326을 설정했으므로 (line 20),point.setSRID(4326)은 중복입니다. 제거해도 무방하지만 명시적으로 두는 것도 가독성 측면에서 괜찮습니다.Based on learnings: "Store SportsFacility.location as JTS Point with SRID 4326"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java` around lines 44 - 46, The Point SRID is being set twice: GEOMETRY_FACTORY is already constructed with SRID 4326, so remove the redundant call to point.setSRID(4326) in KakaoGeocodingService where you create the Point via GEOMETRY_FACTORY.createPoint(new Coordinate(response.lng(), response.lat())); keep GEOMETRY_FACTORY as the single source of truth for the SRID to avoid duplication and potential inconsistency.
24-28: RestClient에 타임아웃 설정 고려외부 API 호출 시 타임아웃이 설정되어 있지 않으면 카카오 API가 응답하지 않을 경우 스레드가 무한 대기할 수 있습니다. 연결 및 읽기 타임아웃 설정을 권장합니다.
♻️ 타임아웃 설정 예시
+import org.springframework.http.client.SimpleClientHttpRequestFactory; +import java.time.Duration; public KakaoGeocodingService(`@Value`("${kakao.rest-api-key}") String restApiKey) { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(Duration.ofSeconds(5)); + factory.setReadTimeout(Duration.ofSeconds(10)); + this.restClient = RestClient.builder() + .requestFactory(factory) .defaultHeader("Authorization", "KakaoAK " + restApiKey) .build(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java` around lines 24 - 28, The RestClient created in KakaoGeocodingService lacks connection/read timeouts; update the constructor that builds restClient to configure timeouts via RestClient.builder(...).use the requestConfigCallback (e.g., requestConfigBuilder -> requestConfigBuilder.setConnectTimeout(...).setSocketTimeout(...)) and/or httpClientConfigCallback as appropriate so the restClient created in the KakaoGeocodingService constructor has sensible connect and socket/read timeout values instead of waiting indefinitely.src/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.java (1)
20-26:lat()/lng()메서드에서 빈 리스트 접근 시 예외 발생 가능
isEmpty()체크 없이lat()또는lng()를 호출하면IndexOutOfBoundsException이 발생합니다. 현재KakaoGeocodingService에서isEmpty()체크 후 사용하고 있지만, 방어적 코딩 관점에서 메서드 내부에서도 검증하는 것이 안전합니다.♻️ 방어적 코드 제안
public double lat() { + if (isEmpty()) { + throw new IllegalStateException("No geocoding results available"); + } return Double.parseDouble(documents.get(0).y()); } public double lng() { + if (isEmpty()) { + throw new IllegalStateException("No geocoding results available"); + } return Double.parseDouble(documents.get(0).x()); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.java` around lines 20 - 26, The lat() and lng() methods in KakaoGeocodingResponse access documents.get(0) without validation and can throw IndexOutOfBoundsException; update both methods (lat() and lng()) to first check that documents is non-null and not empty and, if empty, throw a clear IllegalStateException (or another domain-appropriate runtime exception) with a descriptive message like "No geocoding documents available" so callers get a meaningful error instead of an index exception; keep the return type double and reference the documents field in your checks.src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java (1)
29-56: 시설 관리 엔드포인트에서 관리자 정보 누락 검토 필요.
createFacility,updateFacility,deleteFacility엔드포인트에서@AuthenticationPrincipal UserAuthInfo를 사용하지 않고 있습니다. 매칭 생성에서는userAuthInfo.getId()를 사용하는데, 시설 관리 작업에서도 감사 로그(audit log) 또는 생성자/수정자 추적이 필요하다면 동일하게 관리자 정보를 전달하는 것이 좋습니다.현재 요구사항에 감사 기능이 없다면 무시해도 됩니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java` around lines 29 - 56, The createFacility, updateFacility, and deleteFacility endpoints are missing the administrator identity; add an `@AuthenticationPrincipal` UserAuthInfo userAuthInfo parameter to each controller method (createFacility, updateFacility, deleteFacility) and forward userAuthInfo.getId() (or the full UserAuthInfo if your service expects it) into the corresponding adminService calls (e.g., adminService.createFacility(request, adminId) / updateFacility(facilityId, request, adminId) / deleteFacility(facilityId, adminId)) so audit/creator/updater tracking can be recorded; update the adminService method signatures accordingly to accept and persist the admin identity where needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java`:
- Around line 46-48: SportsFacility.changeAddress에서 전달된 address를 그대로 대입해 DB 무결성
예외가 발생할 수 있으니, 메소드 시작부에서 address가 null이거나 빈 문자열(공백만 포함)인지 검사하고 적절히 처리하세요: 입력을
trim한 뒤 빈값이면 IllegalArgumentException(또는 도메인 전용 예외)을 던지고, 유효하면 this.address에
할당하도록 수정하십시오; 참조 대상은 메소드 changeAddress와 필드 address입니다.
In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`:
- Around line 131-134: The current check-then-act using
matchRoomRepository.existsById(matchId) followed by
matchRoomRepository.deleteById(matchId) can race; instead call
deleteById(matchId) directly inside MatchServiceImpl and catch the
repository-specific "not found" exception (e.g., EmptyResultDataAccessException
or the data-access exception your JPA/DAO layer throws) and rethrow new
CustomException(MatchErrorCode.MATCH_NOT_FOUND). Remove the existsById call and
ensure only the deleteById invocation is used, mapping repository exceptions to
the domain CustomException so concurrent deletes return the correct domain
error.
---
Nitpick comments:
In
`@src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java`:
- Around line 29-56: The createFacility, updateFacility, and deleteFacility
endpoints are missing the administrator identity; add an
`@AuthenticationPrincipal` UserAuthInfo userAuthInfo parameter to each controller
method (createFacility, updateFacility, deleteFacility) and forward
userAuthInfo.getId() (or the full UserAuthInfo if your service expects it) into
the corresponding adminService calls (e.g., adminService.createFacility(request,
adminId) / updateFacility(facilityId, request, adminId) /
deleteFacility(facilityId, adminId)) so audit/creator/updater tracking can be
recorded; update the adminService method signatures accordingly to accept and
persist the admin identity where needed.
In
`@src/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.java`:
- Around line 20-26: The lat() and lng() methods in KakaoGeocodingResponse
access documents.get(0) without validation and can throw
IndexOutOfBoundsException; update both methods (lat() and lng()) to first check
that documents is non-null and not empty and, if empty, throw a clear
IllegalStateException (or another domain-appropriate runtime exception) with a
descriptive message like "No geocoding documents available" so callers get a
meaningful error instead of an index exception; keep the return type double and
reference the documents field in your checks.
In `@src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java`:
- Around line 44-46: The Point SRID is being set twice: GEOMETRY_FACTORY is
already constructed with SRID 4326, so remove the redundant call to
point.setSRID(4326) in KakaoGeocodingService where you create the Point via
GEOMETRY_FACTORY.createPoint(new Coordinate(response.lng(), response.lat()));
keep GEOMETRY_FACTORY as the single source of truth for the SRID to avoid
duplication and potential inconsistency.
- Around line 24-28: The RestClient created in KakaoGeocodingService lacks
connection/read timeouts; update the constructor that builds restClient to
configure timeouts via RestClient.builder(...).use the requestConfigCallback
(e.g., requestConfigBuilder ->
requestConfigBuilder.setConnectTimeout(...).setSocketTimeout(...)) and/or
httpClientConfigCallback as appropriate so the restClient created in the
KakaoGeocodingService constructor has sensible connect and socket/read timeout
values instead of waiting indefinitely.
ℹ️ Review info
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (32)
src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.javasrc/main/java/com/be/sportizebe/domain/admin/dto/request/.gitkeepsrc/main/java/com/be/sportizebe/domain/admin/dto/response/.gitkeepsrc/main/java/com/be/sportizebe/domain/admin/exception/AdminErrorCode.javasrc/main/java/com/be/sportizebe/domain/admin/service/AdminService.javasrc/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.javasrc/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.javasrc/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityCreateRequest.javasrc/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityUpdateRequest.javasrc/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityNearResponse.javasrc/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityResponse.javasrc/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.javasrc/main/java/com/be/sportizebe/domain/facility/exception/FacilityErrorCode.javasrc/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.javasrc/main/java/com/be/sportizebe/domain/facility/repository/SportsFacilityRepository.javasrc/main/java/com/be/sportizebe/domain/facility/repository/projection/FacilityNearProjection.javasrc/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityService.javasrc/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.javasrc/main/java/com/be/sportizebe/domain/match/controller/MatchController.javasrc/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.javasrc/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.javasrc/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.javasrc/main/java/com/be/sportizebe/domain/match/dto/response/MatchResponse.javasrc/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.javasrc/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.javasrc/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.javasrc/main/java/com/be/sportizebe/domain/match/service/MatchService.javasrc/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.javasrc/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.javasrc/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.javasrc/main/java/com/be/sportizebe/global/security/SecurityConfig.javasrc/main/resources
💤 Files with no reviewable changes (2)
- src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java
- src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java
| public void changeAddress(String address) { | ||
| this.address = address; | ||
| } |
There was a problem hiding this comment.
changeAddress에서 null/blank 방어가 없어 DB 무결성 예외로 이어질 수 있습니다.
Line 46-48은 address를 그대로 대입해서 엔티티 레벨에서 불변식을 보장하지 못합니다. 최소한 null/blank 검증을 여기서 처리하는 편이 안전합니다.
제안 수정안
+import java.util.Objects;
+
public void changeAddress(String address) {
- this.address = address;
+ String normalized = Objects.requireNonNull(address, "address must not be null").trim();
+ if (normalized.isEmpty()) {
+ throw new IllegalArgumentException("address must not be blank");
+ }
+ this.address = normalized;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java`
around lines 46 - 48, SportsFacility.changeAddress에서 전달된 address를 그대로 대입해 DB 무결성
예외가 발생할 수 있으니, 메소드 시작부에서 address가 null이거나 빈 문자열(공백만 포함)인지 검사하고 적절히 처리하세요: 입력을
trim한 뒤 빈값이면 IllegalArgumentException(또는 도메인 전용 예외)을 던지고, 유효하면 this.address에
할당하도록 수정하십시오; 참조 대상은 메소드 changeAddress와 필드 address입니다.
| if (!matchRoomRepository.existsById(matchId)) { | ||
| throw new CustomException(MatchErrorCode.MATCH_NOT_FOUND); | ||
| } | ||
| matchRoomRepository.deleteById(matchId); |
There was a problem hiding this comment.
삭제 로직의 check-then-act 패턴은 경쟁 조건을 유발할 수 있습니다.
Line 131-134는 existsById 확인 후 deleteById를 호출해서, 동시 삭제 상황에서 MATCH_NOT_FOUND 대신 예기치 않은 예외로 500이 날 수 있습니다. 한 번의 삭제 호출로 처리하고 예외를 도메인 예외로 매핑하는 방식이 더 안전합니다.
수정 예시
`@Override`
public void deleteMatch(Long matchId) {
- if (!matchRoomRepository.existsById(matchId)) {
- throw new CustomException(MatchErrorCode.MATCH_NOT_FOUND);
- }
- matchRoomRepository.deleteById(matchId);
+ try {
+ matchRoomRepository.deleteById(matchId);
+ } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+ throw new CustomException(MatchErrorCode.MATCH_NOT_FOUND);
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java`
around lines 131 - 134, The current check-then-act using
matchRoomRepository.existsById(matchId) followed by
matchRoomRepository.deleteById(matchId) can race; instead call
deleteById(matchId) directly inside MatchServiceImpl and catch the
repository-specific "not found" exception (e.g., EmptyResultDataAccessException
or the data-access exception your JPA/DAO layer throws) and rethrow new
CustomException(MatchErrorCode.MATCH_NOT_FOUND). Remove the existsById call and
ensure only the deleteById invocation is used, mapping repository exceptions to
the domain CustomException so concurrent deletes return the correct domain
error.
#️⃣ Issue Number
📝 요약(Summary)
/api/admin/**엔드포인트 ADMIN 권한 보안 룰 적용scheduledAt) 필드 추가address) 필드 노출🛠️ PR 유형
어떤 변경 사항이 있나요?
📸스크린샷 (선택)
💬 공유사항 to 리뷰어
✅ PR Checklist
PR이 다음 요구 사항을 충족하는지 확인하세요.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항