From c52f78667cf91e2429c3df255c49110fe98e08e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Sat, 28 Feb 2026 15:58:01 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20Admin=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EA=B6=8C=ED=95=9C=20=EB=B3=B4=EC=95=88?= =?UTF-8?q?=20=EB=A3=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../admin/controller/AdminController.java | 50 +++++++++++++++++++ .../domain/admin/dto/request/.gitkeep | 0 .../domain/admin/dto/response/.gitkeep | 0 .../admin/exception/AdminErrorCode.java | 16 ++++++ .../domain/admin/service/AdminService.java | 13 +++++ .../admin/service/AdminServiceImpl.java | 32 ++++++++++++ .../global/security/SecurityConfig.java | 1 + 7 files changed, 112 insertions(+) create mode 100644 src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java create mode 100644 src/main/java/com/be/sportizebe/domain/admin/dto/request/.gitkeep create mode 100644 src/main/java/com/be/sportizebe/domain/admin/dto/response/.gitkeep create mode 100644 src/main/java/com/be/sportizebe/domain/admin/exception/AdminErrorCode.java create mode 100644 src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java create mode 100644 src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java diff --git a/src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java b/src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java new file mode 100644 index 0000000..ba0954f --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java @@ -0,0 +1,50 @@ +package com.be.sportizebe.domain.admin.controller; + +import com.be.sportizebe.domain.admin.service.AdminService; +import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; +import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; +import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest; +import com.be.sportizebe.domain.match.dto.response.MatchResponse; +import com.be.sportizebe.global.cache.dto.UserAuthInfo; +import com.be.sportizebe.global.response.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/admin") +@Tag(name = "admin", description = "관리자 전용 API") +public class AdminController { + + private final AdminService adminService; + + @Operation(summary = "체육시설 등록 (관리자 전용)", description = "새로운 체육시설을 등록합니다.") + @PostMapping("/facilities") + public ResponseEntity> createFacility( + @RequestBody @Valid FacilityCreateRequest request + ) { + FacilityResponse response = adminService.createFacility(request); + return ResponseEntity.status(HttpStatus.CREATED) + .body(BaseResponse.success("체육시설 등록 성공", response)); + } + + @Operation(summary = "매칭 생성 (관리자 전용)", description = "체육시설 기반 매칭을 생성합니다.") + @PostMapping("/matches") + public ResponseEntity> createMatch( + @AuthenticationPrincipal UserAuthInfo userAuthInfo, + @RequestBody @Valid MatchCreateRequest request + ) { + MatchResponse response = adminService.createMatch(userAuthInfo.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED) + .body(BaseResponse.success("매칭 생성 성공", response)); + } +} diff --git a/src/main/java/com/be/sportizebe/domain/admin/dto/request/.gitkeep b/src/main/java/com/be/sportizebe/domain/admin/dto/request/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/be/sportizebe/domain/admin/dto/response/.gitkeep b/src/main/java/com/be/sportizebe/domain/admin/dto/response/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/be/sportizebe/domain/admin/exception/AdminErrorCode.java b/src/main/java/com/be/sportizebe/domain/admin/exception/AdminErrorCode.java new file mode 100644 index 0000000..7fc6b89 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/admin/exception/AdminErrorCode.java @@ -0,0 +1,16 @@ +package com.be.sportizebe.domain.admin.exception; + +import com.be.sportizebe.global.exception.model.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum AdminErrorCode implements BaseErrorCode { + ADMIN_ACCESS_DENIED("ADMIN_001", "관리자 권한이 없습니다.", HttpStatus.FORBIDDEN); + + private final String code; + private final String message; + private final HttpStatus status; +} diff --git a/src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java b/src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java new file mode 100644 index 0000000..90c7037 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java @@ -0,0 +1,13 @@ +package com.be.sportizebe.domain.admin.service; + +import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; +import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; +import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest; +import com.be.sportizebe.domain.match.dto.response.MatchResponse; + +public interface AdminService { + + FacilityResponse createFacility(FacilityCreateRequest request); // 체육시설 등록 + + MatchResponse createMatch(Long adminId, MatchCreateRequest request); // 매칭 생성 +} diff --git a/src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java new file mode 100644 index 0000000..ab79079 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java @@ -0,0 +1,32 @@ +package com.be.sportizebe.domain.admin.service; + +import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; +import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; +import com.be.sportizebe.domain.facility.service.SportsFacilityService; +import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest; +import com.be.sportizebe.domain.match.dto.response.MatchResponse; +import com.be.sportizebe.domain.match.service.MatchService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminServiceImpl implements AdminService { + + private final SportsFacilityService sportsFacilityService; + private final MatchService matchService; + + @Override + @Transactional + public FacilityResponse createFacility(FacilityCreateRequest request) { + return sportsFacilityService.create(request); + } + + @Override + @Transactional + public MatchResponse createMatch(Long adminId, MatchCreateRequest request) { + return matchService.createMatch(adminId, request); + } +} diff --git a/src/main/java/com/be/sportizebe/global/security/SecurityConfig.java b/src/main/java/com/be/sportizebe/global/security/SecurityConfig.java index 6a481ba..61d15bd 100644 --- a/src/main/java/com/be/sportizebe/global/security/SecurityConfig.java +++ b/src/main/java/com/be/sportizebe/global/security/SecurityConfig.java @@ -34,6 +34,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(cors -> cors.configurationSource(corsConfigurationSource)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/admin/**").hasAuthority("ADMIN") // 이 줄 추가 .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/users/signup").permitAll() .requestMatchers("/api/users/**").authenticated() From 6bae46786eb8dd1958d05c6bf4a0d2a8d31fdf7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Sat, 28 Feb 2026 15:58:15 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=C2=B7=EC=8B=9C=EC=84=A4=20=EC=83=9D=EC=84=B1=20API?= =?UTF-8?q?=EB=A5=BC=20Admin=20=EC=A0=84=EC=9A=A9=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SportsFacilityController.java | 7 ------- .../domain/match/controller/MatchController.java | 14 -------------- .../sportizebe/domain/match/entity/MatchRoom.java | 2 +- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java b/src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java index 52c1690..27e1570 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java +++ b/src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java @@ -1,7 +1,6 @@ // src/main/java/com/be/sportizebe/domain/facility/controller/SportsFacilityController.java package com.be.sportizebe.domain.facility.controller; -import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; import com.be.sportizebe.domain.facility.dto.request.FacilityMarkerRequest; import com.be.sportizebe.domain.facility.dto.request.FacilityNearRequest; import com.be.sportizebe.domain.facility.dto.response.FacilityMarkerResponse; @@ -43,12 +42,6 @@ public List markers( return sportsFacilityService.getMarkers(request); } - @Operation(summary = "체육시설 등록", description = "체육시설을 등록합니다. (관리자/개발자용)") - @PostMapping - public FacilityResponse create(@Valid @RequestBody FacilityCreateRequest request) { - return sportsFacilityService.create(request); - } - @Operation(summary = "체육시설 단건 조회", description = "facilityId로 체육시설을 단건 조회합니다.") @GetMapping("/{facilityId}") public FacilityResponse getById(@PathVariable Long facilityId) { diff --git a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java index 5256d2e..42c8d38 100644 --- a/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java +++ b/src/main/java/com/be/sportizebe/domain/match/controller/MatchController.java @@ -1,10 +1,8 @@ package com.be.sportizebe.domain.match.controller; -import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest; import com.be.sportizebe.domain.match.dto.request.MatchNearRequest; import com.be.sportizebe.domain.match.dto.response.MatchDetailResponse; import com.be.sportizebe.domain.match.dto.response.MatchNearResponse; -import com.be.sportizebe.domain.match.dto.response.MatchResponse; import com.be.sportizebe.domain.match.service.MatchService; import com.be.sportizebe.global.cache.dto.UserAuthInfo; import com.be.sportizebe.global.response.BaseResponse; @@ -16,7 +14,6 @@ import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -24,7 +21,6 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -38,16 +34,6 @@ public class MatchController { private final MatchService matchService; - @Operation(summary = "매칭 생성 (관리자 전용 기능)", description = "현재는 매칭 더미데이터 넣는 용도") - @PostMapping - public ResponseEntity> createMatch( - @AuthenticationPrincipal UserAuthInfo userAuthInfo, - @RequestBody @Valid MatchCreateRequest request - ) { - MatchResponse response = matchService.createMatch(userAuthInfo.getId(), request); - return ResponseEntity.status(HttpStatus.CREATED) - .body(BaseResponse.success("매칭 생성 성공", response)); - } @Operation(summary = "매칭 참여") @PostMapping("/{matchId}/join") public ResponseEntity> joinMatch( diff --git a/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java b/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java index 49c4954..c47e40f 100644 --- a/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java +++ b/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java @@ -15,7 +15,7 @@ @AllArgsConstructor @NoArgsConstructor @Table(name = "match_rooms") -public class MatchRoom extends BaseTimeEntity { +public class MatMatchRoom extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From ab4fcb850acca11317a6235f135d25e5f9d1f942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Sat, 28 Feb 2026 15:58:23 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20Geocoding=20API=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=EC=B2=B4=EC=9C=A1=EC=8B=9C=EC=84=A4=20=EC=A3=BC=EC=86=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=93=B1=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/FacilityCreateRequest.java | 18 ++----- .../dto/response/FacilityResponse.java | 3 ++ .../facility/entity/SportsFacility.java | 3 ++ .../facility/exception/FacilityErrorCode.java | 17 +++++++ .../facility/mapper/FacilityMapper.java | 1 + .../service/SportsFacilityServiceImpl.java | 17 +++---- .../global/kakao/KakaoGeocodingService.java | 49 +++++++++++++++++++ .../dto/response/KakaoGeocodingResponse.java | 27 ++++++++++ 8 files changed, 113 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/be/sportizebe/domain/facility/exception/FacilityErrorCode.java create mode 100644 src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java create mode 100644 src/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.java diff --git a/src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityCreateRequest.java b/src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityCreateRequest.java index 1fc2c96..b280f9c 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityCreateRequest.java +++ b/src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityCreateRequest.java @@ -2,8 +2,6 @@ import com.be.sportizebe.domain.facility.entity.FacilityType; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.DecimalMax; -import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -14,6 +12,10 @@ public record FacilityCreateRequest( @NotBlank String facilityName, + @Schema(description = "도로명 주소", example = "서울특별시 강남구 테헤란로 521") + @NotBlank + String address, + @Schema(description = "시설 소개", example = "샤워실/주차장 있음") String introduce, @@ -22,17 +24,7 @@ public record FacilityCreateRequest( @Schema(description = "시설 종목 타입", example = "SOCCER") @NotNull - FacilityType facilityType, - - @Schema(description = "위도", example = "37.563") - @DecimalMin("-90.0") - @DecimalMax("90.0") - double lat, - - @Schema(description = "경도", example = "126.982") - @DecimalMin("-180.0") - @DecimalMax("180.0") - double lng + FacilityType facilityType ) { } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityResponse.java b/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityResponse.java index d6e8965..d3f538f 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityResponse.java +++ b/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityResponse.java @@ -12,6 +12,9 @@ public record FacilityResponse( @Schema(description = "체육시설 이름", example = "OO 풋살장") String facilityName, + @Schema(description = "도로명 주소", example = "서울특별시 강남구 테헤란로 521") + String address, + @Schema(description = "시설 소개", example = "잔디 상태가 좋아요") String introduce, diff --git a/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java b/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java index 059c90e..2c91ee4 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java +++ b/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java @@ -19,6 +19,9 @@ public class SportsFacility extends BaseTimeEntity { @Column(nullable = false, length = 100) private String facilityName; + @Column(nullable = false) + private String address; // 도로명 주소 (사용자 표시용) + @Column(columnDefinition = "text") private String introduce; diff --git a/src/main/java/com/be/sportizebe/domain/facility/exception/FacilityErrorCode.java b/src/main/java/com/be/sportizebe/domain/facility/exception/FacilityErrorCode.java new file mode 100644 index 0000000..c633b90 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/facility/exception/FacilityErrorCode.java @@ -0,0 +1,17 @@ +package com.be.sportizebe.domain.facility.exception; + +import com.be.sportizebe.global.exception.model.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum FacilityErrorCode implements BaseErrorCode { + FACILITY_NOT_FOUND("FACILITY_001", "존재하지 않는 체육시설입니다.", HttpStatus.NOT_FOUND), + ADDRESS_NOT_FOUND("FACILITY_002", "입력한 주소로 좌표를 찾을 수 없습니다. 주소를 확인해주세요.", HttpStatus.BAD_REQUEST); + + private final String code; + private final String message; + private final HttpStatus status; +} diff --git a/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java b/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java index 3973c32..959e154 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java +++ b/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java @@ -36,6 +36,7 @@ static FacilityResponse toFacilityResponse(SportsFacility sf) { return new FacilityResponse( sf.getId(), sf.getFacilityName(), + sf.getAddress(), sf.getIntroduce(), sf.getThumbnailUrl(), sf.getFacilityType(), diff --git a/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java b/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java index 985214c..559f128 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java @@ -9,11 +9,11 @@ import com.be.sportizebe.domain.facility.entity.SportsFacility; import com.be.sportizebe.domain.facility.mapper.FacilityMapper; import com.be.sportizebe.domain.facility.repository.SportsFacilityRepository; +import com.be.sportizebe.domain.facility.exception.FacilityErrorCode; +import com.be.sportizebe.global.exception.CustomException; +import com.be.sportizebe.global.kakao.KakaoGeocodingService; import lombok.RequiredArgsConstructor; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; -import org.locationtech.jts.geom.PrecisionModel; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,8 +26,7 @@ public class SportsFacilityServiceImpl implements SportsFacilityService { private final SportsFacilityRepository sportsFacilityRepository; - private static final GeometryFactory GEOMETRY_FACTORY = - new GeometryFactory(new PrecisionModel(), 4326); + private final KakaoGeocodingService kakaoGeocodingService; @Override @@ -71,12 +70,12 @@ public List getMarkers(FacilityMarkerRequest request) { @Override public FacilityResponse create(FacilityCreateRequest request) { - // Point(x=lng, y=lat) 순서 주의 - Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(request.lng(), request.lat())); - point.setSRID(4326); + // 주소 → 카카오 Geocoding API → Point(lng, lat) + Point point = kakaoGeocodingService.toPoint(request.address()); SportsFacility facility = SportsFacility.builder() .facilityName(request.facilityName()) + .address(request.address()) .introduce(request.introduce()) .thumbnailUrl(request.thumbnailUrl()) .facilityType(request.facilityType()) @@ -91,7 +90,7 @@ public FacilityResponse create(FacilityCreateRequest request) { @Transactional(readOnly = true) public FacilityResponse getById(Long facilityId) { SportsFacility facility = sportsFacilityRepository.findById(facilityId) - .orElseThrow(() -> new IllegalArgumentException("시설이 존재하지 않습니다. id=" + facilityId)); + .orElseThrow(() -> new CustomException(FacilityErrorCode.FACILITY_NOT_FOUND)); return FacilityMapper.toFacilityResponse(facility); } diff --git a/src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java b/src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java new file mode 100644 index 0000000..8b58df0 --- /dev/null +++ b/src/main/java/com/be/sportizebe/global/kakao/KakaoGeocodingService.java @@ -0,0 +1,49 @@ +package com.be.sportizebe.global.kakao; + +import com.be.sportizebe.domain.facility.exception.FacilityErrorCode; +import com.be.sportizebe.global.exception.CustomException; +import com.be.sportizebe.global.kakao.dto.response.KakaoGeocodingResponse; +import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@Slf4j +@Service +public class KakaoGeocodingService { + + private static final String KAKAO_GEOCODING_URL = "https://dapi.kakao.com/v2/local/search/address.json"; + private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(new PrecisionModel(), 4326); + + private final RestClient restClient; + + public KakaoGeocodingService(@Value("${kakao.rest-api-key}") String restApiKey) { + this.restClient = RestClient.builder() + .defaultHeader("Authorization", "KakaoAK " + restApiKey) + .build(); + } + + /** + * 주소 문자열을 받아 PostGIS Point(lng, lat)로 변환 + */ + public Point toPoint(String address) { + KakaoGeocodingResponse response = restClient.get() + .uri(KAKAO_GEOCODING_URL + "?query={address}", address) + .retrieve() + .body(KakaoGeocodingResponse.class); + + if (response == null || response.isEmpty()) { + log.warn("카카오 Geocoding 결과 없음: {}", address); + throw new CustomException(FacilityErrorCode.ADDRESS_NOT_FOUND); + } + + // JTS Point: x = 경도(lng), y = 위도(lat) + Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(response.lng(), response.lat())); + point.setSRID(4326); + return point; + } +} diff --git a/src/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.java b/src/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.java new file mode 100644 index 0000000..881a7aa --- /dev/null +++ b/src/main/java/com/be/sportizebe/global/kakao/dto/response/KakaoGeocodingResponse.java @@ -0,0 +1,27 @@ +package com.be.sportizebe.global.kakao.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record KakaoGeocodingResponse( + @JsonProperty("documents") List documents +) { + public record Document( + @JsonProperty("address_name") String addressName, + @JsonProperty("x") String x, // 경도(lng) + @JsonProperty("y") String y // 위도(lat) + ) {} + + public boolean isEmpty() { + return documents == null || documents.isEmpty(); + } + + public double lat() { + return Double.parseDouble(documents.get(0).y()); + } + + public double lng() { + return Double.parseDouble(documents.get(0).x()); + } +} From 56ca3b008f077d850b026a883e39c9deb76376c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Sat, 28 Feb 2026 18:15:29 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=A7=A4=EC=B9=AD=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=9D=BC=EC=8B=9C(scheduledAt)=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/match/dto/request/MatchCreateRequest.java | 11 ++++++++++- .../match/dto/response/MatchDetailResponse.java | 9 +++++++-- .../domain/match/dto/response/MatchNearResponse.java | 9 ++++++--- .../domain/match/dto/response/MatchResponse.java | 10 ++++++++-- .../be/sportizebe/domain/match/entity/MatchRoom.java | 7 ++++++- .../domain/match/repository/MatchRoomRepository.java | 1 + .../repository/projection/MatchNearProjection.java | 3 +++ 7 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java b/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java index 4617f43..69a720b 100644 --- a/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java +++ b/src/main/java/com/be/sportizebe/domain/match/dto/request/MatchCreateRequest.java @@ -2,8 +2,12 @@ import com.be.sportizebe.common.enums.SportType; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Future; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; @Schema(description = "매칭 생성 요청 정보") public record MatchCreateRequest( @@ -23,6 +27,11 @@ public record MatchCreateRequest( @Schema(description = "매칭 참여 요금", example = "4000") @Min(value = 1000, message = "참가비는 1000원 이상이어야 합니다.") - int entryFee + int entryFee, + + @Schema(description = "매칭 시작 일시", example = "2026-03-07T14:00:00") + @NotNull(message = "매칭 시작 일시는 필수입니다.") + @Future(message = "매칭 시작 일시는 현재 시간 이후여야 합니다.") + LocalDateTime scheduledAt ) {} \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.java b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.java index 080a103..cb50bf9 100644 --- a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.java +++ b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchDetailResponse.java @@ -6,6 +6,7 @@ import com.be.sportizebe.domain.user.entity.User; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; import java.util.List; @Schema(description = "매칭 상세 응답 정보") @@ -30,7 +31,10 @@ public record MatchDetailResponse( List participantIds, @Schema(description = "요청 유저가 참여 중인지 여부", example = "true") - boolean joined + boolean joined, + + @Schema(description = "매칭 시작 일시", example = "2026-03-07T14:00:00") + LocalDateTime scheduledAt ) { public static MatchDetailResponse of( @@ -53,7 +57,8 @@ public static MatchDetailResponse of( matchRoom.getMaxMembers(), participantIds.size(), participantIds, - joined + joined, + matchRoom.getScheduledAt() ); } } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java index 450c56c..8b95543 100644 --- a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java +++ b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchNearResponse.java @@ -4,6 +4,8 @@ import com.be.sportizebe.domain.match.repository.projection.MatchNearProjection; import com.be.sportizebe.common.enums.SportType; +import java.time.LocalDateTime; + public record MatchNearResponse( Long matchId, SportType sportsName, @@ -14,7 +16,8 @@ public record MatchNearResponse( MatchStatus status, int distanceM, int durationMinutes, - int entryFee + int entryFee, + LocalDateTime scheduledAt ) { public static MatchNearResponse from(MatchNearProjection p) { @@ -28,8 +31,8 @@ public static MatchNearResponse from(MatchNearProjection p) { MatchStatus.valueOf(p.getStatus()), (int) Math.round(p.getDistanceM()), p.getDurationMinutes(), - p.getEntryFee() - + p.getEntryFee(), + p.getScheduledAt() ); } } diff --git a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchResponse.java b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchResponse.java index e75eec2..e054a1b 100644 --- a/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchResponse.java +++ b/src/main/java/com/be/sportizebe/domain/match/dto/response/MatchResponse.java @@ -5,6 +5,8 @@ import com.be.sportizebe.common.enums.SportType; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + @Schema(description = "매칭 응답 정보") public record MatchResponse( // 참여자 목록이 안 들어감 @@ -26,7 +28,10 @@ public record MatchResponse( Integer maxMembers, @Schema(description ="모집 상태", example = "OPEN") - MatchStatus status + MatchStatus status, + + @Schema(description = "매칭 시작 일시", example = "2026-03-07T14:00:00") + LocalDateTime scheduledAt ) { public static MatchResponse from(MatchRoom matchRoom) { @@ -36,7 +41,8 @@ public static MatchResponse from(MatchRoom matchRoom) { matchRoom.getFacilityId(), matchRoom.getCurMembers(), matchRoom.getMaxMembers(), - matchRoom.getStatus() + matchRoom.getStatus(), + matchRoom.getScheduledAt() ); } } diff --git a/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java b/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java index c47e40f..142afd4 100644 --- a/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java +++ b/src/main/java/com/be/sportizebe/domain/match/entity/MatchRoom.java @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -15,7 +16,7 @@ @AllArgsConstructor @NoArgsConstructor @Table(name = "match_rooms") -public class MatMatchRoom extends BaseTimeEntity { +public class MatchRoom extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -45,6 +46,9 @@ public class MatMatchRoom extends BaseTimeEntity { @Column(nullable = false) private int entryFee; + @Column(nullable = false) + private LocalDateTime scheduledAt; // 매칭 시작 일시 + @OneToMany(mappedBy = "matchRoom", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default @@ -63,6 +67,7 @@ public static MatchRoom create(MatchCreateRequest request) { .status(MatchStatus.OPEN) .durationMinutes(request.durationMinutes()) .entryFee(request.entryFee()) + .scheduledAt(request.scheduledAt()) .build(); } public void join() { diff --git a/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java b/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java index 0bc13b7..5a52644 100644 --- a/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java +++ b/src/main/java/com/be/sportizebe/domain/match/repository/MatchRoomRepository.java @@ -21,6 +21,7 @@ public interface MatchRoomRepository extends JpaRepository { mr.status AS status, mr.duration_minutes AS durationMinutes, mr.entry_fee AS entryFee, + mr.scheduled_at AS scheduledAt, ST_Distance( sf.location, diff --git a/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java b/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java index 5908ba2..d916846 100644 --- a/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java +++ b/src/main/java/com/be/sportizebe/domain/match/repository/projection/MatchNearProjection.java @@ -1,5 +1,7 @@ package com.be.sportizebe.domain.match.repository.projection; +import java.time.LocalDateTime; + public interface MatchNearProjection { Long getMatchId(); String getSportsName(); @@ -11,4 +13,5 @@ public interface MatchNearProjection { Double getDistanceM(); Integer getDurationMinutes(); Integer getEntryFee(); + LocalDateTime getScheduledAt(); } From 6eb67fef1974371ddcbbd3e7763d59c6ddece92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Sat, 28 Feb 2026 22:18:09 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=B2=B4=EC=9C=A1?= =?UTF-8?q?=EC=8B=9C=EC=84=A4=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EC=97=90=20=EC=A3=BC=EC=86=8C(address)=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/facility/dto/response/FacilityNearResponse.java | 3 +++ .../be/sportizebe/domain/facility/mapper/FacilityMapper.java | 1 + .../domain/facility/repository/SportsFacilityRepository.java | 1 + .../facility/repository/projection/FacilityNearProjection.java | 1 + 4 files changed, 6 insertions(+) diff --git a/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityNearResponse.java b/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityNearResponse.java index c70cbcd..d8e797c 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityNearResponse.java +++ b/src/main/java/com/be/sportizebe/domain/facility/dto/response/FacilityNearResponse.java @@ -15,6 +15,9 @@ public class FacilityNearResponse { @Schema(description = "시설명", example = "수원종합운동장") private String facilityName; + @Schema(description = "도로명 주소", example = "경기도 수원시 팔달구 월드컵로 310") + private String address; + @Schema(description = "소개", example = "수원시 대표 종합 스포츠 시설 (축구, 육상 등)") private String introduce; diff --git a/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java b/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java index 959e154..f996a18 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java +++ b/src/main/java/com/be/sportizebe/domain/facility/mapper/FacilityMapper.java @@ -13,6 +13,7 @@ static FacilityNearResponse toNearResponse(FacilityNearProjection p){ return FacilityNearResponse.builder() .id(p.getId()) .facilityName(p.getFacilityName()) + .address(p.getAddress()) .introduce(p.getIntroduce()) .thumbnailUrl(p.getThumbnailUrl()) .facilityType(p.getFacilityType()) diff --git a/src/main/java/com/be/sportizebe/domain/facility/repository/SportsFacilityRepository.java b/src/main/java/com/be/sportizebe/domain/facility/repository/SportsFacilityRepository.java index 48cfc88..5259d8e 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/repository/SportsFacilityRepository.java +++ b/src/main/java/com/be/sportizebe/domain/facility/repository/SportsFacilityRepository.java @@ -18,6 +18,7 @@ public interface SportsFacilityRepository extends JpaRepository Date: Sat, 28 Feb 2026 22:18:20 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=E2=9C=A8=20Feat:=20Admin=20=EC=B2=B4?= =?UTF-8?q?=EC=9C=A1=EC=8B=9C=EC=84=A4=20=EC=88=98=EC=A0=95=C2=B7=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=8F=20=EB=A7=A4=EC=B9=AD=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminController.java | 35 ++++++++++++++++--- .../domain/admin/service/AdminService.java | 7 ++++ .../admin/service/AdminServiceImpl.java | 19 ++++++++++ .../dto/request/FacilityUpdateRequest.java | 24 +++++++++++++ .../facility/entity/SportsFacility.java | 4 +++ .../service/SportsFacilityService.java | 5 +++ .../service/SportsFacilityServiceImpl.java | 31 ++++++++++++++++ .../domain/match/service/MatchService.java | 2 ++ .../match/service/MatchServiceImpl.java | 8 +++++ 9 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityUpdateRequest.java diff --git a/src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java b/src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java index ba0954f..8f34b0f 100644 --- a/src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java +++ b/src/main/java/com/be/sportizebe/domain/admin/controller/AdminController.java @@ -2,6 +2,7 @@ import com.be.sportizebe.domain.admin.service.AdminService; import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; +import com.be.sportizebe.domain.facility.dto.request.FacilityUpdateRequest; import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest; import com.be.sportizebe.domain.match.dto.response.MatchResponse; @@ -14,10 +15,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + @RestController @RequiredArgsConstructor @@ -37,6 +36,25 @@ public ResponseEntity> createFacility( .body(BaseResponse.success("체육시설 등록 성공", response)); } + @Operation(summary = "체육시설 수정 (관리자 전용)", description = "체육시설 정보를 수정합니다. null 필드는 수정하지 않습니다.") + @PutMapping("/facilities/{facilityId}") + public ResponseEntity> updateFacility( + @PathVariable Long facilityId, + @RequestBody @Valid FacilityUpdateRequest request + ) { + FacilityResponse response = adminService.updateFacility(facilityId, request); + return ResponseEntity.ok(BaseResponse.success("체육시설 수정 성공", response)); + } + + @Operation(summary = "체육시설 삭제 (관리자 전용)", description = "체육시설을 삭제합니다.") + @DeleteMapping("/facilities/{facilityId}") + public ResponseEntity> deleteFacility( + @PathVariable Long facilityId + ) { + adminService.deleteFacility(facilityId); + return ResponseEntity.ok(BaseResponse.success("체육시설 삭제 성공", null)); + } + @Operation(summary = "매칭 생성 (관리자 전용)", description = "체육시설 기반 매칭을 생성합니다.") @PostMapping("/matches") public ResponseEntity> createMatch( @@ -47,4 +65,13 @@ public ResponseEntity> createMatch( return ResponseEntity.status(HttpStatus.CREATED) .body(BaseResponse.success("매칭 생성 성공", response)); } + + @Operation(summary = "매칭 삭제 (관리자 전용)", description = "매칭을 강제 삭제합니다.") + @DeleteMapping("/matches/{matchId}") + public ResponseEntity> deleteMatch( + @PathVariable Long matchId + ) { + adminService.deleteMatch(matchId); + return ResponseEntity.ok(BaseResponse.success("매칭 삭제 성공", null)); + } } diff --git a/src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java b/src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java index 90c7037..39d048c 100644 --- a/src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java +++ b/src/main/java/com/be/sportizebe/domain/admin/service/AdminService.java @@ -1,6 +1,7 @@ package com.be.sportizebe.domain.admin.service; import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; +import com.be.sportizebe.domain.facility.dto.request.FacilityUpdateRequest; import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest; import com.be.sportizebe.domain.match.dto.response.MatchResponse; @@ -9,5 +10,11 @@ public interface AdminService { FacilityResponse createFacility(FacilityCreateRequest request); // 체육시설 등록 + FacilityResponse updateFacility(Long facilityId, FacilityUpdateRequest request); // 체육시설 수정 + + void deleteFacility(Long facilityId); // 체육시설 삭제 + MatchResponse createMatch(Long adminId, MatchCreateRequest request); // 매칭 생성 + + void deleteMatch(Long matchId); // 매칭 삭제 } diff --git a/src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java b/src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java index ab79079..3b5eaaf 100644 --- a/src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/admin/service/AdminServiceImpl.java @@ -1,6 +1,7 @@ package com.be.sportizebe.domain.admin.service; import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; +import com.be.sportizebe.domain.facility.dto.request.FacilityUpdateRequest; import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; import com.be.sportizebe.domain.facility.service.SportsFacilityService; import com.be.sportizebe.domain.match.dto.request.MatchCreateRequest; @@ -24,9 +25,27 @@ public FacilityResponse createFacility(FacilityCreateRequest request) { return sportsFacilityService.create(request); } + @Override + @Transactional + public FacilityResponse updateFacility(Long facilityId, FacilityUpdateRequest request) { + return sportsFacilityService.update(facilityId, request); + } + + @Override + @Transactional + public void deleteFacility(Long facilityId) { + sportsFacilityService.delete(facilityId); + } + @Override @Transactional public MatchResponse createMatch(Long adminId, MatchCreateRequest request) { return matchService.createMatch(adminId, request); } + + @Override + @Transactional + public void deleteMatch(Long matchId) { + matchService.deleteMatch(matchId); + } } diff --git a/src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityUpdateRequest.java b/src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityUpdateRequest.java new file mode 100644 index 0000000..56fd4ee --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/facility/dto/request/FacilityUpdateRequest.java @@ -0,0 +1,24 @@ +package com.be.sportizebe.domain.facility.dto.request; + +import com.be.sportizebe.domain.facility.entity.FacilityType; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "체육시설 수정 요청 (null 필드는 수정하지 않음)") +public record FacilityUpdateRequest( + + @Schema(description = "체육시설 이름", example = "OO 풋살장 (리모델링)") + String facilityName, + + @Schema(description = "도로명 주소 (변경 시 좌표 자동 재계산)", example = "서울특별시 강남구 테헤란로 521") + String address, + + @Schema(description = "시설 소개", example = "샤워실/주차장 있음") + String introduce, + + @Schema(description = "썸네일 이미지 URL", example = "https://example.com/facility/thumbnail.jpg") + String thumbnailUrl, + + @Schema(description = "시설 종목 타입", example = "SOCCER") + FacilityType facilityType + +) {} diff --git a/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java b/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java index 2c91ee4..69e2d76 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java +++ b/src/main/java/com/be/sportizebe/domain/facility/entity/SportsFacility.java @@ -43,6 +43,10 @@ public void changeInfo(String facilityName, String introduce, String thumbnailUr if (facilityType != null) this.facilityType = facilityType; } + public void changeAddress(String address) { + this.address = address; + } + public void changeLocation(Point location) { this.location = location; } diff --git a/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityService.java b/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityService.java index 3e53b73..44e8f87 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityService.java +++ b/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityService.java @@ -3,6 +3,7 @@ import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; import com.be.sportizebe.domain.facility.dto.request.FacilityMarkerRequest; import com.be.sportizebe.domain.facility.dto.request.FacilityNearRequest; +import com.be.sportizebe.domain.facility.dto.request.FacilityUpdateRequest; import com.be.sportizebe.domain.facility.dto.response.FacilityMarkerResponse; import com.be.sportizebe.domain.facility.dto.response.FacilityNearResponse; import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; @@ -21,5 +22,9 @@ public interface SportsFacilityService { FacilityResponse create(FacilityCreateRequest request); // 체육시설 생성 + FacilityResponse update(Long facilityId, FacilityUpdateRequest request); // 체육시설 수정 + + void delete(Long facilityId); // 체육시설 삭제 + FacilityResponse getById(Long facilityId); // 체육시설 단 건 조회 } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java b/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java index 559f128..046238e 100644 --- a/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/facility/service/SportsFacilityServiceImpl.java @@ -3,6 +3,7 @@ import com.be.sportizebe.domain.facility.dto.request.FacilityCreateRequest; import com.be.sportizebe.domain.facility.dto.request.FacilityMarkerRequest; import com.be.sportizebe.domain.facility.dto.request.FacilityNearRequest; +import com.be.sportizebe.domain.facility.dto.request.FacilityUpdateRequest; import com.be.sportizebe.domain.facility.dto.response.FacilityMarkerResponse; import com.be.sportizebe.domain.facility.dto.response.FacilityNearResponse; import com.be.sportizebe.domain.facility.dto.response.FacilityResponse; @@ -86,6 +87,36 @@ public FacilityResponse create(FacilityCreateRequest request) { return FacilityMapper.toFacilityResponse(saved); } + @Override + public FacilityResponse update(Long facilityId, FacilityUpdateRequest request) { + SportsFacility facility = sportsFacilityRepository.findById(facilityId) + .orElseThrow(() -> new CustomException(FacilityErrorCode.FACILITY_NOT_FOUND)); + + // 주소가 변경된 경우 geocoding 재수행 + if (request.address() != null && !request.address().equals(facility.getAddress())) { + Point newPoint = kakaoGeocodingService.toPoint(request.address()); + facility.changeAddress(request.address()); + facility.changeLocation(newPoint); + } + + facility.changeInfo( + request.facilityName(), + request.introduce(), + request.thumbnailUrl(), + request.facilityType() + ); + + return FacilityMapper.toFacilityResponse(facility); + } + + @Override + public void delete(Long facilityId) { + if (!sportsFacilityRepository.existsById(facilityId)) { + throw new CustomException(FacilityErrorCode.FACILITY_NOT_FOUND); + } + sportsFacilityRepository.deleteById(facilityId); + } + @Override @Transactional(readOnly = true) public FacilityResponse getById(Long facilityId) { diff --git a/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java b/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java index e39bd34..c225293 100644 --- a/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java +++ b/src/main/java/com/be/sportizebe/domain/match/service/MatchService.java @@ -21,4 +21,6 @@ public interface MatchService { List getNearMatches(MatchNearRequest request); // 내 주변 매칭 목록 조회 + void deleteMatch(Long matchId); // 매칭 삭제 (관리자 전용) + } diff --git a/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java b/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java index 5bcac78..721c39e 100644 --- a/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/match/service/MatchServiceImpl.java @@ -126,6 +126,14 @@ public MatchDetailResponse getMatchDetail(Long matchId, Long userId) { return MatchDetailResponse.of(matchRoom, user); } + @Override + public void deleteMatch(Long matchId) { + if (!matchRoomRepository.existsById(matchId)) { + throw new CustomException(MatchErrorCode.MATCH_NOT_FOUND); + } + matchRoomRepository.deleteById(matchId); + } + @Override @Transactional(readOnly = true) public List getNearMatches(MatchNearRequest request) { From 56d16000a5702126e29a3df658c2149598a9aab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=ED=9B=88=EA=B8=B0?= Date: Sat, 28 Feb 2026 22:27:41 +0900 Subject: [PATCH 7/7] submodule pointer update --- src/main/resources | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources b/src/main/resources index 05d0469..b0b5aca 160000 --- a/src/main/resources +++ b/src/main/resources @@ -1 +1 @@ -Subproject commit 05d0469d24d5c627fe5a94af9f6a7e797be874e6 +Subproject commit b0b5acaeef6cc2e32624b2f407d152dc2f5b5d4b