Skip to content

Commit

Permalink
[feature] 큐레이션 API 구현 (#88)
Browse files Browse the repository at this point in the history
* 큐레이션 도메인 구현

* 큐레이션 조회 서비스 구현

* 큐레이션 커맨드 서비스 구현

* lint

* 큐레이션 api 구현

* 오타 수정

* 도메인 수정

* 도메인 테스트 코드 작성

* 쿼리 서비스 테스트 코드 작성

* 기타 수정

* 커맨드 서비스 테스트 작성

* 큐레이션 가게 추가 기능 구현

* 테스트 코드 추가

* 오타 수정

* 오타 수정

* mvc 테스트 작성

* 기타 수정

* api 문서 수정
  • Loading branch information
k-kbk authored Jan 30, 2024
1 parent 7698d47 commit 77fc5e6
Show file tree
Hide file tree
Showing 24 changed files with 952 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.mjucow.eatda.domain.curation.entity

import com.mjucow.eatda.domain.common.BaseEntity
import com.mjucow.eatda.domain.store.entity.Store
import jakarta.persistence.CascadeType
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.JoinColumn
import jakarta.persistence.JoinTable
import jakarta.persistence.ManyToMany
import jakarta.persistence.Table

@Entity
@Table(name = "curation")
class Curation() : BaseEntity() {
constructor(
title: String,
description: String,
) : this() {
this.title = title
this.description = description
}

@Column(nullable = false)
var title: String = ""
set(value) {
validateTitle(value)
field = value
}

@Column(nullable = false)
var description: String = ""
set(value) {
validateDescription(value)
field = value
}

@ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST, CascadeType.MERGE])
@JoinTable(
name = "curation_store",
joinColumns = [JoinColumn(name = "store_id")],
inverseJoinColumns = [JoinColumn(name = "curation_id")]
)
val mutableStores: MutableSet<Store> = mutableSetOf()

fun getStores(): Set<Store> = mutableStores.toSet()

fun addStore(store: Store) {
mutableStores.add(store)
}

private fun validateTitle(title: String) {
require(title.isNotBlank() && title.length <= MAX_TITLE_LENGTH)
}

private fun validateDescription(description: String) {
require(description.isNotBlank() && description.length <= MAX_DESCRIPTION_LENGTH)
}

