From 87f93b601da736db15676650930c556ad0e9cc5b Mon Sep 17 00:00:00 2001 From: WhitePiano Date: Thu, 25 Jan 2024 17:06:20 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20Swagger=20=EC=A0=81=EC=9A=A9=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Swagger를 통한 문서화 * refactor: openAPI v3 spec에 맞게 GET이 req body를 가지지 않도록 변경 * refactor: API description 작성 * refactor: query와 command URL 형식 통일 * refactor: query와 command URL 형식 통일 * fix: 반려식물 생성 시, `PlantInformation`이 제대로 설정되지 않던 문제 해결 * fix: 임시조치, 데일리 기록에 사진에 저장 가능하도록 수정 * doc: 문서 보강(tag, securityRequirement) --- build.gradle.kts | 3 ++ .../plantory/common/config/SwaggerConfig.kt | 20 ++++++++ .../common/support/AccessDeviceToken.kt | 4 ++ .../common/support/DeviceHeaderExtractor.kt | 2 +- .../member/presentation/MemberCommandApi.kt | 9 +++- .../plant/presentation/PlantCommandApi.kt | 46 +++++++++++++------ .../plant/presentation/PlantQueryApi.kt | 33 +++++++++---- .../dto/CompanionPlantCreateRequest.kt | 5 +- .../dto/CompanionPlantDeleteRequest.kt | 5 -- .../presentation/dto/CompanionPlantDto.kt | 24 ---------- .../dto/PlantHistoriesLookupRequest.kt | 8 ---- .../presentation/dto/PlantHistoryRequest.kt | 1 - .../dto/PlantRecordCreateRequest.kt | 1 - .../dto/PlantRecordLookupRequest.kt | 8 ---- .../plantory/plant/service/PlantService.kt | 44 +++++++++++------- .../CompanionPlantAcceptanceTest.kt | 32 ++++--------- .../plantory/acceptance/CompanionPlantStep.kt | 35 +++++++------- .../plantory/fixture/CompanionPlantFixture.kt | 8 ---- .../plant/service/PlantServiceTest.kt | 5 +- 19 files changed, 152 insertions(+), 141 deletions(-) create mode 100644 src/main/kotlin/gdsc/plantory/common/config/SwaggerConfig.kt delete mode 100644 src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDeleteRequest.kt delete mode 100644 src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDto.kt delete mode 100644 src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoriesLookupRequest.kt delete mode 100644 src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordLookupRequest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 75ca3ad..5ce7d53 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,6 +60,9 @@ dependencies { // firebase implementation("com.google.firebase:firebase-admin:9.2.0") + + // swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") } tasks.withType { diff --git a/src/main/kotlin/gdsc/plantory/common/config/SwaggerConfig.kt b/src/main/kotlin/gdsc/plantory/common/config/SwaggerConfig.kt new file mode 100644 index 0000000..88aac86 --- /dev/null +++ b/src/main/kotlin/gdsc/plantory/common/config/SwaggerConfig.kt @@ -0,0 +1,20 @@ +package gdsc.plantory.common.config + +import gdsc.plantory.common.support.AccessDeviceToken +import gdsc.plantory.common.support.DEVICE_ID_HEADER +import io.swagger.v3.oas.models.Operation +import io.swagger.v3.oas.models.security.SecurityRequirement +import org.springdoc.core.customizers.OperationCustomizer +import org.springframework.stereotype.Component +import org.springframework.web.method.HandlerMethod + +@Component +class Operation : OperationCustomizer { + override fun customize(operation: Operation?, handlerMethod: HandlerMethod?): Operation? { + if (handlerMethod?.methodParameters?.find { it.hasParameterAnnotation(AccessDeviceToken::class.java) } != null) { + operation?.addSecurityItem(SecurityRequirement().addList(DEVICE_ID_HEADER)) + } + + return operation + } +} \ No newline at end of file diff --git a/src/main/kotlin/gdsc/plantory/common/support/AccessDeviceToken.kt b/src/main/kotlin/gdsc/plantory/common/support/AccessDeviceToken.kt index 723b604..c7c0af4 100644 --- a/src/main/kotlin/gdsc/plantory/common/support/AccessDeviceToken.kt +++ b/src/main/kotlin/gdsc/plantory/common/support/AccessDeviceToken.kt @@ -1,5 +1,9 @@ package gdsc.plantory.common.support +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.enums.ParameterIn + @Target(AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) +@Parameter(`in` = ParameterIn.HEADER, name = DEVICE_ID_HEADER, description = "사용자 디바이스/인증 토큰") annotation class AccessDeviceToken diff --git a/src/main/kotlin/gdsc/plantory/common/support/DeviceHeaderExtractor.kt b/src/main/kotlin/gdsc/plantory/common/support/DeviceHeaderExtractor.kt index d4eb24d..9d05913 100644 --- a/src/main/kotlin/gdsc/plantory/common/support/DeviceHeaderExtractor.kt +++ b/src/main/kotlin/gdsc/plantory/common/support/DeviceHeaderExtractor.kt @@ -2,7 +2,7 @@ package gdsc.plantory.common.support import org.springframework.web.context.request.NativeWebRequest -private const val DEVICE_ID_HEADER = "Device-Token" +const val DEVICE_ID_HEADER = "Device-Token" class DeviceHeaderExtractor { diff --git a/src/main/kotlin/gdsc/plantory/member/presentation/MemberCommandApi.kt b/src/main/kotlin/gdsc/plantory/member/presentation/MemberCommandApi.kt index 93d831f..9cc9331 100644 --- a/src/main/kotlin/gdsc/plantory/member/presentation/MemberCommandApi.kt +++ b/src/main/kotlin/gdsc/plantory/member/presentation/MemberCommandApi.kt @@ -2,21 +2,28 @@ package gdsc.plantory.member.presentation import gdsc.plantory.member.presentation.dto.MemberCreateRequest import gdsc.plantory.member.service.MemberService +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity 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 = "Member Command", description = "사용자 정보 수정") @RestController @RequestMapping("/api/v1/members") class MemberCommandApi( private val memberService: MemberService, ) { + @Operation(summary = "사용자 등록", description = "사용자를 등록/추가합니다.") + @ApiResponse(responseCode = "200", description = "등록 성공") @PostMapping fun signUp( - @RequestBody request: MemberCreateRequest, + @Parameter(description = "사용자 정보") @RequestBody request: MemberCreateRequest, ): ResponseEntity { memberService.signUp(request.deviceToken) return ResponseEntity.ok().build() diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/PlantCommandApi.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/PlantCommandApi.kt index faee114..45e174c 100644 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/PlantCommandApi.kt +++ b/src/main/kotlin/gdsc/plantory/plant/presentation/PlantCommandApi.kt @@ -4,13 +4,17 @@ import BadRequestException import gdsc.plantory.common.support.AccessDeviceToken import gdsc.plantory.plant.domain.HistoryType import gdsc.plantory.plant.presentation.dto.CompanionPlantCreateRequest -import gdsc.plantory.plant.presentation.dto.CompanionPlantDeleteRequest -import gdsc.plantory.plant.presentation.dto.PlantRecordCreateRequest import gdsc.plantory.plant.presentation.dto.PlantHistoryRequest +import gdsc.plantory.plant.presentation.dto.PlantRecordCreateRequest import gdsc.plantory.plant.service.PlantService +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping +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 @@ -18,50 +22,64 @@ import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile +@Tag(name = "Plant Query", description = "반려식물 정보 조회") @RestController @RequestMapping("/api/v1/plants") class PlantCommandApi( private val plantService: PlantService, ) { + @Operation(summary = "반려식물 등록", description = "사용자의 반려식물을 등록/추가합니다.") + @ApiResponse(responseCode = "200", description = "등록 성공") @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE]) fun create( - @RequestPart(name = "request") request: CompanionPlantCreateRequest, - @RequestPart(name = "image", required = false) image: MultipartFile?, + @Parameter(description = "반려식물 정보") @RequestPart(name = "request") request: CompanionPlantCreateRequest, + @Parameter(description = "반려식물 사진") @RequestPart(name = "image", required = false) image: MultipartFile?, @AccessDeviceToken deviceToken: String, ): ResponseEntity { plantService.create(request, image, deviceToken) return ResponseEntity.ok().build() } - @DeleteMapping(consumes = [MediaType.APPLICATION_JSON_VALUE]) + @Operation(summary = "반려식물 삭제", description = "사용자의 반려식물을 삭제합니다.") + @ApiResponse(responseCode = "204", description = "삭제 성공") + @DeleteMapping("/{companionPlantId}") fun remove( - @RequestBody request: CompanionPlantDeleteRequest, + @Parameter(description = "삭제할 반려식물 ID") @PathVariable companionPlantId: Long, @AccessDeviceToken deviceToken: String, ): ResponseEntity { - plantService.remove(request, deviceToken) + plantService.remove(companionPlantId, deviceToken) return ResponseEntity.noContent().build() } - @PostMapping("/histories", consumes = [MediaType.APPLICATION_JSON_VALUE]) + @Operation(summary = "반려식물 히스토리 등록", description = "반려식물 히스토리(데일리 기록, 물줌, 분갈이)를 등록/추가합니다.") + @ApiResponse(responseCode = "200", description = "등록 성공") + @PostMapping("/{companionPlantId}/histories", consumes = [MediaType.APPLICATION_JSON_VALUE]) fun createPlantHistory( - @RequestBody request: PlantHistoryRequest, + @Parameter(description = "히스토리를 등록할 반려식물 ID") @PathVariable companionPlantId: Long, + @Parameter(description = "히스토리 유형/종류") @RequestBody request: PlantHistoryRequest, @AccessDeviceToken deviceToken: String, ): ResponseEntity { val historyType = HistoryType.byNameIgnoreCaseOrNull(request.historyType) ?: throw BadRequestException("잘못된 히스토리 타입입니다.") - plantService.createPlantHistory(request.companionPlantId, deviceToken, historyType) + plantService.createPlantHistory(companionPlantId, deviceToken, historyType) return ResponseEntity.ok().build() } - @PostMapping("/records", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE]) + @Operation(summary = "반려식물 데일리 기록 등록", description = "반려식물 데일리 기록을 등록/추가합니다.") + @ApiResponse(responseCode = "200", description = "등록 성공") + @PostMapping( + "/{companionPlantId}/records", + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE] + ) fun createPlantRecord( - @RequestPart(name = "request") request: PlantRecordCreateRequest, - @RequestPart(name = "image", required = false) image: MultipartFile?, + @Parameter(description = "데일리 기록을 등록할 반려식물 ID") @PathVariable companionPlantId: Long, + @Parameter(description = "데일리 기록 내용") @RequestPart request: PlantRecordCreateRequest, + @Parameter(description = "데일리 기록 사진") @RequestPart(name = "image", required = false) image: MultipartFile?, @AccessDeviceToken deviceToken: String, ): ResponseEntity { - plantService.createRecord(request, image, deviceToken) + plantService.createRecord(companionPlantId, request, image, deviceToken) return ResponseEntity.ok().build() } } diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/PlantQueryApi.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/PlantQueryApi.kt index d6abe38..a9dcf9a 100644 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/PlantQueryApi.kt +++ b/src/main/kotlin/gdsc/plantory/plant/presentation/PlantQueryApi.kt @@ -2,24 +2,31 @@ package gdsc.plantory.plant.presentation import gdsc.plantory.common.support.AccessDeviceToken import gdsc.plantory.plant.presentation.dto.CompanionPlantsLookupResponse -import gdsc.plantory.plant.presentation.dto.PlantHistoriesLookupRequest import gdsc.plantory.plant.presentation.dto.PlantHistoriesLookupResponse -import gdsc.plantory.plant.presentation.dto.PlantRecordLookupRequest import gdsc.plantory.plant.presentation.dto.PlantRecordLookupResponse import gdsc.plantory.plant.service.PlantService -import org.springframework.http.MediaType +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import java.time.LocalDate +import java.time.YearMonth +@Tag(name = "Plant Command", description = "반려식물 정보 수정") @RestController @RequestMapping("/api/v1/plants") class PlantQueryApi( private val plantService: PlantService ) { + @Operation(summary = "반려식물 조회", description = "사용자의 반려식물 목록을 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공") @GetMapping fun lookupAllCompanionPlantsOfMember( @AccessDeviceToken deviceToken: String @@ -28,21 +35,27 @@ class PlantQueryApi( return ResponseEntity.ok().body(companionPlants) } - @GetMapping("/histories", consumes = [MediaType.APPLICATION_JSON_VALUE]) + @Operation(summary = "반려식물 히스토리(데일리 기록, 물줌, 분갈이) 조회", description = "해당 달의 반려식물 히스토리를 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공") + @GetMapping("/{companionPlantId}/histories") fun lookupAllPlantHistoriesOfMonth( - @RequestBody request: PlantHistoriesLookupRequest, + @Parameter(description = "조회할 반려식물 ID") @PathVariable companionPlantId: Long, + @Parameter(description = "조회 기간(달)") @RequestParam targetMonth: YearMonth, @AccessDeviceToken deviceToken: String ): ResponseEntity { - val histories = plantService.lookupAllPlantHistoriesOfMonth(request, deviceToken) + val histories = plantService.lookupAllPlantHistoriesOfMonth(companionPlantId, targetMonth, deviceToken) return ResponseEntity.ok().body(histories) } - @GetMapping("/records", consumes = [MediaType.APPLICATION_JSON_VALUE]) + @Operation(summary = "반려식물 데일리 기록 조회", description = "데일리 기록(이미지, 일지 등)을 조회합니다.") + @ApiResponse(responseCode = "200", description = "조회 성공") + @GetMapping("/{companionPlantId}/records") fun lookupPlantRecordOfDate( - @RequestBody request: PlantRecordLookupRequest, + @Parameter(description = "데일리 기록의 반려식물") @PathVariable companionPlantId: Long, + @Parameter(description = "데일리 기록의 날짜") @RequestParam recordDate: LocalDate, @AccessDeviceToken deviceToken: String ): ResponseEntity { - val plantRecord = plantService.lookupPlantRecordOfDate(request, deviceToken) + val plantRecord = plantService.lookupPlantRecordOfDate(companionPlantId, recordDate, deviceToken) return ResponseEntity.ok().body(plantRecord) } } diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantCreateRequest.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantCreateRequest.kt index ecb29cc..bfff546 100644 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantCreateRequest.kt +++ b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantCreateRequest.kt @@ -17,9 +17,9 @@ data class CompanionPlantCreateRequest( @PastOrPresent(message = "마지막 물주기 날짜는 과거 또는 현재의 날짜여야 합니다. lastWaterDate: \${validatedValue}") @DateTimeFormat(pattern = "yyyy-MM-dd") val lastWaterDate: LocalDate, ) { - fun toEntity(imagePath: String, memberId: Long, waterCycle: Int): CompanionPlant { + fun toEntity(imagePath: String, memberId: Long, waterCycle: Int, plantInformationId: Long): CompanionPlant { // TODO : Cloud 환경으로 이전 후 제거, 로컬 사진 저장 테스트 용도 - val baseUrl: String = "https://nongsaro.go.kr/" + val baseUrl = "https://nongsaro.go.kr/" return CompanionPlant( _imageUrl = baseUrl + imagePath, _shortDescription = this.shortDescription, @@ -29,6 +29,7 @@ data class CompanionPlantCreateRequest( waterCycle = waterCycle, birthDate = this.birthDate, memberId = memberId, + plantInformationId = plantInformationId ) } } diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDeleteRequest.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDeleteRequest.kt deleted file mode 100644 index 4f37761..0000000 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDeleteRequest.kt +++ /dev/null @@ -1,5 +0,0 @@ -package gdsc.plantory.plant.presentation.dto - -data class CompanionPlantDeleteRequest( - val companionPlantId: Long -) diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDto.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDto.kt deleted file mode 100644 index 7c86960..0000000 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/CompanionPlantDto.kt +++ /dev/null @@ -1,24 +0,0 @@ -package gdsc.plantory.plant.presentation.dto - -import gdsc.plantory.plant.domain.CompanionPlant -import java.time.LocalDate - -data class CompanionPlantDto( - val id: Long, - val imageUrl: String, - val nickname: String, - val shortDescription: String, - val birthDate: LocalDate, - val name: String, -) { - companion object { - fun from(plant: CompanionPlant): CompanionPlantDto = CompanionPlantDto( - id = plant.getId, - imageUrl = plant.getImageUrl, - nickname = plant.getNickName, - shortDescription = plant.getSortDescription, - birthDate = plant.getBirthDate, - name = plant.getName, - ) - } -} diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoriesLookupRequest.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoriesLookupRequest.kt deleted file mode 100644 index fa67bc4..0000000 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoriesLookupRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package gdsc.plantory.plant.presentation.dto - -import java.time.YearMonth - -data class PlantHistoriesLookupRequest( - val companionPlantId: Long, - val targetMonth: YearMonth -) diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoryRequest.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoryRequest.kt index 5eed80b..df64014 100644 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoryRequest.kt +++ b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantHistoryRequest.kt @@ -3,6 +3,5 @@ package gdsc.plantory.plant.presentation.dto import jakarta.validation.constraints.NotBlank data class PlantHistoryRequest( - @NotBlank val companionPlantId: Long, @NotBlank val historyType: String, ) diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordCreateRequest.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordCreateRequest.kt index 5a181f2..c88f420 100644 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordCreateRequest.kt +++ b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordCreateRequest.kt @@ -1,6 +1,5 @@ package gdsc.plantory.plant.presentation.dto data class PlantRecordCreateRequest( - val companionPlantId: Long, val comment: String, ) \ No newline at end of file diff --git a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordLookupRequest.kt b/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordLookupRequest.kt deleted file mode 100644 index 0e63d87..0000000 --- a/src/main/kotlin/gdsc/plantory/plant/presentation/dto/PlantRecordLookupRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package gdsc.plantory.plant.presentation.dto - -import java.time.LocalDate - -data class PlantRecordLookupRequest( - val companionPlantId: Long, - val recordDate: LocalDate -) \ No newline at end of file diff --git a/src/main/kotlin/gdsc/plantory/plant/service/PlantService.kt b/src/main/kotlin/gdsc/plantory/plant/service/PlantService.kt index f6211dc..168440c 100644 --- a/src/main/kotlin/gdsc/plantory/plant/service/PlantService.kt +++ b/src/main/kotlin/gdsc/plantory/plant/service/PlantService.kt @@ -7,18 +7,17 @@ import gdsc.plantory.plant.domain.CompanionPlantRepository import gdsc.plantory.plant.domain.HistoryType import gdsc.plantory.plant.domain.findByIdAndMemberIdOrThrow import gdsc.plantory.plant.presentation.dto.CompanionPlantCreateRequest -import gdsc.plantory.plant.presentation.dto.CompanionPlantDeleteRequest import gdsc.plantory.plant.presentation.dto.CompanionPlantsLookupResponse -import gdsc.plantory.plant.presentation.dto.PlantHistoriesLookupRequest import gdsc.plantory.plant.presentation.dto.PlantHistoriesLookupResponse import gdsc.plantory.plant.presentation.dto.PlantRecordCreateRequest -import gdsc.plantory.plant.presentation.dto.PlantRecordLookupRequest import gdsc.plantory.plant.presentation.dto.PlantRecordLookupResponse import gdsc.plantory.plantInformation.domain.PlantInformationRepository import gdsc.plantory.plantInformation.domain.findByIdOrThrow import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile +import java.time.LocalDate +import java.time.YearMonth @Service @Transactional @@ -34,14 +33,19 @@ class PlantService( val findPlantInformation = plantInformationRepository.findByIdOrThrow(request.plantInformationId) val imagePath: String = saveImageAndGetPath(image, findPlantInformation.getImageUrl) - val companionPlant = request.toEntity(imagePath, findMember.getId, findPlantInformation.getWaterCycle) + val companionPlant = request.toEntity( + imagePath, + findMember.getId, + findPlantInformation.getWaterCycle, + request.plantInformationId + ) companionPlantRepository.save(companionPlant) } - fun remove(request: CompanionPlantDeleteRequest, deviceToken: String) { + fun remove(companionPlantId: Long, deviceToken: String) { val findMember = memberRepository.findByDeviceTokenOrThrow(deviceToken) - companionPlantRepository.removeByIdAndMemberId(request.companionPlantId, findMember.getId) + companionPlantRepository.removeByIdAndMemberId(companionPlantId, findMember.getId) } @Transactional(readOnly = true) @@ -61,45 +65,53 @@ class PlantService( @Transactional(readOnly = true) fun lookupAllPlantHistoriesOfMonth( - request: PlantHistoriesLookupRequest, + companionPlantId: Long, + targetMonth: YearMonth, deviceToken: String ): PlantHistoriesLookupResponse { val findMember = memberRepository.findByDeviceTokenOrThrow(deviceToken) val findPlantHistories = companionPlantRepository.findAllHistoriesByMonth( - request.companionPlantId, + companionPlantId, findMember.getId, - request.targetMonth.year, - request.targetMonth.monthValue + targetMonth.year, + targetMonth.monthValue ) return PlantHistoriesLookupResponse.from(findPlantHistories) } fun createRecord( + companionPlantId: Long, request: PlantRecordCreateRequest, image: MultipartFile?, deviceToken: String, ) { val findMember = memberRepository.findByDeviceTokenOrThrow(deviceToken) val findCompanionPlant = - companionPlantRepository.findByIdAndMemberIdOrThrow(request.companionPlantId, findMember.getId) + companionPlantRepository.findByIdAndMemberIdOrThrow(companionPlantId, findMember.getId) val imagePath: String = saveImageAndGetPath(image, findCompanionPlant.getImageUrl) - findCompanionPlant.saveRecord(request.comment, imagePath) + // TODO : Cloud 환경으로 이전 후 제거, 로컬 사진 저장 테스트 용도 + val baseUrl = "https://nongsaro.go.kr/" + findCompanionPlant.saveRecord(request.comment, baseUrl + imagePath) findCompanionPlant.saveHistory(HistoryType.RECORDING) } @Transactional(readOnly = true) - fun lookupPlantRecordOfDate(request: PlantRecordLookupRequest, deviceToken: String): PlantRecordLookupResponse { + fun lookupPlantRecordOfDate( + companionPlantId: Long, + recordDate: LocalDate, + deviceToken: String + ): PlantRecordLookupResponse { val findMember = memberRepository.findByDeviceTokenOrThrow(deviceToken) val findPlantRecord = companionPlantRepository.findRecordByDate( - request.companionPlantId, + companionPlantId, findMember.getId, - request.recordDate + recordDate ) val historyType = - companionPlantRepository.findAllHistoryTypeByDate(request.companionPlantId, request.recordDate) + companionPlantRepository.findAllHistoryTypeByDate(companionPlantId, recordDate) return PlantRecordLookupResponse.of(findPlantRecord, historyType.contains(HistoryType.WATER_CHANGE)) } diff --git a/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantAcceptanceTest.kt b/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantAcceptanceTest.kt index e47071a..82b080a 100644 --- a/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantAcceptanceTest.kt +++ b/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantAcceptanceTest.kt @@ -16,11 +16,8 @@ import gdsc.plantory.fixture.기록있는_테스트식물_ID import gdsc.plantory.fixture.테스터_디바이스_토큰 import gdsc.plantory.fixture.테스트_식물정보_ID import gdsc.plantory.fixture.CompanionPlantFixture.generateCompanionPlantCreateRequest -import gdsc.plantory.fixture.CompanionPlantFixture.generatePlantRecordCreateRequest -import gdsc.plantory.plant.presentation.dto.CompanionPlantDeleteRequest import gdsc.plantory.plant.presentation.dto.PlantHistoryRequest -import gdsc.plantory.plant.presentation.dto.PlantHistoriesLookupRequest -import gdsc.plantory.plant.presentation.dto.PlantRecordLookupRequest +import gdsc.plantory.plant.presentation.dto.PlantRecordCreateRequest import gdsc.plantory.util.AcceptanceTest import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -45,11 +42,8 @@ class CompanionPlantAcceptanceTest : AcceptanceTest() { @Test fun `반려식물 삭제`() { - // given - val 반려_식물_정보 = CompanionPlantDeleteRequest(기록있는_테스트식물_ID) - // when - val 식물_삭제_요청_응답 = 반려_식물_삭제_요청(반려_식물_정보, 테스터_디바이스_토큰) + val 식물_삭제_요청_응답 = 반려_식물_삭제_요청(기록있는_테스트식물_ID, 테스터_디바이스_토큰) // then 응답_확인(식물_삭제_요청_응답, HttpStatus.NO_CONTENT) @@ -58,10 +52,10 @@ class CompanionPlantAcceptanceTest : AcceptanceTest() { @Test fun `반려식물 물주기 히스토리 등록`() { // given - val 물줌_기록 = PlantHistoryRequest(기록없는_테스트식물_ID, "WATER_CHANGE") + val 물줌_기록 = PlantHistoryRequest("WATER_CHANGE") // when - val 식물_히스토리_생성_응답 = 식물_히스토리_생성_요청(물줌_기록, 테스터_디바이스_토큰) + val 식물_히스토리_생성_응답 = 식물_히스토리_생성_요청(기록없는_테스트식물_ID, 물줌_기록, 테스터_디바이스_토큰) // then 응답_확인(식물_히스토리_생성_응답, HttpStatus.OK) @@ -79,10 +73,10 @@ class CompanionPlantAcceptanceTest : AcceptanceTest() { @Test fun `반려식물 데일리 기록 등록`() { // given - val 데일리_기록_정보 = generatePlantRecordCreateRequest(기록없는_테스트식물_ID) + val 데일리_기록_정보 = PlantRecordCreateRequest("오늘도 즐거운 하루~!") // when - val 데일리_기록_등록_요청_응답 = 데일리_기록_등록_요청(데일리_기록_정보, 테스터_디바이스_토큰) + val 데일리_기록_등록_요청_응답 = 데일리_기록_등록_요청(기록없는_테스트식물_ID, 데일리_기록_정보, 테스터_디바이스_토큰) // then 응답_확인(데일리_기록_등록_요청_응답, HttpStatus.OK) @@ -97,13 +91,13 @@ class CompanionPlantAcceptanceTest : AcceptanceTest() { fun `반려식물 데일리 기록 중복 등록`() { // given 데일리_기록_등록_요청( - generatePlantRecordCreateRequest(기록없는_테스트식물_ID), 테스터_디바이스_토큰 + 기록없는_테스트식물_ID, PlantRecordCreateRequest("오늘도 즐거운 하루~!"), 테스터_디바이스_토큰 ) // when val 데일리_기록_등록_요청_응답 = 데일리_기록_등록_요청( - generatePlantRecordCreateRequest(기록없는_테스트식물_ID), 테스터_디바이스_토큰 + 기록없는_테스트식물_ID, PlantRecordCreateRequest("오늘도 즐거운 하루~!"), 테스터_디바이스_토큰 ) // then @@ -112,11 +106,8 @@ class CompanionPlantAcceptanceTest : AcceptanceTest() { @Test fun `반려식물 데일리 기록 조회`() { - // given - val 데일리_기록_조회_정보 = PlantRecordLookupRequest(기록있는_테스트식물_ID, LocalDate.now()) - // when - val 데일리_기록_조회_요청_응답 = 데일리_기록_조회_요청(데일리_기록_조회_정보, 테스터_디바이스_토큰) + val 데일리_기록_조회_요청_응답 = 데일리_기록_조회_요청(기록있는_테스트식물_ID, LocalDate.now(), 테스터_디바이스_토큰) // then 데일리_기록_조회_응답_확인(데일리_기록_조회_요청_응답) @@ -124,11 +115,8 @@ class CompanionPlantAcceptanceTest : AcceptanceTest() { @Test fun `반려식물 히스토리 조회`() { - // given - val 히스토리_조회_정보 = PlantHistoriesLookupRequest(기록있는_테스트식물_ID, YearMonth.parse("2024-01")) - // when - val 히스토리_조회_요청_응답 = 히스토리_조회_요청(히스토리_조회_정보, 테스터_디바이스_토큰) + val 히스토리_조회_요청_응답 = 히스토리_조회_요청(기록있는_테스트식물_ID, YearMonth.parse("2024-01"), 테스터_디바이스_토큰) // then 히스토리_조회_응답_확인(히스토리_조회_요청_응답) diff --git a/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantStep.kt b/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantStep.kt index 28eddc7..1736209 100644 --- a/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantStep.kt +++ b/src/test/kotlin/gdsc/plantory/acceptance/CompanionPlantStep.kt @@ -1,11 +1,8 @@ package gdsc.plantory.acceptance import gdsc.plantory.plant.presentation.dto.CompanionPlantCreateRequest -import gdsc.plantory.plant.presentation.dto.CompanionPlantDeleteRequest -import gdsc.plantory.plant.presentation.dto.PlantHistoriesLookupRequest import gdsc.plantory.plant.presentation.dto.PlantHistoryRequest import gdsc.plantory.plant.presentation.dto.PlantRecordCreateRequest -import gdsc.plantory.plant.presentation.dto.PlantRecordLookupRequest import io.restassured.RestAssured import io.restassured.builder.MultiPartSpecBuilder import io.restassured.mapper.ObjectMapperType @@ -16,6 +13,8 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.assertAll import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import java.time.LocalDate +import java.time.YearMonth class CompanionPlantStep { @@ -41,17 +40,15 @@ class CompanionPlantStep { } fun 반려_식물_삭제_요청( - request: CompanionPlantDeleteRequest, + companionPlantId: Long, deviceToken: String, ): ExtractableResponse { return RestAssured .given() - .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Device-Token", deviceToken) .log().all() - .body(request) .`when`() - .delete("/api/v1/plants") + .delete("/api/v1/plants/{companionPlantId}", companionPlantId) .then() .log().all() .statusCode(HttpStatus.NO_CONTENT.value()) @@ -59,6 +56,7 @@ class CompanionPlantStep { } fun 식물_히스토리_생성_요청( + companionPlantId: Long, request: PlantHistoryRequest, deviceToken: String, ): ExtractableResponse = @@ -68,7 +66,7 @@ class CompanionPlantStep { .header("Device-Token", deviceToken) .log().all() .body(request) - .`when`().post("/api/v1/plants/histories") + .`when`().post("/api/v1/plants/{companionPlantId}/histories", companionPlantId) .then() .log().all() .extract() @@ -102,6 +100,7 @@ class CompanionPlantStep { } fun 데일리_기록_등록_요청( + companionPlantId: Long, request: PlantRecordCreateRequest, deviceToken: String, ): ExtractableResponse { @@ -114,24 +113,24 @@ class CompanionPlantStep { .header("Device-Token", deviceToken) .log().all() .`when`() - .post("/api/v1/plants/records") + .post("/api/v1/plants/{companionPlantId}/records", companionPlantId) .then() .log().all() .extract() } fun 데일리_기록_조회_요청( - request: PlantRecordLookupRequest, + companionPlantId: Long, + recordDate: LocalDate, deviceToken: String ): ExtractableResponse { return RestAssured .given() - .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Device-Token", deviceToken) .log().all() - .body(request) + .queryParam("recordDate", recordDate.toString()) .`when`() - .get("/api/v1/plants/records") + .get("/api/v1/plants/{companionPlantId}/records", companionPlantId) .then() .log().all() .extract() @@ -143,21 +142,23 @@ class CompanionPlantStep { { assertThat(response.jsonPath().getString("plantRecordId")).isNotBlank() }, { assertThat(response.jsonPath().getString("imageUrl")).isNotBlank() }, { assertThat(response.jsonPath().getString("comment")).isNotBlank() }, + { assertThat(response.jsonPath().getString("nickname")).isNotBlank() }, + { assertThat(response.jsonPath().getString("water")).isNotBlank() }, ) } fun 히스토리_조회_요청( - request: PlantHistoriesLookupRequest, + companionPlantId: Long, + targetMonth: YearMonth, deviceToken: String ): ExtractableResponse { return RestAssured .given() - .contentType(MediaType.APPLICATION_JSON_VALUE) .header("Device-Token", deviceToken) .log().all() - .body(request) + .queryParam("targetMonth", targetMonth.toString()) .`when`() - .get("/api/v1/plants/histories") + .get("/api/v1/plants/{companionPlantId}/histories", companionPlantId) .then() .log().all() .extract() diff --git a/src/test/kotlin/gdsc/plantory/fixture/CompanionPlantFixture.kt b/src/test/kotlin/gdsc/plantory/fixture/CompanionPlantFixture.kt index 105d65c..fbfc034 100644 --- a/src/test/kotlin/gdsc/plantory/fixture/CompanionPlantFixture.kt +++ b/src/test/kotlin/gdsc/plantory/fixture/CompanionPlantFixture.kt @@ -2,7 +2,6 @@ package gdsc.plantory.fixture import gdsc.plantory.plant.domain.CompanionPlant import gdsc.plantory.plant.presentation.dto.CompanionPlantCreateRequest -import gdsc.plantory.plant.presentation.dto.PlantRecordCreateRequest import java.time.LocalDate private var _기록없는_테스트식물_ID = 0L @@ -71,11 +70,4 @@ object CompanionPlantFixture { lastWaterDate = LocalDate.of(2024, 3, 5), ) } - - fun generatePlantRecordCreateRequest(companionPlantId: Long): PlantRecordCreateRequest { - return PlantRecordCreateRequest( - companionPlantId = companionPlantId, - comment = "오늘도 즐거운 하루~!" - ) - } } diff --git a/src/test/kotlin/gdsc/plantory/plant/service/PlantServiceTest.kt b/src/test/kotlin/gdsc/plantory/plant/service/PlantServiceTest.kt index 68fcdc0..3656415 100644 --- a/src/test/kotlin/gdsc/plantory/plant/service/PlantServiceTest.kt +++ b/src/test/kotlin/gdsc/plantory/plant/service/PlantServiceTest.kt @@ -3,7 +3,6 @@ package gdsc.plantory.plant.service import gdsc.plantory.plant.domain.CompanionPlant import gdsc.plantory.plant.domain.CompanionPlantRepository import gdsc.plantory.plant.domain.HistoryType -import gdsc.plantory.plant.presentation.dto.PlantRecordLookupRequest import gdsc.plantory.plantInformation.domain.PlantInformation import gdsc.plantory.plantInformation.domain.PlantInformationRepository import gdsc.plantory.util.AcceptanceTest @@ -70,7 +69,7 @@ class PlantServiceTest( // when val result = plantService.lookupPlantRecordOfDate( - PlantRecordLookupRequest(savedPlant.getId, today), "device-token" + savedPlant.getId, today, "device-token" ) // then @@ -126,7 +125,7 @@ class PlantServiceTest( // when val result = plantService.lookupPlantRecordOfDate( - PlantRecordLookupRequest(savedPlant.getId, today), "device-token" + savedPlant.getId, today, "device-token" ) // then