Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

1차 MVP #46

Merged
merged 6 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/Continuous Delivery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Continuous Delivery

on:
pull_request:
branches:
- 'main'

jobs:
Delivery:
runs-on: ubuntu-22.04

env:
DB_URL: ${{ secrets.DB_URL }}
DB_USER: ${{ secrets.DB_USR }}
DB_PASSWORD: ${{ secrets.DB_PWD }}

steps:
- name: Set up sources
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_TOKEN }}
submodules: true

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: corretto
cache: gradle

- name: Setup Gradle
uses: gradle/gradle-build-action@v2

- name: Execute Gradle build
run: ./gradlew build

- name: Login to docker-hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build Docker Image
run: docker build --tag goldentrash/plantory:latest .

- name: Push docker image
run: docker push goldentrash/plantory:latest
33 changes: 33 additions & 0 deletions .github/workflows/Deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Deploy

on:
pull_request:
types:
- closed
branches:
- 'main'

jobs:
Deploy:
if: github.event.pull_request.merged == true
runs-on: ubuntu-22.04

steps:
# Needed for `google-github-actions`
- uses: 'actions/checkout@v4'

- name: Login to Google
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_CREDENTIALS }}

- name: Update docker container
uses: google-github-actions/ssh-compute@v1
with:
instance_name: plantory
zone: asia-northeast3-a
ssh_private_key: ${{ secrets.GCP_SSH_KEY }}
command: |
docker pull goldentrash/plantory:latest
docker stop plantory
docker run -d --rm -p 8080:8080 --mount type=volume,src=resources,dst=/app/resources --name plantory goldentrash/plantory
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# syntax=docker/dockerfile:1

FROM amazoncorretto:17-alpine

