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
@@ -0,0 +1,77 @@
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.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;
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.*;


@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin")
@Tag(name = "admin", description = "관리자 전용 API")
public class AdminController {

private final AdminService adminService;

@Operation(summary = "체육시설 등록 (관리자 전용)", description = "새로운 체육시설을 등록합니다.")
@PostMapping("/facilities")
public ResponseEntity<BaseResponse<FacilityResponse>> createFacility(
@RequestBody @Valid FacilityCreateRequest request
) {
FacilityResponse response = adminService.createFacility(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(BaseResponse.success("체육시설 등록 성공", response));
}

@Operation(summary = "체육시설 수정 (관리자 전용)", description = "체육시설 정보를 수정합니다. null 필드는 수정하지 않습니다.")
@PutMapping("/facilities/{facilityId}")
public ResponseEntity<BaseResponse<FacilityResponse>> 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<BaseResponse<Void>> deleteFacility(
@PathVariable Long facilityId
) {
adminService.deleteFacility(facilityId);
return ResponseEntity.ok(BaseResponse.success("체육시설 삭제 성공", null));
}

@Operation(summary = "매칭 생성 (관리자 전용)", description = "체육시설 기반 매칭을 생성합니다.")
@PostMapping("/matches")
public ResponseEntity<BaseResponse<MatchResponse>> createMatch(
@AuthenticationPrincipal UserAuthInfo userAuthInfo,
@RequestBody @Valid MatchCreateRequest request
) {
MatchResponse response = adminService.createMatch(userAuthInfo.getId(), request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(BaseResponse.success("매칭 생성 성공", response));
}

@Operation(summary = "매칭 삭제 (관리자 전용)", description = "매칭을 강제 삭제합니다.")
@DeleteMapping("/matches/{matchId}")
public ResponseEntity<BaseResponse<Void>> deleteMatch(
@PathVariable Long matchId
) {
adminService.deleteMatch(matchId);
return ResponseEntity.ok(BaseResponse.success("매칭 삭제 성공", null));
}
}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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;

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); // 매칭 삭제
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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;
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 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -43,12 +42,6 @@ public List<FacilityMarkerResponse> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -14,6 +12,10 @@ public record FacilityCreateRequest(
@NotBlank
String facilityName,

@Schema(description = "도로명 주소", example = "서울특별시 강남구 테헤란로 521")
@NotBlank
String address,

@Schema(description = "시설 소개", example = "샤워실/주차장 있음")
String introduce,

Expand All @@ -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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public record FacilityResponse(
@Schema(description = "체육시설 이름", example = "OO 풋살장")
String facilityName,

@Schema(description = "도로명 주소", example = "서울특별시 강남구 테헤란로 521")
String address,

@Schema(description = "시설 소개", example = "잔디 상태가 좋아요")
String introduce,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -40,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;
}
Comment on lines +46 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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입니다.


public void changeLocation(Point location) {
this.location = location;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -36,6 +37,7 @@ static FacilityResponse toFacilityResponse(SportsFacility sf) {
return new FacilityResponse(
sf.getId(),
sf.getFacilityName(),
sf.getAddress(),
sf.getIntroduce(),
sf.getThumbnailUrl(),
sf.getFacilityType(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public interface SportsFacilityRepository extends JpaRepository<SportsFacility,
SELECT
sf.id AS id,
sf.facility_name AS facilityName,
sf.address AS address,
sf.introduce AS introduce,
sf.thumbnail_url AS thumbnailUrl,
sf.facility_type AS facilityType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public interface FacilityNearProjection {
// 인터페이스로 받아버리면
Long getId();
String getFacilityName();
String getAddress();
String getIntroduce();
String getFacilityType();
String getThumbnailUrl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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); // 체육시설 단 건 조회
}
Loading