diff --git a/src/main/java/com/semosan/api/common/status/ErrorStatus.java b/src/main/java/com/semosan/api/common/status/ErrorStatus.java index 8328069..cd9f067 100644 --- a/src/main/java/com/semosan/api/common/status/ErrorStatus.java +++ b/src/main/java/com/semosan/api/common/status/ErrorStatus.java @@ -163,7 +163,9 @@ public enum ErrorStatus implements BaseStatus { /** * Admin */ - ADMIN_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "ADM_401_1", "아이디 또는 비밀번호가 올바르지 않습니다."); + ADMIN_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "ADM_401_1", "아이디 또는 비밀번호가 올바르지 않습니다."), + RESTAURANT_SECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "ADM_404_1", "맛집 섹션을 찾을 수 없습니다."), + RESTAURANT_NOT_FOUND(HttpStatus.NOT_FOUND, "ADM_404_2", "맛집을 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/semosan/api/common/status/SuccessStatus.java b/src/main/java/com/semosan/api/common/status/SuccessStatus.java index 0256846..e8e9280 100644 --- a/src/main/java/com/semosan/api/common/status/SuccessStatus.java +++ b/src/main/java/com/semosan/api/common/status/SuccessStatus.java @@ -138,7 +138,15 @@ public enum SuccessStatus implements BaseStatus { /** * Admin */ - ADMIN_LOGIN_SUCCESS(HttpStatus.OK, "ADM_200_1", "관리자 로그인에 성공했습니다."); + ADMIN_LOGIN_SUCCESS(HttpStatus.OK, "ADM_200_1", "관리자 로그인에 성공했습니다."), + ADMIN_MOUNTAIN_UPDATE_SUCCESS(HttpStatus.OK, "ADM_200_2", "산 정보 수정에 성공했습니다."), + ADMIN_MOUNTAIN_VISIBILITY_UPDATE_SUCCESS(HttpStatus.OK, "ADM_200_3", "산 공개 상태 변경에 성공했습니다."), + ADMIN_RESTAURANT_SECTION_UPDATE_SUCCESS(HttpStatus.OK, "ADM_200_4", "맛집 섹션 수정에 성공했습니다."), + ADMIN_RESTAURANT_SECTION_DELETE_SUCCESS(HttpStatus.OK, "ADM_200_5", "맛집 섹션 삭제에 성공했습니다."), + ADMIN_RESTAURANT_UPDATE_SUCCESS(HttpStatus.OK, "ADM_200_6", "맛집 수정에 성공했습니다."), + ADMIN_RESTAURANT_DELETE_SUCCESS(HttpStatus.OK, "ADM_200_7", "맛집 삭제에 성공했습니다."), + ADMIN_RESTAURANT_SECTION_CREATE_SUCCESS(HttpStatus.CREATED, "ADM_201_1", "맛집 섹션 생성에 성공했습니다."), + ADMIN_RESTAURANT_CREATE_SUCCESS(HttpStatus.CREATED, "ADM_201_2", "맛집 생성에 성공했습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/com/semosan/api/domain/admin/controller/AdminMountainController.java b/src/main/java/com/semosan/api/domain/admin/controller/AdminMountainController.java new file mode 100644 index 0000000..d018c76 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/admin/controller/AdminMountainController.java @@ -0,0 +1,96 @@ +package com.semosan.api.domain.admin.controller; + +import com.semosan.api.common.response.ApiResponse; +import com.semosan.api.common.status.SuccessStatus; +import com.semosan.api.domain.admin.controller.docs.AdminMountainControllerDocs; +import com.semosan.api.domain.admin.dto.request.AdminMountainUpdateRequest; +import com.semosan.api.domain.admin.dto.request.AdminMountainVisibilityRequest; +import com.semosan.api.domain.admin.dto.request.AdminRestaurantRequest; +import com.semosan.api.domain.admin.dto.request.AdminRestaurantSectionRequest; +import com.semosan.api.domain.admin.service.AdminMountainService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +public class AdminMountainController implements AdminMountainControllerDocs { + + private final AdminMountainService adminMountainService; + + @PutMapping("/mountains/{mountainId}") + @Override + public ResponseEntity> updateMountain( + @PathVariable Long mountainId, + @Valid @RequestBody AdminMountainUpdateRequest request + ) { + adminMountainService.updateMountain(mountainId, request); + return ApiResponse.success(SuccessStatus.ADMIN_MOUNTAIN_UPDATE_SUCCESS); + } + + @PatchMapping("/mountains/{mountainId}/visibility") + @Override + public ResponseEntity> updateVisibility( + @PathVariable Long mountainId, + @Valid @RequestBody AdminMountainVisibilityRequest request + ) { + adminMountainService.updateVisibility(mountainId, request); + return ApiResponse.success(SuccessStatus.ADMIN_MOUNTAIN_VISIBILITY_UPDATE_SUCCESS); + } + + @PostMapping("/mountains/{mountainId}/restaurant-sections") + @Override + public ResponseEntity> createRestaurantSection( + @PathVariable Long mountainId, + @Valid @RequestBody AdminRestaurantSectionRequest request + ) { + Long sectionId = adminMountainService.createRestaurantSection(mountainId, request); + return ApiResponse.success(SuccessStatus.ADMIN_RESTAURANT_SECTION_CREATE_SUCCESS, sectionId); + } + + @PutMapping("/restaurant-sections/{sectionId}") + @Override + public ResponseEntity> updateRestaurantSection( + @PathVariable Long sectionId, + @Valid @RequestBody AdminRestaurantSectionRequest request + ) { + adminMountainService.updateRestaurantSection(sectionId, request); + return ApiResponse.success(SuccessStatus.ADMIN_RESTAURANT_SECTION_UPDATE_SUCCESS); + } + + @DeleteMapping("/restaurant-sections/{sectionId}") + @Override + public ResponseEntity> deleteRestaurantSection(@PathVariable Long sectionId) { + adminMountainService.deleteRestaurantSection(sectionId); + return ApiResponse.success(SuccessStatus.ADMIN_RESTAURANT_SECTION_DELETE_SUCCESS); + } + + @PostMapping("/restaurant-sections/{sectionId}/restaurants") + @Override + public ResponseEntity> createRestaurant( + @PathVariable Long sectionId, + @Valid @RequestBody AdminRestaurantRequest request + ) { + Long restaurantId = adminMountainService.createRestaurant(sectionId, request); + return ApiResponse.success(SuccessStatus.ADMIN_RESTAURANT_CREATE_SUCCESS, restaurantId); + } + + @PutMapping("/restaurants/{restaurantId}") + @Override + public ResponseEntity> updateRestaurant( + @PathVariable Long restaurantId, + @Valid @RequestBody AdminRestaurantRequest request + ) { + adminMountainService.updateRestaurant(restaurantId, request); + return ApiResponse.success(SuccessStatus.ADMIN_RESTAURANT_UPDATE_SUCCESS); + } + + @DeleteMapping("/restaurants/{restaurantId}") + @Override + public ResponseEntity> deleteRestaurant(@PathVariable Long restaurantId) { + adminMountainService.deleteRestaurant(restaurantId); + return ApiResponse.success(SuccessStatus.ADMIN_RESTAURANT_DELETE_SUCCESS); + } +} diff --git a/src/main/java/com/semosan/api/domain/admin/controller/docs/AdminMountainControllerDocs.java b/src/main/java/com/semosan/api/domain/admin/controller/docs/AdminMountainControllerDocs.java new file mode 100644 index 0000000..7a441f6 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/admin/controller/docs/AdminMountainControllerDocs.java @@ -0,0 +1,92 @@ +package com.semosan.api.domain.admin.controller.docs; + +import com.semosan.api.common.response.ApiResponse; +import com.semosan.api.domain.admin.dto.request.AdminMountainUpdateRequest; +import com.semosan.api.domain.admin.dto.request.AdminMountainVisibilityRequest; +import com.semosan.api.domain.admin.dto.request.AdminRestaurantRequest; +import com.semosan.api.domain.admin.dto.request.AdminRestaurantSectionRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "Admin Mountain", description = "관리자 산 정보 관리 API") +public interface AdminMountainControllerDocs { + + @Operation(summary = "산 정보 수정", description = "산의 이름, 주소, 고도, 난이도, 소요시간, 이미지를 수정합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "산 정보 수정 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "산을 찾을 수 없음") + }) + ResponseEntity> updateMountain( + @PathVariable Long mountainId, + @Valid @RequestBody AdminMountainUpdateRequest request + ); + + @Operation(summary = "산 공개/비공개 처리", description = "산의 공개 상태를 변경합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "공개 상태 변경 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "산을 찾을 수 없음") + }) + ResponseEntity> updateVisibility( + @PathVariable Long mountainId, + @Valid @RequestBody AdminMountainVisibilityRequest request + ); + + @Operation(summary = "맛집 섹션 생성", description = "산에 맛집 섹션을 추가합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "맛집 섹션 생성 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "산을 찾을 수 없음") + }) + ResponseEntity> createRestaurantSection( + @PathVariable Long mountainId, + @Valid @RequestBody AdminRestaurantSectionRequest request + ); + + @Operation(summary = "맛집 섹션 수정", description = "맛집 섹션의 제목을 수정합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "맛집 섹션 수정 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "맛집 섹션을 찾을 수 없음") + }) + ResponseEntity> updateRestaurantSection( + @PathVariable Long sectionId, + @Valid @RequestBody AdminRestaurantSectionRequest request + ); + + @Operation(summary = "맛집 섹션 삭제", description = "맛집 섹션과 소속 맛집을 모두 삭제합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "맛집 섹션 삭제 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "맛집 섹션을 찾을 수 없음") + }) + ResponseEntity> deleteRestaurantSection(@PathVariable Long sectionId); + + @Operation(summary = "맛집 추가", description = "맛집 섹션에 맛집을 추가합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "맛집 생성 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "맛집 섹션을 찾을 수 없음") + }) + ResponseEntity> createRestaurant( + @PathVariable Long sectionId, + @Valid @RequestBody AdminRestaurantRequest request + ); + + @Operation(summary = "맛집 수정", description = "맛집 정보를 수정합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "맛집 수정 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "맛집을 찾을 수 없음") + }) + ResponseEntity> updateRestaurant( + @PathVariable Long restaurantId, + @Valid @RequestBody AdminRestaurantRequest request + ); + + @Operation(summary = "맛집 삭제", description = "맛집을 삭제합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "맛집 삭제 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "맛집을 찾을 수 없음") + }) + ResponseEntity> deleteRestaurant(@PathVariable Long restaurantId); +} diff --git a/src/main/java/com/semosan/api/domain/admin/dto/request/AdminMountainUpdateRequest.java b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminMountainUpdateRequest.java new file mode 100644 index 0000000..1971b28 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminMountainUpdateRequest.java @@ -0,0 +1,17 @@ +package com.semosan.api.domain.admin.dto.request; + +import com.semosan.api.domain.mountain.enums.Difficulty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public record AdminMountainUpdateRequest( + @NotBlank String name, + @NotBlank String address, + @NotNull Double altitude, + @NotNull Difficulty difficulty, + Integer duration, + List imageUrls +) { +} diff --git a/src/main/java/com/semosan/api/domain/admin/dto/request/AdminMountainVisibilityRequest.java b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminMountainVisibilityRequest.java new file mode 100644 index 0000000..2dc055c --- /dev/null +++ b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminMountainVisibilityRequest.java @@ -0,0 +1,8 @@ +package com.semosan.api.domain.admin.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record AdminMountainVisibilityRequest( + @NotNull Boolean isPublic +) { +} diff --git a/src/main/java/com/semosan/api/domain/admin/dto/request/AdminRestaurantRequest.java b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminRestaurantRequest.java new file mode 100644 index 0000000..0d28cc9 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminRestaurantRequest.java @@ -0,0 +1,14 @@ +package com.semosan.api.domain.admin.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record AdminRestaurantRequest( + @NotBlank String name, + String category, + String menu, + String description, + String imageUrl, + String mapUrl, + String blogUrl +) { +} diff --git a/src/main/java/com/semosan/api/domain/admin/dto/request/AdminRestaurantSectionRequest.java b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminRestaurantSectionRequest.java new file mode 100644 index 0000000..c601130 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/admin/dto/request/AdminRestaurantSectionRequest.java @@ -0,0 +1,8 @@ +package com.semosan.api.domain.admin.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record AdminRestaurantSectionRequest( + @NotBlank String title +) { +} diff --git a/src/main/java/com/semosan/api/domain/admin/service/AdminMountainService.java b/src/main/java/com/semosan/api/domain/admin/service/AdminMountainService.java new file mode 100644 index 0000000..b6eb341 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/admin/service/AdminMountainService.java @@ -0,0 +1,91 @@ +package com.semosan.api.domain.admin.service; + +import com.semosan.api.common.exception.GeneralException; +import com.semosan.api.common.status.ErrorStatus; +import com.semosan.api.domain.admin.dto.request.AdminMountainUpdateRequest; +import com.semosan.api.domain.admin.dto.request.AdminMountainVisibilityRequest; +import com.semosan.api.domain.admin.dto.request.AdminRestaurantRequest; +import com.semosan.api.domain.admin.dto.request.AdminRestaurantSectionRequest; +import com.semosan.api.domain.mountain.entity.Mountain; +import com.semosan.api.domain.mountain.entity.Restaurant; +import com.semosan.api.domain.mountain.entity.RestaurantSection; +import com.semosan.api.domain.mountain.repository.MountainRepository; +import com.semosan.api.domain.mountain.repository.RestaurantRepository; +import com.semosan.api.domain.mountain.repository.RestaurantSectionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AdminMountainService { + + private final MountainRepository mountainRepository; + private final RestaurantSectionRepository restaurantSectionRepository; + private final RestaurantRepository restaurantRepository; + + @Transactional + public void updateMountain(Long mountainId, AdminMountainUpdateRequest request) { + Mountain mountain = findMountainById(mountainId); + mountain.updateInfo(request.name(), request.address(), request.altitude(), + request.difficulty(), request.duration()); + mountain.updateImageUrls(request.imageUrls()); + } + + @Transactional + public void updateVisibility(Long mountainId, AdminMountainVisibilityRequest request) { + Mountain mountain = findMountainById(mountainId); + mountain.updateVisibility(request.isPublic()); + } + + @Transactional + public Long createRestaurantSection(Long mountainId, AdminRestaurantSectionRequest request) { + Mountain mountain = findMountainById(mountainId); + RestaurantSection section = RestaurantSection.create(mountain, request.title()); + return restaurantSectionRepository.save(section).getId(); + } + + @Transactional + public void updateRestaurantSection(Long sectionId, AdminRestaurantSectionRequest request) { + RestaurantSection section = restaurantSectionRepository.findById(sectionId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESTAURANT_SECTION_NOT_FOUND)); + section.updateTitle(request.title()); + } + + @Transactional + public void deleteRestaurantSection(Long sectionId) { + RestaurantSection section = restaurantSectionRepository.findById(sectionId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESTAURANT_SECTION_NOT_FOUND)); + restaurantSectionRepository.delete(section); + } + + @Transactional + public Long createRestaurant(Long sectionId, AdminRestaurantRequest request) { + RestaurantSection section = restaurantSectionRepository.findById(sectionId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESTAURANT_SECTION_NOT_FOUND)); + Restaurant restaurant = Restaurant.create(section, request.name(), request.category(), + request.menu(), request.description(), request.imageUrl(), + request.mapUrl(), request.blogUrl()); + return restaurantRepository.save(restaurant).getId(); + } + + @Transactional + public void updateRestaurant(Long restaurantId, AdminRestaurantRequest request) { + Restaurant restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESTAURANT_NOT_FOUND)); + restaurant.update(request.name(), request.category(), request.menu(), + request.description(), request.imageUrl(), request.mapUrl(), request.blogUrl()); + } + + @Transactional + public void deleteRestaurant(Long restaurantId) { + Restaurant restaurant = restaurantRepository.findById(restaurantId) + .orElseThrow(() -> new GeneralException(ErrorStatus.RESTAURANT_NOT_FOUND)); + restaurantRepository.delete(restaurant); + } + + private Mountain findMountainById(Long mountainId) { + return mountainRepository.findById(mountainId) + .orElseThrow(() -> new GeneralException(ErrorStatus.MOUNTAIN_NOT_FOUND)); + } +} diff --git a/src/main/java/com/semosan/api/domain/mountain/entity/Mountain.java b/src/main/java/com/semosan/api/domain/mountain/entity/Mountain.java index d86a3e4..493d764 100644 --- a/src/main/java/com/semosan/api/domain/mountain/entity/Mountain.java +++ b/src/main/java/com/semosan/api/domain/mountain/entity/Mountain.java @@ -64,6 +64,22 @@ public class Mountain extends BaseEntity { @Column(name = "is_public", nullable = false) private boolean isPublic = true; + public void updateInfo(String name, String address, Double altitude, Difficulty difficulty, Integer duration) { + this.name = name; + this.address = address; + this.altitude = altitude; + this.difficulty = difficulty; + this.duration = duration; + } + + public void updateImageUrls(List imageUrls) { + this.imageUrls = imageUrls; + } + + public void updateVisibility(boolean isPublic) { + this.isPublic = isPublic; + } + public void updateCoordinates(Double latitude, Double longitude) { this.latitude = latitude; this.longitude = longitude; diff --git a/src/main/java/com/semosan/api/domain/mountain/entity/Restaurant.java b/src/main/java/com/semosan/api/domain/mountain/entity/Restaurant.java index d7a1460..85c3cc9 100644 --- a/src/main/java/com/semosan/api/domain/mountain/entity/Restaurant.java +++ b/src/main/java/com/semosan/api/domain/mountain/entity/Restaurant.java @@ -40,4 +40,30 @@ public class Restaurant extends BaseEntity { @Column(name = "blog_url", columnDefinition = "TEXT") private String blogUrl; + + public static Restaurant create(RestaurantSection section, String name, String category, + String menu, String description, String imageUrl, + String mapUrl, String blogUrl) { + return Restaurant.builder() + .section(section) + .name(name) + .category(category) + .menu(menu) + .description(description) + .imageUrl(imageUrl) + .mapUrl(mapUrl) + .blogUrl(blogUrl) + .build(); + } + + public void update(String name, String category, String menu, String description, + String imageUrl, String mapUrl, String blogUrl) { + this.name = name; + this.category = category; + this.menu = menu; + this.description = description; + this.imageUrl = imageUrl; + this.mapUrl = mapUrl; + this.blogUrl = blogUrl; + } } diff --git a/src/main/java/com/semosan/api/domain/mountain/entity/RestaurantSection.java b/src/main/java/com/semosan/api/domain/mountain/entity/RestaurantSection.java index fb129bd..50d9051 100644 --- a/src/main/java/com/semosan/api/domain/mountain/entity/RestaurantSection.java +++ b/src/main/java/com/semosan/api/domain/mountain/entity/RestaurantSection.java @@ -26,7 +26,18 @@ public class RestaurantSection extends BaseEntity { @Column(name = "title", nullable = false, length = 100) private String title; - @OneToMany(mappedBy = "section", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "section", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) @Builder.Default private List restaurants = new ArrayList<>(); + + public static RestaurantSection create(Mountain mountain, String title) { + return RestaurantSection.builder() + .mountain(mountain) + .title(title) + .build(); + } + + public void updateTitle(String title) { + this.title = title; + } }