diff --git a/build.gradle.kts b/build.gradle.kts index 0b0e0bb..15130f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,12 @@ dependencies { testImplementation("org.springframework.security:spring-security-test") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + // AWS Java S3 SDK - AWS S3 스토리지와 통신을 수행할 때 사용하는 디펜던시 + implementation("software.amazon.awssdk:s3:2.28.16") + + // H2 Database Engine - 테스트에서 사용하는 인메모리 데이터베이스 H2 + testImplementation("com.h2database:h2:2.3.232") + //JWT 의존성 implementation("io.jsonwebtoken:jjwt-api:0.12.3") implementation("io.jsonwebtoken:jjwt-impl:0.12.3") @@ -48,11 +54,9 @@ dependencies { //Swagger implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - } kotlin { diff --git a/src/main/kotlin/org/tenten/bittakotlin/demo/User.java b/src/main/kotlin/org/tenten/bittakotlin/demo/User.java new file mode 100644 index 0000000..5dfa461 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/demo/User.java @@ -0,0 +1,25 @@ +package org.tenten.bittakotlin.demo; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private String email; + private String phone; + + public User() {} +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/demo/UserController.java b/src/main/kotlin/org/tenten/bittakotlin/demo/UserController.java new file mode 100644 index 0000000..e5f765b --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/demo/UserController.java @@ -0,0 +1,24 @@ +package org.tenten.bittakotlin.demo; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + @PostMapping + public User addUser(@RequestBody User user) { + return userRepository.save(user); + } + + @GetMapping + public List getAllUsers() { + return userRepository.findAll(); + } +} diff --git a/src/main/kotlin/org/tenten/bittakotlin/demo/UserRepository.java b/src/main/kotlin/org/tenten/bittakotlin/demo/UserRepository.java new file mode 100644 index 0000000..c160cd4 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/demo/UserRepository.java @@ -0,0 +1,6 @@ +package org.tenten.bittakotlin.demo; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository {} + diff --git a/src/main/kotlin/org/tenten/bittakotlin/global/config/JpaAuditConfig.kt b/src/main/kotlin/org/tenten/bittakotlin/global/config/JpaAuditConfig.kt new file mode 100644 index 0000000..f25288b --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/global/config/JpaAuditConfig.kt @@ -0,0 +1,9 @@ +package org.tenten.bittakotlin.global.config + +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaAuditing + +@Configuration +@EnableJpaAuditing +class JpaAuditConfig { +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/global/config/S3Config.kt b/src/main/kotlin/org/tenten/bittakotlin/global/config/S3Config.kt new file mode 100644 index 0000000..9692d09 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/global/config/S3Config.kt @@ -0,0 +1,41 @@ +package org.tenten.bittakotlin.global.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.presigner.S3Presigner + +@Configuration +class S3Config( + @Value("\${s3.access.id}") + private val accessId: String, + + @Value("\${s3.secret.key}") + private val secretKey: String +) { + @Bean + fun s3Client() : S3Client { + val basicCredentials: AwsBasicCredentials + = AwsBasicCredentials.create(accessId, secretKey) + + return S3Client.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(StaticCredentialsProvider.create(basicCredentials)) + .build() + } + + @Bean + fun s3Presigner() : S3Presigner { + val basicCredentials: AwsBasicCredentials + = AwsBasicCredentials.create(accessId, secretKey) + + return S3Presigner.builder() + .region(Region.AP_NORTHEAST_2) + .credentialsProvider(StaticCredentialsProvider.create(basicCredentials)) + .build() + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/constant/MediaError.kt b/src/main/kotlin/org/tenten/bittakotlin/media/constant/MediaError.kt new file mode 100644 index 0000000..e73e818 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/constant/MediaError.kt @@ -0,0 +1,10 @@ +package org.tenten.bittakotlin.media.constant + +import org.springframework.http.HttpStatus + +enum class MediaError(val code: Int, val message: String) { + WRONG_FILE_SIZE(HttpStatus.BAD_REQUEST.value(), "이미지 파일은 10MB, 비디오 파일은 30MB를 초과할 수 없습니다."), + WRONG_MIME_TYPE(HttpStatus.BAD_REQUEST.value(), "올바르지 않은 파일 유형입니다."), + CANNOT_FOUND(HttpStatus.NOT_FOUND.value(), "DB에 파일이 존재하지 않습니다."), + S3_CANNOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR.value(), "S3 서버에 파일이 존재하지 않습니다.") +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/constant/MediaType.kt b/src/main/kotlin/org/tenten/bittakotlin/media/constant/MediaType.kt new file mode 100644 index 0000000..6277821 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/constant/MediaType.kt @@ -0,0 +1,5 @@ +package org.tenten.bittakotlin.media.constant + +enum class MediaType { + IMAGE, VIDEO +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaController.kt b/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaController.kt new file mode 100644 index 0000000..bdd4a0f --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaController.kt @@ -0,0 +1,31 @@ +package org.tenten.bittakotlin.media.controller + +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.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.tenten.bittakotlin.media.dto.MediaRequestDto +import org.tenten.bittakotlin.media.service.MediaService + +@RestController +@RequestMapping("/api/v1/media") +class MediaController( + private val mediaService: MediaService +) { + @GetMapping("/upload") + fun upload(@RequestBody requestDto: MediaRequestDto.Upload): ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "파일 업로드 링크를 성공적으로 생성했습니다.", + "url" to mediaService.upload(requestDto) + )) + } + + @GetMapping("/read") + fun read(@RequestBody requestDto: MediaRequestDto.Read): ResponseEntity> { + return ResponseEntity.ok(mapOf( + "message" to "파일 조회 링크를 성공적으로 생성했습니다.", + "url" to mediaService.read(requestDto) + )) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaControllerAdvice.kt b/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaControllerAdvice.kt new file mode 100644 index 0000000..78d9dca --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/controller/MediaControllerAdvice.kt @@ -0,0 +1,14 @@ +package org.tenten.bittakotlin.media.controller + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.tenten.bittakotlin.media.exception.MediaException + +@RestControllerAdvice +class MediaControllerAdvice { + @ExceptionHandler(MediaException::class) + fun handleMediaException(e: MediaException): ResponseEntity> { + return ResponseEntity.status(e.code).body(mapOf("error" to e.message)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaRequestDto.kt b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaRequestDto.kt new file mode 100644 index 0000000..5eb69e3 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaRequestDto.kt @@ -0,0 +1,34 @@ +package org.tenten.bittakotlin.media.dto + +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern + +class MediaRequestDto { + data class Read ( + @field:NotBlank(message = "파일명이 비어있습니다.") + val filename: String + ) + + data class Upload ( + @field:NotBlank(message = "파일명이 비어있습니다.") + val filename: String, + + @field:Min(value = 0, message = "비어있는 파일은 업로드할 수 없습니다.") + @field:NotBlank(message = "파일 크기가 비어있습니다.") + val filesize: Int, + + @field:Pattern(regexp = "(image/(jpeg|png|gif|bmp|webp|svg\\+xml))|(video/(mp4|webm|ogg|x-msvideo|x-matroska))" + , message = "지원하지 않는 파일 형식입니다.") + @field:NotBlank(message = "파일 형식이 비어있습니다.") + val mimetype: String + ) + + data class Delete ( + @field:NotBlank(message = "회원명이 비어있습니다.") + val username: String, + + @field:NotBlank(message = "파일명이 비어있습니다.") + val filename: String + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaResponseDto.kt b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaResponseDto.kt new file mode 100644 index 0000000..03eb6c5 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/dto/MediaResponseDto.kt @@ -0,0 +1,15 @@ +package org.tenten.bittakotlin.media.dto + +import jakarta.validation.constraints.NotBlank + +class MediaResponseDto { + data class Read ( + @field:NotBlank(message = "조회 링크가 비어있습니다.") + val link: String + ) + + data class Upload ( + @field:NotBlank(message = "업로드 링크가 비어있습니다.") + val link: String + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/entity/Media.kt b/src/main/kotlin/org/tenten/bittakotlin/media/entity/Media.kt new file mode 100644 index 0000000..cb359d5 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/entity/Media.kt @@ -0,0 +1,37 @@ +package org.tenten.bittakotlin.media.entity + +import jakarta.persistence.* +import org.hibernate.annotations.ColumnDefault +import org.springframework.data.annotation.CreatedDate +import org.springframework.data.jpa.domain.support.AuditingEntityListener +import org.tenten.bittakotlin.media.constant.MediaType +import java.time.LocalDateTime + +@Entity +@EntityListeners(AuditingEntityListener::class) +data class Media ( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @Column(nullable = false) + val filename: String, + + @Column(nullable = false) + @ColumnDefault("0") + val filesize: Int, + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + val filetype: MediaType, + + @CreatedDate + @Column(nullable = false, updatable = false) + val savedAt: LocalDateTime? = null, + + /* + 회원과 1:N으로 연결할 예정입니다. + 임시로 문자열로 구성해놓았습니다. + */ + val member: String? = null +) \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/exception/MediaException.kt b/src/main/kotlin/org/tenten/bittakotlin/media/exception/MediaException.kt new file mode 100644 index 0000000..72292a3 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/exception/MediaException.kt @@ -0,0 +1,14 @@ +package org.tenten.bittakotlin.media.exception + +import org.tenten.bittakotlin.media.constant.MediaError + +class MediaException( + val code: Int, + + override val message: String +) : RuntimeException(message) { + constructor(mediaError: MediaError) : this( + code = mediaError.code, + message = mediaError.message + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/repository/MediaRepository.kt b/src/main/kotlin/org/tenten/bittakotlin/media/repository/MediaRepository.kt new file mode 100644 index 0000000..3367a19 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/repository/MediaRepository.kt @@ -0,0 +1,17 @@ +package org.tenten.bittakotlin.media.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import org.tenten.bittakotlin.media.entity.Media +import java.util.Optional + +@Repository +interface MediaRepository : JpaRepository { + @Query("SELECT m FROM Media m WHERE m.filename = :filename") + fun findByFilename(@Param("filename") filename: String): Optional + + @Query("SELECT m FROM Media m WHERE m.filename = :filename AND m.member = :member") + fun findByFilenameAndUsername(@Param("filename") filename: String, @Param("member") member: String): Optional +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaService.kt b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaService.kt new file mode 100644 index 0000000..412674c --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaService.kt @@ -0,0 +1,11 @@ +package org.tenten.bittakotlin.media.service + +import org.tenten.bittakotlin.media.dto.MediaRequestDto + +interface MediaService { + fun read(requestDto: MediaRequestDto.Read): String + + fun upload(requestDto: MediaRequestDto.Upload): String + + fun delete(requestDto: MediaRequestDto.Delete): Unit +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaServiceImpl.kt new file mode 100644 index 0000000..0c4d5e7 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/service/MediaServiceImpl.kt @@ -0,0 +1,97 @@ +package org.tenten.bittakotlin.media.service + +import jakarta.transaction.Transactional +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.tenten.bittakotlin.media.constant.MediaError +import org.tenten.bittakotlin.media.constant.MediaType +import org.tenten.bittakotlin.media.dto.MediaRequestDto +import org.tenten.bittakotlin.media.entity.Media +import org.tenten.bittakotlin.media.exception.MediaException +import org.tenten.bittakotlin.media.repository.MediaRepository +import java.util.* + +@Service +class MediaServiceImpl( + private val mediaRepository: MediaRepository, + + private val s3Service: S3Service, + + @Value("\${file.max.size.image}") + private val imageMaxSize: Int, + + @Value("\${file.max.size.video}") + private val videoMaxSize: Int +) : MediaService { + companion object { + private val logger: Logger = LoggerFactory.getLogger(MediaServiceImpl::class.java) + } + + override fun read(requestDto: MediaRequestDto.Read): String { + val result: Media = mediaRepository.findByFilename(requestDto.filename) + .orElseThrow { MediaException(MediaError.CANNOT_FOUND) } + + return s3Service.getReadUrl(result.filename) + } + + override fun upload(requestDto: MediaRequestDto.Upload): String { + val filename: String = UUID.randomUUID().toString() + val filetype: MediaType = checkMimetype(requestDto.mimetype) + val filesize: Int = checkFileSize(requestDto.filesize, filetype) + + mediaRepository.save(Media( + filename = filename, + filetype = filetype, + filesize = filesize + )) + + return s3Service.getUploadUrl(filename, requestDto.mimetype) + } + + @Transactional + override fun delete(requestDto: MediaRequestDto.Delete) { + val filename: String = requestDto.filename + val username: String = requestDto.username + val result: Media = mediaRepository.findByFilenameAndUsername(filename, username) + .orElseThrow { + logger.warn("The file data does not exist in DB: filename=$filename, username=$username") + + MediaException(MediaError.CANNOT_FOUND) + } + + s3Service.delete(result.filename) + mediaRepository.deleteById(result.id!!) + } + + private fun checkMimetype(mimetype: String): MediaType { + if (mimetype.matches(Regex("image/(jpeg|png|gif|bmp|webp|svg\\+xml)"))) { + logger.info("") + return MediaType.IMAGE + } + + if (mimetype.matches(Regex("video/(mp4|webm|ogg|x-msvideo|x-matroska)"))) { + logger.info("") + return MediaType.VIDEO + } + + logger.warn("The file type is not valid: filetype=$mimetype") + throw MediaException(MediaError.WRONG_MIME_TYPE) + } + + private fun checkFileSize(filesize: Int, type: MediaType): Int { + val maxSize: Int = if (type == MediaType.IMAGE) { + imageMaxSize + } else { + videoMaxSize + } + + if (filesize > maxSize) { + logger.warn("The file size exceeds the allowed limit: filesize=$filesize") + throw MediaException(MediaError.WRONG_FILE_SIZE) + } + + return filesize + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/service/S3Service.kt b/src/main/kotlin/org/tenten/bittakotlin/media/service/S3Service.kt new file mode 100644 index 0000000..dbcc5b4 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/service/S3Service.kt @@ -0,0 +1,9 @@ +package org.tenten.bittakotlin.media.service + +interface S3Service { + fun getReadUrl(name: String): String + + fun getUploadUrl(name: String, contentType: String): String + + fun delete(filename: String): Unit +} \ No newline at end of file diff --git a/src/main/kotlin/org/tenten/bittakotlin/media/service/S3ServiceImpl.kt b/src/main/kotlin/org/tenten/bittakotlin/media/service/S3ServiceImpl.kt new file mode 100644 index 0000000..0396fe9 --- /dev/null +++ b/src/main/kotlin/org/tenten/bittakotlin/media/service/S3ServiceImpl.kt @@ -0,0 +1,83 @@ +package org.tenten.bittakotlin.media.service + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.tenten.bittakotlin.media.constant.MediaError +import org.tenten.bittakotlin.media.exception.MediaException +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.HeadObjectRequest +import software.amazon.awssdk.services.s3.model.NoSuchKeyException +import software.amazon.awssdk.services.s3.presigner.S3Presigner +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest +import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest +import java.time.Duration + +@Service +class S3ServiceImpl( + @Value("\${s3.bucket.name}") + private val s3Bucket: String, + + private val s3Client: S3Client, + + private val s3Presigner: S3Presigner +) : S3Service { + companion object { + private val logger: Logger = LoggerFactory.getLogger(S3ServiceImpl::class.java) + } + + override fun getReadUrl(name: String): String { + existsInBucket(name) + + val presignedGetRequest: PresignedGetObjectRequest = s3Presigner.presignGetObject { request -> request + .signatureDuration(Duration.ofMinutes(10)) + .getObjectRequest {getRequest -> getRequest + .bucket(s3Bucket) + .key(name) + .build() + } + } + + return presignedGetRequest.url().toString() + } + + override fun getUploadUrl(name: String, contentType: String): String { + val presignedPutRequest: PresignedPutObjectRequest = s3Presigner.presignPutObject { request -> request + .signatureDuration(Duration.ofMinutes(3)) + .putObjectRequest { putRequest -> putRequest + .bucket(s3Bucket) + .key(name) + .contentType(contentType) + .build() + } + } + + return presignedPutRequest.url().toString() + } + + override fun delete(name: String): Unit { + existsInBucket(name) + + s3Client.deleteObject { delRequest -> delRequest + .bucket(s3Bucket) + .key(name) + .build() + } + } + + private fun existsInBucket(name: String): Unit { + try { + val headRequest: HeadObjectRequest = HeadObjectRequest.builder() + .bucket(s3Bucket) + .key(name) + .build() + + s3Client.headObject(headRequest) + } catch (e: NoSuchKeyException) { + logger.warn("Cannot found any file in bucket: $name") + + throw MediaException(MediaError.S3_CANNOT_FOUND) + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b98664d..3ade8d4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,6 @@ -# Application name +# application name spring.application.name=bitta-kotlin + +# file size limit +file.max.size.image=10485760 +file.max.size.video=31457280 \ No newline at end of file diff --git a/src/test/kotlin/org/tenten/bittakotlin/BittaKotlinApplicationKtTest.kt b/src/test/kotlin/org/tenten/bittakotlin/BittaKotlinApplicationKtTest.kt new file mode 100644 index 0000000..7139d72 --- /dev/null +++ b/src/test/kotlin/org/tenten/bittakotlin/BittaKotlinApplicationKtTest.kt @@ -0,0 +1,19 @@ +package org.tenten.bittakotlin + +import org.junit.jupiter.api.Test + +/* + @SpringBootTest는 사용하지 말아 주시길 바랍니다. + 테스트할 때 모든 데이터를 불러오므로 에러가 발생할 수 있습니다. + 현재 S3 액세스 키, 시크릿 키 등의 유출되면 안 되는 데이터를 요구하는데, + test/resources/application.properties에 추가하면 안됩니다. + + 컨트롤러는 @WebMvcTest, 레포지토리는 @DataJpaTest, + 서비스는 @ExtendWith(MockitoExtension::class.java)로 대체할 수 있습니다. + */ +class BittaKotlinApplicationKtTest { + + @Test + fun main() { + } +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..96651d2 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,12 @@ +# application name +spring.application.name=bitta-kotlin + +# DB connection settings +spring.datasource.url=jdbc:h2:mem:test +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# JPA settings +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.defer-datasource-initialization=true \ No newline at end of file