WORKDIR /app
COPY /build/libs/plantory-*-SNAPSHOT.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<KotlinCompile> {
Expand Down
20 changes: 20 additions & 0 deletions src/main/kotlin/gdsc/plantory/common/config/SwaggerConfig.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.multipart.MaxUploadSizeExceededException

@RestControllerAdvice
class CommonExceptionHandler {
Expand All @@ -27,4 +28,9 @@ class CommonExceptionHandler {

return ResponseEntity.internalServerError().body(ErrorResponse("서버 에러가 발생했습니다. 관리자에게 문의해주세요."))
}

@ExceptionHandler
fun handleMaxSizeException(ex: MaxUploadSizeExceededException): ResponseEntity<ErrorResponse> {
return ResponseEntity.badRequest().body(ErrorResponse("파일의 최대 사이즈를 확인해주세요."))
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class PhotoLocalManager(

private fun uploadFileInLocal(multipartFile: MultipartFile, uploadPath: File) {
try {
multipartFile.transferTo(uploadPath)
multipartFile.transferTo(uploadPath.toPath())
} catch (e: IOException) {
throw IllegalStateException("파일 변환이 실패했습니다.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit> {
memberService.signUp(request.deviceToken)
return ResponseEntity.ok().build()
Expand Down
24 changes: 21 additions & 3 deletions src/main/kotlin/gdsc/plantory/plant/domain/CompanionPlant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class CompanionPlant(
val getNickName: String
get() = this.nickname.value

val getName: String
get() = this.nickname.value

val getSortDescription: String
get() = this.shortDescription.value

Expand All @@ -97,12 +100,11 @@ class CompanionPlant(
}

this.records.add(PlantRecord(imageUrl, comment, this))
this.saveRecordHistory(date)
}

fun saveHistory(historyType: HistoryType, date: LocalDate = LocalDate.now()) {
if (isNotCurrentDay(date)) {
throw IllegalArgumentException("물을 줄 날짜는 오늘 날짜여야 합니다.")
}
validateInput(historyType, date)

if (historyType == HistoryType.WATER_CHANGE) {
this.lastWaterDate = date
Expand All @@ -122,6 +124,22 @@ class CompanionPlant(
return ChronoUnit.DAYS.between(birthDate, currentDate).toInt() + 1
}

private fun validateInput(historyType: HistoryType, date: LocalDate) {
if (isRecordType(historyType)) {
throw IllegalArgumentException("데일리 기록은 히스토리 타입을 직접 추가할 수 없습니다.")
}

if (isNotCurrentDay(date)) {
throw IllegalArgumentException("물을 줄 날짜는 오늘 날짜여야 합니다.")
}
}

private fun isRecordType(historyType: HistoryType) = historyType == HistoryType.RECORDING

private fun saveRecordHistory(date: LocalDate) {
this.histories.add(PlantHistory(HistoryType.RECORDING, date, this))
}

private fun isNotCurrentDay(date: LocalDate) = !date.isEqual(LocalDate.now())

override fun equals(other: Any?): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package gdsc.plantory.plant.domain

import NotFoundException
import gdsc.plantory.plant.presentation.dto.CompanionPlantLookupDto
import gdsc.plantory.plant.presentation.dto.CompanionPlantWaterCycleDto
import gdsc.plantory.plant.presentation.dto.PlantRecordDto
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import java.time.LocalDate
Expand All @@ -10,16 +12,56 @@ fun CompanionPlantRepository.findByIdAndMemberIdOrThrow(id: Long, memberId: Long
return findByIdAndMemberId(id, memberId) ?: throw NotFoundException("식물 정보가 없어요")
}

fun CompanionPlantRepository.findRecordByDateOrThrow(id: Long, memberId: Long, date: LocalDate): PlantRecord {
return findRecordByDate(id, memberId, date) ?: throw NotFoundException("데일리 기록이 없어요")
}

interface CompanionPlantRepository : JpaRepository<CompanionPlant, Long> {

fun findAllByMemberId(memberId: Long): List<CompanionPlant>
fun findByIdAndMemberId(id: Long, memberId: Long): CompanionPlant?
fun removeByIdAndMemberId(id: Long, memberId: Long)

@Query(
"""
SELECT new gdsc.plantory.plant.presentation.dto.PlantRecordDto(
r.id,
r.imageUrl._value,
r.comment.content,
cp.nickname._value
)
FROM CompanionPlant cp LEFT JOIN PlantRecord r
ON cp.id = r.companionPlant.id
WHERE cp.id = :companionPlantId
AND cp.memberId = :memberId
AND DATE(r.createAt) = :recordDate
"""
)
fun findRecordByDate(companionPlantId: Long, memberId: Long, recordDate: LocalDate): PlantRecordDto

@Query(
"""
SELECT history.type
FROM PlantHistory history
WHERE
history.companionPlant.id = :companionPlantId
AND DATE(history.createAt) = :recordDate
"""
)
fun findAllHistoryTypeByDate(companionPlantId: Long, recordDate: LocalDate): List<HistoryType>

@Query(
"""
SELECT new gdsc.plantory.plant.presentation.dto.CompanionPlantLookupDto(
plant.id,
plant.imageUrl._value,
plant.nickname._value,
plant.shortDescription._value,
plant.birthDate,
information.species.name
)
FROM PlantInformation information JOIN CompanionPlant plant
ON information.id = plant.plantInformationId
WHERE plant.memberId = :memberId
"""
)
fun findAllByMemberId(memberId: Long): List<CompanionPlantLookupDto>

@Query(
"""
SELECT new gdsc.plantory.plant.presentation.dto.CompanionPlantWaterCycleDto(
Expand All @@ -44,15 +86,4 @@ interface CompanionPlantRepository : JpaRepository<CompanionPlant, Long> {
"""
)
fun findAllHistoriesByMonth(id: Long, memberId: Long, year: Int, month: Int): List<PlantHistory>

@Query(
"""
SELECT record FROM PlantRecord record
WHERE
record.companionPlant.id = :id
AND record.companionPlant.memberId = :memberId
AND DATE(record.createAt) = :date
"""
)
fun findRecordByDate(id: Long, memberId: Long, date: LocalDate): PlantRecord?
}
Loading
Loading