diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml new file mode 100644 index 0000000..a8d71f2 --- /dev/null +++ b/.github/workflows/cd-dev.yml @@ -0,0 +1,60 @@ +name: Java CI with Gradle + +on: + pull_request: + branches: [ "dev" ] + +jobs: + build: + ## checkout후 자바 21 버전으로 설정을 합니다 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + ## gradlew 의 권한을 줍니다. + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + ## gradle build + - name: Build with Gradle + run: ./gradlew clean build --debug + env: + DB_URL: ${{ secrets.DB_URL }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + JASYPT_ENCRYPTOR_PASSWORD: ${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} + + ## 이미지 태그에 시간 설정을 하기위해서 현재 시간을 가져옵니다. + - name: Get current time + uses: 1466587594/get-current-time@v2 + id: current-time + with: + format: YYYY-MM-DDTHH-mm-ss + utcOffset: "+09:00" + + - name: Show Current Time + run: echo "CurrentTime=${{steps.current-time.outputs.formattedTime}}" + ## AWS에 로그인. aws-region은 서울로 설정(ap-northeast-2) + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + ## ECR에 로그인 + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + ## sample라는 ECR 리파지터리에 현재 시간 태그를 생성하고, 푸쉬 + ## 앞의 스탭에서 ${{steps.current-time.outputs.formattedTime}}로 현재 시간을 가져옵니다. + - name: Build, tag, and push image to Amazon ECR + run: | + docker build --build-arg PASSWORD=$PASSWORD -t polabo:${{steps.current-time.outputs.formattedTime}} . + docker tag polabo:${{steps.current-time.outputs.formattedTime}} 058264417437.dkr.ecr.ap-northeast-2.amazonaws.com/polabo:${{steps.current-time.outputs.formattedTime}} + docker push 058264417437.dkr.ecr.ap-northeast-2.amazonaws.com/polabo:${{steps.current-time.outputs.formattedTime}} + env: + PASSWORD: ${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..482bfa6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM openjdk:21 +ARG JAR_FILE=./build/libs/*SNAPSHOT.jar +ARG PASSWORD +COPY ${JAR_FILE} polabo.jar +ENV JASYPT_ENCRYPTOR_PASSWORD=${PASSWORD} +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} ${ENVIRONMENT_VALUE} -jar /polabo.jar.jar", "-Djasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD}"] \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 170e646..9da232e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ -import org.jetbrains.kotlin.gradle.idea.proto.com.google.protobuf.GeneratedCodeInfoKt.annotation +import nu.studer.gradle.jooq.JooqEdition +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { val kotlinVersion = "1.9.24" @@ -9,6 +10,7 @@ plugins { kotlin("plugin.jpa") version kotlinVersion kotlin("plugin.allopen") version kotlinVersion kotlin("kapt") version kotlinVersion + id("nu.studer.jooq") version "9.0" } group = "com.ddd" @@ -16,7 +18,44 @@ version = "0.0.1-SNAPSHOT" java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + +jooq { + version.set("3.18.10") + edition.set(JooqEdition.OSS) + + configurations { + create("main") { + generateSchemaSourceOnCompilation.set(true) + jooqConfiguration.apply { + logging = org.jooq.meta.jaxb.Logging.WARN + jdbc.apply { + driver = "com.mysql.cj.jdbc.Driver" + url = System.getenv("DB_URL") ?: "jdbc:mysql://localhost:3306/polabo" + user = System.getenv("DB_USER") ?: "polabo" + password = System.getenv("DB_PASSWORD") ?: "polabo" + } + generator.apply { + name = "org.jooq.codegen.KotlinGenerator" + database.apply { + name = "org.jooq.meta.mysql.MySQLDatabase" + excludes = "sys" + } + generate.apply { + isDeprecated = false + isFluentSetters = true + isRecords = true + } + target.apply { + packageName = "com.ddd.sonnypolabobe.jooq" + directory = "build/generated-src/jooq/main" + } + strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy" + } + } + } } } @@ -34,20 +73,36 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0") -// runtimeOnly("org.mariadb.jdbc:mariadb-java-client") // implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.jetbrains.kotlin:kotlin-reflect") + runtimeOnly("com.mysql:mysql-connector-j") + implementation("org.springframework.boot:spring-boot-starter-jooq") + jooqGenerator("com.mysql:mysql-connector-j") + jooqGenerator("org.jooq:jooq-meta:3.18.10") + jooqGenerator("org.jooq:jooq-codegen:3.18.10") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation ("com.github.f4b6a3:uuid-creator:5.3.3") + implementation("software.amazon.awssdk:s3:2.20.68") + implementation("com.amazonaws:aws-java-sdk-s3:1.12.561") + implementation("com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5") + } -kotlin { - compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict") +tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "21" } } tasks.withType { useJUnitPlatform() } + +tasks.withType { + enabled = false +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d8507bf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.7' + +services: + mysql: + image: mysql:latest + container_name: polabo_mysql + hostname: polabo_mysql + volumes: + - ./mysqldata:/var/lib/mysql + environment: + - MYSQL_USER=polabo + - MYSQL_PASSWORD=polabo + - MYSQL_ROOT_PASSWORD=polabo + - MYSQL_HOST=localhost + - MYSQL_PORT=3306 + - MYSQL_DATABASE=polabo + ports: + - "3306:3306" + + +# redis: +# image: redis +# container_name: polabo_redis +# hostname: polabo_redis +# ports: +# - "6379:6379" \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a441313..dab2a01 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,4 +4,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt b/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt index c3ec9a2..bfdbd6a 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt @@ -1,11 +1,12 @@ package com.ddd.sonnypolabobe +import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class SonnyPolaboBeApplication - +inline fun T.logger() = LoggerFactory.getLogger(T::class.java)!! fun main(args: Array) { runApplication(*args) } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt new file mode 100644 index 0000000..510168c --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt @@ -0,0 +1,42 @@ +package com.ddd.sonnypolabobe.domain.board.controller + +import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardCreateRequest +import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardGetResponse +import com.ddd.sonnypolabobe.domain.board.service.BoardService +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.web.bind.annotation.GetMapping +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.RestController + +@Tag(name = "Board API", description = "보드 관련 API") +@RestController +@RequestMapping("/api/v1/boards") +class BoardController( + private val boardService: BoardService +) { + @Operation(summary = "보드 생성", description = """ + 보드를 생성합니다. + userId는 추후 회원가입 기능이 추가될 것을 대비한 것입니다. 지금은 null로 주세요. + """) + @PostMapping + fun create(@RequestBody request : BoardCreateRequest) + = ApplicationResponse.ok(this.boardService.create(request)) + + @Operation(summary = "보드 조회", description = """ + 보드를 조회합니다. + """) + @GetMapping("/{id}") + fun get(@PathVariable id : String) + = ApplicationResponse.ok(this.boardService.getById(id)) + + @Operation(summary = "보드 누적 생성 수 조회", description = """ + 보드 누적 생성 수를 조회합니다. + """) + @GetMapping("/total-count") + fun getTotalCount() = ApplicationResponse.ok(this.boardService.getTotalCount()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardCreateRequest.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardCreateRequest.kt new file mode 100644 index 0000000..e852b3d --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardCreateRequest.kt @@ -0,0 +1,15 @@ +package com.ddd.sonnypolabobe.domain.board.controller.dto + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern +import java.util.* + +data class BoardCreateRequest( + @Schema(description = "제목", example = "쏘니의 보드") + @field:NotBlank + @field:Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*()_+=-])(?=.*[ㄱ-ㅎㅏ-ㅣ가-힣]).{1,20}$", message = "제목은 국문, 영문, 숫자, 특수문자, 띄어쓰기를 포함한 20자 이내여야 합니다.") + val title: String, + @Schema(description = "작성자 아이디", example = "null", required = false) + val userId: UUID? = null +) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardGetResponse.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardGetResponse.kt new file mode 100644 index 0000000..a39545f --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardGetResponse.kt @@ -0,0 +1,11 @@ +package com.ddd.sonnypolabobe.domain.board.controller.dto + +import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidGetResponse +import io.swagger.v3.oas.annotations.media.Schema + +data class BoardGetResponse( + @Schema(description = "제목", example = "쏘니의 보드") + val title: String, + @Schema(description = "작성자", example = "작성자입니다.") + val items: List +) \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/entity/BoardEntity.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/entity/BoardEntity.kt new file mode 100644 index 0000000..439bb3f --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/entity/BoardEntity.kt @@ -0,0 +1,15 @@ +package com.ddd.sonnypolabobe.domain.board.entity + +import com.ddd.sonnypolabobe.global.entity.BaseEntity +import com.ddd.sonnypolabobe.global.util.UuidGenerator +import java.time.LocalDateTime +import java.util.* + +class BoardEntity() : BaseEntity { + override val id: UUID = UuidGenerator.create() + var title: String = "" + var userId : UUID? = null + override var yn: Boolean = true + override val createdAt: LocalDateTime = LocalDateTime.now() + override var updatedAt: LocalDateTime = LocalDateTime.now() +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt new file mode 100644 index 0000000..0592e43 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt @@ -0,0 +1,12 @@ +package com.ddd.sonnypolabobe.domain.board.repository + +import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardCreateRequest +import org.jooq.Record6 +import java.time.LocalDateTime +import java.util.* + +interface BoardJooqRepository { + fun insertOne(request: BoardCreateRequest): ByteArray? + fun selectOneById(id: UUID) : Array> + fun selectTotalCount(): Long +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt new file mode 100644 index 0000000..f3163dd --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -0,0 +1,69 @@ +package com.ddd.sonnypolabobe.domain.board.repository + +import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardCreateRequest +import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardGetResponse +import com.ddd.sonnypolabobe.global.util.UuidConverter +import com.ddd.sonnypolabobe.global.util.UuidGenerator +import com.ddd.sonnypolabobe.jooq.polabo.tables.Board +import com.ddd.sonnypolabobe.jooq.polabo.tables.Polaroid +import org.jooq.DSLContext +import org.jooq.Record6 +import org.springframework.stereotype.Repository +import java.time.LocalDateTime +import java.util.* + +@Repository +class BoardJooqRepositoryImpl( + private val dslContext: DSLContext +) : BoardJooqRepository { + override fun insertOne(request: BoardCreateRequest): ByteArray? { + val jBoard = Board.BOARD + val id = UuidConverter.uuidToByteArray(UuidGenerator.create()) + val insertValue = jBoard.newRecord().apply { + this.id = id + this.title = request.title + this.createdAt = LocalDateTime.now() + this.yn = 1 + this.activeyn = 1 + } + val result = this.dslContext.insertInto(jBoard) + .set(insertValue) + .execute() + + return if (result == 1) id else null + } + + override fun selectOneById(id: UUID): Array> { + val jBoard = Board.BOARD + val jPolaroid = Polaroid.POLAROID + return this.dslContext + .select( + jBoard.TITLE, + jPolaroid.ID, + jPolaroid.IMAGE_KEY, + jPolaroid.ONE_LINE_MESSAGE, + jPolaroid.CREATED_AT, + jPolaroid.USER_ID + ) + .from(jBoard) + .leftJoin(jPolaroid).on( + jBoard.ID.eq(jPolaroid.BOARD_ID).and(jPolaroid.YN.eq(1)) + .and(jPolaroid.ACTIVEYN.eq(1)) + ) + .where( + jBoard.ID.eq(UuidConverter.uuidToByteArray(id)).and(jBoard.YN.eq(1)) + .and(jBoard.ACTIVEYN.eq(1)) + ) + .fetchArray() + + } + + override fun selectTotalCount(): Long { + val jBoard = Board.BOARD + return this.dslContext + .selectCount() + .from(jBoard) + .where(jBoard.YN.eq(1).and(jBoard.ACTIVEYN.eq(1))) + .fetchOne(0, Long::class.java) ?: 0 + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt new file mode 100644 index 0000000..61c23a9 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt @@ -0,0 +1,52 @@ +package com.ddd.sonnypolabobe.domain.board.service + +import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardCreateRequest +import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardGetResponse +import com.ddd.sonnypolabobe.domain.board.repository.BoardJooqRepository +import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidGetResponse +import com.ddd.sonnypolabobe.global.util.S3Util +import com.ddd.sonnypolabobe.global.util.UuidConverter +import org.springframework.stereotype.Service +import java.util.* + +@Service +class BoardService( + private val boardJooqRepository: BoardJooqRepository, + private val s3Util: S3Util +) { + fun create(request: BoardCreateRequest): UUID? { + return this.boardJooqRepository.insertOne(request)?.let { UuidConverter.byteArrayToUUID(it) } + } + + fun getById(id: String): List { + return id.run { + val result = boardJooqRepository.selectOneById(UuidConverter.stringToUUID(this@run)) + result.map { + val polaroidId = it.value2() + if (polaroidId != null) { + BoardGetResponse( + title = it.value1() ?: "폴라보의 보드", + items = listOf( + PolaroidGetResponse( + id = polaroidId, + imageUrl = it.value3()?.let { it1 -> s3Util.getImgUrl(it1) } ?: "", + oneLineMessage = it.value4() ?: "폴라보와의 추억 한 줄", + userId = it.value6()?.let { it1 -> UuidConverter.byteArrayToUUID(it1) }, + ) + ) + ) + } else { + BoardGetResponse( + title = it.value1() ?: "폴라보의 보드", + items = emptyList() + ) + } + } + } + } + + fun getTotalCount(): String { + return this.boardJooqRepository.selectTotalCount().toString() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/FileController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/FileController.kt new file mode 100644 index 0000000..7ac2b1c --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/FileController.kt @@ -0,0 +1,63 @@ +package com.ddd.sonnypolabobe.domain.file.controller + +import com.ddd.sonnypolabobe.domain.file.controller.dto.ImageResignedUrlResponse +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import com.ddd.sonnypolabobe.global.util.S3Util +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.web.bind.annotation.* +import java.io.File +import java.net.URL +import java.util.* + + +@RestController +@RequestMapping("/api/v1/file") +class FileController( + private val s3Util: S3Util +) { + + @GetMapping("/pre-signed-url") + @Operation( + summary = "S3에 이미지를 저장하기 위한 PreSignedUrl 을 반환합니다.", + description = """ + 1. 해당 API를 호출하면, 응답 데이터에 url과 imageKey를 반환합니다. + 2. Url은 2분간 유효합니다. + 3. ImageKey는 S3에 저장될 파일의 이름입니다. + 4. PUT 메소드로 Url에 이미지 파일을 binary로 보내면, s3에 업로드됩니다. + """ + ) + fun getPreSignedUrl( + @RequestParam(value = "fileKey") @Schema( + title = "유저의 보드 uuid", + defaultValue = "01906259-94b2-74ef-8c13-554385c42943", + example = "01906259-94b2-74ef-8c13-554385c42943" + ) fileKey: String, + ): ApplicationResponse { // fileName = env/fileKey/uuid + var fileName = UUID.randomUUID().toString() + fileName = (fileKey + File.separator) + fileName + val url: URL = this.s3Util.getPreSignedUrl(fileName) + val data = ImageResignedUrlResponse(fileName, url.toString()) + return ApplicationResponse.ok(data) + } + + @Operation( + summary = "S3 이미지 삭제", description = """ + S3에 저장된 이미지를 삭제합니다. +""" + ) + @DeleteMapping("/uploaded-image") + fun deleteImage( + @RequestParam(value = "imageKey") imageKey: String + ): ApplicationResponse { + this.s3Util.deleteImage(imageKey) + return ApplicationResponse.ok() + } + + @Operation( + summary = "S3 이미지 접근 주소" + ) + @GetMapping("image-url") + fun getImageUrl(@RequestParam(value = "imageKey") imageKey: String) = + ApplicationResponse.ok(this.s3Util.getImgUrl(imageKey)) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/dto/ImageResignedUrlResponse.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/dto/ImageResignedUrlResponse.kt new file mode 100644 index 0000000..5ddac9f --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/dto/ImageResignedUrlResponse.kt @@ -0,0 +1,6 @@ +package com.ddd.sonnypolabobe.domain.file.controller.dto + +data class ImageResignedUrlResponse( + val imageKey : String, + val url : String +) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/BoardPolaroidController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/BoardPolaroidController.kt new file mode 100644 index 0000000..6f2065e --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/BoardPolaroidController.kt @@ -0,0 +1,20 @@ +package com.ddd.sonnypolabobe.domain.polaroid.controller + +import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidCreateRequest +import com.ddd.sonnypolabobe.domain.polaroid.service.PolaroidService +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import io.swagger.v3.oas.annotations.Operation +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/v1/boards/{boardId}/polaroids") +class BoardPolaroidController(private val polaroidService: PolaroidService) { + + @Operation(summary = "폴라로이드 생성", description = """ + 폴라로이드를 생성합니다. + """) + @PostMapping + fun create(@PathVariable boardId : String, @RequestBody request : PolaroidCreateRequest) + = ApplicationResponse.ok(this.polaroidService.create(boardId, request)) + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/PolaroidController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/PolaroidController.kt new file mode 100644 index 0000000..35ae562 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/PolaroidController.kt @@ -0,0 +1,20 @@ +package com.ddd.sonnypolabobe.domain.polaroid.controller + +import com.ddd.sonnypolabobe.domain.polaroid.service.PolaroidService +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import io.swagger.v3.oas.annotations.Operation +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/api/v1/polaroids") +class PolaroidController(private val polaroidService: PolaroidService) { + + @Operation(summary = "폴라로이드 조회", description = """ + 폴라로이드를 조회합니다. + """) + @GetMapping("/{id}") + fun getById(@PathVariable id: Long) = ApplicationResponse.ok(this.polaroidService.getById(id)) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidCreateRequest.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidCreateRequest.kt new file mode 100644 index 0000000..3a9873e --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidCreateRequest.kt @@ -0,0 +1,11 @@ +package com.ddd.sonnypolabobe.domain.polaroid.controller.dto + +import io.swagger.v3.oas.annotations.media.Schema + +@Schema(description = "폴라로이드 생성 요청") +data class PolaroidCreateRequest( + @Schema(description = "이미지 키", example = "imageKey") + val imageKey : String, + @Schema(description = "한 줄 문구", example = "한 줄 메시지입니다.") + val oneLineMessage : String +) \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidGetResponse.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidGetResponse.kt new file mode 100644 index 0000000..0b843b7 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidGetResponse.kt @@ -0,0 +1,15 @@ +package com.ddd.sonnypolabobe.domain.polaroid.controller.dto + +import io.swagger.v3.oas.annotations.media.Schema +import java.util.UUID + +data class PolaroidGetResponse( + @Schema(description = "폴라로이드 ID", example = "1") + val id: Long, + @Schema(description = "이미지 주소", example = "https://image.com/image.jpg") + val imageUrl: String, + @Schema(description = "한 줄 문구", example = "한 줄 메시지입니다.") + val oneLineMessage: String, + @Schema(description = "작성자 ID", example = "userId") + val userId: UUID?, +) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepository.kt new file mode 100644 index 0000000..a753ad8 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepository.kt @@ -0,0 +1,9 @@ +package com.ddd.sonnypolabobe.domain.polaroid.repository + +import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidCreateRequest +import com.ddd.sonnypolabobe.jooq.polabo.tables.records.PolaroidRecord + +interface PolaroidJooqRepository { + fun insertOne(boardId: ByteArray, request: PolaroidCreateRequest): Long + fun selectOneById(id: Long): PolaroidRecord +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt new file mode 100644 index 0000000..e17400b --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt @@ -0,0 +1,42 @@ +package com.ddd.sonnypolabobe.domain.polaroid.repository + +import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidCreateRequest +import com.ddd.sonnypolabobe.global.exception.ApplicationException +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode +import com.ddd.sonnypolabobe.jooq.polabo.tables.Polaroid +import com.ddd.sonnypolabobe.jooq.polabo.tables.records.PolaroidRecord +import org.jooq.DSLContext +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class PolaroidJooqRepositoryImpl(private val dslContext: DSLContext) : PolaroidJooqRepository { + override fun insertOne(boardId: ByteArray, request: PolaroidCreateRequest): Long { + val jPolaroid = Polaroid.POLAROID + val insertValue = jPolaroid.newRecord().apply { + this.boardId = boardId + this.imageKey = request.imageKey + this.oneLineMessage = request.oneLineMessage + this.createdAt = LocalDateTime.now() + this.yn = 1 + this.activeyn = 1 + } + return this.dslContext.insertInto(jPolaroid) + .set(insertValue) + .returningResult(jPolaroid.ID) + .fetchOne()?.value1() ?: 0 + } + + override fun selectOneById(id: Long): PolaroidRecord { + val jPolaroid = Polaroid.POLAROID + return this.dslContext + .selectFrom(jPolaroid) + .where( + jPolaroid.ID.eq(id) + .and(jPolaroid.YN.eq(1)) + .and(jPolaroid.ACTIVEYN.eq(1)) + ) + .fetchOne()?.original() + ?: throw ApplicationException(CustomErrorCode.POLAROID_NOT_FOUND) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt new file mode 100644 index 0000000..4cfd7b3 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt @@ -0,0 +1,28 @@ +package com.ddd.sonnypolabobe.domain.polaroid.service + +import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidCreateRequest +import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidGetResponse +import com.ddd.sonnypolabobe.domain.polaroid.repository.PolaroidJooqRepository +import com.ddd.sonnypolabobe.global.util.S3Util +import com.ddd.sonnypolabobe.global.util.UuidConverter +import org.springframework.stereotype.Service + +@Service +class PolaroidService(private val polaroidJooqRepository: PolaroidJooqRepository, private val s3Util: S3Util){ + fun create(boardId: String, request: PolaroidCreateRequest): Long { + val boardIdUuid = UuidConverter.stringToUUID(boardId) + return this.polaroidJooqRepository.insertOne(UuidConverter.uuidToByteArray(boardIdUuid), request) + } + + fun getById(id: Long): PolaroidGetResponse { + return this.polaroidJooqRepository.selectOneById(id).let { + PolaroidGetResponse( + id = it.id!!, + imageUrl = s3Util.getImgUrl(it.imageKey!!), + oneLineMessage = it.oneLineMessage ?: "", + userId = it.userId?.let { it1 -> UuidConverter.byteArrayToUUID(it1) } + ) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/JasyptConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/JasyptConfig.kt new file mode 100644 index 0000000..1f0dbce --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/JasyptConfig.kt @@ -0,0 +1,26 @@ +package com.ddd.sonnypolabobe.global.config + +import org.jasypt.encryption.StringEncryptor +import org.jasypt.encryption.pbe.PooledPBEStringEncryptor +import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class JasyptConfig { + + @Bean("jasyptStringEncryptor") + fun stringEncryptor(): StringEncryptor { + val encryptor = PooledPBEStringEncryptor() + val config = SimpleStringPBEConfig() + config.password = System.getenv("JASYPT_ENCRYPTOR_PASSWORD") + config.algorithm = "PBEWithMD5AndDES" + config.setKeyObtentionIterations("1000") + config.setPoolSize("1") + config.stringOutputType = "base64" + config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator") + config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator") + encryptor.setConfig(config) + return encryptor + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/BaseEntity.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/BaseEntity.kt new file mode 100644 index 0000000..08c6aa3 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/BaseEntity.kt @@ -0,0 +1,12 @@ +package com.ddd.sonnypolabobe.global.entity + +import java.time.LocalDateTime +import java.util.UUID + +interface BaseEntity { + val id : UUID + val yn : Boolean + val createdAt : LocalDateTime + val updatedAt : LocalDateTime + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/ApplicationException.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/ApplicationException.kt new file mode 100644 index 0000000..4131ab8 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/ApplicationException.kt @@ -0,0 +1,5 @@ +package com.ddd.sonnypolabobe.global.exception + +data class ApplicationException( + val error : CustomErrorCode +) :RuntimeException(error.message) \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt new file mode 100644 index 0000000..e8bbe29 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt @@ -0,0 +1,16 @@ +package com.ddd.sonnypolabobe.global.exception + +import org.springframework.http.HttpStatus + +enum class CustomErrorCode( + val status: HttpStatus, + val code: String, + val message: String +) { + INVALID_VALUE_EXCEPTION(HttpStatus.BAD_REQUEST, "COM001", "잘못된 값입니다."), + INTERNAL_SERVER_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "COM002", "서버 내부 오류입니다."), + + + POLAROID_NOT_FOUND(HttpStatus.NOT_FOUND, "POL001", "폴라로이드를 찾을 수 없습니다.") + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt new file mode 100644 index 0000000..7d7478a --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt @@ -0,0 +1,31 @@ +package com.ddd.sonnypolabobe.global.exception + +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import com.ddd.sonnypolabobe.logger +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(ApplicationException::class) + fun applicationException(ex: ApplicationException): ResponseEntity> { + logger().info("error : ${ex.error}") + return ResponseEntity.status(ex.error.status).body(ApplicationResponse.error(ex.error)) + } + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun validationException(ex: MethodArgumentNotValidException): ResponseEntity> { + logger().info("error : ${ex.bindingResult.allErrors[0].defaultMessage}") + return ResponseEntity.status(CustomErrorCode.INVALID_VALUE_EXCEPTION.status) + .body(ApplicationResponse.error(CustomErrorCode.INVALID_VALUE_EXCEPTION, ex.bindingResult.allErrors[0].defaultMessage!!)) + } + + @ExceptionHandler(RuntimeException::class) + fun runtimeException(ex: RuntimeException): ResponseEntity> { + logger().info("error : ${ex.message}") + return ResponseEntity.status(CustomErrorCode.INTERNAL_SERVER_EXCEPTION.status) + .body(ApplicationResponse.error(CustomErrorCode.INTERNAL_SERVER_EXCEPTION)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/response/ApplicationResponse.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/response/ApplicationResponse.kt new file mode 100644 index 0000000..275712e --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/response/ApplicationResponse.kt @@ -0,0 +1,45 @@ +package com.ddd.sonnypolabobe.global.response + +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode +import com.fasterxml.jackson.annotation.JsonInclude +import jakarta.annotation.Generated +import java.time.LocalDateTime + +data class ApplicationResponse( + val localDateTime: LocalDateTime, + val code: String, + val message: String, + @JsonInclude(JsonInclude.Include.NON_NULL) + val data: T? +) { + @Generated + companion object { + fun ok() = ApplicationResponse( + localDateTime = LocalDateTime.now(), + code = "SUCCESS", + message = "성공", + data = null + ) + + fun ok(data: T): ApplicationResponse = ApplicationResponse( + localDateTime = LocalDateTime.now(), + code = "SUCCESS", + message = "성공", + data = data + ) + + fun error(errorCode: CustomErrorCode) = ApplicationResponse( + localDateTime = LocalDateTime.now(), + code = errorCode.code, + message = errorCode.message, + data = null + ) + + fun error(errorCode: CustomErrorCode, message: String) = ApplicationResponse( + localDateTime = LocalDateTime.now(), + code = errorCode.code, + message = message, + data = null + ) + } +} diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/S3Util.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/S3Util.kt new file mode 100644 index 0000000..38c84cf --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/S3Util.kt @@ -0,0 +1,67 @@ +package com.ddd.sonnypolabobe.global.util + +import com.amazonaws.HttpMethod +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import com.amazonaws.services.s3.model.DeleteObjectRequest +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest +import com.ddd.sonnypolabobe.logger +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.io.File +import java.net.URL +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* + + +@Component +class S3Util( + @Value("\${cloud.aws.credentials.access-key}") + private val accessKey: String, + @Value("\${cloud.aws.credentials.secret-key}") + private val secretKey: String, + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String, + @Value("\${cloud.aws.region.static}") + private val region: String, + @Value("\${running.name}") + private val runningName: String +) { + + fun awsCredentials(): BasicAWSCredentials { + return BasicAWSCredentials(accessKey, secretKey) + } + + fun amazonS3Client(): AmazonS3 { + return AmazonS3ClientBuilder.standard() + .withCredentials(AWSStaticCredentialsProvider(awsCredentials())) + .withRegion(region) + .build() + } + + fun getPreSignedUrl(fileName: String): URL { + val path: String = (runningName + File.separator) + fileName + val request = GeneratePresignedUrlRequest(bucket, path) + request.expiration = Date(Instant.now().plus(2, ChronoUnit.MINUTES).toEpochMilli()) + request.method = HttpMethod.PUT + return amazonS3Client().generatePresignedUrl(request) + } + + fun getImgUrl(fileName: String): String { + val url: URL = amazonS3Client().getUrl(bucket, runningName + File.separator + fileName) + return url.toString() + } + + fun deleteImage(fileUrl: String) { + try { + val fileKey = "$runningName/$fileUrl" + amazonS3Client().deleteObject(DeleteObjectRequest(bucket, fileKey)) + } catch (e: Exception) { + e.printStackTrace() + logger().error("S3 이미지 삭제 실패 fileUrl: {}", fileUrl) + } + } +} diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt new file mode 100644 index 0000000..355e8ce --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt @@ -0,0 +1,28 @@ +package com.ddd.sonnypolabobe.global.util + +import java.nio.ByteBuffer +import java.util.* + +object UuidConverter { + fun byteArrayToUUID(byteArray: ByteArray): UUID { + val byteBuffer = ByteBuffer.wrap(byteArray) + val mostSigBits = byteBuffer.long + val leastSigBits = byteBuffer.long + return UUID(mostSigBits, leastSigBits) + } + + fun uuidToByteArray(uuid: UUID): ByteArray { + val byteBuffer = ByteBuffer.allocate(16) + byteBuffer.putLong(uuid.mostSignificantBits) + byteBuffer.putLong(uuid.leastSignificantBits) + return byteBuffer.array() + } + + fun stringToUUID(uuid: String): UUID { + return UUID.fromString(uuid) + } + + fun uuidToString(uuid: UUID): String { + return uuid.toString() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidGenerator.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidGenerator.kt new file mode 100644 index 0000000..26bb83d --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidGenerator.kt @@ -0,0 +1,10 @@ +package com.ddd.sonnypolabobe.global.util + +import com.github.f4b6a3.uuid.UuidCreator +import java.util.* + +object UuidGenerator { + fun create(): UUID { + return UuidCreator.getTimeOrderedEpochPlus1() + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..fa0f964 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,34 @@ +spring: + config: + activate: + on-profile: dev + datasource: + url: ENC(X2FpAk74GuXAycAhIQA10hnzAdRj1ftWqILNqC7oQagciq3msqQ7iUJ8oGozxeBqtXb9rx9R7tF88KpemlvML4OTAFhUBUPGgjZ1/CwobQJtdjYx0tXXyOzZpwQXI+U867G4dEB33tbpcFRZt4Ww9vElUSsAZnMWRdfBu2x8TyHzSZDO0V8H2CYqt7qdSJxtW0PsKI4/iwSvAGYND9cwJz/iGsHIuUVAOf7dmd6uzXrovuHF3H4jfw==) + username: ENC(6B0VWBlhLIFr2ynq8c8jug==) + password: ENC(klG0liQdqtFK3VDxA89xC1ZIYYzUIz08) + hikari: + minimum-idle: 2 + maximum-pool-size: 2 +# jpa: +# hibernate: +# naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy +# ddl-auto: none +# show-sql: true +# properties: +# hibernate: +# format_sql: true + jooq: + sql-dialect: mysql + +cloud: + aws: + credentials: + access-key: ENC(PkcItbhMBDzk8j3pjYqRITA5f560l3HOe5rJDCuhxxA=) + secret-key: ENC(f7qtLdA9YxsjCmV7lNEBnNJlWFeZHl9ztH8Mbe8dgK0N8LJSo8VfrKqlNZKR58fP+fXcxrOJP10=) + s3: + bucket: ENC(nLu55KOteQJS+LGFKjfITxZdfaWyZV6C) + region: + static: ap-northeast-2 + +running: + name: dev \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..6756b6e --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,34 @@ +spring: + config: + activate: + on-profile: local + datasource: + url: jdbc:mysql://localhost:3306/polabo?useUnicode=true&charset=utf8mb4&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull + username: polabo + password: polabo + hikari: + minimum-idle: 2 + maximum-pool-size: 2 +# jpa: +# hibernate: +# naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy +# ddl-auto: none +# show-sql: true +# properties: +# hibernate: +# format_sql: true + jooq: + sql-dialect: mysql + +cloud: + aws: + credentials: + access-key: ENC(PkcItbhMBDzk8j3pjYqRITA5f560l3HOe5rJDCuhxxA=) + secret-key: ENC(f7qtLdA9YxsjCmV7lNEBnNJlWFeZHl9ztH8Mbe8dgK0N8LJSo8VfrKqlNZKR58fP+fXcxrOJP10=) + s3: + bucket: ENC(nLu55KOteQJS+LGFKjfITxZdfaWyZV6C) + region: + static: ap-northeast-2 + +running: + name: local \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4352ae0..1355e31 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,15 @@ spring: application: - name: polabo-api \ No newline at end of file + name: polabo-api + +server: + port: 8080 + shutdown: graceful + +logging: + level: + root: INFO + +jasypt: + encryptor: + bean: jasyptStringEncryptor \ No newline at end of file