diff --git a/src/main/kotlin/com/routebox/routebox/application/route/CreateRoutePointUseCase.kt b/src/main/kotlin/com/routebox/routebox/application/route/CreateRoutePointUseCase.kt new file mode 100644 index 0000000..42d0eea --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/application/route/CreateRoutePointUseCase.kt @@ -0,0 +1,29 @@ +package com.routebox.routebox.application.route + +import com.routebox.routebox.application.route.dto.CreateRoutePointCommand +import com.routebox.routebox.application.route.dto.CreateRoutePointResult +import com.routebox.routebox.domain.route.RouteService +import org.springframework.stereotype.Component + +@Component +class CreateRoutePointUseCase( + private val routeService: RouteService, +) { + /** + * 루트 위치 기록 + * + * @param command user id, route id, latitude, longitude, point order + * @return 루트 point id + * @throws + */ + operator fun invoke(command: CreateRoutePointCommand): CreateRoutePointResult { + // TODO: 루트 소유자와 요청자가 같은지 확인 + val routePoint = routeService.createRoutePoint( + routeId = command.routeId, + latitude = command.latitude, + longitude = command.longitude, + pointOrder = command.pointOrder, + ) + return CreateRoutePointResult.from(routePoint) + } +} diff --git a/src/main/kotlin/com/routebox/routebox/application/route/CreateRouteUseCase.kt b/src/main/kotlin/com/routebox/routebox/application/route/CreateRouteUseCase.kt new file mode 100644 index 0000000..8b093b0 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/application/route/CreateRouteUseCase.kt @@ -0,0 +1,26 @@ +package com.routebox.routebox.application.route + +import com.routebox.routebox.application.route.dto.CreateRouteCommand +import com.routebox.routebox.application.route.dto.CreateRouteResult +import com.routebox.routebox.domain.route.RouteService +import com.routebox.routebox.domain.user.UserService +import org.springframework.stereotype.Component + +@Component +class CreateRouteUseCase( + private val routeService: RouteService, + private val userService: UserService, +) { + /** + * 루트 등록 + * + * @param command user id, startTime, endTime + * @return 루트 id + * @throws + */ + operator fun invoke(command: CreateRouteCommand): CreateRouteResult { + val user = userService.getUserById(command.userId) + val route = routeService.createRoute(user = user, startTime = command.startTime, endTime = command.endTime) + return CreateRouteResult.from(route) + } +} diff --git a/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRouteCommand.kt b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRouteCommand.kt new file mode 100644 index 0000000..81f6cd0 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRouteCommand.kt @@ -0,0 +1,9 @@ +package com.routebox.routebox.application.route.dto + +import java.time.LocalDateTime + +data class CreateRouteCommand( + val userId: Long, + val startTime: LocalDateTime, + val endTime: LocalDateTime, +) diff --git a/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRoutePointCommand.kt b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRoutePointCommand.kt new file mode 100644 index 0000000..7152334 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRoutePointCommand.kt @@ -0,0 +1,9 @@ +package com.routebox.routebox.application.route.dto + +data class CreateRoutePointCommand( + val userId: Long, + val routeId: Long, + val latitude: String, + val longitude: String, + val pointOrder: Int, +) diff --git a/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRoutePointResult.kt b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRoutePointResult.kt new file mode 100644 index 0000000..d1ccef7 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRoutePointResult.kt @@ -0,0 +1,13 @@ +package com.routebox.routebox.application.route.dto + +import com.routebox.routebox.domain.route.RoutePoint + +data class CreateRoutePointResult( + val pointId: Long, +) { + companion object { + fun from(point: RoutePoint): CreateRoutePointResult = CreateRoutePointResult( + pointId = point.id, + ) + } +} diff --git a/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRouteResult.kt b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRouteResult.kt new file mode 100644 index 0000000..3ec81e3 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/application/route/dto/CreateRouteResult.kt @@ -0,0 +1,13 @@ +package com.routebox.routebox.application.route.dto + +import com.routebox.routebox.domain.route.Route + +data class CreateRouteResult( + val routeId: Long, +) { + companion object { + fun from(route: Route): CreateRouteResult = CreateRouteResult( + routeId = route.id, + ) + } +} diff --git a/src/main/kotlin/com/routebox/routebox/application/route/dto/GetRouteDetailResult.kt b/src/main/kotlin/com/routebox/routebox/application/route/dto/GetRouteDetailResult.kt index 045113b..07cbe83 100644 --- a/src/main/kotlin/com/routebox/routebox/application/route/dto/GetRouteDetailResult.kt +++ b/src/main/kotlin/com/routebox/routebox/application/route/dto/GetRouteDetailResult.kt @@ -8,8 +8,8 @@ data class GetRouteDetailResult( val userId: Long, val nickname: String, val profileImageUrl: String, - val routeName: String, - val routeDescription: String, + val routeName: String?, + val routeDescription: String?, val routeImageUrls: List, val isPurchased: Boolean, val purchaseCount: Int, diff --git a/src/main/kotlin/com/routebox/routebox/controller/route/RouteCommandController.kt b/src/main/kotlin/com/routebox/routebox/controller/route/RouteCommandController.kt new file mode 100644 index 0000000..5946998 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/controller/route/RouteCommandController.kt @@ -0,0 +1,66 @@ +package com.routebox.routebox.controller.route + +import com.routebox.routebox.application.route.CreateRoutePointUseCase +import com.routebox.routebox.application.route.CreateRouteUseCase +import com.routebox.routebox.controller.route.dto.CreateRoutePointRequest +import com.routebox.routebox.controller.route.dto.CreateRoutePointResponse +import com.routebox.routebox.controller.route.dto.CreateRouteRequest +import com.routebox.routebox.controller.route.dto.CreateRouteResponse +import com.routebox.routebox.security.UserPrincipal +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.validation.Valid +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.validation.annotation.Validated +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 + +@Tag(name = "내루트 관련 API") +@RestController +@Validated +@RequestMapping("/api") +class RouteCommandController( + private val createRouteUseCase: CreateRouteUseCase, + private val createRoutePointUseCase: CreateRoutePointUseCase, +) { + @Operation( + summary = "루트 생성 (루트 기록 시작)", + description = "루트 기록 시작일시, 종료일시 등록", + security = [SecurityRequirement(name = "access-token")], + ) + @ApiResponses( + ApiResponse(responseCode = "200"), + ) + @PostMapping("/v1/routes/start") + fun createRoute( + @AuthenticationPrincipal userPrincipal: UserPrincipal, + @RequestBody @Valid request: CreateRouteRequest, + ): CreateRouteResponse { + val routeResponse = createRouteUseCase(request.toCommand(userId = userPrincipal.userId)) + return CreateRouteResponse.from(routeResponse.routeId) + } + + @Operation( + summary = "루트 경로(점) 기록", + description = "1분마다 현재 위치 보내서 루트 경로의 점 찍는데 사용", + security = [SecurityRequirement(name = "access-token")], + ) + @ApiResponses( + ApiResponse(responseCode = "200"), + ) + @PostMapping("/v1/routes/{routeId}/point") + fun createRoutePoint( + @AuthenticationPrincipal userPrincipal: UserPrincipal, + @PathVariable routeId: Long, + @RequestBody @Valid request: CreateRoutePointRequest, + ): CreateRoutePointResponse { + val routeResponse = createRoutePointUseCase(request.toCommand(userId = userPrincipal.userId, routeId = routeId)) + return CreateRoutePointResponse.from(routeResponse.pointId) + } +} diff --git a/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRoutePointRequest.kt b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRoutePointRequest.kt new file mode 100644 index 0000000..9720106 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRoutePointRequest.kt @@ -0,0 +1,22 @@ +package com.routebox.routebox.controller.route.dto + +import com.routebox.routebox.application.route.dto.CreateRoutePointCommand +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateRoutePointRequest( + @Schema(description = "위도", example = "37.123456") + val latitude: String, + @Schema(description = "경도", example = "127.123456") + val longitude: String, + @Schema(description = "경로 순서, 1부터 시작, 위치 기록할 때마다 +1 해서 전송", example = "1") + val pointOrder: Int, +) { + fun toCommand(userId: Long, routeId: Long): CreateRoutePointCommand = + CreateRoutePointCommand( + userId = userId, + routeId = routeId, + latitude = latitude, + longitude = longitude, + pointOrder = pointOrder, + ) +} diff --git a/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRoutePointResponse.kt b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRoutePointResponse.kt new file mode 100644 index 0000000..bca186f --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRoutePointResponse.kt @@ -0,0 +1,14 @@ +package com.routebox.routebox.controller.route.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateRoutePointResponse( + @Schema(description = "위치 점 id", example = "1") + val pointId: Long, +) { + companion object { + fun from(pointId: Long): CreateRoutePointResponse = CreateRoutePointResponse( + pointId = pointId, + ) + } +} diff --git a/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRouteRequest.kt b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRouteRequest.kt new file mode 100644 index 0000000..1a520df --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRouteRequest.kt @@ -0,0 +1,22 @@ +package com.routebox.routebox.controller.route.dto + +import com.routebox.routebox.application.route.dto.CreateRouteCommand +import com.routebox.routebox.util.DateTimeUtil +import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.format.annotation.DateTimeFormat + +data class CreateRouteRequest( + @Schema(description = "루트 기록 시작일시", example = "2024-08-01T00:00:00") + @field:DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + val startTime: String, + @Schema(description = "루트 기록 종료일시", example = "2024-08-01T00:00:00") + @field:DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + val endTime: String, +) { + fun toCommand(userId: Long): CreateRouteCommand = + CreateRouteCommand( + userId = userId, + startTime = DateTimeUtil.parseToLocalDateTime(startTime), + endTime = DateTimeUtil.parseToLocalDateTime(endTime), + ) +} diff --git a/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRouteResponse.kt b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRouteResponse.kt new file mode 100644 index 0000000..7423d34 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/controller/route/dto/CreateRouteResponse.kt @@ -0,0 +1,14 @@ +package com.routebox.routebox.controller.route.dto + +import io.swagger.v3.oas.annotations.media.Schema + +data class CreateRouteResponse( + @Schema(description = "루트 id", example = "1") + val routeId: Long, +) { + companion object { + fun from(routeId: Long): CreateRouteResponse = CreateRouteResponse( + routeId = routeId, + ) + } +} diff --git a/src/main/kotlin/com/routebox/routebox/controller/route/dto/RouteResponse.kt b/src/main/kotlin/com/routebox/routebox/controller/route/dto/RouteResponse.kt index b882805..f51c4df 100644 --- a/src/main/kotlin/com/routebox/routebox/controller/route/dto/RouteResponse.kt +++ b/src/main/kotlin/com/routebox/routebox/controller/route/dto/RouteResponse.kt @@ -18,10 +18,10 @@ data class RouteResponse( val nickname: String, @Schema(description = "루트 제목", example = "서울의 작가들") - val routeName: String, + val routeName: String?, @Schema(description = "루트 설명", example = "서울의 작가들을 만나보세요.") - val routeDescription: String, + val routeDescription: String?, @Schema(description = "루트 이미지들", example = "[\"https://route-image1\", \"https://route-image2\"]") val routeImageUrls: List, diff --git a/src/main/kotlin/com/routebox/routebox/domain/route/Route.kt b/src/main/kotlin/com/routebox/routebox/domain/route/Route.kt index 60eaf4a..7be6678 100644 --- a/src/main/kotlin/com/routebox/routebox/domain/route/Route.kt +++ b/src/main/kotlin/com/routebox/routebox/domain/route/Route.kt @@ -12,6 +12,7 @@ import jakarta.persistence.GenerationType import jakarta.persistence.Id import jakarta.persistence.JoinColumn import jakarta.persistence.ManyToOne +import jakarta.persistence.OneToMany import jakarta.persistence.Table import java.time.LocalDateTime @@ -19,13 +20,13 @@ import java.time.LocalDateTime @Entity class Route( id: Long = 0, - name: String, - description: String, + name: String?, + description: String?, startTime: LocalDateTime, endTime: LocalDateTime, - whoWith: String, - numberOfPeople: Int, - numberOfDays: String, + whoWith: String?, + numberOfPeople: Int?, + numberOfDays: String?, style: Array, transportation: Array, isPublic: Boolean = false, @@ -40,9 +41,9 @@ class Route( @JoinColumn(name = "user_id") val user: User? = user - var name: String = name + var name: String? = name - var description: String = description + var description: String? = description var startTime: LocalDateTime = startTime @@ -63,4 +64,7 @@ class Route( var transportation: Array = transportation var isPublic: Boolean = isPublic + + @OneToMany(mappedBy = "route") + var routePoints: List = mutableListOf() } diff --git a/src/main/kotlin/com/routebox/routebox/domain/route/RoutePoint.kt b/src/main/kotlin/com/routebox/routebox/domain/route/RoutePoint.kt new file mode 100644 index 0000000..0d7c853 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/domain/route/RoutePoint.kt @@ -0,0 +1,40 @@ +package com.routebox.routebox.domain.route + +import com.routebox.routebox.domain.common.TimeTrackedBaseEntity +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table + +@Table(name = "route_points") +@Entity +class RoutePoint( + id: Long = 0, + route: Route, + latitude: String, + longitude: String, + pointOrder: Int, +) : TimeTrackedBaseEntity() { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "point_id") + val id: Long = id + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "route_id", nullable = false) + val route: Route = route + + @Column(name = "latitude", nullable = false) + var latitude: String = latitude + + @Column(name = "longitude", nullable = false) + var longitude: String = longitude + + @Column(name = "point_order", nullable = false) + var pointOrder: Int = pointOrder +} diff --git a/src/main/kotlin/com/routebox/routebox/domain/route/RouteService.kt b/src/main/kotlin/com/routebox/routebox/domain/route/RouteService.kt index c0a02c2..cdb71ee 100644 --- a/src/main/kotlin/com/routebox/routebox/domain/route/RouteService.kt +++ b/src/main/kotlin/com/routebox/routebox/domain/route/RouteService.kt @@ -1,11 +1,14 @@ package com.routebox.routebox.domain.route +import com.routebox.routebox.domain.user.User +import com.routebox.routebox.infrastructure.route.RoutePointRepository import com.routebox.routebox.infrastructure.route.RouteRepository import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service +import java.time.LocalDateTime @Service -class RouteService(private val routeRepository: RouteRepository) { +class RouteService(private val routeRepository: RouteRepository, private val routePointRepository: RoutePointRepository) { /** * 루트 탐색 - 최신순 루트 조회 */ @@ -18,4 +21,38 @@ class RouteService(private val routeRepository: RouteRepository) { * 루트 상세 조회 */ fun getRouteById(id: Long): Route? = routeRepository.findById(id).orElse(null) + + /** + * 루트 생성 + */ + fun createRoute(user: User, startTime: LocalDateTime, endTime: LocalDateTime): Route { + val route = Route( + user = user, + startTime = startTime, + endTime = endTime, + name = null, + description = null, + whoWith = null, + numberOfPeople = null, + numberOfDays = null, + style = emptyArray(), + transportation = emptyArray(), + isPublic = false, + ) + return routeRepository.save(route) + } + + /** + * 루트 점찍기 + */ + fun createRoutePoint(routeId: Long, latitude: String, longitude: String, pointOrder: Int): RoutePoint { + val route = getRouteById(routeId) ?: throw IllegalArgumentException("Route not found") + val routePoint = RoutePoint( + route = route, + latitude = latitude, + longitude = longitude, + pointOrder = pointOrder, + ) + return routePointRepository.save(routePoint) + } } diff --git a/src/main/kotlin/com/routebox/routebox/infrastructure/route/RoutePointRepository.kt b/src/main/kotlin/com/routebox/routebox/infrastructure/route/RoutePointRepository.kt new file mode 100644 index 0000000..c33ce76 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/infrastructure/route/RoutePointRepository.kt @@ -0,0 +1,8 @@ +package com.routebox.routebox.infrastructure.route + +import com.routebox.routebox.domain.route.RoutePoint +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface RoutePointRepository : JpaRepository diff --git a/src/main/kotlin/com/routebox/routebox/util/DateTimeUtil.kt b/src/main/kotlin/com/routebox/routebox/util/DateTimeUtil.kt new file mode 100644 index 0000000..3ad09d4 --- /dev/null +++ b/src/main/kotlin/com/routebox/routebox/util/DateTimeUtil.kt @@ -0,0 +1,15 @@ +package com.routebox.routebox.util + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +object DateTimeUtil { + + /** + * 기본 ISO_LOCAL_DATE_TIME 포맷 ("yyyy-MM-ddTHH:mm:ss")을 사용한 변환 + */ + fun parseToLocalDateTime(dateTimeString: String): LocalDateTime { + val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME + return LocalDateTime.parse(dateTimeString, formatter) + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8f810c6..7e4c700 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -122,13 +122,13 @@ CREATE TABLE routes ( route_id BIGINT AUTO_INCREMENT, user_id BIGINT, - name VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - start_time TIMESTAMP NOT NULL, - end_time TIMESTAMP NOT NULL, - who_with VARCHAR(255) NOT NULL, - number_of_people INT NOT NULL, - number_of_days VARCHAR(255) NOT NULL, + name VARCHAR(255), + description TEXT, + start_time TIMESTAMP, + end_time TIMESTAMP, + who_with VARCHAR(255), + number_of_people INT, + number_of_days VARCHAR(255), style JSON, transportation JSON, is_public BOOLEAN NOT NULL DEFAULT FALSE, @@ -137,3 +137,15 @@ CREATE TABLE routes PRIMARY KEY (route_id) ); CREATE INDEX idx__routes__user_id ON routes (user_id); + +CREATE TABLE route_points ( + point_id BIGINT AUTO_INCREMENT PRIMARY KEY, + route_id BIGINT NOT NULL, + latitude VARCHAR(255) NOT NULL, + longitude VARCHAR(255) NOT NULL, + point_order INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (route_id) REFERENCES routes(route_id) +); + diff --git a/src/test/kotlin/com/routebox/routebox/domain/route/RouteServiceTest.kt b/src/test/kotlin/com/routebox/routebox/domain/route/RouteServiceTest.kt index d605982..555f13a 100644 --- a/src/test/kotlin/com/routebox/routebox/domain/route/RouteServiceTest.kt +++ b/src/test/kotlin/com/routebox/routebox/domain/route/RouteServiceTest.kt @@ -1,5 +1,6 @@ package com.routebox.routebox.domain.route +import com.routebox.routebox.infrastructure.route.RoutePointRepository import com.routebox.routebox.infrastructure.route.RouteRepository import org.apache.commons.lang3.RandomStringUtils import org.assertj.core.api.Assertions @@ -25,6 +26,9 @@ class RouteServiceTest { @Mock private lateinit var routeRepository: RouteRepository + @Mock + private lateinit var routePointRepository: RoutePointRepository + @Test fun `루트를 최신순으로 반환한다`() { // given