Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] 큐레이션 API 구현 #88

Merged
merged 18 commits into from
Jan 30, 2024
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))
)
}
}
}

Check warning on line 29 in src/main/kotlin/com/mjucow/eatda/domain/curation/service/query/dto/CurationDto.kt

View check run for this annotation

Codecov / codecov/patch

src/main/kotlin/com/mjucow/eatda/domain/curation/service/query/dto/CurationDto.kt#L29

Added line #L29 was not covered by tests
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))
}
}
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
Loading