From 3b99d6786f9de853b707d38a61bac5d5f368df02 Mon Sep 17 00:00:00 2001 From: DongGeon0908 Date: Wed, 21 Aug 2024 00:22:48 +0900 Subject: [PATCH] feat: pose layout feature --- sql/DDL.sql | 23 ++++++++ .../pose/application/PoseLayoutFacade.kt | 58 +++++++++++++++++++ .../application/PoseLayoutPointService.kt | 24 ++++++++ .../pose/application/PoseLayoutService.kt | 36 ++++++++++++ .../alignlab/domain/pose/domain/PoseLayout.kt | 16 +++++ .../domain/pose/domain/PoseLayoutPoint.kt | 27 +++++++++ .../PoseLayoutPointRepository.kt | 12 ++++ .../infrastructure/PoseLayoutRepository.kt | 14 +++++ .../pose/model/request/PoseLayoutRequest.kt | 14 +++++ .../model/response/GetPoseLayoutResponse.kt | 7 +++ .../response/GetRecentPoseLayoutResponse.kt | 27 +++++++++ .../response/RegisterPoseLayoutResponse.kt | 5 ++ .../pose/resource/PoseLayoutResource.kt | 48 +++++++++++++++ .../com/hero/alignlab/exception/ErrorCode.kt | 3 + 14 files changed, 314 insertions(+) create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutFacade.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutPointService.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutService.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayout.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayoutPoint.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutPointRepository.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutRepository.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/model/request/PoseLayoutRequest.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetPoseLayoutResponse.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetRecentPoseLayoutResponse.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/model/response/RegisterPoseLayoutResponse.kt create mode 100644 src/main/kotlin/com/hero/alignlab/domain/pose/resource/PoseLayoutResource.kt diff --git a/sql/DDL.sql b/sql/DDL.sql index a294807..cd19772 100644 --- a/sql/DDL.sql +++ b/sql/DDL.sql @@ -183,3 +183,26 @@ CREATE TABLE `pose_noti` PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT '자세 알림'; CREATE INDEX uidx__uid ON pose_noti (uid); + +-- 포즈 레이아웃 +CREATE TABLE `pose_layout` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `uid` bigint NOT NULL COMMENT 'uid', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일', + `modified_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT '포즈 레이아웃'; +CREATE INDEX idx__uid ON pose_layout (uid); + +-- 포즈 레이아웃 포인트 +CREATE TABLE `pose_layout_point` +( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'pose key point snapshot id', + `pose_layout_id` bigint NOT NULL COMMENT 'pose layout id', + `position` VARCHAR(32) NOT NULL COMMENT '스냅샷 위치', + `x` DECIMAL(20, 16) NOT NULL COMMENT 'x 좌표', + `y` DECIMAL(20, 16) NOT NULL COMMENT 'y 좌표', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='포즈 레이아웃 point'; +CREATE INDEX uidx__pose_snapshot_id__position ON pose_layout_point (pose_layout_id, position); diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutFacade.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutFacade.kt new file mode 100644 index 0000000..33b96d5 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutFacade.kt @@ -0,0 +1,58 @@ +package com.hero.alignlab.domain.pose.application + +import com.hero.alignlab.common.extension.executes +import com.hero.alignlab.config.database.TransactionTemplates +import com.hero.alignlab.domain.auth.model.AuthUser +import com.hero.alignlab.domain.pose.domain.PoseLayout +import com.hero.alignlab.domain.pose.domain.PoseLayoutPoint +import com.hero.alignlab.domain.pose.model.request.PoseLayoutRequest +import com.hero.alignlab.domain.pose.model.response.GetPoseLayoutResponse +import com.hero.alignlab.domain.pose.model.response.GetRecentPoseLayoutResponse +import com.hero.alignlab.domain.pose.model.response.PoseLayoutPointResponse +import com.hero.alignlab.domain.pose.model.response.RegisterPoseLayoutResponse +import org.springframework.stereotype.Component + +@Component +class PoseLayoutFacade( + private val txTemplates: TransactionTemplates, + private val poseLayoutService: PoseLayoutService, + private val postLayoutPointService: PoseLayoutPointService, +) { + suspend fun getRecentPoseLayout(user: AuthUser): GetRecentPoseLayoutResponse { + val poseLayout = poseLayoutService.findTop1ByUidOrderByIdDesc(user.uid) + ?: return GetRecentPoseLayoutResponse() + + val points = postLayoutPointService.findAllByPoseLayoutId(poseLayout.id) + .map { point -> PoseLayoutPointResponse.from(point) } + + return GetRecentPoseLayoutResponse(poseLayout.id, points) + } + + suspend fun getPoseLayout(user: AuthUser, id: Long): GetPoseLayoutResponse { + val poseLayout = poseLayoutService.findByIdAndUidOrThrow(id, user.uid) + + val points = postLayoutPointService.findAllByPoseLayoutId(poseLayout.id) + .map { point -> PoseLayoutPointResponse.from(point) } + + return GetPoseLayoutResponse(poseLayout.id, points) + } + + suspend fun register(user: AuthUser, request: PoseLayoutRequest): RegisterPoseLayoutResponse { + val createdPoseLayout = txTemplates.writer.executes { + val poseLayout = poseLayoutService.saveSync(PoseLayout(uid = user.uid)) + + request.points.map { point -> + PoseLayoutPoint( + poseLayoutId = poseLayout.id, + position = point.position, + x = point.x, + y = point.y, + ) + }.run { postLayoutPointService.saveAllSync(this) } + + poseLayout + } + + return RegisterPoseLayoutResponse(createdPoseLayout.id) + } +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutPointService.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutPointService.kt new file mode 100644 index 0000000..01b014b --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutPointService.kt @@ -0,0 +1,24 @@ +package com.hero.alignlab.domain.pose.application + +import com.hero.alignlab.domain.pose.domain.PoseLayoutPoint +import com.hero.alignlab.domain.pose.infrastructure.PoseLayoutPointRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class PoseLayoutPointService( + private val poseLayoutPointRepository: PoseLayoutPointRepository, +) { + suspend fun findAllByPoseLayoutId(poseLayoutPointId: Long): List { + return withContext(Dispatchers.IO) { + poseLayoutPointRepository.findAllByPoseLayoutId(poseLayoutPointId) + } + } + + @Transactional + fun saveAllSync(poseLayoutPoints: List): List { + return poseLayoutPointRepository.saveAll(poseLayoutPoints) + } +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutService.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutService.kt new file mode 100644 index 0000000..b2945ef --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/application/PoseLayoutService.kt @@ -0,0 +1,36 @@ +package com.hero.alignlab.domain.pose.application + +import com.hero.alignlab.domain.pose.domain.PoseLayout +import com.hero.alignlab.domain.pose.infrastructure.PoseLayoutRepository +import com.hero.alignlab.exception.ErrorCode +import com.hero.alignlab.exception.NotFoundException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class PoseLayoutService( + private val poseLayoutRepository: PoseLayoutRepository +) { + suspend fun findTop1ByUidOrderByIdDesc(uid: Long): PoseLayout? { + return withContext(Dispatchers.IO) { + poseLayoutRepository.findTop1ByUidOrderByIdDesc(uid) + } + } + + suspend fun findByIdAndUidOrThrow(id: Long, uid: Long): PoseLayout { + return findByIdAndUidOrNull(id, uid) ?: throw NotFoundException(ErrorCode.NOT_FOUND_POSE_LAYOUT_ERROR) + } + + suspend fun findByIdAndUidOrNull(id: Long, uid: Long): PoseLayout? { + return withContext(Dispatchers.IO) { + poseLayoutRepository.findByIdAndUid(id, uid) + } + } + + @Transactional + fun saveSync(poseLayout: PoseLayout): PoseLayout { + return poseLayoutRepository.save(poseLayout) + } +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayout.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayout.kt new file mode 100644 index 0000000..12142f7 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayout.kt @@ -0,0 +1,16 @@ +package com.hero.alignlab.domain.pose.domain + +import com.hero.alignlab.domain.common.domain.BaseEntity +import jakarta.persistence.* + +@Entity +@Table(name = "pose_layout") +class PoseLayout( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Long = -1, + + @Column(name = "uid") + val uid: Long, +) : BaseEntity() diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayoutPoint.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayoutPoint.kt new file mode 100644 index 0000000..0b68bea --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/domain/PoseLayoutPoint.kt @@ -0,0 +1,27 @@ +package com.hero.alignlab.domain.pose.domain + +import com.hero.alignlab.domain.pose.domain.vo.PosePosition +import jakarta.persistence.* +import java.math.BigDecimal + +@Entity +@Table(name = "pose_layout_point") +class PoseLayoutPoint( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Long = -1, + + @Column(name = "pose_layout_id") + val poseLayoutId: Long, + + @Column(name = "position") + @Enumerated(EnumType.STRING) + val position: PosePosition, + + @Column(name = "x") + val x: BigDecimal, + + @Column(name = "y") + val y: BigDecimal, +) diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutPointRepository.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutPointRepository.kt new file mode 100644 index 0000000..a07f6ad --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutPointRepository.kt @@ -0,0 +1,12 @@ +package com.hero.alignlab.domain.pose.infrastructure + +import com.hero.alignlab.domain.pose.domain.PoseLayoutPoint +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Transactional(readOnly = true) +@Repository +interface PoseLayoutPointRepository : JpaRepository { + fun findAllByPoseLayoutId(poseLayoutPointId: Long): List +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutRepository.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutRepository.kt new file mode 100644 index 0000000..3bd7a99 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/infrastructure/PoseLayoutRepository.kt @@ -0,0 +1,14 @@ +package com.hero.alignlab.domain.pose.infrastructure + +import com.hero.alignlab.domain.pose.domain.PoseLayout +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional + +@Transactional(readOnly = true) +@Repository +interface PoseLayoutRepository : JpaRepository { + fun findTop1ByUidOrderByIdDesc(uid: Long): PoseLayout? + + fun findByIdAndUid(id: Long, uid: Long): PoseLayout? +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/model/request/PoseLayoutRequest.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/model/request/PoseLayoutRequest.kt new file mode 100644 index 0000000..3e9ebaa --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/model/request/PoseLayoutRequest.kt @@ -0,0 +1,14 @@ +package com.hero.alignlab.domain.pose.model.request + +import com.hero.alignlab.domain.pose.domain.vo.PosePosition +import java.math.BigDecimal + +data class PoseLayoutRequest( + val points: List +) + +data class PoseLayoutPointRequest( + val y: BigDecimal, + val x: BigDecimal, + val position: PosePosition, +) diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetPoseLayoutResponse.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetPoseLayoutResponse.kt new file mode 100644 index 0000000..ccc8444 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetPoseLayoutResponse.kt @@ -0,0 +1,7 @@ +package com.hero.alignlab.domain.pose.model.response + +data class GetPoseLayoutResponse( + /** id가 -1인 경우, 데이터가 없음을 의미 */ + val id: Long = -1, + val points: List = emptyList(), +) diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetRecentPoseLayoutResponse.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetRecentPoseLayoutResponse.kt new file mode 100644 index 0000000..92378cf --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/GetRecentPoseLayoutResponse.kt @@ -0,0 +1,27 @@ +package com.hero.alignlab.domain.pose.model.response + +import com.hero.alignlab.domain.pose.domain.PoseLayoutPoint +import com.hero.alignlab.domain.pose.domain.vo.PosePosition +import java.math.BigDecimal + +data class GetRecentPoseLayoutResponse( + /** id가 -1인 경우, 데이터가 없음을 의미 */ + val id: Long = -1, + val points: List = emptyList(), +) + +data class PoseLayoutPointResponse( + val position: PosePosition, + val x: BigDecimal, + val y: BigDecimal, +) { + companion object { + fun from(poseLayoutPoint: PoseLayoutPoint): PoseLayoutPointResponse { + return PoseLayoutPointResponse( + position = poseLayoutPoint.position, + x = poseLayoutPoint.x, + y = poseLayoutPoint.y, + ) + } + } +} diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/RegisterPoseLayoutResponse.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/RegisterPoseLayoutResponse.kt new file mode 100644 index 0000000..b32d7cf --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/model/response/RegisterPoseLayoutResponse.kt @@ -0,0 +1,5 @@ +package com.hero.alignlab.domain.pose.model.response + +data class RegisterPoseLayoutResponse( + val id: Long, +) diff --git a/src/main/kotlin/com/hero/alignlab/domain/pose/resource/PoseLayoutResource.kt b/src/main/kotlin/com/hero/alignlab/domain/pose/resource/PoseLayoutResource.kt new file mode 100644 index 0000000..907ad56 --- /dev/null +++ b/src/main/kotlin/com/hero/alignlab/domain/pose/resource/PoseLayoutResource.kt @@ -0,0 +1,48 @@ +package com.hero.alignlab.domain.pose.resource + +import com.hero.alignlab.common.extension.wrapCreated +import com.hero.alignlab.common.extension.wrapOk +import com.hero.alignlab.domain.auth.model.AuthUser +import com.hero.alignlab.domain.pose.application.PoseLayoutFacade +import com.hero.alignlab.domain.pose.model.request.PoseLayoutRequest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.* + +@Tag(name = "Pose Layout API") +@RestController +@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) +class PoseLayoutResource( + private val poseLayoutFacade: PoseLayoutFacade +) { + /** + * - 가장 최근에 생성된 레이아웃 데이터를 조회 + * - 만약, 데이터가 없는 경우, 다음과 같이 데이터를 제공 + * ``` + * { + * "id" : -1, + * "points": [] + * } + * ``` + */ + @Operation(summary = "가장 최근 포즈 레이아웃 데이터 조회") + @GetMapping("/api/v1/pose-layouts/recent") + suspend fun getRecentPoseLayout( + user: AuthUser, + ) = poseLayoutFacade.getRecentPoseLayout(user).wrapOk() + + @Operation(summary = "포즈 레이아웃 조회") + @GetMapping("/api/v1/pose-layouts/{id}") + suspend fun getPoseLayout( + user: AuthUser, + @PathVariable id: Long, + ) = poseLayoutFacade.getPoseLayout(user, id).wrapOk() + + @Operation(summary = "포즈 레이아웃 생성") + @PostMapping("/api/v1/pose-layouts") + suspend fun register( + user: AuthUser, + @RequestBody request: PoseLayoutRequest, + ) = poseLayoutFacade.register(user, request).wrapCreated() +} diff --git a/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt b/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt index 6a1acf5..df1e2d1 100644 --- a/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt +++ b/src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt @@ -48,5 +48,8 @@ enum class ErrorCode(val status: HttpStatus, val description: String) { /** Pose Notification Error Code */ NOT_FOUND_POSE_NOTIFICATION_ERROR(HttpStatus.NOT_FOUND, "자세 알림 정보를 찾을 수 없습니다."), + + /** Pose Layout Error Code */ + NOT_FOUND_POSE_LAYOUT_ERROR(HttpStatus.NOT_FOUND, "포즈 레이아웃을 찾을 수 없습니다."), ; }