companion object {
const val MAX_TITLE_LENGTH = 255
const val MAX_DESCRIPTION_LENGTH = 255
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.mjucow.eatda.domain.curation.service.command

import com.mjucow.eatda.domain.curation.entity.Curation
import com.mjucow.eatda.domain.curation.service.command.dto.AddStoreCommand
import com.mjucow.eatda.domain.curation.service.command.dto.CreateCurationCommand
import com.mjucow.eatda.domain.curation.service.command.dto.UpdateCurationCommand
import com.mjucow.eatda.persistence.curation.CurationRepository
import com.mjucow.eatda.persistence.store.StoreRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class CurationCommandService(
private val repository: CurationRepository,
private val storeRepository: StoreRepository,

) {
fun create(command: CreateCurationCommand): Long {
return repository.save(Curation(command.title, command.description)).id
}

fun update(id: Long, command: UpdateCurationCommand) {
val (newTitle, newDescription) = command
val updatedCuration = repository.getReferenceById(id).apply {
title = newTitle
description = newDescription
}

repository.save(updatedCuration)
}

fun delete(id: Long) {
val curation = repository.findByIdOrNull(id) ?: return

repository.delete(curation)
}

fun addStore(id: Long, command: AddStoreCommand): Long {
val (storeId) = command
val curation = repository.getReferenceById(id)
val store = storeRepository.getReferenceById(storeId)

curation.addStore(store)

return repository.save(curation).id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mjucow.eatda.domain.curation.service.command.dto

import io.swagger.v3.oas.annotations.media.Schema

data class AddStoreCommand(
@Schema(name = "storeId", example = "1")
val storeId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mjucow.eatda.domain.curation.service.command.dto

import io.swagger.v3.oas.annotations.media.Schema

data class CreateCurationCommand(
@Schema(name = "title", example = "큐레이션 제목")
val title: String,
@Schema(name = "description", example = "큐레이션 설명")
val description: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mjucow.eatda.domain.curation.service.command.dto

import io.swagger.v3.oas.annotations.media.Schema

data class UpdateCurationCommand(
@Schema(name = "title", example = "수정할 큐레이션 제목")
val title: String,
@Schema(name = "description", example = "수정할 큐레이션 설명")
val description: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mjucow.eatda.domain.curation.service.query

import com.mjucow.eatda.domain.curation.service.query.dto.CurationDto
import com.mjucow.eatda.domain.curation.service.query.dto.Curations
import com.mjucow.eatda.persistence.curation.CurationRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class CurationQueryService(
private val repository: CurationRepository,
) {
fun findAll(): Curations {
return Curations(repository.findAll().map(CurationDto::from))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.mjucow.eatda.domain.curation.service.query.dto

import com.fasterxml.jackson.annotation.JsonUnwrapped
import com.mjucow.eatda.domain.curation.entity.Curation
import com.mjucow.eatda.domain.store.service.query.dto.StoreDto
import com.mjucow.eatda.domain.store.service.query.dto.Stores
import io.swagger.v3.oas.annotations.media.Schema

data class CurationDto(
@Schema(name = "id", example = "1")
val id: Long,
@Schema(name = "title", example = "명지대 점심 특선")
val title: String,
@Schema(name = "description", example = "점심 특선 메뉴를 판매하는 음식점들이에요.")
val description: String,
@JsonUnwrapped val stores: Stores? = null,
) {

companion object {
fun from(domain: Curation): CurationDto {
return CurationDto(
id = domain.id,
title = domain.title,
description = domain.description,
stores = Stores(domain.getStores().map(StoreDto::from))
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mjucow.eatda.domain.curation.service.query.dto

data class Curations(
val curationList: List<CurationDto>,
) : ArrayList<CurationDto>(curationList)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mjucow.eatda.domain.store.service.query.dto

data class Stores(
val storeList: List<StoreDto>,
) : ArrayList<StoreDto>(storeList)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mjucow.eatda.persistence.curation

import com.mjucow.eatda.domain.curation.entity.Curation
import org.springframework.data.jpa.repository.JpaRepository

interface CurationRepository : JpaRepository<Curation, Long>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.mjucow.eatda.presentation.curation

import com.mjucow.eatda.domain.curation.service.command.dto.AddStoreCommand
import com.mjucow.eatda.domain.curation.service.command.dto.CreateCurationCommand
import com.mjucow.eatda.domain.curation.service.command.dto.UpdateCurationCommand
import com.mjucow.eatda.domain.curation.service.query.dto.Curations
import com.mjucow.eatda.presentation.common.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag

@Tag(name = "큐레이션 API", description = "큐레이션을 관리해주는 API")
interface CurationApiPresentation {
@Operation(summary = "큐레이션 생성", description = "큐레이션를 생성합니다.")
fun create(command: CreateCurationCommand): ApiResponse<Long>

@Operation(summary = "큐레이션 전체 조회", description = "모든 큐레이션를 조회합니다.")
fun findAll(): ApiResponse<Curations>

@Operation(summary = "큐레이션 수정", description = "큐레이션의 내용을 수정합니다.")
@Parameter(name = "curationId", description = "수정할 큐레이션의 ID")
fun update(id: Long, command: UpdateCurationCommand)

@Operation(summary = "큐레이션 삭제", description = "큐레이션을 삭제합니다.")
@Parameter(name = "curationId", description = "삭제할 큐레이션의 ID")
fun delete(id: Long)

@Operation(summary = "큐레이션 가게 추가", description = "큐레이션에 가게를 추가합니다.")
@Parameter(name = "curationId", description = "큐레이션의 ID")
fun addStore(id: Long, command: AddStoreCommand): ApiResponse<Long>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.mjucow.eatda.presentation.curation

import com.mjucow.eatda.domain.curation.service.command.CurationCommandService
import com.mjucow.eatda.domain.curation.service.command.dto.AddStoreCommand
import com.mjucow.eatda.domain.curation.service.command.dto.CreateCurationCommand
import com.mjucow.eatda.domain.curation.service.command.dto.UpdateCurationCommand
import com.mjucow.eatda.domain.curation.service.query.CurationQueryService
import com.mjucow.eatda.domain.curation.service.query.dto.Curations
import com.mjucow.eatda.presentation.common.ApiResponse
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
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.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RequestMapping("/api/v1/curations")
@RestController
class CurationController(
private val curationQueryService: CurationQueryService,
private val curationCommandService: CurationCommandService,
) : CurationApiPresentation {

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
override fun create(@RequestBody command: CreateCurationCommand): ApiResponse<Long> {
return ApiResponse.success(curationCommandService.create(command))
}

@GetMapping
@ResponseStatus(HttpStatus.OK)
override fun findAll(): ApiResponse<Curations> {
return ApiResponse.success(curationQueryService.findAll())
}

@PatchMapping("/{curationId}")
@ResponseStatus(HttpStatus.OK)
override fun update(
@PathVariable("curationId") id: Long,
@RequestBody command: UpdateCurationCommand,
) {
return curationCommandService.update(id, command)
}

@DeleteMapping("/{curationId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
override fun delete(@PathVariable("curationId") id: Long) {
return curationCommandService.delete(id)
}

@PostMapping("/{curationId}/store")
@ResponseStatus(HttpStatus.CREATED)
override fun addStore(
@PathVariable("curationId") id: Long,
@RequestBody command: AddStoreCommand,
): ApiResponse<Long> {
return ApiResponse.success(curationCommandService.addStore(id, command))
}
}
2 changes: 1 addition & 1 deletion src/main/resources/db/changelog/231105-expired_banner.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ CREATE TABLE expired_banner
link varchar(255) NOT NULL,
image_address varchar(255) NOT NULL,
expired_at timestamp
);
);
18 changes: 18 additions & 0 deletions src/main/resources/db/changelog/240129-curation.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- liquibase formatted sql

-- changeset liquibase:8
CREATE TABLE curation (
id bigserial NOT NULL PRIMARY KEY,
title varchar(255) NOT NULL,
description varchar(255) NOT NULL,
created_at timestamp,
updated_at timestamp NOT NULL
);

CREATE TABLE curation_store (
id bigserial NOT NULL PRIMARY KEY,
curation_id bigint NOT NULL REFERENCES curation,
store_id bigint NOT NULL REFERENCES store,
created_at timestamp NOT NULL DEFAULT NOW(),
updated_at timestamp NOT NULL
);
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class BannerQueryServiceTest : AbstractDataTest() {

@DisplayName("전체 배너를 반환한다")
@Test
fun returnCategories() {
fun task2() {
// given
repository.save(BannerMother.create())

Expand Down
Loading

0 comments on commit 77fc5e6

Please sign in to comment.