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
10 changes: 10 additions & 0 deletions src/main/java/com/semosan/api/common/status/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ public enum ErrorStatus implements BaseStatus {
APPLE_PUBLIC_KEY_NOT_FOUND(HttpStatus.UNAUTHORIZED, "APPLE_401_1", "유효한 애플 공개키를 찾을 수 없습니다."),
APPLE_IDENTITY_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "APPLE_401_2", "애플 identity token이 유효하지 않습니다."),

/**
* Mountain
*/
MOUNTAIN_NOT_FOUND(HttpStatus.NOT_FOUND, "MTN_404_1", "산을 찾을 수 없습니다."),

/**
* Image
*/
IMAGE_UPLOAD_FAILED(HttpStatus.BAD_GATEWAY, "IMG_502_1", "이미지 업로드 URL 생성에 실패했습니다."),

/**
* Withdraw
*/
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/semosan/api/common/status/SuccessStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ public enum SuccessStatus implements BaseStatus {
*/
WITHDRAW_SUCCESS(HttpStatus.OK, "USER_200_1", "회원 탈퇴가 완료되었습니다."),

/**
* Mountain
*/
MOUNTAIN_LIST_SUCCESS(HttpStatus.OK, "MTN_200_1", "산 목록 조회에 성공했습니다."),
MOUNTAIN_SEARCH_SUCCESS(HttpStatus.OK, "MTN_200_2", "산 검색에 성공했습니다."),
MOUNTAIN_DETAIL_SUCCESS(HttpStatus.OK, "MTN_200_3", "산 상세 정보 조회에 성공했습니다."),

/**
* Image
*/
PRESIGNED_URL_SUCCESS(HttpStatus.OK, "IMG_200_1", "Presigned URL 발급에 성공했습니다.");

/**
* FCM / Notification
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.semosan.api.domain.mountain.controller;

import com.semosan.api.common.response.ApiResponse;
import com.semosan.api.common.status.SuccessStatus;
import com.semosan.api.domain.mountain.controller.docs.MountainControllerDocs;
import com.semosan.api.domain.mountain.dto.response.MountainDetailResponse;
import com.semosan.api.domain.mountain.dto.response.MountainListResponse;
import com.semosan.api.domain.mountain.service.MountainService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/mountains")
@RequiredArgsConstructor
public class MountainController implements MountainControllerDocs {

private final MountainService mountainService;

@GetMapping
@Override
public ResponseEntity<ApiResponse<List<MountainListResponse>>> getMountains() {
List<MountainListResponse> response = mountainService.getMountains();
return ApiResponse.success(SuccessStatus.MOUNTAIN_LIST_SUCCESS, response);
}

@GetMapping("/search")
@Override
public ResponseEntity<ApiResponse<List<MountainListResponse>>> searchMountains(
@RequestParam String keyword
) {
List<MountainListResponse> response = mountainService.searchMountains(keyword);
return ApiResponse.success(SuccessStatus.MOUNTAIN_SEARCH_SUCCESS, response);
}

@GetMapping("/{mountainId}")
@Override
public ResponseEntity<ApiResponse<MountainDetailResponse>> getMountainDetail(
@PathVariable Long mountainId
) {
MountainDetailResponse response = mountainService.getMountainDetail(mountainId);
return ApiResponse.success(SuccessStatus.MOUNTAIN_DETAIL_SUCCESS, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.semosan.api.domain.mountain.controller.docs;

import com.semosan.api.common.response.ApiResponse;
import com.semosan.api.domain.mountain.dto.response.MountainDetailResponse;
import com.semosan.api.domain.mountain.dto.response.MountainListResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Tag(name = "Mountain", description = "산 관련 API")
public interface MountainControllerDocs {

@Operation(
summary = "산 목록 조회",
description = "등록된 모든 산의 목록을 조회합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "산 목록 조회 성공"
)
})
ResponseEntity<ApiResponse<List<MountainListResponse>>> getMountains();

@Operation(
summary = "산 검색",
description = "산 이름 또는 주소로 검색합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "산 검색 성공"
)
})
ResponseEntity<ApiResponse<List<MountainListResponse>>> searchMountains(
@Parameter(description = "검색 키워드 (산 이름 또는 주소)", required = true)
@RequestParam String keyword
);

@Operation(
summary = "산 상세 정보 조회",
description = "산의 상세 정보를 조회합니다. 코스, 교통, 편의시설, 맛집, 리뷰 정보를 포함합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "산 상세 정보 조회 성공"
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "404",
description = "산을 찾을 수 없음",
content = @Content(schema = @Schema(implementation = ApiResponse.class))
)
})
ResponseEntity<ApiResponse<MountainDetailResponse>> getMountainDetail(
@Parameter(description = "산 ID", required = true)
@PathVariable Long mountainId
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.semosan.api.domain.mountain.dto.response;

import com.semosan.api.domain.mountain.entity.*;
import com.semosan.api.domain.mountain.enums.AmenityType;
import com.semosan.api.domain.mountain.enums.Difficulty;
import com.semosan.api.domain.mountain.enums.TransportationType;
import com.semosan.api.domain.review.entity.Review;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public record MountainDetailResponse(
MountainInfo mountain,
List<CourseInfo> courses,
TransportationGroup transportations,
Map<String, List<AmenityType>> amenities,
List<RestaurantSectionInfo> restaurantSections,
List<ReviewInfo> reviews
) {

public record MountainInfo(
Long mountainId,
String name,
String address,
Double altitude,
Difficulty difficulty,
Integer duration,
String imageUrl
) {
public static MountainInfo from(Mountain mountain) {
return new MountainInfo(
mountain.getId(),
mountain.getName(),
mountain.getAddress(),
mountain.getAltitude(),
mountain.getDifficulty(),
mountain.getDuration(),
mountain.getImageUrl()
);
}
}

public record CourseInfo(
Long courseId,
String name,
Difficulty difficulty,
Double distance,
Integer duration
) {
public static CourseInfo from(Course course) {
return new CourseInfo(
course.getId(),
course.getName(),
course.getDifficulty(),
course.getDistance(),
course.getDuration()
);
}
}

public record TransportationGroup(
Map<String, List<TransportationItem>> publicTransport,
Map<String, List<TransportationItem>> parking
) {
public static TransportationGroup from(List<Transportation> transportations) {
Map<String, List<TransportationItem>> publicTransport = transportations.stream()
.filter(t -> t.getType() == TransportationType.SUBWAY || t.getType() == TransportationType.BUS)
.collect(Collectors.groupingBy(
Transportation::getDirection,
Collectors.mapping(TransportationItem::from, Collectors.toList())
));
Map<String, List<TransportationItem>> parking = transportations.stream()
.filter(t -> t.getType() == TransportationType.PARKING)
.collect(Collectors.groupingBy(
Transportation::getDirection,
Collectors.mapping(TransportationItem::from, Collectors.toList())
));
return new TransportationGroup(publicTransport, parking);
}
}

public record TransportationItem(
Long transportationId,
TransportationType type,
String name,
String description
) {
public static TransportationItem from(Transportation transportation) {
return new TransportationItem(
transportation.getId(),
transportation.getType(),
transportation.getName(),
transportation.getDescription()
);
}
}

public static Map<String, List<AmenityType>> groupAmenities(List<Amenity> amenities) {
return amenities.stream()
.collect(Collectors.groupingBy(
Amenity::getDirection,
Collectors.mapping(Amenity::getType, Collectors.toList())
));
}

public record RestaurantSectionInfo(
String title,
List<RestaurantInfo> restaurants
) {
public static RestaurantSectionInfo from(RestaurantSection section) {
return new RestaurantSectionInfo(
section.getTitle(),
section.getRestaurants().stream()
.map(RestaurantInfo::from)
.toList()
);
}
}

public record RestaurantInfo(
Long restaurantId,
String name,
String category,
String imageUrl
) {
public static RestaurantInfo from(Restaurant restaurant) {
return new RestaurantInfo(
restaurant.getId(),
restaurant.getName(),
restaurant.getCategory(),
restaurant.getImageUrl()
);
}
}

public record ReviewInfo(
Long reviewId,
String imageUrl,
String authorName,
String content,
Difficulty difficulty,
String courseName
) {
public static ReviewInfo from(Review review) {
return new ReviewInfo(
review.getId(),
review.getImageUrl(),
review.getUser().getName(),
review.getContent(),
review.getDifficulty(),
review.getCourse() != null ? review.getCourse().getName() : null
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.semosan.api.domain.mountain.dto.response;

import com.semosan.api.domain.mountain.entity.Mountain;
import com.semosan.api.domain.mountain.enums.Difficulty;

public record MountainListResponse(
Long mountainId,
String name,
String address,
Double altitude,
Difficulty difficulty,
Integer duration,
String imageUrl,
Double latitude,
Double longitude
) {

public static MountainListResponse from(Mountain mountain) {
return new MountainListResponse(
mountain.getId(),
mountain.getName(),
mountain.getAddress(),
mountain.getAltitude(),
mountain.getDifficulty(),
mountain.getDuration(),
mountain.getImageUrl(),
mountain.getLatitude(),
mountain.getLongitude()
);
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/semosan/api/domain/mountain/entity/Amenity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.semosan.api.domain.mountain.entity;

import com.semosan.api.common.base.BaseEntity;
import com.semosan.api.domain.mountain.enums.AmenityType;
import jakarta.persistence.*;
import lombok.*;

@Table(name = "amenities")
@Getter
@Entity
@Builder(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Amenity extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mountain_id", nullable = false)
private Mountain mountain;

@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false, length = 20)
private AmenityType type;

@Column(name = "direction", nullable = false, length = 50)
private String direction;
}
Loading