From 13f533b47918a304392074c3b9fbff9c29bb5fb1 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 20:57:20 +0900 Subject: [PATCH 01/48] =?UTF-8?q?feat(POLABO-32):=201=EC=B0=A8=20MVP=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 60 ++++++++++++++++ Dockerfile | 6 ++ build.gradle.kts | 67 ++++++++++++++++-- docker-compose.yml | 26 +++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- .../sonnypolabobe/SonnyPolaboBeApplication.kt | 3 +- .../board/controller/BoardController.kt | 42 +++++++++++ .../controller/dto/BoardCreateRequest.kt | 15 ++++ .../board/controller/dto/BoardGetResponse.kt | 11 +++ .../domain/board/entity/BoardEntity.kt | 15 ++++ .../board/repository/BoardJooqRepository.kt | 12 ++++ .../repository/BoardJooqRepositoryImpl.kt | 69 +++++++++++++++++++ .../domain/board/service/BoardService.kt | 52 ++++++++++++++ .../domain/file/controller/FileController.kt | 63 +++++++++++++++++ .../dto/ImageResignedUrlResponse.kt | 6 ++ .../controller/BoardPolaroidController.kt | 20 ++++++ .../polaroid/controller/PolaroidController.kt | 20 ++++++ .../controller/dto/PolaroidCreateRequest.kt | 11 +++ .../controller/dto/PolaroidGetResponse.kt | 15 ++++ .../repository/PolaroidJooqRepository.kt | 9 +++ .../repository/PolaroidJooqRepositoryImpl.kt | 42 +++++++++++ .../polaroid/service/PolaroidService.kt | 28 ++++++++ .../global/config/JasyptConfig.kt | 26 +++++++ .../sonnypolabobe/global/entity/BaseEntity.kt | 12 ++++ .../global/exception/ApplicationException.kt | 5 ++ .../global/exception/CustomErrorCode.kt | 16 +++++ .../exception/GlobalExceptionHandler.kt | 31 +++++++++ .../global/response/ApplicationResponse.kt | 45 ++++++++++++ .../ddd/sonnypolabobe/global/util/S3Util.kt | 67 ++++++++++++++++++ .../global/util/UuidConverter.kt | 28 ++++++++ .../global/util/UuidGenerator.kt | 10 +++ src/main/resources/application-dev.yml | 34 +++++++++ src/main/resources/application-local.yml | 34 +++++++++ src/main/resources/application.yml | 14 +++- 34 files changed, 907 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/cd-dev.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardCreateRequest.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/dto/BoardGetResponse.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/entity/BoardEntity.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/FileController.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/file/controller/dto/ImageResignedUrlResponse.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/BoardPolaroidController.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/PolaroidController.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidCreateRequest.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidGetResponse.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepository.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/config/JasyptConfig.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/entity/BaseEntity.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/exception/ApplicationException.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/response/ApplicationResponse.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/util/S3Util.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidGenerator.kt create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-local.yml 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 From b00f11e0c5ffb9c8d329daa2d682be7ddddd5d70 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 21:14:28 +0900 Subject: [PATCH 02/48] =?UTF-8?q?chore(POLABO-31):=20=EA=B9=83=ED=97=99=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 7 +------ gradle/wrapper/gradle-wrapper.properties | 5 ++++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index a8d71f2..28b03a4 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -21,12 +21,7 @@ jobs: ## 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 }} + run: ./gradlew clean build -PDB_URL=${{ secrets.DB_URL }} -PDB_USER=${{ secrets.DB_USER }} -PDB_PASSWORD=${{ secrets.DB_PASSWORD }} -PJASYPT_ENCRYPTOR_PASSWORD=${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} ## 이미지 태그에 시간 설정을 하기위해서 현재 시간을 가져옵니다. - name: Get current time diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dab2a01..be3e029 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,4 +4,7 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists +org.gradle.daemon=true +org.gradle.configureondemand=true +org.gradle.jvmargs=-Xms4g -Xmx4g -XX:+CMSClassUnloadingEnabled --illegal-access=permit \ No newline at end of file From 6a43448b2989986fba2f2a891d2da64c9f7059ca Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 21:56:42 +0900 Subject: [PATCH 03/48] =?UTF-8?q?chore(POLABO-31):=20=EA=B9=83=ED=97=99=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20env=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 28b03a4..96f8c9d 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -6,6 +6,7 @@ on: jobs: build: + environment: dev ## checkout후 자바 21 버전으로 설정을 합니다 runs-on: ubuntu-latest steps: From add61702b6d752d7826e4e933ea00f00e3339814 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 22:06:07 +0900 Subject: [PATCH 04/48] =?UTF-8?q?chore(POLABO-31):=20=EA=B9=83=ED=97=99=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20env=20=EC=A7=80=EC=A0=95=202=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 96f8c9d..28b03a4 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -6,7 +6,6 @@ on: jobs: build: - environment: dev ## checkout후 자바 21 버전으로 설정을 합니다 runs-on: ubuntu-latest steps: From 004ff13448f2302768c59ca9c9347c9dd86e2b45 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 22:12:15 +0900 Subject: [PATCH 05/48] =?UTF-8?q?chore(POLABO-31):=20=EA=B9=83=ED=97=99=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20env=20=EC=A7=80=EC=A0=95=203=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 28b03a4..467e911 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -8,6 +8,12 @@ jobs: build: ## checkout후 자바 21 버전으로 설정을 합니다 runs-on: ubuntu-latest + env: + DB_URL: ${{ secrets.DB_URL }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + JASYPT_ENCRYPTOR_PASSWORD: ${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} + steps: - uses: actions/checkout@v4 - name: Set up JDK 21 @@ -21,7 +27,7 @@ jobs: ## gradle build - name: Build with Gradle - run: ./gradlew clean build -PDB_URL=${{ secrets.DB_URL }} -PDB_USER=${{ secrets.DB_USER }} -PDB_PASSWORD=${{ secrets.DB_PASSWORD }} -PJASYPT_ENCRYPTOR_PASSWORD=${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} + run: ./gradlew clean build -x test ## 이미지 태그에 시간 설정을 하기위해서 현재 시간을 가져옵니다. - name: Get current time From e43e2397042cc32adc3ab7e2eb3da80339a1bb6f Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 22:26:46 +0900 Subject: [PATCH 06/48] =?UTF-8?q?chore(POLABO-31):=20=EA=B9=83=ED=97=99=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20env=20=EC=A7=80=EC=A0=95=204=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 9 +++++++++ gradle/gradle.properties | 2 ++ 2 files changed, 11 insertions(+) create mode 100644 gradle/gradle.properties diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 467e911..5f781c9 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -21,6 +21,15 @@ jobs: with: java-version: '21' distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/gradle-build-action@v2 + + - name: Increase Gradle memory settings + run: | + echo "org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8" >> ~/.gradle/gradle.properties + echo "kotlin.daemon.jvmargs=-Xmx2g" >> ~/.gradle/gradle.properties + ## gradlew 의 권한을 줍니다. - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/gradle/gradle.properties b/gradle/gradle.properties new file mode 100644 index 0000000..324ca94 --- /dev/null +++ b/gradle/gradle.properties @@ -0,0 +1,2 @@ +# Increase the maximum heap size for the Kotlin daemon +kotlin.daemon.jvmargs=-Xmx2g From 0e31cfcf48f3dbfdc3f3b493ef665b54173e2f57 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 22:32:45 +0900 Subject: [PATCH 07/48] =?UTF-8?q?chore(POLABO-31):=20=EA=B9=83=ED=97=99=20?= =?UTF-8?q?=EC=95=A1=EC=85=98=20env=20=EC=A7=80=EC=A0=95=205=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 4 ++-- gradle/gradle.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 5f781c9..b567959 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -27,8 +27,8 @@ jobs: - name: Increase Gradle memory settings run: | - echo "org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8" >> ~/.gradle/gradle.properties - echo "kotlin.daemon.jvmargs=-Xmx2g" >> ~/.gradle/gradle.properties + echo "org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8" >> ~/.gradle/gradle.properties + echo "kotlin.daemon.jvmargs=-Xmx4g" >> ~/.gradle/gradle.properties ## gradlew 의 권한을 줍니다. - name: Grant execute permission for gradlew diff --git a/gradle/gradle.properties b/gradle/gradle.properties index 324ca94..4fa9d49 100644 --- a/gradle/gradle.properties +++ b/gradle/gradle.properties @@ -1,2 +1,2 @@ # Increase the maximum heap size for the Kotlin daemon -kotlin.daemon.jvmargs=-Xmx2g +kotlin.daemon.jvmargs=-Xmx4g From a307ac78a7620320638f061061b7d3ea6c7d8fcf Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 22:53:54 +0900 Subject: [PATCH 08/48] =?UTF-8?q?chore(POLABO-31):=20database=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index fa0f964..febe92d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,7 +3,7 @@ spring: activate: on-profile: dev datasource: - url: ENC(X2FpAk74GuXAycAhIQA10hnzAdRj1ftWqILNqC7oQagciq3msqQ7iUJ8oGozxeBqtXb9rx9R7tF88KpemlvML4OTAFhUBUPGgjZ1/CwobQJtdjYx0tXXyOzZpwQXI+U867G4dEB33tbpcFRZt4Ww9vElUSsAZnMWRdfBu2x8TyHzSZDO0V8H2CYqt7qdSJxtW0PsKI4/iwSvAGYND9cwJz/iGsHIuUVAOf7dmd6uzXrovuHF3H4jfw==) + url: ENC(pz7EEQu20leZT3uKvGuyvt7YhmLSOJ03yp09o3HSmvk8QqSORYES8Y0iqtjYt3An0uSXVGvd+PdNxXF4h1KXbLm6aMSl2K9gWh2XR5GbIpPAKasqkuha20MGAVckb9NRQC3s0IowzCMvt+jOv0Z1RrAnsfzJy08ZbWe41Slcz/VaQU1hSF6HVohMUrBPApDELlW558sM0jWPfi7NPYNGSlyB4f3bYa0WqjOIAAKPVb3ptvrQwmN7Wg==) username: ENC(6B0VWBlhLIFr2ynq8c8jug==) password: ENC(klG0liQdqtFK3VDxA89xC1ZIYYzUIz08) hikari: From 9c2938dd3584c29f36a2f51d850078c83f01baaa Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 23:41:35 +0900 Subject: [PATCH 09/48] =?UTF-8?q?chore(POLABO-31):=20heap=20memory=20leak?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 15 ++++++++------- gradle/wrapper/gradle-wrapper.properties | 5 +---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9da232e..6928301 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,8 +7,8 @@ plugins { id("io.spring.dependency-management") version "1.1.5" kotlin("jvm") version kotlinVersion kotlin("plugin.spring") version kotlinVersion - kotlin("plugin.jpa") version kotlinVersion - kotlin("plugin.allopen") version kotlinVersion +// kotlin("plugin.jpa") version kotlinVersion +// kotlin("plugin.allopen") version kotlinVersion kotlin("kapt") version kotlinVersion id("nu.studer.jooq") version "9.0" } @@ -59,11 +59,11 @@ jooq { } } -allOpen { - annotation("jakarta.persistence.Entity") - annotation("jakarta.persistence.MappedSuperclass") - annotation("jakarta.persistence.Embeddable") -} +//allOpen { +// annotation("jakarta.persistence.Entity") +// annotation("jakarta.persistence.MappedSuperclass") +// annotation("jakarta.persistence.Embeddable") +//} repositories { mavenCentral() @@ -96,6 +96,7 @@ tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" jvmTarget = "21" + kotlinDaemonJvmArguments = listOf("-Xmx4096m", "-Xms2560m", "-XX:+UseParallelGC") } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index be3e029..dab2a01 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,7 +4,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -org.gradle.daemon=true -org.gradle.configureondemand=true -org.gradle.jvmargs=-Xms4g -Xmx4g -XX:+CMSClassUnloadingEnabled --illegal-access=permit \ No newline at end of file +zipStorePath=wrapper/dists \ No newline at end of file From 71bab0ada8fb0bbf7a50dff02ebf0e0d92f053fd Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 29 Jun 2024 23:52:52 +0900 Subject: [PATCH 10/48] =?UTF-8?q?chore(POLABO-31):=20github=20actions=206?= =?UTF-8?q?=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 5 ++++- Dockerfile | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index b567959..7437db7 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -36,7 +36,10 @@ jobs: ## gradle build - name: Build with Gradle - run: ./gradlew clean build -x test + run: ./gradlew clean bootJar + + - name: List build/libs directory + run: ls -la build/libs ## 이미지 태그에 시간 설정을 하기위해서 현재 시간을 가져옵니다. - name: Get current time diff --git a/Dockerfile b/Dockerfile index 482bfa6..b710362 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,10 @@ FROM openjdk:21 -ARG JAR_FILE=./build/libs/*SNAPSHOT.jar + +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 + +ENTRYPOINT ["sh", "-c", "java -jar /polabo.jar -Djasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD}"] From bbef8057c50cbee68e92b19e57bd6edf1e8438d1 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 30 Jun 2024 00:18:43 +0900 Subject: [PATCH 11/48] =?UTF-8?q?chore(POLABO-31):=20github=20actions=207?= =?UTF-8?q?=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 7437db7..1eb61be 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -36,7 +36,7 @@ jobs: ## gradle build - name: Build with Gradle - run: ./gradlew clean bootJar + run: ./gradlew clean build -x test --info - name: List build/libs directory run: ls -la build/libs From 97f71902ad52383f78444beb43d32b94176bfc42 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 30 Jun 2024 00:25:32 +0900 Subject: [PATCH 12/48] =?UTF-8?q?chore(POLABO-31):=20github=20actions=208?= =?UTF-8?q?=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 6928301..350dd8f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -103,7 +103,3 @@ tasks.withType { tasks.withType { useJUnitPlatform() } - -tasks.withType { - enabled = false -} From 5d2f7c446b3f942a408f904910f25e37a8da35ef Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 30 Jun 2024 01:02:37 +0900 Subject: [PATCH 13/48] =?UTF-8?q?chore(POLABO-31):=20Dockerfile=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 5 +---- Dockerfile | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 1eb61be..b567959 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -36,10 +36,7 @@ jobs: ## gradle build - name: Build with Gradle - run: ./gradlew clean build -x test --info - - - name: List build/libs directory - run: ls -la build/libs + run: ./gradlew clean build -x test ## 이미지 태그에 시간 설정을 하기위해서 현재 시간을 가져옵니다. - name: Get current time diff --git a/Dockerfile b/Dockerfile index b710362..8bbf32d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,4 +7,5 @@ COPY ${JAR_FILE} polabo.jar ENV JASYPT_ENCRYPTOR_PASSWORD=${PASSWORD} -ENTRYPOINT ["sh", "-c", "java -jar /polabo.jar -Djasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD}"] +#ENTRYPOINT ["sh", "-c", "java -jar /polabo.jar -Djasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD}"] +ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} ${ENVIRONMENT_VALUE} -jar /polabo.jar", "-Djasypt.encryptor.password=${JASYPT_ENCRYPTOR_PASSWORD}"] \ No newline at end of file From 8e79f038bc63da3afe63300c3ef5c5cbbfdf16eb Mon Sep 17 00:00:00 2001 From: dldmsql Date: Tue, 9 Jul 2024 18:41:45 +0900 Subject: [PATCH 14/48] =?UTF-8?q?chore(POLABO-31):=20CD=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index a8d71f2..aae55cd 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -54,7 +54,20 @@ jobs: - 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}} + docker tag polabo:${{steps.current-time.outputs.formattedTime}} ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + docker push ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} env: - PASSWORD: ${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} \ No newline at end of file + PASSWORD: ${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} + + - name: Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_KEY }} + script: | + docker stop polabo-dev + docker rm polabo-dev + docker pull ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=dev --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + docker rmi -f $(docker images -f "dangling=true" -q) \ No newline at end of file From 956d29fd1798220fee9e1a6a04f239e14c574634 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Tue, 9 Jul 2024 18:42:18 +0900 Subject: [PATCH 15/48] =?UTF-8?q?feat(POLABO-38):=20=ED=95=98=EB=A3=A8=201?= =?UTF-8?q?00=EA=B0=9C=20=EC=9A=94=EC=B2=AD=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=94=94=EC=8A=A4=EC=BD=94=EB=93=9C=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 +- .../sonnypolabobe/SonnyPolaboBeApplication.kt | 2 + .../repository/BoardJooqRepositoryImpl.kt | 3 +- .../repository/PolaroidJooqRepositoryImpl.kt | 3 +- .../global/HealthCheckController.kt | 3 +- .../global/config/SecurityConfig.kt | 50 +++++++ .../sonnypolabobe/global/config/WebConfig.kt | 16 +++ .../exception/GlobalExceptionHandler.kt | 20 ++- .../global/security/LoggingFilter.kt | 124 ++++++++++++++++++ .../security/RateLimitingInterceptor.kt | 24 ++++ .../global/security/RateLimitingService.kt | 35 +++++ .../global/util/DateConverter.kt | 11 ++ .../global/util/DiscordApiClient.kt | 99 ++++++++++++++ .../ddd/sonnypolabobe/global/util/HttpLog.kt | 12 ++ src/main/resources/application-dev.yml | 6 +- src/main/resources/application-local.yml | 6 +- src/main/resources/application.yml | 5 +- src/main/resources/logback-spring.xml | 46 +++++++ 18 files changed, 459 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/config/WebConfig.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingInterceptor.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingService.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/util/HttpLog.kt create mode 100644 src/main/resources/logback-spring.xml diff --git a/build.gradle.kts b/build.gradle.kts index 9da232e..dc76596 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -89,7 +89,8 @@ dependencies { 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") - + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64") } tasks.withType { diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt b/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt index bfdbd6a..bafe9f3 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/SonnyPolaboBeApplication.kt @@ -3,7 +3,9 @@ package com.ddd.sonnypolabobe import org.slf4j.LoggerFactory import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling +@EnableScheduling @SpringBootApplication class SonnyPolaboBeApplication inline fun T.logger() = LoggerFactory.getLogger(T::class.java)!! 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 index f3163dd..1b4cb74 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -2,6 +2,7 @@ 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.DateConverter import com.ddd.sonnypolabobe.global.util.UuidConverter import com.ddd.sonnypolabobe.global.util.UuidGenerator import com.ddd.sonnypolabobe.jooq.polabo.tables.Board @@ -22,7 +23,7 @@ class BoardJooqRepositoryImpl( val insertValue = jBoard.newRecord().apply { this.id = id this.title = request.title - this.createdAt = LocalDateTime.now() + this.createdAt = DateConverter.convertToKst(LocalDateTime.now()) this.yn = 1 this.activeyn = 1 } 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 index e17400b..b62be16 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt @@ -3,6 +3,7 @@ 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.global.util.DateConverter import com.ddd.sonnypolabobe.jooq.polabo.tables.Polaroid import com.ddd.sonnypolabobe.jooq.polabo.tables.records.PolaroidRecord import org.jooq.DSLContext @@ -17,7 +18,7 @@ class PolaroidJooqRepositoryImpl(private val dslContext: DSLContext) : PolaroidJ this.boardId = boardId this.imageKey = request.imageKey this.oneLineMessage = request.oneLineMessage - this.createdAt = LocalDateTime.now() + this.createdAt = DateConverter.convertToKst(LocalDateTime.now()) this.yn = 1 this.activeyn = 1 } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt index b171ce0..d430f7a 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt @@ -8,6 +8,7 @@ class HealthCheckController { @GetMapping("/health") fun healthCheck(): String { - return "OK" + throw RuntimeException("error") +// return "OK" } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt new file mode 100644 index 0000000..521e9ea --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -0,0 +1,50 @@ +package com.ddd.sonnypolabobe.global.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.util.matcher.AntPathRequestMatcher +import org.springframework.security.web.util.matcher.RequestMatcher +import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.UrlBasedCorsConfigurationSource + +@Configuration +@EnableMethodSecurity +class SecurityConfig() { + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + return http + .cors { + it.configurationSource(corsConfigurationSource()) + } + .csrf{ + it.disable() + } + .httpBasic { + it.disable() + } + .formLogin { it.disable() } + .authorizeHttpRequests { + it.anyRequest().permitAll() + } + .build() + } + + fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("http://localhost:3000") // Allow all origins + configuration.allowedMethods = + listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow common methods + configuration.allowedHeaders = listOf("*") // Allow all headers + configuration.allowCredentials = true // Allow credentials + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration( + "/**", + configuration + ) // Apply configuration to all endpoints + return source + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/WebConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/WebConfig.kt new file mode 100644 index 0000000..86f8df7 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/WebConfig.kt @@ -0,0 +1,16 @@ +package com.ddd.sonnypolabobe.global.config + +import com.ddd.sonnypolabobe.global.security.RateLimitingInterceptor +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.InterceptorRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig(private val rateLimitingInterceptor: RateLimitingInterceptor) : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(rateLimitingInterceptor) + .addPathPatterns("/api/v1/boards") + } + +} \ 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 index 7d7478a..9700834 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt @@ -1,6 +1,7 @@ package com.ddd.sonnypolabobe.global.exception import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import com.ddd.sonnypolabobe.global.util.DiscordApiClient import com.ddd.sonnypolabobe.logger import org.springframework.http.ResponseEntity import org.springframework.web.bind.MethodArgumentNotValidException @@ -8,10 +9,16 @@ import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice @RestControllerAdvice -class GlobalExceptionHandler { +class GlobalExceptionHandler( + private val discordApiClient: DiscordApiClient +) { @ExceptionHandler(ApplicationException::class) fun applicationException(ex: ApplicationException): ResponseEntity> { logger().info("error : ${ex.error}") + this.discordApiClient.sendErrorTrace( + ex.error.code, ex.message, + ex.stackTrace.contentToString() + ) return ResponseEntity.status(ex.error.status).body(ApplicationResponse.error(ex.error)) } @@ -19,12 +26,21 @@ class GlobalExceptionHandler { 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!!)) + .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}") + this.discordApiClient.sendErrorTrace( + "500", ex.message, + ex.stackTrace.contentToString() + ) return ResponseEntity.status(CustomErrorCode.INTERNAL_SERVER_EXCEPTION.status) .body(ApplicationResponse.error(CustomErrorCode.INTERNAL_SERVER_EXCEPTION)) } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt new file mode 100644 index 0000000..39e89a6 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt @@ -0,0 +1,124 @@ +package com.ddd.sonnypolabobe.global.security + +import com.ddd.sonnypolabobe.global.util.DiscordApiClient +import com.ddd.sonnypolabobe.global.util.HttpLog +import com.ddd.sonnypolabobe.logger +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletRequest +import jakarta.servlet.ServletResponse +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.GenericFilterBean +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper +import org.springframework.web.util.WebUtils +import java.io.UnsupportedEncodingException +import java.util.* + +@Component +class LoggingFilter( + private val discordApiClient: DiscordApiClient +) : GenericFilterBean() { + private val excludedUrls = setOf("/actuator", "/swagger-ui") + + override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + val requestWrapper: ContentCachingRequestWrapper = + ContentCachingRequestWrapper(request as HttpServletRequest) + val responseWrapper: ContentCachingResponseWrapper = + ContentCachingResponseWrapper(response as HttpServletResponse) + if (excludeLogging(request.requestURI)) { + chain.doFilter(request, response) + } else { + val startedAt = System.currentTimeMillis() + chain.doFilter(requestWrapper, responseWrapper) + val endedAt = System.currentTimeMillis() + + logger().info( + "\n" + + "[REQUEST] ${request.method} - ${request.requestURI} ${responseWrapper.status} - ${(endedAt - startedAt) / 10000.0} \n" + + "Headers : ${getHeaders(request)} \n" + + "Parameters : ${getRequestParams(request)} \n" + + "Request body : ${getRequestBody(requestWrapper)} \n" + + "Response body : ${getResponseBody(responseWrapper)}" + ) + + if(responseWrapper.status >= 400) { + this.discordApiClient.sendErrorLog( + HttpLog( + request.method, + request.requestURI, + responseWrapper.status, + (endedAt - startedAt) / 10000.0, + getHeaders(request), + getRequestParams(request), + getRequestBody(requestWrapper), + getResponseBody(responseWrapper) + ) + ) + } + } + } + + private fun excludeLogging(requestURI: String): Boolean { + return excludedUrls.contains(requestURI) + } + + private fun getResponseBody(response: ContentCachingResponseWrapper): String { + var payload: String? = null + response.characterEncoding = "utf-8" + val wrapper = + WebUtils.getNativeResponse(response, ContentCachingResponseWrapper::class.java) + if (wrapper != null) { + val buf = wrapper.contentAsByteArray + if (buf.isNotEmpty()) { + payload = String(buf, 0, buf.size, charset(wrapper.characterEncoding)) + wrapper.copyBodyToResponse() + } + } + return payload ?: " - " + } + + private fun getRequestBody(request: ContentCachingRequestWrapper): String { + request.characterEncoding = "utf-8" + val wrapper = WebUtils.getNativeRequest( + request, + ContentCachingRequestWrapper::class.java + ) + if (wrapper != null) { + val buf = wrapper.contentAsByteArray + if (buf.isNotEmpty()) { + return try { + String(buf, 0, buf.size, charset(wrapper.characterEncoding)) + } catch (e: UnsupportedEncodingException) { + " - " + } + } + } + return " - " + } + + private fun getRequestParams(request: HttpServletRequest): Map { + val parameterMap: MutableMap = HashMap() + request.characterEncoding = "utf-8" + val parameterArray: Enumeration<*> = request.parameterNames + + while (parameterArray.hasMoreElements()) { + val parameterName = parameterArray.nextElement() as String + parameterMap[parameterName] = request.getParameter(parameterName) + } + + return parameterMap + } + + private fun getHeaders(request: HttpServletRequest): Map { + val headerMap: MutableMap = HashMap() + + val headerArray: Enumeration<*> = request.headerNames + while (headerArray.hasMoreElements()) { + val headerName = headerArray.nextElement() as String + headerMap[headerName] = request.getHeader(headerName) + } + return headerMap + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingInterceptor.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingInterceptor.kt new file mode 100644 index 0000000..95d96fb --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingInterceptor.kt @@ -0,0 +1,24 @@ +package com.ddd.sonnypolabobe.global.security + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.servlet.HandlerInterceptor + +@Component +class RateLimitingInterceptor(private val rateLimitingService: RateLimitingService) : + HandlerInterceptor { + + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + // 특정 URL 패턴을 필터링합니다. + if (request.requestURI == "/api/v1/boards" && request.method == "POST") { + if (!rateLimitingService.incrementRequestCount()) { + response.status = HttpStatus.TOO_MANY_REQUESTS.value() + response.writer.write("Daily request limit exceeded") + return false + } + } + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingService.kt new file mode 100644 index 0000000..b7dbfd2 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/RateLimitingService.kt @@ -0,0 +1,35 @@ +package com.ddd.sonnypolabobe.global.security + +import com.ddd.sonnypolabobe.logger +import org.springframework.beans.factory.annotation.Value +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.util.concurrent.ConcurrentHashMap + +@Service +class RateLimitingService( + @Value("\${limit.count}") + private val limit: Int +) { + + private val requestCounts = ConcurrentHashMap() + private val LIMIT = limit + private val REQUEST_KEY = "api_request_count" + + fun incrementRequestCount(): Boolean { + val currentCount = requestCounts.getOrDefault(REQUEST_KEY, 0) + + if (currentCount >= LIMIT) { + return false + } + + requestCounts[REQUEST_KEY] = currentCount + 1 + logger().info("Request count: ${requestCounts[REQUEST_KEY]}") + return true + } + + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") + fun resetRequestCount() { + requestCounts.clear() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt new file mode 100644 index 0000000..5ca7b2a --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt @@ -0,0 +1,11 @@ +package com.ddd.sonnypolabobe.global.util + +import java.time.LocalDateTime + +object DateConverter { + + fun convertToKst(date: LocalDateTime): LocalDateTime { + return date.plusHours(9) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt new file mode 100644 index 0000000..d00a866 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt @@ -0,0 +1,99 @@ +package com.ddd.sonnypolabobe.global.util + +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import reactor.netty.http.client.HttpClient +import java.time.Duration + + +@Component +class DiscordApiClient( + @Value("\${logging.discord.webhook-uri}") + private val discordWebhookUri: String +) { + fun sendDiscordComm(): WebClient = WebClient.builder().baseUrl(discordWebhookUri) + .clientConnector( + ReactorClientHttpConnector( + HttpClient.create().responseTimeout(Duration.ofMillis(2500)) + ) + ) + .build() + + fun sendErrorLog(req: HttpLog) { + val embedData: MutableMap = HashMap() + + embedData["title"] = "서버 에러 발생" + + val field1: MutableMap = HashMap() + field1["name"] = "요청 정보" + field1["value"] = req.requestMethod + " " + req.requestURI + " " + req.elapsedTime + "ms" + + val field2: MutableMap = HashMap() + field2["name"] = "응답 코드" + field2["value"] = req.responseStatus.toString() + + val field3: MutableMap = HashMap() + field3["name"] = "요청 헤더" + field3["value"] = req.headers.map { it.key + " : " + it.value }.joinToString("\n") + + val field4: MutableMap = HashMap() + field4["name"] = "요청 본문" + field4["value"] = req.requestBody + + val field5: MutableMap = HashMap() + field5["name"] = "요청 파람" + field5["value"] = req.parameters.map { it.key + " : " + it.value }.joinToString("\n") + + val field6: MutableMap = HashMap() + field6["name"] = "응답 본문" + field6["value"] = req.responseBody + + embedData["fields"] = listOf>(field1, field2, field3, field4, field5, field6) + + val payload: MutableMap = HashMap() + payload["embeds"] = arrayOf(embedData) + + sendDiscordComm() + .post() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(payload) + .retrieve() + .bodyToMono(Void::class.java) + .block() + } + + fun sendErrorTrace(errorCode: String, message: String?, trace: String) { + val embedData: MutableMap = HashMap() + + embedData["title"] = "서버 에러 발생" + + val field1: MutableMap = HashMap() + field1["name"] = "트레이스" + field1["value"] = trace + + val field2: MutableMap = HashMap() + field2["name"] = "에러 코드" + field2["value"] = errorCode + + val field3: MutableMap = HashMap() + field3["name"] = "메시지" + field3["value"] = message ?: "메시지 없음" + + embedData["fields"] = listOf>(field1) + + val payload: MutableMap = HashMap() + payload["embeds"] = arrayOf(embedData) + + sendDiscordComm() + .post() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(payload) + .retrieve() + .bodyToMono(Void::class.java) + .block() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/HttpLog.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/HttpLog.kt new file mode 100644 index 0000000..a9cfe8a --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/HttpLog.kt @@ -0,0 +1,12 @@ +package com.ddd.sonnypolabobe.global.util + +data class HttpLog( + val requestMethod : String, + val requestURI : String, + val responseStatus : Int, + val elapsedTime : Double, + val headers : Map, + val parameters : Map, + val requestBody : String, + val responseBody : String +) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index fa0f964..73033f2 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -31,4 +31,8 @@ cloud: static: ap-northeast-2 running: - name: dev \ No newline at end of file + name: dev + +logging: + discord: + webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 6756b6e..e58fda8 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -31,4 +31,8 @@ cloud: static: ap-northeast-2 running: - name: local \ No newline at end of file + name: local + +logging: + discord: + webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1355e31..91403d0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,4 +12,7 @@ logging: jasypt: encryptor: - bean: jasyptStringEncryptor \ No newline at end of file + bean: jasyptStringEncryptor + +limit: + count: 100 \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..f8d6274 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,46 @@ + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta(%-4relative) --- [${appName}, %blue(%X{traceId}), %green(%X{spanId}) %X{sessionId}] %cyan(%logger{20}) : %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 058a51348760cffb47e340575ff43826e667d24b Mon Sep 17 00:00:00 2001 From: dldmsql Date: Tue, 9 Jul 2024 18:47:11 +0900 Subject: [PATCH 16/48] =?UTF-8?q?feat(POLABO-38):=20=ED=95=98=EB=A3=A8=201?= =?UTF-8?q?00=EA=B0=9C=20=EC=9A=94=EC=B2=AD=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=94=94=EC=8A=A4=EC=BD=94=EB=93=9C=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ddd/sonnypolabobe/global/HealthCheckController.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt index d430f7a..b171ce0 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/HealthCheckController.kt @@ -8,7 +8,6 @@ class HealthCheckController { @GetMapping("/health") fun healthCheck(): String { - throw RuntimeException("error") -// return "OK" + return "OK" } } \ No newline at end of file From 2acfbd3ee9d64dd08a6af50490c6da436e4c9a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EB=B9=84?= <61505572+dldmsql@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:54:47 +0900 Subject: [PATCH 17/48] =?UTF-8?q?chore(POLABO-39):=20CD=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=202=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index aae55cd..8eec114 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -66,8 +66,9 @@ jobs: username: ${{ secrets.EC2_USERNAME }} key: ${{ secrets.EC2_KEY }} script: | + aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY }} docker stop polabo-dev docker rm polabo-dev docker pull ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=dev --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} - docker rmi -f $(docker images -f "dangling=true" -q) \ No newline at end of file + docker rmi -f $(docker images -f "dangling=true" -q) From 0153962aead0bd8b3a2e7c085c5a03d44fcae0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EB=B9=84?= <61505572+dldmsql@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:06:13 +0900 Subject: [PATCH 18/48] =?UTF-8?q?chore(POLABO-39):=20CD=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=203=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 8eec114..fc753ec 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -71,4 +71,4 @@ jobs: docker rm polabo-dev docker pull ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=dev --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} - docker rmi -f $(docker images -f "dangling=true" -q) + From 0a50201a6c81a7d91175be462f1aa1c403774d0a Mon Sep 17 00:00:00 2001 From: dldmsql Date: Wed, 10 Jul 2024 22:09:12 +0900 Subject: [PATCH 19/48] =?UTF-8?q?fix(POLABO-38):=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=B0=9C=EC=86=A1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/LoggingFilter.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt index 39e89a6..ac1715b 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt @@ -43,20 +43,20 @@ class LoggingFilter( "Response body : ${getResponseBody(responseWrapper)}" ) - if(responseWrapper.status >= 400) { - this.discordApiClient.sendErrorLog( - HttpLog( - request.method, - request.requestURI, - responseWrapper.status, - (endedAt - startedAt) / 10000.0, - getHeaders(request), - getRequestParams(request), - getRequestBody(requestWrapper), - getResponseBody(responseWrapper) - ) - ) - } +// if(responseWrapper.status >= 400) { +// this.discordApiClient.sendErrorLog( +// HttpLog( +// request.method, +// request.requestURI, +// responseWrapper.status, +// (endedAt - startedAt) / 10000.0, +// getHeaders(request), +// getRequestParams(request), +// getRequestBody(requestWrapper), +// getResponseBody(responseWrapper) +// ) +// ) +// } } } From 0d0e0dbc57b75c8fc87a1dc1c92009d11e79be41 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 13 Jul 2024 01:00:52 +0900 Subject: [PATCH 20/48] =?UTF-8?q?fix(POLABO-38):=20=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8F=B4=EB=9D=BC=EB=A1=9C=EC=9D=B4=EB=93=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/service/BoardService.kt | 30 +++++++------------ .../controller/BoardPolaroidController.kt | 3 +- .../controller/dto/PolaroidCreateRequest.kt | 4 ++- .../repository/PolaroidJooqRepository.kt | 1 + .../repository/PolaroidJooqRepositoryImpl.kt | 13 ++++++++ .../polaroid/service/PolaroidService.kt | 4 +++ .../global/exception/CustomErrorCode.kt | 3 +- 7 files changed, 36 insertions(+), 22 deletions(-) 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 index 61c23a9..d035df0 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt @@ -20,27 +20,19 @@ class BoardService( 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() + val queryResult = boardJooqRepository.selectOneById(UuidConverter.stringToUUID(this@run)) + val groupByTitle = queryResult.groupBy { it.value1() } + groupByTitle.map { entry -> + val title = entry.key + val polaroids = entry.value.map { + PolaroidGetResponse( + id = it.value2() ?: 0L, + imageUrl = it.value3()?.let { it1 -> s3Util.getImgUrl(it1) } ?: "", + oneLineMessage = it.value4() ?: "폴라보와의 추억 한 줄", + userId = it.value6()?.let { it1 -> UuidConverter.byteArrayToUUID(it1) } ) } + BoardGetResponse(title = title ?: "", items = polaroids) } } } 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 index 6f2065e..399a249 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/BoardPolaroidController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/BoardPolaroidController.kt @@ -4,6 +4,7 @@ import com.ddd.sonnypolabobe.domain.polaroid.controller.dto.PolaroidCreateReques import com.ddd.sonnypolabobe.domain.polaroid.service.PolaroidService import com.ddd.sonnypolabobe.global.response.ApplicationResponse import io.swagger.v3.oas.annotations.Operation +import jakarta.validation.Valid import org.springframework.web.bind.annotation.* @RestController @@ -14,7 +15,7 @@ class BoardPolaroidController(private val polaroidService: PolaroidService) { 폴라로이드를 생성합니다. """) @PostMapping - fun create(@PathVariable boardId : String, @RequestBody request : PolaroidCreateRequest) + fun create(@PathVariable boardId : String, @RequestBody @Valid 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/dto/PolaroidCreateRequest.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/controller/dto/PolaroidCreateRequest.kt index 3a9873e..05bed06 100644 --- 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 @@ -1,11 +1,13 @@ package com.ddd.sonnypolabobe.domain.polaroid.controller.dto import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Size @Schema(description = "폴라로이드 생성 요청") data class PolaroidCreateRequest( @Schema(description = "이미지 키", example = "imageKey") val imageKey : String, - @Schema(description = "한 줄 문구", example = "한 줄 메시지입니다.") + @Schema(description = "한 줄 문구", example = "한 줄 메시지입니다. 최대 20자까지 가능합니다.") + @field:Size(max = 20) val oneLineMessage : String ) \ No newline at end of file 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 index a753ad8..61345b9 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepository.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepository.kt @@ -6,4 +6,5 @@ import com.ddd.sonnypolabobe.jooq.polabo.tables.records.PolaroidRecord interface PolaroidJooqRepository { fun insertOne(boardId: ByteArray, request: PolaroidCreateRequest): Long fun selectOneById(id: Long): PolaroidRecord + fun countByBoardId(uuidToByteArray: ByteArray): Int } \ 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 index b62be16..a775a83 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/repository/PolaroidJooqRepositoryImpl.kt @@ -40,4 +40,17 @@ class PolaroidJooqRepositoryImpl(private val dslContext: DSLContext) : PolaroidJ .fetchOne()?.original() ?: throw ApplicationException(CustomErrorCode.POLAROID_NOT_FOUND) } + + override fun countByBoardId(uuidToByteArray: ByteArray): Int { + val jPolaroid = Polaroid.POLAROID + return this.dslContext + .selectCount() + .from(jPolaroid) + .where( + jPolaroid.BOARD_ID.eq(uuidToByteArray) + .and(jPolaroid.YN.eq(1)) + .and(jPolaroid.ACTIVEYN.eq(1)) + ) + .fetchOne(0, Int::class.java) ?: 0 + } } \ 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 index 4cfd7b3..95bd0ec 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt @@ -3,6 +3,8 @@ 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.exception.ApplicationException +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode import com.ddd.sonnypolabobe.global.util.S3Util import com.ddd.sonnypolabobe.global.util.UuidConverter import org.springframework.stereotype.Service @@ -11,6 +13,8 @@ import org.springframework.stereotype.Service class PolaroidService(private val polaroidJooqRepository: PolaroidJooqRepository, private val s3Util: S3Util){ fun create(boardId: String, request: PolaroidCreateRequest): Long { val boardIdUuid = UuidConverter.stringToUUID(boardId) + val countByBoardId = this.polaroidJooqRepository.countByBoardId(UuidConverter.uuidToByteArray(boardIdUuid)) + if(countByBoardId > 50) throw ApplicationException(CustomErrorCode.POLAROID_COUNT_EXCEEDED) return this.polaroidJooqRepository.insertOne(UuidConverter.uuidToByteArray(boardIdUuid), request) } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt index e8bbe29..e7c7ef4 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt @@ -11,6 +11,7 @@ enum class CustomErrorCode( INTERNAL_SERVER_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR, "COM002", "서버 내부 오류입니다."), - POLAROID_NOT_FOUND(HttpStatus.NOT_FOUND, "POL001", "폴라로이드를 찾을 수 없습니다.") + POLAROID_NOT_FOUND(HttpStatus.NOT_FOUND, "POL001", "폴라로이드를 찾을 수 없습니다."), + POLAROID_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "POL002", "보드당 폴라로이드는 50개까지만 생성 가능합니다.") } \ No newline at end of file From c0b56026b9494cc7ca8990a16599775e3fea4c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=9D=80=EB=B9=84?= <61505572+dldmsql@users.noreply.github.com> Date: Sat, 13 Jul 2024 01:05:20 +0900 Subject: [PATCH 21/48] =?UTF-8?q?chore(POLABO-39):=20CD=20=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=204=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 1958123..0d30638 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -1,7 +1,7 @@ name: Java CI with Gradle on: - pull_request: + push: branches: [ "dev" ] jobs: From 84b43521bd9ed9b0aabe619c285127428d856230 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 14 Jul 2024 17:25:04 +0900 Subject: [PATCH 22/48] =?UTF-8?q?fix(POLABO-88):=20=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EB=88=84=EC=A0=81=20=EC=83=9D=EC=84=B1=20=EC=88=98=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddd/sonnypolabobe/domain/board/service/BoardService.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 index d035df0..cdfa950 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt @@ -37,8 +37,6 @@ class BoardService( } } - fun getTotalCount(): String { - return this.boardJooqRepository.selectTotalCount().toString() - } + fun getTotalCount(): Long = this.boardJooqRepository.selectTotalCount() } \ No newline at end of file From c9b22a5041f0a78aa4c88c5440c4f27df5a1fe6c Mon Sep 17 00:00:00 2001 From: dldmsql Date: Wed, 17 Jul 2024 22:53:39 +0900 Subject: [PATCH 23/48] =?UTF-8?q?fix(POLABO-103):=20=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=9C,=20=ED=8F=B4=EB=9D=BC=EB=A1=9C?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EB=8D=94=EB=AF=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddd/sonnypolabobe/domain/board/service/BoardService.kt | 2 +- .../com/ddd/sonnypolabobe/global/security/LoggingFilter.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index cdfa950..afd4317 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt @@ -31,7 +31,7 @@ class BoardService( oneLineMessage = it.value4() ?: "폴라보와의 추억 한 줄", userId = it.value6()?.let { it1 -> UuidConverter.byteArrayToUUID(it1) } ) - } + }.filter { it.id != 0L } BoardGetResponse(title = title ?: "", items = polaroids) } } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt index ac1715b..0334a49 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt @@ -20,7 +20,7 @@ import java.util.* class LoggingFilter( private val discordApiClient: DiscordApiClient ) : GenericFilterBean() { - private val excludedUrls = setOf("/actuator", "/swagger-ui") + private val excludedUrls = setOf("/actuator", "/swagger-ui", "/v3/api-docs") override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { val requestWrapper: ContentCachingRequestWrapper = @@ -61,7 +61,7 @@ class LoggingFilter( } private fun excludeLogging(requestURI: String): Boolean { - return excludedUrls.contains(requestURI) + return excludedUrls.any { requestURI.startsWith(it) } } private fun getResponseBody(response: ContentCachingResponseWrapper): String { From 64a788f98305b421527a64a8025230a06b0e78e8 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Wed, 17 Jul 2024 23:09:24 +0900 Subject: [PATCH 24/48] =?UTF-8?q?fix(POLABO-104):=20cors=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SecurityConfig.kt | 4 ++- .../exception/GlobalExceptionHandler.kt | 6 ++-- .../global/security/LoggingFilter.kt | 29 ++++++++++--------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index 521e9ea..8b24558 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -1,5 +1,6 @@ package com.ddd.sonnypolabobe.global.config +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity @@ -35,7 +36,8 @@ class SecurityConfig() { fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { val configuration = CorsConfiguration() - configuration.allowedOrigins = listOf("http://localhost:3000") // Allow all origins + configuration.allowedOrigins = listOf("http://localhost:3000", "https://polabo.site", + "http://polabo.site", "http://dev.polabo.site", "https://dev.polabo.site") // Allow all origins configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow common methods configuration.allowedHeaders = listOf("*") // Allow all headers diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt index 9700834..aa728e6 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt @@ -14,7 +14,7 @@ class GlobalExceptionHandler( ) { @ExceptionHandler(ApplicationException::class) fun applicationException(ex: ApplicationException): ResponseEntity> { - logger().info("error : ${ex.error}") + logger().error("error : ${ex.error}") this.discordApiClient.sendErrorTrace( ex.error.code, ex.message, ex.stackTrace.contentToString() @@ -24,7 +24,7 @@ class GlobalExceptionHandler( @ExceptionHandler(MethodArgumentNotValidException::class) fun validationException(ex: MethodArgumentNotValidException): ResponseEntity> { - logger().info("error : ${ex.bindingResult.allErrors[0].defaultMessage}") + logger().error("error : ${ex.bindingResult.allErrors[0].defaultMessage}") return ResponseEntity.status(CustomErrorCode.INVALID_VALUE_EXCEPTION.status) .body( ApplicationResponse.error( @@ -36,7 +36,7 @@ class GlobalExceptionHandler( @ExceptionHandler(RuntimeException::class) fun runtimeException(ex: RuntimeException): ResponseEntity> { - logger().info("error : ${ex.message}") + logger().error("error : ${ex.message}") this.discordApiClient.sendErrorTrace( "500", ex.message, ex.stackTrace.contentToString() diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt index 0334a49..938fdef 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt @@ -1,5 +1,6 @@ package com.ddd.sonnypolabobe.global.security +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode import com.ddd.sonnypolabobe.global.util.DiscordApiClient import com.ddd.sonnypolabobe.global.util.HttpLog import com.ddd.sonnypolabobe.logger @@ -43,20 +44,20 @@ class LoggingFilter( "Response body : ${getResponseBody(responseWrapper)}" ) -// if(responseWrapper.status >= 400) { -// this.discordApiClient.sendErrorLog( -// HttpLog( -// request.method, -// request.requestURI, -// responseWrapper.status, -// (endedAt - startedAt) / 10000.0, -// getHeaders(request), -// getRequestParams(request), -// getRequestBody(requestWrapper), -// getResponseBody(responseWrapper) -// ) -// ) -// } + if(responseWrapper.status >= 400 && getResponseBody(responseWrapper).contains(CustomErrorCode.INTERNAL_SERVER_EXCEPTION.message)) { + this.discordApiClient.sendErrorLog( + HttpLog( + request.method, + request.requestURI, + responseWrapper.status, + (endedAt - startedAt) / 10000.0, + getHeaders(request), + getRequestParams(request), + getRequestBody(requestWrapper), + getResponseBody(responseWrapper) + ) + ) + } } } From 603784e1649b35d1c7856dd382c91c1273290164 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 18 Jul 2024 00:00:37 +0900 Subject: [PATCH 25/48] =?UTF-8?q?fix(POLABO-104):=20cors=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index 8b24558..e8aa0af 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -34,6 +34,7 @@ class SecurityConfig() { .build() } + @Bean fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { val configuration = CorsConfiguration() configuration.allowedOrigins = listOf("http://localhost:3000", "https://polabo.site", From 5de07a10b3d1db18ec2ea9b185d2be61a42ef744 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 18 Jul 2024 00:17:40 +0900 Subject: [PATCH 26/48] =?UTF-8?q?fix(POLABO-104):=20cors=20=EC=9D=B4?= =?UTF-8?q?=EC=8A=88=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SecurityConfig.kt | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index e8aa0af..cd7c435 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -3,12 +3,14 @@ package com.ddd.sonnypolabobe.global.config import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.util.matcher.AntPathRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.cors.CorsConfigurationSource import org.springframework.web.cors.UrlBasedCorsConfigurationSource @Configuration @@ -18,9 +20,7 @@ class SecurityConfig() { @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { return http - .cors { - it.configurationSource(corsConfigurationSource()) - } + .cors {} .csrf{ it.disable() } @@ -35,19 +35,26 @@ class SecurityConfig() { } @Bean - fun corsConfigurationSource(): UrlBasedCorsConfigurationSource { - val configuration = CorsConfiguration() - configuration.allowedOrigins = listOf("http://localhost:3000", "https://polabo.site", - "http://polabo.site", "http://dev.polabo.site", "https://dev.polabo.site") // Allow all origins - configuration.allowedMethods = - listOf("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow common methods - configuration.allowedHeaders = listOf("*") // Allow all headers - configuration.allowCredentials = true // Allow credentials + fun corsConfigurationSource(): CorsConfigurationSource { + + val configuration = CorsConfiguration().apply { + allowCredentials = true + allowedOrigins = listOf("http://localhost:3000", "https://polabo.site", + "http://polabo.site", "http://dev.polabo.site", "https://dev.polabo.site") + allowedMethods = listOf( + HttpMethod.POST.name(), + HttpMethod.GET.name(), + HttpMethod.PUT.name(), + HttpMethod.DELETE.name(), + HttpMethod.OPTIONS.name() + ) + allowedHeaders = listOf("*") + } + val source = UrlBasedCorsConfigurationSource() - source.registerCorsConfiguration( - "/**", - configuration - ) // Apply configuration to all endpoints + source.registerCorsConfiguration("/**", configuration) + return source + } } \ No newline at end of file From c29ab7d5598c76d95ac77a39e03c0b3851c9bbbe Mon Sep 17 00:00:00 2001 From: dldmsql Date: Fri, 19 Jul 2024 22:25:18 +0900 Subject: [PATCH 27/48] =?UTF-8?q?feat(POLABO-108):=20=EC=98=A4=EB=8A=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B0=80=EB=8A=A5=ED=95=9C=20=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/controller/BoardController.kt | 6 ++++++ .../domain/board/repository/BoardJooqRepository.kt | 1 + .../board/repository/BoardJooqRepositoryImpl.kt | 14 ++++++++++++++ .../domain/board/service/BoardService.kt | 10 +++++++++- 4 files changed, 30 insertions(+), 1 deletion(-) 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 index 510168c..31774de 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt @@ -39,4 +39,10 @@ class BoardController( """) @GetMapping("/total-count") fun getTotalCount() = ApplicationResponse.ok(this.boardService.getTotalCount()) + + @Operation(summary = "오늘 생성 가능한 보드 수 조회", description = """ + 오늘 생성 가능한 보드 수를 조회합니다. + """) + @GetMapping("/create-available") + fun createAvailable() = ApplicationResponse.ok(this.boardService.createAvailable()) } \ 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 index 0592e43..c927452 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt @@ -9,4 +9,5 @@ interface BoardJooqRepository { fun insertOne(request: BoardCreateRequest): ByteArray? fun selectOneById(id: UUID) : Array> fun selectTotalCount(): Long + fun selectTodayTotalCount(): 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 index 1b4cb74..c11d2cb 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -67,4 +67,18 @@ class BoardJooqRepositoryImpl( .where(jBoard.YN.eq(1).and(jBoard.ACTIVEYN.eq(1))) .fetchOne(0, Long::class.java) ?: 0 } + + override fun selectTodayTotalCount(): Long { + val jBoard = Board.BOARD + return this.dslContext + .selectCount() + .from(jBoard) + .where( + jBoard.CREATED_AT.greaterOrEqual(DateConverter.convertToKst(LocalDateTime.now().withHour(0).withMinute(0).withSecond(0))) + .and(jBoard.CREATED_AT.lessThan(DateConverter.convertToKst(LocalDateTime.now().withHour(23).withMinute(59).withSecond(59))) + .and(jBoard.YN.eq(1)) + .and(jBoard.ACTIVEYN.eq(1)) + )) + .fetchOne(0, Long::class.java) ?: 0L + } } \ 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 index afd4317..2ab0a5f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt @@ -6,13 +6,16 @@ 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.beans.factory.annotation.Value import org.springframework.stereotype.Service import java.util.* @Service class BoardService( private val boardJooqRepository: BoardJooqRepository, - private val s3Util: S3Util + private val s3Util: S3Util, + @Value("\${limit.count}") + private val limit: Int ) { fun create(request: BoardCreateRequest): UUID? { return this.boardJooqRepository.insertOne(request)?.let { UuidConverter.byteArrayToUUID(it) } @@ -38,5 +41,10 @@ class BoardService( } fun getTotalCount(): Long = this.boardJooqRepository.selectTotalCount() + fun createAvailable(): Long { + return this.boardJooqRepository.selectTodayTotalCount().let { + if (it > limit) 0 else limit - it + } + } } \ No newline at end of file From 2a925b9bf3178e3bdc7a910564c92b636a885e8f Mon Sep 17 00:00:00 2001 From: dldmsql Date: Fri, 19 Jul 2024 22:26:10 +0900 Subject: [PATCH 28/48] =?UTF-8?q?fix(POLABO-107):=20=ED=8F=B4=EB=9D=BC?= =?UTF-8?q?=EB=A1=9C=EC=9D=B4=EB=93=9C=20=EC=B5=9C=EC=8B=A0=EC=88=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/repository/BoardJooqRepositoryImpl.kt | 1 + 1 file changed, 1 insertion(+) 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 index c11d2cb..beb7cc1 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -55,6 +55,7 @@ class BoardJooqRepositoryImpl( jBoard.ID.eq(UuidConverter.uuidToByteArray(id)).and(jBoard.YN.eq(1)) .and(jBoard.ACTIVEYN.eq(1)) ) + .orderBy(jPolaroid.CREATED_AT.desc()) .fetchArray() } From 4c72785828a71c685b30aff0c7c293d01ccab3c1 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Tue, 6 Aug 2024 21:20:09 +0900 Subject: [PATCH 29/48] =?UTF-8?q?chore(POLABO-107):=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=20=EC=84=9C=EB=B2=84=20=EB=B0=B0=ED=8F=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 4 +- .github/workflows/cd-prod.yml | 84 ++++++++++ build.gradle.kts | 4 + .../board/controller/BoardController.kt | 11 +- .../controller/dto/BoardCreateRequest.kt | 2 +- .../board/my/controller/MyBoardController.kt | 71 ++++++++ .../domain/board/my/dto/MyBoardDto.kt | 32 ++++ .../domain/board/my/service/MyBoardService.kt | 27 +++ .../board/repository/BoardJooqRepository.kt | 7 +- .../repository/BoardJooqRepositoryImpl.kt | 33 +++- .../domain/board/service/BoardService.kt | 2 +- .../oauth/controller/OauthController.kt | 27 +++ .../domain/oauth/service/OauthService.kt | 157 ++++++++++++++++++ .../controller/dto/PolaroidGetResponse.kt | 2 +- .../polaroid/service/PolaroidService.kt | 2 +- .../domain/user/controller/UserController.kt | 55 ++++++ .../sonnypolabobe/domain/user/dto/UserDto.kt | 70 ++++++++ .../domain/user/dto/WithdrawType.kt | 9 + .../user/repository/UserJooqRepository.kt | 11 ++ .../user/repository/UserJooqRepositoryImpl.kt | 80 +++++++++ .../user/repository/WithdrawJooqRepository.kt | 7 + .../repository/WithdrawJooqRepositoryImpl.kt | 30 ++++ .../domain/user/service/UserService.kt | 33 ++++ .../domain/user/token/dto/UserTokenDto.kt | 9 + .../repository/UserTokenJooqRepository.kt | 9 + .../repository/UserTokenJooqRepositoryImpl.kt | 62 +++++++ .../global/config/SecurityConfig.kt | 24 ++- .../global/config/SwaggerConfig.kt | 32 ++++ .../sonnypolabobe/global/entity/PageDto.kt | 8 + .../global/exception/CustomErrorCode.kt | 10 +- .../global/security/AuthenticatedMember.kt | 10 ++ .../security/CustomUserDetailsService.kt | 18 ++ .../security/JwtAuthenticationFilter.kt | 47 ++++++ .../sonnypolabobe/global/security/JwtUtil.kt | 92 ++++++++++ .../sonnypolabobe/global/security/KakaoDto.kt | 43 +++++ .../global/util/DateConverter.kt | 6 + .../global/util/WebClientUtil.kt | 28 ++++ src/main/resources/application-dev.yml | 27 +++ src/main/resources/application-local.yml | 27 +++ 39 files changed, 1200 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/cd-prod.yml create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/WithdrawType.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepository.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepositoryImpl.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/AuthenticatedMember.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/CustomUserDetailsService.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/KakaoDto.kt create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/util/WebClientUtil.kt diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 0d30638..215e602 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -72,7 +72,7 @@ jobs: - name: Deploy uses: appleboy/ssh-action@master with: - host: ${{ secrets.EC2_HOST }} + host: ${{ secrets.EC2_HOST_DEV }} username: ${{ secrets.EC2_USERNAME }} key: ${{ secrets.EC2_KEY }} script: | @@ -80,5 +80,5 @@ jobs: docker stop polabo-dev docker rm polabo-dev docker pull ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} - docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=dev --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=local --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml new file mode 100644 index 0000000..78ea501 --- /dev/null +++ b/.github/workflows/cd-prod.yml @@ -0,0 +1,84 @@ +name: Java CI with Gradle + +on: + push: + branches: [ "main" ] + +jobs: + build: + ## checkout후 자바 21 버전으로 설정을 합니다 + runs-on: ubuntu-latest + env: + DB_URL: ${{ secrets.DB_URL }} + DB_USER: ${{ secrets.DB_USER }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + JASYPT_ENCRYPTOR_PASSWORD: ${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Set up Gradle + uses: gradle/gradle-build-action@v2 + + - name: Increase Gradle memory settings + run: | + echo "org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8" >> ~/.gradle/gradle.properties + echo "kotlin.daemon.jvmargs=-Xmx4g" >> ~/.gradle/gradle.properties + + ## gradlew 의 권한을 줍니다. + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + ## gradle build + - name: Build with Gradle + run: ./gradlew clean build -x test + + ## 이미지 태그에 시간 설정을 하기위해서 현재 시간을 가져옵니다. + - 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}} ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + docker push ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + env: + PASSWORD: ${{ secrets.JASYPT_ENCRYPTOR_PASSWORD }} + + - name: Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_KEY }} + script: | + aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPOSITORY }} + docker stop polabo-dev + docker rm polabo-dev + docker pull ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=dev --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + diff --git a/build.gradle.kts b/build.gradle.kts index 5ce6603..2058318 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -91,6 +91,10 @@ dependencies { implementation("com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5") implementation("org.springframework.boot:spring-boot-starter-security") implementation("io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64") + + implementation("io.jsonwebtoken:jjwt-api:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") } tasks.withType { 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 index 31774de..e36c9a9 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt @@ -3,9 +3,12 @@ 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.domain.user.dto.UserDto import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import com.ddd.sonnypolabobe.logger import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping @@ -22,10 +25,16 @@ class BoardController( @Operation(summary = "보드 생성", description = """ 보드를 생성합니다. userId는 추후 회원가입 기능이 추가될 것을 대비한 것입니다. 지금은 null로 주세요. + + userId 타입을 UUID -> Long으로 변경하였습니다. - 2024.08.03 """) @PostMapping fun create(@RequestBody request : BoardCreateRequest) - = ApplicationResponse.ok(this.boardService.create(request)) + = run { + val user = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + request.userId = user.id + ApplicationResponse.ok(this.boardService.create(request)) + } @Operation(summary = "보드 조회", description = """ 보드를 조회합니다. 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 index e852b3d..422042b 100644 --- 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 @@ -11,5 +11,5 @@ data class BoardCreateRequest( @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 + var userId: Long? = null ) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt new file mode 100644 index 0000000..89b06a6 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt @@ -0,0 +1,71 @@ +package com.ddd.sonnypolabobe.domain.board.my.controller + +import com.ddd.sonnypolabobe.domain.board.my.dto.MyBoardDto +import com.ddd.sonnypolabobe.domain.board.my.service.MyBoardService +import com.ddd.sonnypolabobe.domain.user.dto.UserDto +import com.ddd.sonnypolabobe.global.entity.PageDto +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import com.ddd.sonnypolabobe.global.util.DateConverter.convertToKst +import io.swagger.v3.oas.annotations.Operation +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.bind.annotation.* +import java.time.LocalDateTime + +@RestController +@RequestMapping("/api/v1/my/boards") +class MyBoardController(private val myBoardService : MyBoardService) { + + @Operation(summary = "내 보드 목록 조회", description = """ + 내 보드 목록을 조회합니다. + """) + @GetMapping + fun getMyBoards( + @RequestParam page : Int, + @RequestParam size : Int + ) = ApplicationResponse.ok( + PageDto( + content = listOf( + MyBoardDto.Companion.PageListRes( + id = 1L, + title = "보드 제목", + createdAt = convertToKst(LocalDateTime.now()), + totalCount = 1L + ) + ), + page = page, + size = size, + total = 10 + ) + ) + + @Operation( + summary = "내 보드 이름 수정", + description = """ + 내 보드 이름을 수정합니다. + """ + ) + @PutMapping("/{id}") + fun updateMyBoard( + @PathVariable id : String, + @RequestBody request : MyBoardDto.Companion.MBUpdateReq + ) = run { + val userId = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + this.myBoardService.updateMyBoard(id, request, userId.id) + ApplicationResponse.ok() + } + + @Operation( + summary = "내 보드 삭제", + description = """ + 내 보드를 삭제합니다. + """ + ) + @DeleteMapping("/{id}") + fun deleteMyBoard( + @PathVariable id : String + ) = run { + val userId = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + this.myBoardService.deleteMyBoard(id, userId.id) + ApplicationResponse.ok() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt new file mode 100644 index 0000000..e4ec7d6 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt @@ -0,0 +1,32 @@ +package com.ddd.sonnypolabobe.domain.board.my.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import org.springframework.format.annotation.DateTimeFormat +import java.time.LocalDateTime +import java.util.UUID + +class MyBoardDto { + companion object { + data class MBUpdateReq( + @JsonProperty("title") + val title: String + ) + + data class PageListRes( + val id: Long, + val title: String, + @DateTimeFormat(pattern = "yyyy-MM-dd", iso = DateTimeFormat.ISO.DATE) + val createdAt: LocalDateTime, + val totalCount: Long + ) + + data class GetOneRes( + val id: UUID, + val title: String, + @DateTimeFormat(pattern = "yyyy-MM-dd", iso = DateTimeFormat.ISO.DATE) + val createdAt: LocalDateTime, + val userId : Long? + ) + + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt new file mode 100644 index 0000000..bdbc151 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt @@ -0,0 +1,27 @@ +package com.ddd.sonnypolabobe.domain.board.my.service + +import com.ddd.sonnypolabobe.domain.board.my.dto.MyBoardDto +import com.ddd.sonnypolabobe.domain.board.repository.BoardJooqRepository +import com.ddd.sonnypolabobe.global.util.UuidConverter +import org.springframework.stereotype.Service + +@Service +class MyBoardService(private val boardJooqRepository: BoardJooqRepository) { + fun updateMyBoard(id: String, request: MyBoardDto.Companion.MBUpdateReq, userId: Long) { + val board = this.boardJooqRepository.findById(UuidConverter.stringToUUID(id)) + ?: throw IllegalArgumentException("해당 보드가 존재하지 않습니다.") + if (board.userId != userId) { + throw IllegalArgumentException("해당 보드에 대한 권한이 없습니다.") + } + this.boardJooqRepository.updateTitle(UuidConverter.stringToUUID(id), request.title) + } + + fun deleteMyBoard(id: String, userId: Long) { + val board = this.boardJooqRepository.findById(UuidConverter.stringToUUID(id)) + ?: throw IllegalArgumentException("해당 보드가 존재하지 않습니다.") + if (board.userId != userId) { + throw IllegalArgumentException("해당 보드에 대한 권한이 없습니다.") + } + this.boardJooqRepository.delete(UuidConverter.stringToUUID(id)) + } +} \ 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 index c927452..05082ad 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt @@ -1,13 +1,18 @@ package com.ddd.sonnypolabobe.domain.board.repository import com.ddd.sonnypolabobe.domain.board.controller.dto.BoardCreateRequest +import com.ddd.sonnypolabobe.domain.board.my.dto.MyBoardDto +import com.ddd.sonnypolabobe.jooq.polabo.tables.Board import org.jooq.Record6 import java.time.LocalDateTime import java.util.* interface BoardJooqRepository { fun insertOne(request: BoardCreateRequest): ByteArray? - fun selectOneById(id: UUID) : Array> + fun selectOneById(id: UUID) : Array> fun selectTotalCount(): Long fun selectTodayTotalCount(): Long + fun findById(id: UUID): MyBoardDto.Companion.GetOneRes? + fun updateTitle(id: UUID, title: String) + fun delete(id: UUID) } \ 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 index beb7cc1..31a0095 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -2,6 +2,7 @@ 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.domain.board.my.dto.MyBoardDto import com.ddd.sonnypolabobe.global.util.DateConverter import com.ddd.sonnypolabobe.global.util.UuidConverter import com.ddd.sonnypolabobe.global.util.UuidGenerator @@ -34,7 +35,7 @@ class BoardJooqRepositoryImpl( return if (result == 1) id else null } - override fun selectOneById(id: UUID): Array> { + override fun selectOneById(id: UUID): Array> { val jBoard = Board.BOARD val jPolaroid = Polaroid.POLAROID return this.dslContext @@ -82,4 +83,34 @@ class BoardJooqRepositoryImpl( )) .fetchOne(0, Long::class.java) ?: 0L } + + override fun findById(id: UUID): MyBoardDto.Companion.GetOneRes? { + val jBoard = Board.BOARD + return this.dslContext.selectFrom(jBoard) + .where(jBoard.ID.eq(UuidConverter.uuidToByteArray(id))) + .fetchOne()?.map { + MyBoardDto.Companion.GetOneRes( + id = UuidConverter.byteArrayToUUID(it.get("id", ByteArray::class.java)!!), + title = it.get("title", String::class.java)!!, + createdAt = it.get("created_at", LocalDateTime::class.java)!!, + userId = it.get("user_id", Long::class.java), + ) + } + } + + override fun updateTitle(id: UUID, title: String) { + val jBoard = Board.BOARD + this.dslContext.update(jBoard) + .set(jBoard.TITLE, title) + .where(jBoard.ID.eq(UuidConverter.uuidToByteArray(id))) + .execute() + } + + override fun delete(id: UUID) { + val jBoard = Board.BOARD + this.dslContext.update(jBoard) + .set(jBoard.YN, 0) + .where(jBoard.ID.eq(UuidConverter.uuidToByteArray(id))) + .execute() + } } \ 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 index 2ab0a5f..feef5f2 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt @@ -32,7 +32,7 @@ class BoardService( id = it.value2() ?: 0L, imageUrl = it.value3()?.let { it1 -> s3Util.getImgUrl(it1) } ?: "", oneLineMessage = it.value4() ?: "폴라보와의 추억 한 줄", - userId = it.value6()?.let { it1 -> UuidConverter.byteArrayToUUID(it1) } + userId = it.value6() ?: 0L ) }.filter { it.id != 0L } BoardGetResponse(title = title ?: "", items = polaroids) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt new file mode 100644 index 0000000..252876b --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt @@ -0,0 +1,27 @@ +package com.ddd.sonnypolabobe.domain.oauth.controller + +import com.ddd.sonnypolabobe.domain.oauth.service.OauthService +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import io.swagger.v3.oas.annotations.Operation +import org.springframework.http.HttpHeaders +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/v1/oauth") +class OauthController(private val oauthService: OauthService) { + + @Operation(summary = "카카오 소셜 로그인", description = """ + 카카오 소셜 로그인을 진행합니다. + 인가 코드를 파라미터로 넘겨주세요. + """) + @GetMapping("/sign-in") + fun login(@RequestParam(name = "code") code : String) = ApplicationResponse.ok(this.oauthService.signIn(code)) + + @PutMapping("/re-issue") + fun reIssue( + @RequestHeader(name = "Authorization", required = true) header: String + ) = run { + ApplicationResponse.ok(this.oauthService.reIssue(header)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt new file mode 100644 index 0000000..ee770a5 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt @@ -0,0 +1,157 @@ +package com.ddd.sonnypolabobe.domain.oauth.service + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto +import com.ddd.sonnypolabobe.domain.user.repository.UserJooqRepository +import com.ddd.sonnypolabobe.domain.user.token.dto.UserTokenDto +import com.ddd.sonnypolabobe.domain.user.token.repository.UserTokenJooqRepository +import com.ddd.sonnypolabobe.global.security.JwtUtil +import com.ddd.sonnypolabobe.global.security.KakaoDto +import com.ddd.sonnypolabobe.global.util.DateConverter.dateToLocalDateTime +import com.ddd.sonnypolabobe.global.util.WebClientUtil +import com.ddd.sonnypolabobe.logger +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.reactive.function.BodyInserters + + +@Service +class OauthService( + private val webClient : WebClientUtil, + private val objectMapper: ObjectMapper, + @Value("\${spring.security.oauth2.client.registration.kakao.client-id}") private val clientId : String, + @Value("\${spring.security.oauth2.client.registration.kakao.redirect-uri}") private val redirectUri : String, + @Value("\${spring.security.oauth2.client.registration.kakao.client-secret}") private val clientSecret : String, + private val userRepository : UserJooqRepository, + private val jwtUtil: JwtUtil, + private val userTokenRepository: UserTokenJooqRepository + ) { + + fun getAccessToken(code: String) : KakaoDto.Companion.Token { + + //요청 본문 + val params: MultiValueMap = LinkedMultiValueMap() + params.add("grant_type", "authorization_code") + params.add("client_id", clientId) + params.add("redirect_uri", redirectUri) + params.add("code", code) + params.add("client_secret", clientSecret) + + logger().error("params : $params") + // 요청 보내기 및 응답 수신 + val response = webClient.create("https://kauth.kakao.com") + .post() + .uri("/oauth/token") + .header("Content-type", "application/x-www-form-urlencoded") + .body(BodyInserters.fromFormData(params)) + .retrieve() + .bodyToMono(String::class.java) + .block() + + + return try { + this.objectMapper.readValue(response, KakaoDto.Companion.Token::class.java) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + fun getKakaoInfo(accessToken: String): KakaoDto.Companion.UserInfo { + + // 요청 보내기 및 응답 수신 + val response = webClient.create("https://kapi.kakao.com") + .post() + .uri("/v2/user/me") + .header("Content-type", "application/x-www-form-urlencoded") + .header("Authorization", "Bearer $accessToken") + .retrieve() + .bodyToMono(String::class.java) + .block() + + return try { + this.objectMapper.readValue(response, KakaoDto.Companion.UserInfo::class.java) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + @Transactional + fun signIn(code: String): UserDto.Companion.TokenRes { + val token = getAccessToken(code) + val userInfo = getKakaoInfo(token.access_token) + + // DB에 저장 + val request = UserDto.Companion.CreateReq( + nickName = userInfo.kakao_account.profile.nickname, + email = userInfo.kakao_account.email + ) + + this.userRepository.findByEmail(request.email)?.let { + val tokenRequest = UserDto.Companion.CreateTokenReq( + id = it.id, + email = it.email, + nickName = it.nickName + ) + + val tokenRes = this.jwtUtil.generateAccessToken(tokenRequest) + + val userToken = UserTokenDto( + userId = it.id, + accessToken = tokenRes.accessToken, + expiredAt = dateToLocalDateTime(tokenRes.expiredDate) + ) + + this.userTokenRepository.updateByUserId(userToken) + return tokenRes + } ?: run { + val userId = this.userRepository.insertOne(request) + + // 토큰 생성 + val tokenRequest = UserDto.Companion.CreateTokenReq( + id = userId, + email = userInfo.kakao_account.email, + nickName = userInfo.kakao_account.profile.nickname + ) + + val tokenRes = this.jwtUtil.generateAccessToken(tokenRequest) + + val userToken = UserTokenDto( + userId = userId, + accessToken = tokenRes.accessToken, + expiredAt = dateToLocalDateTime(tokenRes.expiredDate) + ) + + this.userTokenRepository.insertOne(userToken) + return tokenRes + } + } + + fun reIssue(token: String?): UserDto.Companion.TokenRes{ + val tokenFromDB = token?.let { + val slicedToken = if(it.startsWith("Bearer ")) it.substring(7) else it + this.userTokenRepository.findByAccessToken(slicedToken) + } ?: throw RuntimeException("Token Not Found") + val user = this.userRepository.findById(tokenFromDB.userId) ?: throw RuntimeException("User Not Found") + + // 토큰 생성 + val tokenRequest = UserDto.Companion.CreateTokenReq( + id = user.id, + email = user.email, + nickName = user.nickName + ) + + val tokenRes = this.jwtUtil.generateAccessToken(tokenRequest) + + val userToken = UserTokenDto( + userId = user.id, + accessToken = tokenRes.accessToken, + expiredAt = dateToLocalDateTime(tokenRes.expiredDate) + ) + + this.userTokenRepository.updateByUserId(userToken) + return tokenRes + } +} \ 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 index 0b843b7..092882c 100644 --- 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 @@ -11,5 +11,5 @@ data class PolaroidGetResponse( @Schema(description = "한 줄 문구", example = "한 줄 메시지입니다.") val oneLineMessage: String, @Schema(description = "작성자 ID", example = "userId") - val userId: UUID?, + val userId: Long?, ) 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 index 95bd0ec..01e540b 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/polaroid/service/PolaroidService.kt @@ -24,7 +24,7 @@ class PolaroidService(private val polaroidJooqRepository: PolaroidJooqRepository id = it.id!!, imageUrl = s3Util.getImgUrl(it.imageKey!!), oneLineMessage = it.oneLineMessage ?: "", - userId = it.userId?.let { it1 -> UuidConverter.byteArrayToUUID(it1) } + userId = it.userId ) } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt new file mode 100644 index 0000000..c753b06 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt @@ -0,0 +1,55 @@ +package com.ddd.sonnypolabobe.domain.user.controller + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto +import com.ddd.sonnypolabobe.domain.user.service.UserService +import com.ddd.sonnypolabobe.global.response.ApplicationResponse +import com.ddd.sonnypolabobe.global.util.DateConverter +import io.swagger.v3.oas.annotations.Operation +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.time.LocalDateTime + +@RestController +@RequestMapping("/api/v1/user") +class UserController( + private val userService: UserService +) { + + @Operation(summary = "닉네임 변경", description = """ + 닉네임을 변경합니다. + """) + @PutMapping("/nickname") + fun updateNickname(@RequestBody request: UserDto.Companion.UpdateReq) + = run { + val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + this.userService.updateProfile(request, userInfoFromToken.id) + ApplicationResponse.ok() + } + + @Operation(summary = "프로필 조회", description = """ + 프로필을 조회합니다. + """) + @GetMapping("/profile") + fun getProfile() = run { + val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + ApplicationResponse.ok(this.userService.findById(userInfoFromToken.id)) + } + + @Operation(summary = "회원 탈퇴", description = """ + 회원 탈퇴를 진행합니다. + 탈퇴 사유를 입력해주세요. + 사유가 '기타'인 경우에만 reason 필드를 채워주세요. + """) + @PutMapping("/withdraw") + fun withdraw(@RequestBody request: UserDto.Companion.WithdrawReq) = run { + val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + this.userService.withdraw(request, userInfoFromToken.id) + ApplicationResponse.ok() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt new file mode 100644 index 0000000..3921ee1 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt @@ -0,0 +1,70 @@ +package com.ddd.sonnypolabobe.domain.user.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import java.time.LocalDateTime +import java.util.Date +import java.util.stream.Collectors + +class UserDto { + companion object { + data class CreateReq( + val email: String, + val nickName: String + ) + + data class UpdateReq( + @JsonProperty("nickName") + val nickName : String + ) + + data class CreateTokenReq( + val id : Long, + val email: String, + val nickName: String + ) + + data class TokenRes( + val accessToken: String, + val expiredDate: Date + ) + + data class ProfileRes( + val id: Long, + val email: String, + val nickName: String, + val createdAt: LocalDateTime, + ) + + data class WithdrawReq( + val type: WithdrawType, + val reason : String? + ) + + data class Res( + val id: Long, + val email: String, + val nickName: String, + val yn: Boolean, + val createdAt: LocalDateTime, + val updatedAt: LocalDateTime? + ) : UserDetails { + override fun getAuthorities(): MutableCollection { + val roles = mutableListOf("ROLE_USER") + return roles.stream() + .map { role -> SimpleGrantedAuthority(role) } + .collect(Collectors.toList()) + } + + override fun getPassword(): String { + return "" + } + + override fun getUsername(): String { + return id.toString() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/WithdrawType.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/WithdrawType.kt new file mode 100644 index 0000000..2dbbeca --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/WithdrawType.kt @@ -0,0 +1,9 @@ +package com.ddd.sonnypolabobe.domain.user.dto + +enum class WithdrawType(val description : String) { + NOT_USE("더이상 사용하지 않아요"), + WORRY_ABOUT_PERSONAL_INFO("개인정보 우려"), + DROP_MY_DATA("내 데이터 삭제"), + WANT_TO_NEW_ACCOUNT("새로운 계정을 만들고 싶어요"), + OTHER("기타") +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt new file mode 100644 index 0000000..0a7199e --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt @@ -0,0 +1,11 @@ +package com.ddd.sonnypolabobe.domain.user.repository + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto + +interface UserJooqRepository { + fun insertOne(request: UserDto.Companion.CreateReq): Long + + fun findById(id: Long): UserDto.Companion.Res? + fun findByEmail(email: String): UserDto.Companion.Res? + fun updateProfile(request: UserDto.Companion.UpdateReq, userId: Long) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt new file mode 100644 index 0000000..1d65641 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt @@ -0,0 +1,80 @@ +package com.ddd.sonnypolabobe.domain.user.repository + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto +import com.ddd.sonnypolabobe.global.util.DateConverter +import com.ddd.sonnypolabobe.jooq.polabo.tables.User +import org.jooq.DSLContext +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepository{ + override fun insertOne(request: UserDto.Companion.CreateReq): Long { + val jUser = User.USER + val insertValue = jUser.newRecord().apply { + this.email = request.email + this.nickName = request.nickName + this.createdAt = DateConverter.convertToKst(LocalDateTime.now()) + this.yn = 1 + } + return this.dslContext.insertInto(jUser, + jUser.EMAIL, + jUser.NICK_NAME, + jUser.CREATED_AT, + jUser.YN + ) + .values( + insertValue.email, + insertValue.nickName, + insertValue.createdAt, + insertValue.yn + ) + .returningResult(jUser.ID) + .fetchOne(0, Long::class.java) ?: 0 + } + + override fun findById(id: Long): UserDto.Companion.Res? { + val jUser = User.USER + val record = this.dslContext.selectFrom(jUser) + .where(jUser.ID.eq(id)) + .fetchOne() + + return record?.let { + UserDto.Companion.Res( + id = it.id!!, + email = it.email!!, + nickName = it.nickName!!, + yn = it.yn?.toInt() == 1, + createdAt = it.createdAt!!, + updatedAt = it.updatedAt + ) + } + } + + override fun findByEmail(email: String): UserDto.Companion.Res? { + val jUser = User.USER + val record = this.dslContext.selectFrom(jUser) + .where(jUser.EMAIL.eq(email)) + .fetchOne() + + return record?.let { + UserDto.Companion.Res( + id = it.id!!, + email = it.email!!, + nickName = it.nickName!!, + yn = it.yn?.toInt() == 1, + createdAt = it.createdAt!!, + updatedAt = it.updatedAt + ) + } + } + + override fun updateProfile(request: UserDto.Companion.UpdateReq, userId: Long) { + val jUser = User.USER + this.dslContext.update(jUser) + .set(jUser.NICK_NAME, request.nickName) + .set(jUser.UPDATED_AT, DateConverter.convertToKst(LocalDateTime.now())) + .where(jUser.ID.eq(userId)) + .execute() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepository.kt new file mode 100644 index 0000000..ec9b0be --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepository.kt @@ -0,0 +1,7 @@ +package com.ddd.sonnypolabobe.domain.user.repository + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto + +interface WithdrawJooqRepository { + fun insertOne(request: UserDto.Companion.WithdrawReq, userId: Long) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepositoryImpl.kt new file mode 100644 index 0000000..a5c50d4 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/WithdrawJooqRepositoryImpl.kt @@ -0,0 +1,30 @@ +package com.ddd.sonnypolabobe.domain.user.repository + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto +import com.ddd.sonnypolabobe.global.util.DateConverter +import com.ddd.sonnypolabobe.jooq.polabo.enums.WithdrawType +import com.ddd.sonnypolabobe.jooq.polabo.tables.Withdraw +import org.jooq.DSLContext +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class WithdrawJooqRepositoryImpl(private val dslContext: DSLContext): WithdrawJooqRepository { + override fun insertOne(request: UserDto.Companion.WithdrawReq, userId: Long) { + val jWithdraw = Withdraw.WITHDRAW + + this.dslContext.insertInto(jWithdraw, + jWithdraw.USER_ID, + jWithdraw.TYPE, + jWithdraw.REASON, + jWithdraw.CREATED_AT + ) + .values( + userId, + WithdrawType.valueOf(request.type.name), + request.reason, + DateConverter.convertToKst(LocalDateTime.now()) + ) + .execute() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt new file mode 100644 index 0000000..5185e10 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt @@ -0,0 +1,33 @@ +package com.ddd.sonnypolabobe.domain.user.service + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto +import com.ddd.sonnypolabobe.domain.user.repository.UserJooqRepository +import com.ddd.sonnypolabobe.domain.user.repository.WithdrawJooqRepository +import org.springframework.stereotype.Service + +@Service +class UserService( + private val userJooqRepository: UserJooqRepository, + private val withdrawJooqRepository: WithdrawJooqRepository +) { + fun updateProfile(request: UserDto.Companion.UpdateReq, userId: Long) { + this.userJooqRepository.updateProfile(request, userId) + } + + fun findById(id: Long): UserDto.Companion.ProfileRes { + return this.userJooqRepository.findById(id).let { + UserDto.Companion.ProfileRes( + id = it!!.id, + nickName = it.nickName, + email = it.email, + createdAt = it.createdAt + ) + } + } + + fun withdraw(request: UserDto.Companion.WithdrawReq, id: Long) { + this.withdrawJooqRepository.insertOne(request, id) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt new file mode 100644 index 0000000..a076783 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt @@ -0,0 +1,9 @@ +package com.ddd.sonnypolabobe.domain.user.token.dto + +import java.time.LocalDateTime + +data class UserTokenDto( + val userId: Long, + val accessToken: String, + val expiredAt: LocalDateTime +) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt new file mode 100644 index 0000000..b240ef3 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt @@ -0,0 +1,9 @@ +package com.ddd.sonnypolabobe.domain.user.token.repository + +import com.ddd.sonnypolabobe.domain.user.token.dto.UserTokenDto + +interface UserTokenJooqRepository { + fun insertOne(userToken: UserTokenDto) + fun findByAccessToken(token: String): UserTokenDto? + fun updateByUserId(userToken: UserTokenDto) +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt new file mode 100644 index 0000000..9713c2e --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt @@ -0,0 +1,62 @@ +package com.ddd.sonnypolabobe.domain.user.token.repository + +import com.ddd.sonnypolabobe.domain.user.token.dto.UserTokenDto +import com.ddd.sonnypolabobe.global.util.DateConverter +import com.ddd.sonnypolabobe.jooq.polabo.tables.UserToken +import org.jooq.DSLContext +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class UserTokenJooqRepositoryImpl(private val dslContext: DSLContext) : UserTokenJooqRepository { + override fun insertOne(userToken: UserTokenDto) { + val jUserToken = UserToken.USER_TOKEN + val insertValue = jUserToken.newRecord().apply { + this.userId = userToken.userId + this.accessToken = userToken.accessToken + this.accessExpiredAt = userToken.expiredAt + this.createdAt = DateConverter.convertToKst(LocalDateTime.now()) + this.updatedAt = DateConverter.convertToKst(LocalDateTime.now()) + } + this.dslContext.insertInto(jUserToken, + jUserToken.USER_ID, + jUserToken.ACCESS_TOKEN, + jUserToken.ACCESS_EXPIRED_AT, + jUserToken.CREATED_AT, + jUserToken.UPDATED_AT + ) + .values( + insertValue.userId, + insertValue.accessToken, + insertValue.accessExpiredAt, + insertValue.createdAt, + insertValue.updatedAt + ) + .onDuplicateKeyUpdate() + .set(jUserToken.USER_ID, insertValue.userId) + .execute() + } + + override fun findByAccessToken(token: String): UserTokenDto? { + val jUserToken = UserToken.USER_TOKEN + return this.dslContext.selectFrom(jUserToken) + .where(jUserToken.ACCESS_TOKEN.eq(token)) + .fetchOne()?.map { + UserTokenDto( + userId = it.get(jUserToken.USER_ID)!!, + accessToken = it.get(jUserToken.ACCESS_TOKEN)!!, + expiredAt = it.get(jUserToken.ACCESS_EXPIRED_AT)!! + ) + } + } + + override fun updateByUserId(userToken: UserTokenDto) { + val jUserToken = UserToken.USER_TOKEN + this.dslContext.update(jUserToken) + .set(jUserToken.ACCESS_TOKEN, userToken.accessToken) + .set(jUserToken.ACCESS_EXPIRED_AT, userToken.expiredAt) + .set(jUserToken.UPDATED_AT, DateConverter.convertToKst(LocalDateTime.now())) + .where(jUserToken.USER_ID.eq(userToken.userId)) + .execute() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index cd7c435..b2eff81 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -1,12 +1,15 @@ package com.ddd.sonnypolabobe.global.config +import com.ddd.sonnypolabobe.global.security.JwtAuthenticationFilter import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.util.matcher.AntPathRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher import org.springframework.web.cors.CorsConfiguration @@ -15,7 +18,9 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource @Configuration @EnableMethodSecurity -class SecurityConfig() { +class SecurityConfig( + private val jwtAuthenticationFilter: JwtAuthenticationFilter, +) { @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { @@ -28,8 +33,23 @@ class SecurityConfig() { it.disable() } .formLogin { it.disable() } + .sessionManagement { sessionManagementConfig -> + sessionManagementConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) .authorizeHttpRequests { - it.anyRequest().permitAll() + it.requestMatchers("/api/v1/boards/create-available").permitAll() + it.requestMatchers("/api/v1/boards/total-count").permitAll() + it.requestMatchers("/api/v1/file/**").permitAll() + it.requestMatchers("/api/v1/oauth/sign-in", "/api/v1/oauth/re-issue").permitAll() + it.requestMatchers("/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + it.requestMatchers("/api/v1/boards/{id}").permitAll() + it.anyRequest().authenticated() + } + .exceptionHandling{ + it.authenticationEntryPoint { _, response, _ -> + response.sendError(401) + } } .build() } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt new file mode 100644 index 0000000..cb0bfc2 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt @@ -0,0 +1,32 @@ +package com.ddd.sonnypolabobe.global.config + +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.security.SecurityRequirement +import io.swagger.v3.oas.models.security.SecurityScheme +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class SwaggerConfig { + + @Bean + fun openAPI(): OpenAPI { + val securityScheme: SecurityScheme = getSecurityScheme() + val securityRequirement: SecurityRequirement = getSecurityRequireMent() + + return OpenAPI() + .components(Components().addSecuritySchemes("bearerAuth", securityScheme)) + .security(listOf(securityRequirement)) + } + + private fun getSecurityScheme(): SecurityScheme { + return SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("Bearer").bearerFormat("JWT") + .`in`(SecurityScheme.In.HEADER).name("Authorization") + } + + private fun getSecurityRequireMent(): SecurityRequirement { + return SecurityRequirement().addList("bearerAuth") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt new file mode 100644 index 0000000..f4abbe5 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt @@ -0,0 +1,8 @@ +package com.ddd.sonnypolabobe.global.entity + +data class PageDto( + val page: Int, + val size: Int, + val total: Int, + val content: List +) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt index e7c7ef4..72e93dd 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt @@ -12,6 +12,14 @@ enum class CustomErrorCode( POLAROID_NOT_FOUND(HttpStatus.NOT_FOUND, "POL001", "폴라로이드를 찾을 수 없습니다."), - POLAROID_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "POL002", "보드당 폴라로이드는 50개까지만 생성 가능합니다.") + POLAROID_COUNT_EXCEEDED(HttpStatus.BAD_REQUEST, "POL002", "보드당 폴라로이드는 50개까지만 생성 가능합니다."), + + // jwt + JWT_INVALID(HttpStatus.UNAUTHORIZED, "JWT001", "유효하지 않은 토큰입니다."), + JWT_EXPIRED(HttpStatus.UNAUTHORIZED, "JWT002", "만료된 토큰입니다."), + JWT_MALFORMED(HttpStatus.UNAUTHORIZED, "JWT003", "잘못된 토큰입니다."), + JWT_UNSUPPORTED(HttpStatus.UNAUTHORIZED, "JWT004", "지원되지 않는 토큰입니다."), + JWT_ILLEGAL_ARGUMENT(HttpStatus.UNAUTHORIZED, "JWT005", "잘못된 인자입니다."), + JWT_SIGNATURE(HttpStatus.UNAUTHORIZED, "JWT006", "잘못된 서명입니다."), } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/AuthenticatedMember.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/AuthenticatedMember.kt new file mode 100644 index 0000000..7600fd9 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/AuthenticatedMember.kt @@ -0,0 +1,10 @@ +package com.ddd.sonnypolabobe.global.security + +import java.util.* + +data class AuthenticatedMember( + val id: String, + val email: String, + val nickname: String, + val expiredAt: Date +) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/CustomUserDetailsService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/CustomUserDetailsService.kt new file mode 100644 index 0000000..18af849 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/CustomUserDetailsService.kt @@ -0,0 +1,18 @@ +package com.ddd.sonnypolabobe.global.security + +import com.ddd.sonnypolabobe.domain.user.repository.UserJooqRepository +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class CustomUserDetailsService( + private val userJooqRepository: UserJooqRepository +) : UserDetailsService { + override fun loadUserByUsername(id: String): UserDetails { + return userJooqRepository.findById(id.toLong()) + ?: throw IllegalArgumentException("해당하는 사용자를 찾을 수 없습니다.") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt new file mode 100644 index 0000000..0d884d6 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt @@ -0,0 +1,47 @@ +package com.ddd.sonnypolabobe.global.security + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthenticationFilter( + private val jwtUtil: JwtUtil, + private val customUserDetailsService: CustomUserDetailsService +) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authorizationHeader = request.getHeader("Authorization") + + //JWT가 헤더에 있는 경우 + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + if(request.requestURI.contains("/api/v1/oauth")) { + filterChain.doFilter(request, response) + return + } + //JWT 유효성 검증 + if (jwtUtil.validateToken(authorizationHeader)) { + val userId = jwtUtil.getAuthenticatedMemberFromToken(authorizationHeader).id + + //유저와 토큰 일치 시 userDetails 생성 + val userDetails = customUserDetailsService.loadUserByUsername(userId) + + //UserDetsils, Password, Role -> 접근권한 인증 Token 생성 + val usernamePasswordAuthenticationToken = + UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities) + + //현재 Request의 Security Context에 접근권한 설정 + SecurityContextHolder.getContext().authentication = + usernamePasswordAuthenticationToken + } + } + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt new file mode 100644 index 0000000..1b1d658 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt @@ -0,0 +1,92 @@ +package com.ddd.sonnypolabobe.global.security + +import com.ddd.sonnypolabobe.domain.user.dto.UserDto +import com.ddd.sonnypolabobe.global.exception.ApplicationException +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode +import com.ddd.sonnypolabobe.logger +import io.jsonwebtoken.* +import io.jsonwebtoken.security.Keys +import jakarta.xml.bind.DatatypeConverter +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.security.Key +import java.util.* + +@Component +class JwtUtil( + @Value("\${jwt.access-key}") + private val accessSecretKey: String, + @Value("\${jwt.validity.access-seconds}") + private val accessTokenExpirationMs: Long, +) { + + fun generateAccessToken(request: UserDto.Companion.CreateTokenReq): UserDto.Companion.TokenRes { + val now = Date() + val expiredDate = Date(now.time + accessTokenExpirationMs) + val claims: MutableMap = HashMap() + claims["CLAIM_KEY_ID"] = request.id.toString() + claims["CLAIM_EMAIL"] = request.email + claims["CLAIM_NICKNAME"] = request.nickName + val accessToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(getKey(accessSecretKey), SignatureAlgorithm.HS512) + .compact() + return UserDto.Companion.TokenRes(accessToken, expiredDate) + } + + fun getAuthenticatedMemberFromToken(accessToken: String): AuthenticatedMember { + val claims = getClaimsFromAccessToken(subPrefix(accessToken), accessSecretKey) + val id = claims["CLAIM_KEY_ID"].toString() + val email = claims["CLAIM_EMAIL"].toString() + val nickname = claims["CLAIM_NICKNAME"].toString() + return AuthenticatedMember(id, email, nickname, claims.expiration) + } + + fun validateToken(accessToken: String): Boolean { + try { + val key = getKey(accessSecretKey) + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(subPrefix(accessToken)) + return true + } catch (e: Exception) { + logger().error("error : $e") + throw ApplicationException(CustomErrorCode.JWT_INVALID) + } + } + + fun getClaimsFromAccessToken(token: String, secretKey: String): Claims { + try { + val key = getKey(secretKey) + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).body + } catch (e: io.jsonwebtoken.security.SecurityException) { + throw ApplicationException(CustomErrorCode.JWT_SIGNATURE) + } catch (e: MalformedJwtException) { + throw ApplicationException(CustomErrorCode.JWT_MALFORMED) + } catch (e: ExpiredJwtException) { + throw ApplicationException(CustomErrorCode.JWT_EXPIRED) + } catch (e: UnsupportedJwtException) { + throw ApplicationException(CustomErrorCode.JWT_UNSUPPORTED) + } catch (e: IllegalArgumentException) { + throw ApplicationException(CustomErrorCode.JWT_ILLEGAL_ARGUMENT) + } + } + + private fun subPrefix(token: String): String { + return if (token.isNotEmpty() && token.startsWith("Bearer ")) { + token.substring(7) + } else { + token + } + } + + private fun getKeyBytes(secretKey: String): ByteArray { + return DatatypeConverter.parseBase64Binary((secretKey)) + } + + private fun getKey(secretKey: String): Key { + val keyBytes = Base64.getDecoder().decode(secretKey) + return Keys.hmacShaKeyFor(keyBytes) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/KakaoDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/KakaoDto.kt new file mode 100644 index 0000000..9ae1626 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/KakaoDto.kt @@ -0,0 +1,43 @@ +package com.ddd.sonnypolabobe.global.security + +import java.util.* + +class KakaoDto { + + companion object { + data class Token( + val access_token: String, + val refresh_token: String, + val token_type: String, + val expires_in: Int, + val refresh_token_expires_in: Int, + val scope: String + ) + + data class UserInfo( + val id: Long, + val connected_at: String, + val properties: Map, + val kakao_account: KakaoAccount + ) + + data class KakaoAccount( + val profile_nickname_needs_agreement: Boolean, + val profile_image_needs_agreement: Boolean, + val profile: Profile, + val has_email: Boolean, + val email_needs_agreement: Boolean, + val is_email_valid: Boolean, + val is_email_verified: Boolean, + val email: String + ) + + data class Profile( + val nickname: String, + val thumbnail_image_url: String, + val profile_image_url: String, + val is_default_image: Boolean, + val is_default_nickname: Boolean + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt index 5ca7b2a..7836126 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DateConverter.kt @@ -1,6 +1,8 @@ package com.ddd.sonnypolabobe.global.util import java.time.LocalDateTime +import java.time.ZoneId +import java.util.Date object DateConverter { @@ -8,4 +10,8 @@ object DateConverter { return date.plusHours(9) } + fun dateToLocalDateTime(date: Date) : LocalDateTime { + return LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("Asia/Seoul")) + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/WebClientUtil.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/WebClientUtil.kt new file mode 100644 index 0000000..9a8709a --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/WebClientUtil.kt @@ -0,0 +1,28 @@ +package com.ddd.sonnypolabobe.global.util + +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import reactor.netty.http.client.HttpClient +import java.time.Duration + + +@Component +class WebClientUtil() { + + fun create(baseUrl: String): WebClient { + return WebClient.builder() + .baseUrl(baseUrl) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .codecs { it.defaultCodecs().enableLoggingRequestDetails(true) } + .clientConnector( + ReactorClientHttpConnector( + HttpClient.create().responseTimeout(Duration.ofMillis(2500)) + ) + ) + .build() + } + +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index db2a54a..0c2a978 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -20,6 +20,28 @@ spring: jooq: sql-dialect: mysql + security: + oauth2: + client: + registration: + kakao: + client-id: ENC(BD9NHDqbUHDpLXYtua4QLLznXweUau5/N3dA1IQqKhQW2sWvniKSDTS3+Z9t/oct) + client-secret: ENC(dMTqjTdS4VJz/1Gduapvl1rDDXUUKkp0bilgqRWMI9X4DaAMVDXY13Fb7QMBDUkI) + scope: + - account_email + - profile_nickname + authorization-grant-type: authorization_code + redirect-uri: https://api.polabo.site/api/v1/oauth/sign-in + client-name: Kakao + client-authentication-method: client_secret_post + + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + cloud: aws: credentials: @@ -33,6 +55,11 @@ cloud: running: name: dev +jwt: + access-key: ENC(dMTqjTdS4VJz/1Gduapvl1rDDXUUKkp0bilgqRWMI9X4DaAMVDXY13Fb7QMBDUkI) + validity: + access-seconds: ENC(KeRwqvZAr0MfEVmxw8nBQQFEITQ0v/Fl) + logging: discord: webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index e58fda8..f8f405c 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -20,6 +20,28 @@ spring: jooq: sql-dialect: mysql + security: + oauth2: + client: + registration: + kakao: + client-id: ENC(BD9NHDqbUHDpLXYtua4QLLznXweUau5/N3dA1IQqKhQW2sWvniKSDTS3+Z9t/oct) + client-secret: ENC(dMTqjTdS4VJz/1Gduapvl1rDDXUUKkp0bilgqRWMI9X4DaAMVDXY13Fb7QMBDUkI) + scope: + - account_email + - profile_nickname + authorization-grant-type: authorization_code + redirect-uri: https://api-dev.polabo.site/api/v1/oauth/sign-in + client-name: Kakao + client-authentication-method: client_secret_post + + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + cloud: aws: credentials: @@ -33,6 +55,11 @@ cloud: running: name: local +jwt: + access-key: ENC(dMTqjTdS4VJz/1Gduapvl1rDDXUUKkp0bilgqRWMI9X4DaAMVDXY13Fb7QMBDUkI) + validity: + access-seconds: 86400000 + logging: discord: webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) \ No newline at end of file From 8829ccd47f362b025ae8b7916c51d040debfb3fd Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 8 Aug 2024 22:44:12 +0900 Subject: [PATCH 30/48] =?UTF-8?q?feat(POLABO-107):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/BoardController.kt | 7 +- .../board/my/controller/MyBoardController.kt | 32 +++------ .../domain/board/my/dto/MyBoardDto.kt | 3 +- .../domain/board/my/service/MyBoardService.kt | 10 +++ .../board/repository/BoardJooqRepository.kt | 2 + .../repository/BoardJooqRepositoryImpl.kt | 36 ++++++++++ .../domain/board/service/BoardService.kt | 7 +- .../oauth/controller/OauthController.kt | 16 ++--- .../domain/oauth/service/OauthService.kt | 68 +------------------ .../domain/user/controller/UserController.kt | 12 ++-- .../global/config/SecurityConfig.kt | 2 +- .../sonnypolabobe/global/entity/PageDto.kt | 13 ++-- .../global/exception/CustomErrorCode.kt | 3 + 13 files changed, 95 insertions(+), 116 deletions(-) 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 index e36c9a9..e3084b5 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/controller/BoardController.kt @@ -15,6 +15,7 @@ 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 +import java.util.UUID @Tag(name = "Board API", description = "보드 관련 API") @RestController @@ -26,14 +27,14 @@ class BoardController( 보드를 생성합니다. userId는 추후 회원가입 기능이 추가될 것을 대비한 것입니다. 지금은 null로 주세요. - userId 타입을 UUID -> Long으로 변경하였습니다. - 2024.08.03 + userId 데이터는 백에서 채울 것입니다.! """) @PostMapping fun create(@RequestBody request : BoardCreateRequest) - = run { + : ApplicationResponse { val user = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res request.userId = user.id - ApplicationResponse.ok(this.boardService.create(request)) + return ApplicationResponse.ok(this.boardService.create(request)) } @Operation(summary = "보드 조회", description = """ diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt index 89b06a6..178fe45 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt @@ -5,11 +5,9 @@ import com.ddd.sonnypolabobe.domain.board.my.service.MyBoardService import com.ddd.sonnypolabobe.domain.user.dto.UserDto import com.ddd.sonnypolabobe.global.entity.PageDto import com.ddd.sonnypolabobe.global.response.ApplicationResponse -import com.ddd.sonnypolabobe.global.util.DateConverter.convertToKst import io.swagger.v3.oas.annotations.Operation import org.springframework.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.* -import java.time.LocalDateTime @RestController @RequestMapping("/api/v1/my/boards") @@ -20,23 +18,13 @@ class MyBoardController(private val myBoardService : MyBoardService) { """) @GetMapping fun getMyBoards( - @RequestParam page : Int, + @RequestParam(name = "page", defaultValue = "0") page : Int, @RequestParam size : Int - ) = ApplicationResponse.ok( - PageDto( - content = listOf( - MyBoardDto.Companion.PageListRes( - id = 1L, - title = "보드 제목", - createdAt = convertToKst(LocalDateTime.now()), - totalCount = 1L - ) - ), - page = page, - size = size, - total = 10 - ) - ) + ) : ApplicationResponse> { + val user = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + return ApplicationResponse.ok(this.myBoardService.getMyBoards(user.id, page, size)) + } + @Operation( summary = "내 보드 이름 수정", @@ -48,10 +36,10 @@ class MyBoardController(private val myBoardService : MyBoardService) { fun updateMyBoard( @PathVariable id : String, @RequestBody request : MyBoardDto.Companion.MBUpdateReq - ) = run { + ) : ApplicationResponse { val userId = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res this.myBoardService.updateMyBoard(id, request, userId.id) - ApplicationResponse.ok() + return ApplicationResponse.ok() } @Operation( @@ -63,9 +51,9 @@ class MyBoardController(private val myBoardService : MyBoardService) { @DeleteMapping("/{id}") fun deleteMyBoard( @PathVariable id : String - ) = run { + ) : ApplicationResponse { val userId = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res this.myBoardService.deleteMyBoard(id, userId.id) - ApplicationResponse.ok() + return ApplicationResponse.ok() } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt index e4ec7d6..91ce2eb 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/dto/MyBoardDto.kt @@ -13,11 +13,10 @@ class MyBoardDto { ) data class PageListRes( - val id: Long, + val id: UUID, val title: String, @DateTimeFormat(pattern = "yyyy-MM-dd", iso = DateTimeFormat.ISO.DATE) val createdAt: LocalDateTime, - val totalCount: Long ) data class GetOneRes( diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt index bdbc151..e577f3c 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt @@ -2,8 +2,10 @@ package com.ddd.sonnypolabobe.domain.board.my.service import com.ddd.sonnypolabobe.domain.board.my.dto.MyBoardDto import com.ddd.sonnypolabobe.domain.board.repository.BoardJooqRepository +import com.ddd.sonnypolabobe.global.entity.PageDto import com.ddd.sonnypolabobe.global.util.UuidConverter import org.springframework.stereotype.Service +import sun.jvm.hotspot.debugger.Page @Service class MyBoardService(private val boardJooqRepository: BoardJooqRepository) { @@ -24,4 +26,12 @@ class MyBoardService(private val boardJooqRepository: BoardJooqRepository) { } this.boardJooqRepository.delete(UuidConverter.stringToUUID(id)) } + + fun getMyBoards(userId: Long, page: Int, size: Int): PageDto { + val data = this.boardJooqRepository.findAllByUserId(userId, page, size) + val totalCount = this.boardJooqRepository.selectTotalCountByUserId(userId) + val totalPage = totalCount.toInt() / size + 1 + + return PageDto(data, totalCount, totalPage, size) + } } \ 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 index 05082ad..1520e6f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepository.kt @@ -15,4 +15,6 @@ interface BoardJooqRepository { fun findById(id: UUID): MyBoardDto.Companion.GetOneRes? fun updateTitle(id: UUID, title: String) fun delete(id: UUID) + fun findAllByUserId(userId: Long, page: Int, size: Int): List + fun selectTotalCountByUserId(userId: Long): 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 index 31a0095..3e4ac71 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -113,4 +113,40 @@ class BoardJooqRepositoryImpl( .where(jBoard.ID.eq(UuidConverter.uuidToByteArray(id))) .execute() } + + override fun findAllByUserId( + userId: Long, + page: Int, + size: Int + ): List { + val jBoard = Board.BOARD + val data = this.dslContext.select( + jBoard.ID, + jBoard.TITLE, + jBoard.CREATED_AT + ) + .from(jBoard) + .where(jBoard.USER_ID.eq(userId).and(jBoard.YN.eq(1)).and(jBoard.ACTIVEYN.eq(1))) + .orderBy(jBoard.CREATED_AT.desc()) + .limit(size) + .offset(page * size) + .fetch() + + return data.map { + MyBoardDto.Companion.PageListRes( + id = UuidConverter.byteArrayToUUID(it.get("id", ByteArray::class.java)!!), + title = it.get("title", String::class.java)!!, + createdAt = it.get("created_at", LocalDateTime::class.java)!!, + ) + } + } + + override fun selectTotalCountByUserId(userId: Long): Long { + val jBoard = Board.BOARD + return this.dslContext + .selectCount() + .from(jBoard) + .where(jBoard.USER_ID.eq(userId).and(jBoard.YN.eq(1)).and(jBoard.ACTIVEYN.eq(1))) + .fetchOne(0, Long::class.java) ?: 0L + } } \ 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 index feef5f2..15b3d16 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/service/BoardService.kt @@ -4,6 +4,8 @@ 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.exception.ApplicationException +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode import com.ddd.sonnypolabobe.global.util.S3Util import com.ddd.sonnypolabobe.global.util.UuidConverter import org.springframework.beans.factory.annotation.Value @@ -17,9 +19,8 @@ class BoardService( @Value("\${limit.count}") private val limit: Int ) { - fun create(request: BoardCreateRequest): UUID? { - return this.boardJooqRepository.insertOne(request)?.let { UuidConverter.byteArrayToUUID(it) } - } + fun create(request: BoardCreateRequest): UUID = this.boardJooqRepository.insertOne(request)?.let { UuidConverter.byteArrayToUUID(it) } + ?: throw ApplicationException(CustomErrorCode.BOARD_CREATED_FAILED) fun getById(id: String): List { return id.run { diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt index 252876b..0aff4ee 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt @@ -1,6 +1,7 @@ package com.ddd.sonnypolabobe.domain.oauth.controller import com.ddd.sonnypolabobe.domain.oauth.service.OauthService +import com.ddd.sonnypolabobe.domain.user.dto.UserDto import com.ddd.sonnypolabobe.global.response.ApplicationResponse import io.swagger.v3.oas.annotations.Operation import org.springframework.http.HttpHeaders @@ -11,17 +12,16 @@ import org.springframework.web.bind.annotation.* @RequestMapping("/api/v1/oauth") class OauthController(private val oauthService: OauthService) { - @Operation(summary = "카카오 소셜 로그인", description = """ - 카카오 소셜 로그인을 진행합니다. - 인가 코드를 파라미터로 넘겨주세요. + @Operation(summary = "회원가입/로그인", description = """ + 회원가입/로그인을 진행합니다. + 이미 가입된 회원이라면 로그인을 진행하고, 가입되지 않은 회원이라면 회원가입을 진행합니다. """) - @GetMapping("/sign-in") - fun login(@RequestParam(name = "code") code : String) = ApplicationResponse.ok(this.oauthService.signIn(code)) + @PostMapping("/sign-in") + fun signIn(@RequestBody request: UserDto.Companion.CreateReq) + = ApplicationResponse.ok(this.oauthService.signIn(request)) @PutMapping("/re-issue") fun reIssue( @RequestHeader(name = "Authorization", required = true) header: String - ) = run { - ApplicationResponse.ok(this.oauthService.reIssue(header)) - } + ) = ApplicationResponse.ok(this.oauthService.reIssue(header)) } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt index ee770a5..aab606c 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt @@ -20,75 +20,13 @@ import org.springframework.web.reactive.function.BodyInserters @Service class OauthService( - private val webClient : WebClientUtil, - private val objectMapper: ObjectMapper, - @Value("\${spring.security.oauth2.client.registration.kakao.client-id}") private val clientId : String, - @Value("\${spring.security.oauth2.client.registration.kakao.redirect-uri}") private val redirectUri : String, - @Value("\${spring.security.oauth2.client.registration.kakao.client-secret}") private val clientSecret : String, private val userRepository : UserJooqRepository, private val jwtUtil: JwtUtil, private val userTokenRepository: UserTokenJooqRepository ) { - fun getAccessToken(code: String) : KakaoDto.Companion.Token { - - //요청 본문 - val params: MultiValueMap = LinkedMultiValueMap() - params.add("grant_type", "authorization_code") - params.add("client_id", clientId) - params.add("redirect_uri", redirectUri) - params.add("code", code) - params.add("client_secret", clientSecret) - - logger().error("params : $params") - // 요청 보내기 및 응답 수신 - val response = webClient.create("https://kauth.kakao.com") - .post() - .uri("/oauth/token") - .header("Content-type", "application/x-www-form-urlencoded") - .body(BodyInserters.fromFormData(params)) - .retrieve() - .bodyToMono(String::class.java) - .block() - - - return try { - this.objectMapper.readValue(response, KakaoDto.Companion.Token::class.java) - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - fun getKakaoInfo(accessToken: String): KakaoDto.Companion.UserInfo { - - // 요청 보내기 및 응답 수신 - val response = webClient.create("https://kapi.kakao.com") - .post() - .uri("/v2/user/me") - .header("Content-type", "application/x-www-form-urlencoded") - .header("Authorization", "Bearer $accessToken") - .retrieve() - .bodyToMono(String::class.java) - .block() - - return try { - this.objectMapper.readValue(response, KakaoDto.Companion.UserInfo::class.java) - } catch (e: Exception) { - throw RuntimeException(e) - } - } - @Transactional - fun signIn(code: String): UserDto.Companion.TokenRes { - val token = getAccessToken(code) - val userInfo = getKakaoInfo(token.access_token) - - // DB에 저장 - val request = UserDto.Companion.CreateReq( - nickName = userInfo.kakao_account.profile.nickname, - email = userInfo.kakao_account.email - ) - + fun signIn(request: UserDto.Companion.CreateReq): UserDto.Companion.TokenRes { this.userRepository.findByEmail(request.email)?.let { val tokenRequest = UserDto.Companion.CreateTokenReq( id = it.id, @@ -112,8 +50,8 @@ class OauthService( // 토큰 생성 val tokenRequest = UserDto.Companion.CreateTokenReq( id = userId, - email = userInfo.kakao_account.email, - nickName = userInfo.kakao_account.profile.nickname + email = request.email, + nickName = request.nickName ) val tokenRes = this.jwtUtil.generateAccessToken(tokenRequest) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt index c753b06..d8e99d9 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt @@ -26,19 +26,19 @@ class UserController( """) @PutMapping("/nickname") fun updateNickname(@RequestBody request: UserDto.Companion.UpdateReq) - = run { + : ApplicationResponse { val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res this.userService.updateProfile(request, userInfoFromToken.id) - ApplicationResponse.ok() + return ApplicationResponse.ok() } @Operation(summary = "프로필 조회", description = """ 프로필을 조회합니다. """) @GetMapping("/profile") - fun getProfile() = run { + fun getProfile() : ApplicationResponse { val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res - ApplicationResponse.ok(this.userService.findById(userInfoFromToken.id)) + return ApplicationResponse.ok(this.userService.findById(userInfoFromToken.id)) } @Operation(summary = "회원 탈퇴", description = """ @@ -47,9 +47,9 @@ class UserController( 사유가 '기타'인 경우에만 reason 필드를 채워주세요. """) @PutMapping("/withdraw") - fun withdraw(@RequestBody request: UserDto.Companion.WithdrawReq) = run { + fun withdraw(@RequestBody request: UserDto.Companion.WithdrawReq) : ApplicationResponse { val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res this.userService.withdraw(request, userInfoFromToken.id) - ApplicationResponse.ok() + return ApplicationResponse.ok() } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index b2eff81..39e058f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -43,7 +43,7 @@ class SecurityConfig( it.requestMatchers("/api/v1/file/**").permitAll() it.requestMatchers("/api/v1/oauth/sign-in", "/api/v1/oauth/re-issue").permitAll() it.requestMatchers("/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll() - it.requestMatchers("/api/v1/boards/{id}").permitAll() + it.requestMatchers("/api/v1/boards/{id}", "/api/v1/polaroids/{id}", "/api/v1/boards/{boardId}/polaroids").permitAll() it.anyRequest().authenticated() } .exceptionHandling{ diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt index f4abbe5..e4dbb95 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt @@ -1,8 +1,9 @@ package com.ddd.sonnypolabobe.global.entity -data class PageDto( - val page: Int, - val size: Int, - val total: Int, - val content: List -) + +data class PageDto( + val data: List, + val totalCount: Long, + val totalPage: Int, + val currentPage: Int +) \ 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 index 72e93dd..6b06174 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt @@ -22,4 +22,7 @@ enum class CustomErrorCode( JWT_ILLEGAL_ARGUMENT(HttpStatus.UNAUTHORIZED, "JWT005", "잘못된 인자입니다."), JWT_SIGNATURE(HttpStatus.UNAUTHORIZED, "JWT006", "잘못된 서명입니다."), + // board + BOARD_CREATED_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "BOARD001", "보드 생성에 실패했습니다."), + } \ No newline at end of file From abf5ae760a083c952d856ac3101b2124c8d4e2b2 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 8 Aug 2024 23:23:32 +0900 Subject: [PATCH 31/48] =?UTF-8?q?feat(POLABO-107):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt index e577f3c..ae3f7f3 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt @@ -5,7 +5,6 @@ import com.ddd.sonnypolabobe.domain.board.repository.BoardJooqRepository import com.ddd.sonnypolabobe.global.entity.PageDto import com.ddd.sonnypolabobe.global.util.UuidConverter import org.springframework.stereotype.Service -import sun.jvm.hotspot.debugger.Page @Service class MyBoardService(private val boardJooqRepository: BoardJooqRepository) { From 18605c3a0da108babfdb896f7f09425dde2f8b52 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 8 Aug 2024 23:38:51 +0900 Subject: [PATCH 32/48] =?UTF-8?q?chore(POLABO-108):=20https=20swagger=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt index cb0bfc2..8b1147d 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SwaggerConfig.kt @@ -4,6 +4,7 @@ import io.swagger.v3.oas.models.Components import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.servers.Server import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -17,6 +18,7 @@ class SwaggerConfig { val securityRequirement: SecurityRequirement = getSecurityRequireMent() return OpenAPI() + .addServersItem(Server().url("/")) .components(Components().addSecuritySchemes("bearerAuth", securityScheme)) .security(listOf(securityRequirement)) } From 8122d86a1241bc3577b50663b5c6f57c02512080 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 10 Aug 2024 00:54:12 +0900 Subject: [PATCH 33/48] =?UTF-8?q?chore(POLABO-108):=20https=20swagger=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-dev.yml b/.github/workflows/cd-dev.yml index 215e602..4e8c86b 100644 --- a/.github/workflows/cd-dev.yml +++ b/.github/workflows/cd-dev.yml @@ -80,5 +80,5 @@ jobs: docker stop polabo-dev docker rm polabo-dev docker pull ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} - docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=local --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} + docker run -d -v /etc/localtime:/etc/localtime:ro -v /usr/share/zoneinfo/Asia/Seoul:/etc/timezone:ro -e ENVIRONMENT_VALUE=-Dspring.profiles.active=dev --name polabo-dev -p 8080:8080 --restart=always --network host ${{ secrets.ECR_REGISTRY }}/polabo:${{steps.current-time.outputs.formattedTime}} From b23da3b0250b084d29f3e637aabd5617cb25248c Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 11 Aug 2024 17:19:06 +0900 Subject: [PATCH 34/48] =?UTF-8?q?feat(POLABO-108):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth/controller/OauthController.kt | 2 ++ .../domain/user/controller/UserController.kt | 15 +++++++------ .../domain/user/dto/GenderType.kt | 7 +++++++ .../sonnypolabobe/domain/user/dto/UserDto.kt | 11 +++++++--- .../user/repository/UserJooqRepositoryImpl.kt | 21 +++++++++---------- .../domain/user/service/UserService.kt | 4 ++++ .../global/config/SecurityConfig.kt | 2 +- .../sonnypolabobe/global/security/JwtUtil.kt | 2 +- 8 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/GenderType.kt diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt index 0aff4ee..cf6eda9 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt @@ -15,6 +15,8 @@ class OauthController(private val oauthService: OauthService) { @Operation(summary = "회원가입/로그인", description = """ 회원가입/로그인을 진행합니다. 이미 가입된 회원이라면 로그인을 진행하고, 가입되지 않은 회원이라면 회원가입을 진행합니다. + + 요청 바디의 값이 변경되었습니다. - 2024.08.11 """) @PostMapping("/sign-in") fun signIn(@RequestBody request: UserDto.Companion.CreateReq) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt index d8e99d9..9784fc6 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt @@ -7,12 +7,7 @@ import com.ddd.sonnypolabobe.global.util.DateConverter import io.swagger.v3.oas.annotations.Operation import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController +import org.springframework.web.bind.annotation.* import java.time.LocalDateTime @RestController @@ -52,4 +47,12 @@ class UserController( this.userService.withdraw(request, userInfoFromToken.id) return ApplicationResponse.ok() } + + @Operation(summary = "회원 계정 존재 여부 확인", description = """ + 이메일로 계정 등록 여부를 확인합니다. + """) + @GetMapping("/check-exist") + fun checkExist( + @RequestParam("email") email: String + ) = ApplicationResponse.ok(this.userService.checkExist(email)) } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/GenderType.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/GenderType.kt new file mode 100644 index 0000000..747afa4 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/GenderType.kt @@ -0,0 +1,7 @@ +package com.ddd.sonnypolabobe.domain.user.dto + +enum class GenderType(val description: String) { + F("여성"), + M("남성"), + NONE("없음") +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt index 3921ee1..dee3e3b 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.UserDetails +import java.time.LocalDate import java.time.LocalDateTime import java.util.Date import java.util.stream.Collectors @@ -12,8 +13,10 @@ class UserDto { companion object { data class CreateReq( val email: String, - val nickName: String - ) + val nickName: String, + val birthDt : LocalDate, + val gender : GenderType + ) data class UpdateReq( @JsonProperty("nickName") @@ -28,7 +31,9 @@ class UserDto { data class TokenRes( val accessToken: String, - val expiredDate: Date + val expiredDate: Date, + val isNewUser : Boolean, + val nickName: String ) data class ProfileRes( diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt index 1d65641..66d25ee 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.ddd.sonnypolabobe.domain.user.repository import com.ddd.sonnypolabobe.domain.user.dto.UserDto import com.ddd.sonnypolabobe.global.util.DateConverter +import com.ddd.sonnypolabobe.jooq.polabo.enums.UserGender import com.ddd.sonnypolabobe.jooq.polabo.tables.User import org.jooq.DSLContext import org.springframework.stereotype.Repository @@ -11,23 +12,21 @@ import java.time.LocalDateTime class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepository{ override fun insertOne(request: UserDto.Companion.CreateReq): Long { val jUser = User.USER - val insertValue = jUser.newRecord().apply { - this.email = request.email - this.nickName = request.nickName - this.createdAt = DateConverter.convertToKst(LocalDateTime.now()) - this.yn = 1 - } return this.dslContext.insertInto(jUser, jUser.EMAIL, jUser.NICK_NAME, jUser.CREATED_AT, - jUser.YN + jUser.YN, + jUser.BIRTH_DT, + jUser.GENDER ) .values( - insertValue.email, - insertValue.nickName, - insertValue.createdAt, - insertValue.yn + request.email, + request.nickName, + DateConverter.convertToKst(LocalDateTime.now()), + 1, + request.birthDt, + UserGender.valueOf(request.gender.name) ) .returningResult(jUser.ID) .fetchOne(0, Long::class.java) ?: 0 diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt index 5185e10..b3be1fe 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt @@ -29,5 +29,9 @@ class UserService( this.withdrawJooqRepository.insertOne(request, id) } + fun checkExist(email: String): Boolean { + return this.userJooqRepository.findByEmail(email) != null + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index 39e058f..58dd5bd 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -41,7 +41,7 @@ class SecurityConfig( it.requestMatchers("/api/v1/boards/create-available").permitAll() it.requestMatchers("/api/v1/boards/total-count").permitAll() it.requestMatchers("/api/v1/file/**").permitAll() - it.requestMatchers("/api/v1/oauth/sign-in", "/api/v1/oauth/re-issue").permitAll() + it.requestMatchers("/api/v1/oauth/sign-in", "/api/v1/oauth/re-issue", "/api/v1/user/check-exist").permitAll() it.requestMatchers("/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll() it.requestMatchers("/api/v1/boards/{id}", "/api/v1/polaroids/{id}", "/api/v1/boards/{boardId}/polaroids").permitAll() it.anyRequest().authenticated() diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt index 1b1d658..f5c0191 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt @@ -33,7 +33,7 @@ class JwtUtil( .setExpiration(expiredDate) .signWith(getKey(accessSecretKey), SignatureAlgorithm.HS512) .compact() - return UserDto.Companion.TokenRes(accessToken, expiredDate) + return UserDto.Companion.TokenRes(accessToken, expiredDate, true, request.nickName) } fun getAuthenticatedMemberFromToken(accessToken: String): AuthenticatedMember { From d28c65cf537760df7fb5ba1e7b015d1658b1c96f Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 11 Aug 2024 20:25:48 +0900 Subject: [PATCH 35/48] =?UTF-8?q?fix(POLABO-108):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=202=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt | 4 ++-- .../domain/user/repository/UserJooqRepositoryImpl.kt | 2 +- .../com/ddd/sonnypolabobe/global/config/SecurityConfig.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt index dee3e3b..57de7e7 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt @@ -14,8 +14,8 @@ class UserDto { data class CreateReq( val email: String, val nickName: String, - val birthDt : LocalDate, - val gender : GenderType + val birthDt : LocalDate?, + val gender : GenderType? ) data class UpdateReq( diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt index 66d25ee..0efa05d 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt @@ -26,7 +26,7 @@ class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepos DateConverter.convertToKst(LocalDateTime.now()), 1, request.birthDt, - UserGender.valueOf(request.gender.name) + UserGender.valueOf(request.gender?.name ?: UserGender.NONE.name) ) .returningResult(jUser.ID) .fetchOne(0, Long::class.java) ?: 0 diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index 58dd5bd..ca82e55 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -41,7 +41,7 @@ class SecurityConfig( it.requestMatchers("/api/v1/boards/create-available").permitAll() it.requestMatchers("/api/v1/boards/total-count").permitAll() it.requestMatchers("/api/v1/file/**").permitAll() - it.requestMatchers("/api/v1/oauth/sign-in", "/api/v1/oauth/re-issue", "/api/v1/user/check-exist").permitAll() + it.requestMatchers("/api/v1/oauth/**", "/api/v1/user/check-exist").permitAll() it.requestMatchers("/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll() it.requestMatchers("/api/v1/boards/{id}", "/api/v1/polaroids/{id}", "/api/v1/boards/{boardId}/polaroids").permitAll() it.anyRequest().authenticated() From e054017fea679276697aa2c617a3e351decee460 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 11 Aug 2024 20:55:30 +0900 Subject: [PATCH 36/48] =?UTF-8?q?fix(POLABO-108):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=202=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/JwtAuthenticationFilter.kt | 9 +++++---- .../com/ddd/sonnypolabobe/global/security/JwtUtil.kt | 7 ++++--- src/main/resources/application-dev.yml | 2 +- src/main/resources/application-local.yml | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt index 0d884d6..1499496 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt @@ -19,13 +19,14 @@ class JwtAuthenticationFilter( filterChain: FilterChain ) { val authorizationHeader = request.getHeader("Authorization") +// if(request.requestURI.contains("/api/v1/oauth")) { +// filterChain.doFilter(request, response) +// return +// } //JWT가 헤더에 있는 경우 if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { - if(request.requestURI.contains("/api/v1/oauth")) { - filterChain.doFilter(request, response) - return - } + //JWT 유효성 검증 if (jwtUtil.validateToken(authorizationHeader)) { val userId = jwtUtil.getAuthenticatedMemberFromToken(authorizationHeader).id diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt index f5c0191..8037513 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt @@ -9,6 +9,7 @@ import io.jsonwebtoken.security.Keys import jakarta.xml.bind.DatatypeConverter import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component +import java.nio.charset.StandardCharsets import java.security.Key import java.util.* @@ -31,7 +32,7 @@ class JwtUtil( .setClaims(claims) .setIssuedAt(now) .setExpiration(expiredDate) - .signWith(getKey(accessSecretKey), SignatureAlgorithm.HS512) + .signWith(SignatureAlgorithm.HS512, getKeyBytes(accessSecretKey)) .compact() return UserDto.Companion.TokenRes(accessToken, expiredDate, true, request.nickName) } @@ -46,7 +47,7 @@ class JwtUtil( fun validateToken(accessToken: String): Boolean { try { - val key = getKey(accessSecretKey) + val key = getKeyBytes(accessSecretKey) Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(subPrefix(accessToken)) return true } catch (e: Exception) { @@ -57,7 +58,7 @@ class JwtUtil( fun getClaimsFromAccessToken(token: String, secretKey: String): Claims { try { - val key = getKey(secretKey) + val key = getKeyBytes(secretKey) return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).body } catch (e: io.jsonwebtoken.security.SecurityException) { throw ApplicationException(CustomErrorCode.JWT_SIGNATURE) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0c2a978..3165a2f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -56,7 +56,7 @@ running: name: dev jwt: - access-key: ENC(dMTqjTdS4VJz/1Gduapvl1rDDXUUKkp0bilgqRWMI9X4DaAMVDXY13Fb7QMBDUkI) + access-key: ENC(43L/PYzgJ86ciTJurdPNLgq49TUMtOgF6ke+Z7d4/wEN2WMu2iakpdDlD2C8d9GUNZzFl0Y9ftK4tk545L5TSv05I5OgbYkBGf2m5O51cjUZmSRg897e+5o+Z5aLgwb1+9tcOcawZR/X9lgkA5hTxlzGc7DHf1vMepLDnbaQBk0I56ieDV314JEEqIaeID72) validity: access-seconds: ENC(KeRwqvZAr0MfEVmxw8nBQQFEITQ0v/Fl) diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index f8f405c..138276f 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -56,7 +56,7 @@ running: name: local jwt: - access-key: ENC(dMTqjTdS4VJz/1Gduapvl1rDDXUUKkp0bilgqRWMI9X4DaAMVDXY13Fb7QMBDUkI) + access-key: ENC(43L/PYzgJ86ciTJurdPNLgq49TUMtOgF6ke+Z7d4/wEN2WMu2iakpdDlD2C8d9GUNZzFl0Y9ftK4tk545L5TSv05I5OgbYkBGf2m5O51cjUZmSRg897e+5o+Z5aLgwb1+9tcOcawZR/X9lgkA5hTxlzGc7DHf1vMepLDnbaQBk0I56ieDV314JEEqIaeID72) validity: access-seconds: 86400000 From 53da3db5991dd466f9030857a9146c2bd38fd3f8 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 11 Aug 2024 22:52:35 +0900 Subject: [PATCH 37/48] =?UTF-8?q?fix(POLABO-108):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=203=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/oauth/service/OauthService.kt | 25 +++++------ .../sonnypolabobe/domain/user/dto/UserDto.kt | 3 +- .../domain/user/token/dto/UserTokenDto.kt | 3 +- .../repository/UserTokenJooqRepository.kt | 2 +- .../repository/UserTokenJooqRepositoryImpl.kt | 14 ++++--- .../sonnypolabobe/global/security/JwtUtil.kt | 41 ++++++++++++++++++- src/main/resources/application-dev.yml | 2 + 7 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt index aab606c..537de1f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt @@ -5,17 +5,9 @@ import com.ddd.sonnypolabobe.domain.user.repository.UserJooqRepository import com.ddd.sonnypolabobe.domain.user.token.dto.UserTokenDto import com.ddd.sonnypolabobe.domain.user.token.repository.UserTokenJooqRepository import com.ddd.sonnypolabobe.global.security.JwtUtil -import com.ddd.sonnypolabobe.global.security.KakaoDto import com.ddd.sonnypolabobe.global.util.DateConverter.dateToLocalDateTime -import com.ddd.sonnypolabobe.global.util.WebClientUtil -import com.ddd.sonnypolabobe.logger -import com.fasterxml.jackson.databind.ObjectMapper -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import org.springframework.util.LinkedMultiValueMap -import org.springframework.util.MultiValueMap -import org.springframework.web.reactive.function.BodyInserters @Service @@ -39,11 +31,12 @@ class OauthService( val userToken = UserTokenDto( userId = it.id, accessToken = tokenRes.accessToken, - expiredAt = dateToLocalDateTime(tokenRes.expiredDate) + expiredAt = dateToLocalDateTime(tokenRes.expiredDate), + refreshToken = tokenRes.refreshToken ) this.userTokenRepository.updateByUserId(userToken) - return tokenRes + return tokenRes.also { it.isNewUser = false } } ?: run { val userId = this.userRepository.insertOne(request) @@ -59,20 +52,21 @@ class OauthService( val userToken = UserTokenDto( userId = userId, accessToken = tokenRes.accessToken, - expiredAt = dateToLocalDateTime(tokenRes.expiredDate) + expiredAt = dateToLocalDateTime(tokenRes.expiredDate), + refreshToken = tokenRes.refreshToken ) this.userTokenRepository.insertOne(userToken) - return tokenRes + return tokenRes.also { it.isNewUser = true } } } fun reIssue(token: String?): UserDto.Companion.TokenRes{ val tokenFromDB = token?.let { val slicedToken = if(it.startsWith("Bearer ")) it.substring(7) else it - this.userTokenRepository.findByAccessToken(slicedToken) + this.jwtUtil.getAuthenticatedMemberFromRefreshToken(slicedToken) } ?: throw RuntimeException("Token Not Found") - val user = this.userRepository.findById(tokenFromDB.userId) ?: throw RuntimeException("User Not Found") + val user = this.userRepository.findById(tokenFromDB.id.toLong()) ?: throw RuntimeException("User Not Found") // 토큰 생성 val tokenRequest = UserDto.Companion.CreateTokenReq( @@ -86,7 +80,8 @@ class OauthService( val userToken = UserTokenDto( userId = user.id, accessToken = tokenRes.accessToken, - expiredAt = dateToLocalDateTime(tokenRes.expiredDate) + expiredAt = dateToLocalDateTime(tokenRes.expiredDate), + refreshToken = tokenRes.refreshToken ) this.userTokenRepository.updateByUserId(userToken) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt index 57de7e7..af4b0b0 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt @@ -31,8 +31,9 @@ class UserDto { data class TokenRes( val accessToken: String, + val refreshToken: String, val expiredDate: Date, - val isNewUser : Boolean, + var isNewUser : Boolean, val nickName: String ) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt index a076783..0067e0f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/dto/UserTokenDto.kt @@ -5,5 +5,6 @@ import java.time.LocalDateTime data class UserTokenDto( val userId: Long, val accessToken: String, - val expiredAt: LocalDateTime + val expiredAt: LocalDateTime, + val refreshToken: String ) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt index b240ef3..75be1ae 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt @@ -4,6 +4,6 @@ import com.ddd.sonnypolabobe.domain.user.token.dto.UserTokenDto interface UserTokenJooqRepository { fun insertOne(userToken: UserTokenDto) - fun findByAccessToken(token: String): UserTokenDto? + fun findByRefreshToken(token: String): UserTokenDto? fun updateByUserId(userToken: UserTokenDto) } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt index 9713c2e..0ce2882 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt @@ -17,35 +17,39 @@ class UserTokenJooqRepositoryImpl(private val dslContext: DSLContext) : UserToke this.accessExpiredAt = userToken.expiredAt this.createdAt = DateConverter.convertToKst(LocalDateTime.now()) this.updatedAt = DateConverter.convertToKst(LocalDateTime.now()) + this.refreshToken = userToken.refreshToken } this.dslContext.insertInto(jUserToken, jUserToken.USER_ID, jUserToken.ACCESS_TOKEN, jUserToken.ACCESS_EXPIRED_AT, jUserToken.CREATED_AT, - jUserToken.UPDATED_AT + jUserToken.UPDATED_AT, + jUserToken.REFRESH_TOKEN ) .values( insertValue.userId, insertValue.accessToken, insertValue.accessExpiredAt, insertValue.createdAt, - insertValue.updatedAt + insertValue.updatedAt, + insertValue.refreshToken ) .onDuplicateKeyUpdate() .set(jUserToken.USER_ID, insertValue.userId) .execute() } - override fun findByAccessToken(token: String): UserTokenDto? { + override fun findByRefreshToken(token: String): UserTokenDto? { val jUserToken = UserToken.USER_TOKEN return this.dslContext.selectFrom(jUserToken) - .where(jUserToken.ACCESS_TOKEN.eq(token)) + .where(jUserToken.REFRESH_TOKEN.eq(token)) .fetchOne()?.map { UserTokenDto( userId = it.get(jUserToken.USER_ID)!!, accessToken = it.get(jUserToken.ACCESS_TOKEN)!!, - expiredAt = it.get(jUserToken.ACCESS_EXPIRED_AT)!! + expiredAt = it.get(jUserToken.ACCESS_EXPIRED_AT)!!, + refreshToken = it.get(jUserToken.REFRESH_TOKEN)!! ) } } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt index 8037513..7081300 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtUtil.kt @@ -19,6 +19,10 @@ class JwtUtil( private val accessSecretKey: String, @Value("\${jwt.validity.access-seconds}") private val accessTokenExpirationMs: Long, + @Value("\${jwt.refresh-key}") + private val refreshSecretKey: String, + @Value("\${jwt.validity.refresh-seconds}") + private val refreshTokenExpirationMs: Long ) { fun generateAccessToken(request: UserDto.Companion.CreateTokenReq): UserDto.Companion.TokenRes { @@ -34,7 +38,23 @@ class JwtUtil( .setExpiration(expiredDate) .signWith(SignatureAlgorithm.HS512, getKeyBytes(accessSecretKey)) .compact() - return UserDto.Companion.TokenRes(accessToken, expiredDate, true, request.nickName) + return UserDto.Companion.TokenRes(accessToken, generateRefreshToken(request), expiredDate, false, request.nickName) + } + + fun generateRefreshToken(request: UserDto.Companion.CreateTokenReq): String { + val now = Date() + val expiredDate = Date(now.time + refreshTokenExpirationMs) + val claims: MutableMap = HashMap() + claims["CLAIM_KEY_ID"] = request.id.toString() + claims["CLAIM_EMAIL"] = request.email + claims["CLAIM_NICKNAME"] = request.nickName + val refreshToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, getKeyBytes(refreshSecretKey)) + .compact() + return refreshToken } fun getAuthenticatedMemberFromToken(accessToken: String): AuthenticatedMember { @@ -45,6 +65,25 @@ class JwtUtil( return AuthenticatedMember(id, email, nickname, claims.expiration) } + fun getAuthenticatedMemberFromRefreshToken(refreshToken: String): AuthenticatedMember { + val claims = getClaimsFromAccessToken(subPrefix(refreshToken), refreshSecretKey) + val id = claims["CLAIM_KEY_ID"].toString() + val email = claims["CLAIM_EMAIL"].toString() + val nickname = claims["CLAIM_NICKNAME"].toString() + return AuthenticatedMember(id, email, nickname, claims.expiration) + } + + fun validateRefreshToken(refreshToken: String): Boolean { + try { + val key = getKeyBytes(refreshSecretKey) + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(subPrefix(refreshToken)) + return true + } catch (e: Exception) { + logger().error("error : $e") + throw ApplicationException(CustomErrorCode.JWT_INVALID) + } + } + fun validateToken(accessToken: String): Boolean { try { val key = getKeyBytes(accessSecretKey) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 3165a2f..6b7d9ec 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -57,8 +57,10 @@ running: jwt: access-key: ENC(43L/PYzgJ86ciTJurdPNLgq49TUMtOgF6ke+Z7d4/wEN2WMu2iakpdDlD2C8d9GUNZzFl0Y9ftK4tk545L5TSv05I5OgbYkBGf2m5O51cjUZmSRg897e+5o+Z5aLgwb1+9tcOcawZR/X9lgkA5hTxlzGc7DHf1vMepLDnbaQBk0I56ieDV314JEEqIaeID72) + refresh-key: ENC(Tg3e1h7BGfmOmB1kbSHW/Wdi9QQh34DR6XsD6Su7GkMH236k11Ffl9siE2FsDVvJX3MRmF7szRveRfUZR4DbgiYXzSoC35yGQHAiljtt7Cb77QuofiCLpKMetX6B3egTHFpoJWHqrsn1IRYi31rcYLIWnGff0vAMCy3IX2jnI2PzjVgzW0/UjpqMXspUj3rIuDjK926M/fCijH+xgswjVNqJcnQQmIk+5FUTSyut5tY=) validity: access-seconds: ENC(KeRwqvZAr0MfEVmxw8nBQQFEITQ0v/Fl) + refresh-seconds: ENC(bk/TKkZx9JLOphcFQLgwIA==) logging: discord: From 34e194cd6db613a034eb69672e2d5bb404fff957 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Tue, 13 Aug 2024 21:57:15 +0900 Subject: [PATCH 38/48] =?UTF-8?q?fix(POLABO-108):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=203=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/token/repository/UserTokenJooqRepositoryImpl.kt | 1 + .../com/ddd/sonnypolabobe/global/config/SecurityConfig.kt | 2 +- .../global/security/JwtAuthenticationFilter.kt | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt index 0ce2882..040e415 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt @@ -60,6 +60,7 @@ class UserTokenJooqRepositoryImpl(private val dslContext: DSLContext) : UserToke .set(jUserToken.ACCESS_TOKEN, userToken.accessToken) .set(jUserToken.ACCESS_EXPIRED_AT, userToken.expiredAt) .set(jUserToken.UPDATED_AT, DateConverter.convertToKst(LocalDateTime.now())) + .set(jUserToken.REFRESH_TOKEN, userToken.refreshToken) .where(jUserToken.USER_ID.eq(userToken.userId)) .execute() } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index ca82e55..91bdc94 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -48,7 +48,7 @@ class SecurityConfig( } .exceptionHandling{ it.authenticationEntryPoint { _, response, _ -> - response.sendError(401) + response.sendError(500) } } .build() diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt index 1499496..5fdfb33 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt @@ -19,10 +19,10 @@ class JwtAuthenticationFilter( filterChain: FilterChain ) { val authorizationHeader = request.getHeader("Authorization") -// if(request.requestURI.contains("/api/v1/oauth")) { -// filterChain.doFilter(request, response) -// return -// } + if(request.requestURI.contains("/api/v1/oauth")) { + filterChain.doFilter(request, response) + return + } //JWT가 헤더에 있는 경우 if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { From 27d58c4ac76e47a79963415a81edc1c9416e76ce Mon Sep 17 00:00:00 2001 From: dldmsql Date: Wed, 14 Aug 2024 23:36:35 +0900 Subject: [PATCH 39/48] =?UTF-8?q?fix(POLABO-108):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=204=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oauth/controller/OauthController.kt | 12 +- .../domain/oauth/service/OauthService.kt | 5 + .../domain/user/controller/UserController.kt | 7 +- .../sonnypolabobe/domain/user/dto/UserDto.kt | 5 +- .../user/repository/UserJooqRepository.kt | 1 + .../user/repository/UserJooqRepositoryImpl.kt | 24 +- .../domain/user/service/UserService.kt | 1 + .../repository/UserTokenJooqRepository.kt | 1 + .../repository/UserTokenJooqRepositoryImpl.kt | 7 + .../global/config/SecurityConfig.kt | 34 ++- .../global/exception/CustomErrorCode.kt | 1 + .../exception/GlobalExceptionHandler.kt | 10 +- .../security/JwtAuthenticationFilter.kt | 2 +- .../global/security/JwtExceptionFilter.kt | 144 ++++++++++ .../global/security/LoggingFilter.kt | 250 +++++++++--------- .../global/util/UuidConverter.kt | 10 +- src/main/resources/application-dev.yml | 2 +- 17 files changed, 363 insertions(+), 153 deletions(-) create mode 100644 src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt index cf6eda9..051f49d 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt @@ -4,7 +4,6 @@ import com.ddd.sonnypolabobe.domain.oauth.service.OauthService import com.ddd.sonnypolabobe.domain.user.dto.UserDto import com.ddd.sonnypolabobe.global.response.ApplicationResponse import io.swagger.v3.oas.annotations.Operation -import org.springframework.http.HttpHeaders import org.springframework.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.* @@ -26,4 +25,15 @@ class OauthController(private val oauthService: OauthService) { fun reIssue( @RequestHeader(name = "Authorization", required = true) header: String ) = ApplicationResponse.ok(this.oauthService.reIssue(header)) + + @Operation(summary = "로그아웃", description = """ + 로그아웃을 진행합니다. + 액세스 토큰을 헤더에 담아주세요. + """) + @PostMapping("/sign-out") + fun signOut() : ApplicationResponse { + val userId = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res + this.oauthService.signOut(userId.id) + return ApplicationResponse.ok() + } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt index 537de1f..49a52c1 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt @@ -6,6 +6,7 @@ import com.ddd.sonnypolabobe.domain.user.token.dto.UserTokenDto import com.ddd.sonnypolabobe.domain.user.token.repository.UserTokenJooqRepository import com.ddd.sonnypolabobe.global.security.JwtUtil import com.ddd.sonnypolabobe.global.util.DateConverter.dateToLocalDateTime +import com.ddd.sonnypolabobe.logger import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -87,4 +88,8 @@ class OauthService( this.userTokenRepository.updateByUserId(userToken) return tokenRes } + + fun signOut(id: Long) { + this.userTokenRepository.deleteByUserId(id) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt index 9784fc6..5f5aa31 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt @@ -16,8 +16,11 @@ class UserController( private val userService: UserService ) { - @Operation(summary = "닉네임 변경", description = """ - 닉네임을 변경합니다. + @Operation(summary = "프로필 변경", description = """ + 프로필 사항을 변경합니다. + + 유저가 가진 정보 중 변경한 값 + 변경하지 않은 값 모두 보내주세요. + 보내는 값을 그대로 디비에 저장합니다. """) @PutMapping("/nickname") fun updateNickname(@RequestBody request: UserDto.Companion.UpdateReq) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt index af4b0b0..f0b20fc 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt @@ -20,7 +20,10 @@ class UserDto { data class UpdateReq( @JsonProperty("nickName") - val nickName : String + val nickName : String, + @JsonProperty("birthDt") + val birthDt : LocalDate?, + val gender : GenderType? ) data class CreateTokenReq( diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt index 0a7199e..969ae7f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepository.kt @@ -8,4 +8,5 @@ interface UserJooqRepository { fun findById(id: Long): UserDto.Companion.Res? fun findByEmail(email: String): UserDto.Companion.Res? fun updateProfile(request: UserDto.Companion.UpdateReq, userId: Long) + fun deleteById(id: Long) } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt index 0efa05d..939e149 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt @@ -12,7 +12,7 @@ import java.time.LocalDateTime class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepository{ override fun insertOne(request: UserDto.Companion.CreateReq): Long { val jUser = User.USER - return this.dslContext.insertInto(jUser, + val result = this.dslContext.insertInto(jUser, jUser.EMAIL, jUser.NICK_NAME, jUser.CREATED_AT, @@ -27,9 +27,13 @@ class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepos 1, request.birthDt, UserGender.valueOf(request.gender?.name ?: UserGender.NONE.name) - ) - .returningResult(jUser.ID) - .fetchOne(0, Long::class.java) ?: 0 + ).execute() + if(result == 0) throw Exception("Failed to insert user") + + return this.dslContext.select(jUser.ID) + .from(jUser) + .where(jUser.EMAIL.eq(request.email).and(jUser.YN.eq(1))) + .fetchOneInto(Long::class.java) ?: throw Exception("Failed to get user id") } override fun findById(id: Long): UserDto.Companion.Res? { @@ -53,7 +57,7 @@ class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepos override fun findByEmail(email: String): UserDto.Companion.Res? { val jUser = User.USER val record = this.dslContext.selectFrom(jUser) - .where(jUser.EMAIL.eq(email)) + .where(jUser.EMAIL.eq(email).and(jUser.YN.eq(1))) .fetchOne() return record?.let { @@ -72,8 +76,18 @@ class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepos val jUser = User.USER this.dslContext.update(jUser) .set(jUser.NICK_NAME, request.nickName) + .set(jUser.BIRTH_DT, request.birthDt) + .set(jUser.GENDER, UserGender.valueOf(request.gender?.name ?: UserGender.NONE.name)) .set(jUser.UPDATED_AT, DateConverter.convertToKst(LocalDateTime.now())) .where(jUser.ID.eq(userId)) .execute() } + + override fun deleteById(id: Long) { + val jUser = User.USER + this.dslContext.update(jUser) + .set(jUser.YN, 0) + .where(jUser.ID.eq(id)) + .execute() + } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt index b3be1fe..de96dc5 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/service/UserService.kt @@ -27,6 +27,7 @@ class UserService( fun withdraw(request: UserDto.Companion.WithdrawReq, id: Long) { this.withdrawJooqRepository.insertOne(request, id) + this.userJooqRepository.deleteById(id) } fun checkExist(email: String): Boolean { diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt index 75be1ae..7fef16b 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepository.kt @@ -6,4 +6,5 @@ interface UserTokenJooqRepository { fun insertOne(userToken: UserTokenDto) fun findByRefreshToken(token: String): UserTokenDto? fun updateByUserId(userToken: UserTokenDto) + fun deleteByUserId(userId: Long) } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt index 040e415..425b41e 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/token/repository/UserTokenJooqRepositoryImpl.kt @@ -64,4 +64,11 @@ class UserTokenJooqRepositoryImpl(private val dslContext: DSLContext) : UserToke .where(jUserToken.USER_ID.eq(userToken.userId)) .execute() } + + override fun deleteByUserId(userId: Long) { + val jUserToken = UserToken.USER_TOKEN + this.dslContext.deleteFrom(jUserToken) + .where(jUserToken.USER_ID.eq(userId)) + .execute() + } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt index 91bdc94..589d7c5 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/config/SecurityConfig.kt @@ -1,6 +1,7 @@ package com.ddd.sonnypolabobe.global.config import com.ddd.sonnypolabobe.global.security.JwtAuthenticationFilter +import com.ddd.sonnypolabobe.global.security.JwtExceptionFilter import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -20,8 +21,24 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource @EnableMethodSecurity class SecurityConfig( private val jwtAuthenticationFilter: JwtAuthenticationFilter, + private val jwtExceptionFilter: JwtExceptionFilter ) { - + companion object { + val ALLOW_URLS = listOf( + "/api/v1/boards/{id}", + "/api/v1/boards/create-available", + "/api/v1/boards/total-count", + "/api/v1/file/**", + "/api/v1/oauth/sign-in", + "/api/v1/oauth/re-issue", + "/api/v1/user/check-exist", + "/health", + "/swagger-ui/**", + "/v3/api-docs/**", + "/api/v1/polaroids/{id}", + "/api/v1/boards/{boardId}/polaroids" + ) + } @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { return http @@ -37,20 +54,13 @@ class SecurityConfig( sessionManagementConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) + .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter::class.java) .authorizeHttpRequests { - it.requestMatchers("/api/v1/boards/create-available").permitAll() - it.requestMatchers("/api/v1/boards/total-count").permitAll() - it.requestMatchers("/api/v1/file/**").permitAll() - it.requestMatchers("/api/v1/oauth/**", "/api/v1/user/check-exist").permitAll() - it.requestMatchers("/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll() - it.requestMatchers("/api/v1/boards/{id}", "/api/v1/polaroids/{id}", "/api/v1/boards/{boardId}/polaroids").permitAll() + it.requestMatchers(RequestMatcher { request -> + ALLOW_URLS.any { url -> AntPathRequestMatcher(url).matches(request) } + }).permitAll() it.anyRequest().authenticated() } - .exceptionHandling{ - it.authenticationEntryPoint { _, response, _ -> - response.sendError(500) - } - } .build() } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt index 6b06174..6a519d7 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/CustomErrorCode.kt @@ -24,5 +24,6 @@ enum class CustomErrorCode( // board BOARD_CREATED_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "BOARD001", "보드 생성에 실패했습니다."), + BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "BOARD002", "보드를 찾을 수 없습니다."), } \ 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 index aa728e6..d90670f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/exception/GlobalExceptionHandler.kt @@ -15,10 +15,12 @@ class GlobalExceptionHandler( @ExceptionHandler(ApplicationException::class) fun applicationException(ex: ApplicationException): ResponseEntity> { logger().error("error : ${ex.error}") - this.discordApiClient.sendErrorTrace( - ex.error.code, ex.message, - ex.stackTrace.contentToString() - ) + if(ex.error.status.is5xxServerError) { + this.discordApiClient.sendErrorTrace( + ex.error.status.toString(), ex.error.message, + ex.stackTrace.contentToString() + ) + } return ResponseEntity.status(ex.error.status).body(ApplicationResponse.error(ex.error)) } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt index 5fdfb33..484716b 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtAuthenticationFilter.kt @@ -19,7 +19,7 @@ class JwtAuthenticationFilter( filterChain: FilterChain ) { val authorizationHeader = request.getHeader("Authorization") - if(request.requestURI.contains("/api/v1/oauth")) { + if(request.requestURI.contains("/api/v1/oauth/re-issue")) { filterChain.doFilter(request, response) return } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt new file mode 100644 index 0000000..02e1cd6 --- /dev/null +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt @@ -0,0 +1,144 @@ +package com.ddd.sonnypolabobe.global.security + +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode +import com.ddd.sonnypolabobe.global.util.DiscordApiClient +import com.ddd.sonnypolabobe.global.util.HttpLog +import com.ddd.sonnypolabobe.logger +import com.fasterxml.jackson.databind.ObjectMapper +import io.jsonwebtoken.JwtException +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.util.ContentCachingRequestWrapper +import org.springframework.web.util.ContentCachingResponseWrapper +import org.springframework.web.util.WebUtils +import java.io.UnsupportedEncodingException +import java.time.LocalDateTime +import java.util.* + + +@Component +class JwtExceptionFilter( + private val discordApiClient: DiscordApiClient +) : OncePerRequestFilter() { + private val excludedUrls = setOf("/actuator", "/swagger-ui", "/v3/api-docs") + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + val requestWrapper: ContentCachingRequestWrapper = + ContentCachingRequestWrapper(request as HttpServletRequest) + val responseWrapper: ContentCachingResponseWrapper = + ContentCachingResponseWrapper(response as HttpServletResponse) + if (excludeLogging(request.requestURI)) { + filterChain.doFilter(request, response) + } else { + val startedAt = System.currentTimeMillis() + filterChain.doFilter(requestWrapper, responseWrapper) + val endedAt = System.currentTimeMillis() + logger().info( + "\n" + + "[REQUEST] ${request.method} - ${request.requestURI} ${responseWrapper.status} - ${(endedAt - startedAt) / 10000.0} \n" + + "Headers : ${getHeaders(request)} \n" + + "Parameters : ${getRequestParams(request)} \n" + + "Request body : ${getRequestBody(requestWrapper)} \n" + + "Response body : ${getResponseBody(responseWrapper)}" + ) + if (responseWrapper.status >= 400 && getResponseBody(responseWrapper).contains( + CustomErrorCode.INTERNAL_SERVER_EXCEPTION.message + ) + ) { + this.discordApiClient.sendErrorLog( + HttpLog( + request.method, + request.requestURI, + responseWrapper.status, + (endedAt - startedAt) / 10000.0, + getHeaders(request), + getRequestParams(request), + getRequestBody(requestWrapper), + getResponseBody(responseWrapper) + ) + ) + } + } + } catch (e: JwtException) { + response.contentType = "application/json;charset=UTF-8" + response.status = HttpStatus.UNAUTHORIZED.value() + response.characterEncoding = "utf-8" + + val mapper = ObjectMapper() + val errorJson = mapper.createObjectNode() + errorJson.put("message", e.message) + response.writer.write(mapper.writeValueAsString(errorJson)) + } + } + + private fun excludeLogging(requestURI: String): Boolean { + return excludedUrls.any { requestURI.startsWith(it) } + } + + private fun getResponseBody(response: ContentCachingResponseWrapper): String { + var payload: String? = null + response.characterEncoding = "utf-8" + val wrapper = + WebUtils.getNativeResponse(response, ContentCachingResponseWrapper::class.java) + if (wrapper != null) { + val buf = wrapper.contentAsByteArray + if (buf.isNotEmpty()) { + payload = String(buf, 0, buf.size, charset(wrapper.characterEncoding)) + wrapper.copyBodyToResponse() + } + } + return payload ?: " - " + } + + private fun getRequestBody(request: ContentCachingRequestWrapper): String { + request.characterEncoding = "utf-8" + val wrapper = WebUtils.getNativeRequest( + request, + ContentCachingRequestWrapper::class.java + ) + if (wrapper != null) { + val buf = wrapper.contentAsByteArray + if (buf.isNotEmpty()) { + return try { + String(buf, 0, buf.size, charset(wrapper.characterEncoding)) + } catch (e: UnsupportedEncodingException) { + " - " + } + } + } + return " - " + } + + private fun getRequestParams(request: HttpServletRequest): Map { + val parameterMap: MutableMap = HashMap() + request.characterEncoding = "utf-8" + val parameterArray: Enumeration<*> = request.parameterNames + + while (parameterArray.hasMoreElements()) { + val parameterName = parameterArray.nextElement() as String + parameterMap[parameterName] = request.getParameter(parameterName) + } + + return parameterMap + } + + private fun getHeaders(request: HttpServletRequest): Map { + val headerMap: MutableMap = HashMap() + + val headerArray: Enumeration<*> = request.headerNames + while (headerArray.hasMoreElements()) { + val headerName = headerArray.nextElement() as String + headerMap[headerName] = request.getHeader(headerName) + } + return headerMap + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt index 938fdef..ccd0de8 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/LoggingFilter.kt @@ -1,125 +1,125 @@ -package com.ddd.sonnypolabobe.global.security - -import com.ddd.sonnypolabobe.global.exception.CustomErrorCode -import com.ddd.sonnypolabobe.global.util.DiscordApiClient -import com.ddd.sonnypolabobe.global.util.HttpLog -import com.ddd.sonnypolabobe.logger -import jakarta.servlet.FilterChain -import jakarta.servlet.ServletRequest -import jakarta.servlet.ServletResponse -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.springframework.stereotype.Component -import org.springframework.web.filter.GenericFilterBean -import org.springframework.web.util.ContentCachingRequestWrapper -import org.springframework.web.util.ContentCachingResponseWrapper -import org.springframework.web.util.WebUtils -import java.io.UnsupportedEncodingException -import java.util.* - -@Component -class LoggingFilter( - private val discordApiClient: DiscordApiClient -) : GenericFilterBean() { - private val excludedUrls = setOf("/actuator", "/swagger-ui", "/v3/api-docs") - - override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { - val requestWrapper: ContentCachingRequestWrapper = - ContentCachingRequestWrapper(request as HttpServletRequest) - val responseWrapper: ContentCachingResponseWrapper = - ContentCachingResponseWrapper(response as HttpServletResponse) - if (excludeLogging(request.requestURI)) { - chain.doFilter(request, response) - } else { - val startedAt = System.currentTimeMillis() - chain.doFilter(requestWrapper, responseWrapper) - val endedAt = System.currentTimeMillis() - - logger().info( - "\n" + - "[REQUEST] ${request.method} - ${request.requestURI} ${responseWrapper.status} - ${(endedAt - startedAt) / 10000.0} \n" + - "Headers : ${getHeaders(request)} \n" + - "Parameters : ${getRequestParams(request)} \n" + - "Request body : ${getRequestBody(requestWrapper)} \n" + - "Response body : ${getResponseBody(responseWrapper)}" - ) - - if(responseWrapper.status >= 400 && getResponseBody(responseWrapper).contains(CustomErrorCode.INTERNAL_SERVER_EXCEPTION.message)) { - this.discordApiClient.sendErrorLog( - HttpLog( - request.method, - request.requestURI, - responseWrapper.status, - (endedAt - startedAt) / 10000.0, - getHeaders(request), - getRequestParams(request), - getRequestBody(requestWrapper), - getResponseBody(responseWrapper) - ) - ) - } - } - } - - private fun excludeLogging(requestURI: String): Boolean { - return excludedUrls.any { requestURI.startsWith(it) } - } - - private fun getResponseBody(response: ContentCachingResponseWrapper): String { - var payload: String? = null - response.characterEncoding = "utf-8" - val wrapper = - WebUtils.getNativeResponse(response, ContentCachingResponseWrapper::class.java) - if (wrapper != null) { - val buf = wrapper.contentAsByteArray - if (buf.isNotEmpty()) { - payload = String(buf, 0, buf.size, charset(wrapper.characterEncoding)) - wrapper.copyBodyToResponse() - } - } - return payload ?: " - " - } - - private fun getRequestBody(request: ContentCachingRequestWrapper): String { - request.characterEncoding = "utf-8" - val wrapper = WebUtils.getNativeRequest( - request, - ContentCachingRequestWrapper::class.java - ) - if (wrapper != null) { - val buf = wrapper.contentAsByteArray - if (buf.isNotEmpty()) { - return try { - String(buf, 0, buf.size, charset(wrapper.characterEncoding)) - } catch (e: UnsupportedEncodingException) { - " - " - } - } - } - return " - " - } - - private fun getRequestParams(request: HttpServletRequest): Map { - val parameterMap: MutableMap = HashMap() - request.characterEncoding = "utf-8" - val parameterArray: Enumeration<*> = request.parameterNames - - while (parameterArray.hasMoreElements()) { - val parameterName = parameterArray.nextElement() as String - parameterMap[parameterName] = request.getParameter(parameterName) - } - - return parameterMap - } - - private fun getHeaders(request: HttpServletRequest): Map { - val headerMap: MutableMap = HashMap() - - val headerArray: Enumeration<*> = request.headerNames - while (headerArray.hasMoreElements()) { - val headerName = headerArray.nextElement() as String - headerMap[headerName] = request.getHeader(headerName) - } - return headerMap - } -} \ No newline at end of file +//package com.ddd.sonnypolabobe.global.security +// +//import com.ddd.sonnypolabobe.global.exception.CustomErrorCode +//import com.ddd.sonnypolabobe.global.util.DiscordApiClient +//import com.ddd.sonnypolabobe.global.util.HttpLog +//import com.ddd.sonnypolabobe.logger +//import jakarta.servlet.FilterChain +//import jakarta.servlet.ServletRequest +//import jakarta.servlet.ServletResponse +//import jakarta.servlet.http.HttpServletRequest +//import jakarta.servlet.http.HttpServletResponse +//import org.springframework.stereotype.Component +//import org.springframework.web.filter.GenericFilterBean +//import org.springframework.web.util.ContentCachingRequestWrapper +//import org.springframework.web.util.ContentCachingResponseWrapper +//import org.springframework.web.util.WebUtils +//import java.io.UnsupportedEncodingException +//import java.util.* +// +//@Component +//class LoggingFilter( +// private val discordApiClient: DiscordApiClient +//) : GenericFilterBean() { +// private val excludedUrls = setOf("/actuator", "/swagger-ui", "/v3/api-docs") +// +// override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { +// val requestWrapper: ContentCachingRequestWrapper = +// ContentCachingRequestWrapper(request as HttpServletRequest) +// val responseWrapper: ContentCachingResponseWrapper = +// ContentCachingResponseWrapper(response as HttpServletResponse) +// if (excludeLogging(request.requestURI)) { +// chain.doFilter(request, response) +// } else { +// val startedAt = System.currentTimeMillis() +// chain.doFilter(requestWrapper, responseWrapper) +// val endedAt = System.currentTimeMillis() +// +// logger().info( +// "\n" + +// "[REQUEST] ${request.method} - ${request.requestURI} ${responseWrapper.status} - ${(endedAt - startedAt) / 10000.0} \n" + +// "Headers : ${getHeaders(request)} \n" + +// "Parameters : ${getRequestParams(request)} \n" + +// "Request body : ${getRequestBody(requestWrapper)} \n" + +// "Response body : ${getResponseBody(responseWrapper)}" +// ) +// +// if(responseWrapper.status >= 400 && getResponseBody(responseWrapper).contains(CustomErrorCode.INTERNAL_SERVER_EXCEPTION.message)) { +// this.discordApiClient.sendErrorLog( +// HttpLog( +// request.method, +// request.requestURI, +// responseWrapper.status, +// (endedAt - startedAt) / 10000.0, +// getHeaders(request), +// getRequestParams(request), +// getRequestBody(requestWrapper), +// getResponseBody(responseWrapper) +// ) +// ) +// } +// } +// } +// +// private fun excludeLogging(requestURI: String): Boolean { +// return excludedUrls.any { requestURI.startsWith(it) } +// } +// +// private fun getResponseBody(response: ContentCachingResponseWrapper): String { +// var payload: String? = null +// response.characterEncoding = "utf-8" +// val wrapper = +// WebUtils.getNativeResponse(response, ContentCachingResponseWrapper::class.java) +// if (wrapper != null) { +// val buf = wrapper.contentAsByteArray +// if (buf.isNotEmpty()) { +// payload = String(buf, 0, buf.size, charset(wrapper.characterEncoding)) +// wrapper.copyBodyToResponse() +// } +// } +// return payload ?: " - " +// } +// +// private fun getRequestBody(request: ContentCachingRequestWrapper): String { +// request.characterEncoding = "utf-8" +// val wrapper = WebUtils.getNativeRequest( +// request, +// ContentCachingRequestWrapper::class.java +// ) +// if (wrapper != null) { +// val buf = wrapper.contentAsByteArray +// if (buf.isNotEmpty()) { +// return try { +// String(buf, 0, buf.size, charset(wrapper.characterEncoding)) +// } catch (e: UnsupportedEncodingException) { +// " - " +// } +// } +// } +// return " - " +// } +// +// private fun getRequestParams(request: HttpServletRequest): Map { +// val parameterMap: MutableMap = HashMap() +// request.characterEncoding = "utf-8" +// val parameterArray: Enumeration<*> = request.parameterNames +// +// while (parameterArray.hasMoreElements()) { +// val parameterName = parameterArray.nextElement() as String +// parameterMap[parameterName] = request.getParameter(parameterName) +// } +// +// return parameterMap +// } +// +// private fun getHeaders(request: HttpServletRequest): Map { +// val headerMap: MutableMap = HashMap() +// +// val headerArray: Enumeration<*> = request.headerNames +// while (headerArray.hasMoreElements()) { +// val headerName = headerArray.nextElement() as String +// headerMap[headerName] = request.getHeader(headerName) +// } +// return headerMap +// } +//} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt index 355e8ce..1abe579 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/UuidConverter.kt @@ -1,5 +1,7 @@ package com.ddd.sonnypolabobe.global.util +import com.ddd.sonnypolabobe.global.exception.ApplicationException +import com.ddd.sonnypolabobe.global.exception.CustomErrorCode import java.nio.ByteBuffer import java.util.* @@ -19,7 +21,13 @@ object UuidConverter { } fun stringToUUID(uuid: String): UUID { - return UUID.fromString(uuid) + return try { + UUID.fromString(uuid) + } catch (e: IllegalArgumentException) { + throw ApplicationException( + CustomErrorCode.BOARD_NOT_FOUND + ) + } } fun uuidToString(uuid: UUID): String { diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6b7d9ec..ef237b1 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -60,7 +60,7 @@ jwt: refresh-key: ENC(Tg3e1h7BGfmOmB1kbSHW/Wdi9QQh34DR6XsD6Su7GkMH236k11Ffl9siE2FsDVvJX3MRmF7szRveRfUZR4DbgiYXzSoC35yGQHAiljtt7Cb77QuofiCLpKMetX6B3egTHFpoJWHqrsn1IRYi31rcYLIWnGff0vAMCy3IX2jnI2PzjVgzW0/UjpqMXspUj3rIuDjK926M/fCijH+xgswjVNqJcnQQmIk+5FUTSyut5tY=) validity: access-seconds: ENC(KeRwqvZAr0MfEVmxw8nBQQFEITQ0v/Fl) - refresh-seconds: ENC(bk/TKkZx9JLOphcFQLgwIA==) + refresh-seconds: ENC(RvppXDZLdo/Yw2ycPoRogGU9rumQgnQy) logging: discord: From 523f273ec56fef46c4b1c058f9388eebfc7267d4 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Wed, 14 Aug 2024 23:43:39 +0900 Subject: [PATCH 40/48] =?UTF-8?q?fix(POLABO-108):=20mvp=202=EC=B0=A8=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=204=EC=B0=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ddd/sonnypolabobe/domain/user/controller/UserController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt index 5f5aa31..d48e9f2 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt @@ -22,7 +22,7 @@ class UserController( 유저가 가진 정보 중 변경한 값 + 변경하지 않은 값 모두 보내주세요. 보내는 값을 그대로 디비에 저장합니다. """) - @PutMapping("/nickname") + @PutMapping("/profile") fun updateNickname(@RequestBody request: UserDto.Companion.UpdateReq) : ApplicationResponse { val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res From acf73d7ccdf94bb3bb9ee92457de4516600e197d Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 15 Aug 2024 22:58:25 +0900 Subject: [PATCH 41/48] =?UTF-8?q?fix(POLABO-108):=20=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C,=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=20=EB=B0=94=EC=9D=B8=EB=94=A9=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/repository/BoardJooqRepositoryImpl.kt | 1 + 1 file changed, 1 insertion(+) 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 index 3e4ac71..3356ff4 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -27,6 +27,7 @@ class BoardJooqRepositoryImpl( this.createdAt = DateConverter.convertToKst(LocalDateTime.now()) this.yn = 1 this.activeyn = 1 + this.userId = request.userId } val result = this.dslContext.insertInto(jBoard) .set(insertValue) From afb84bae607997e550857f281cdb7afbb48f9a6b Mon Sep 17 00:00:00 2001 From: dldmsql Date: Fri, 16 Aug 2024 23:54:59 +0900 Subject: [PATCH 42/48] =?UTF-8?q?fix(POLABO-108):=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=ED=99=94=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/my/controller/MyBoardController.kt | 4 ++-- .../sonnypolabobe/domain/board/my/service/MyBoardService.kt | 2 +- .../kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt index 178fe45..2a5150d 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt @@ -18,11 +18,11 @@ class MyBoardController(private val myBoardService : MyBoardService) { """) @GetMapping fun getMyBoards( - @RequestParam(name = "page", defaultValue = "0") page : Int, + @RequestParam(name = "page", defaultValue = "1") page : Int, @RequestParam size : Int ) : ApplicationResponse> { val user = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res - return ApplicationResponse.ok(this.myBoardService.getMyBoards(user.id, page, size)) + return ApplicationResponse.ok(this.myBoardService.getMyBoards(user.id, page-1, size)) } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt index ae3f7f3..9f2dfde 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt @@ -31,6 +31,6 @@ class MyBoardService(private val boardJooqRepository: BoardJooqRepository) { val totalCount = this.boardJooqRepository.selectTotalCountByUserId(userId) val totalPage = totalCount.toInt() / size + 1 - return PageDto(data, totalCount, totalPage, size) + return PageDto(data, totalCount, totalPage, page, size) } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt index e4dbb95..8b8f005 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt @@ -5,5 +5,6 @@ data class PageDto( val data: List, val totalCount: Long, val totalPage: Int, - val currentPage: Int + val currentPage: Int, + val size : Int ) \ No newline at end of file From d4e44d062471d42cbd45389aeee98badc3b52cee Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 17 Aug 2024 00:10:50 +0900 Subject: [PATCH 43/48] =?UTF-8?q?fix(POLABO-108):=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=ED=99=94=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/my/controller/MyBoardController.kt | 2 +- .../ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt index 2a5150d..10522e3 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/controller/MyBoardController.kt @@ -22,7 +22,7 @@ class MyBoardController(private val myBoardService : MyBoardService) { @RequestParam size : Int ) : ApplicationResponse> { val user = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res - return ApplicationResponse.ok(this.myBoardService.getMyBoards(user.id, page-1, size)) + return ApplicationResponse.ok(this.myBoardService.getMyBoards(user.id, page, size)) } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt index 9f2dfde..aa2a357 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt @@ -27,7 +27,7 @@ class MyBoardService(private val boardJooqRepository: BoardJooqRepository) { } fun getMyBoards(userId: Long, page: Int, size: Int): PageDto { - val data = this.boardJooqRepository.findAllByUserId(userId, page, size) + val data = this.boardJooqRepository.findAllByUserId(userId, page-1, size) val totalCount = this.boardJooqRepository.selectTotalCountByUserId(userId) val totalPage = totalCount.toInt() / size + 1 From 782ea974f8662923a82e3530e547757816442211 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 18 Aug 2024 17:37:35 +0900 Subject: [PATCH 44/48] =?UTF-8?q?fix(POLABO-108):=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=ED=99=94=20=EC=9E=91=EC=97=85=202=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/my/service/MyBoardService.kt | 3 +- .../oauth/controller/OauthController.kt | 6 ++- .../domain/oauth/service/OauthService.kt | 13 ++++- .../domain/user/controller/UserController.kt | 2 +- .../sonnypolabobe/domain/user/dto/UserDto.kt | 47 ++++++++++++++----- .../user/repository/UserJooqRepositoryImpl.kt | 13 ++--- .../sonnypolabobe/global/entity/PageDto.kt | 16 ++++++- .../global/util/DiscordApiClient.kt | 6 +-- 8 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt index aa2a357..6382951 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/my/service/MyBoardService.kt @@ -29,8 +29,7 @@ class MyBoardService(private val boardJooqRepository: BoardJooqRepository) { fun getMyBoards(userId: Long, page: Int, size: Int): PageDto { val data = this.boardJooqRepository.findAllByUserId(userId, page-1, size) val totalCount = this.boardJooqRepository.selectTotalCountByUserId(userId) - val totalPage = totalCount.toInt() / size + 1 - return PageDto(data, totalCount, totalPage, page, size) + return PageDto(data, totalCount, page, size) } } \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt index 051f49d..4275be1 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/controller/OauthController.kt @@ -4,6 +4,7 @@ import com.ddd.sonnypolabobe.domain.oauth.service.OauthService import com.ddd.sonnypolabobe.domain.user.dto.UserDto import com.ddd.sonnypolabobe.global.response.ApplicationResponse import io.swagger.v3.oas.annotations.Operation +import jakarta.validation.Valid import org.springframework.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.* @@ -16,9 +17,12 @@ class OauthController(private val oauthService: OauthService) { 이미 가입된 회원이라면 로그인을 진행하고, 가입되지 않은 회원이라면 회원가입을 진행합니다. 요청 바디의 값이 변경되었습니다. - 2024.08.11 + + - birthDt, gender 필드는 프로필 수정에서 업데이트 가능합니다. + 응답 DTO에 기본값 바인딩으로 채웠습니다. """) @PostMapping("/sign-in") - fun signIn(@RequestBody request: UserDto.Companion.CreateReq) + fun signIn(@RequestBody @Valid request: UserDto.Companion.CreateReq) = ApplicationResponse.ok(this.oauthService.signIn(request)) @PutMapping("/re-issue") diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt index 49a52c1..dc6ed19 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/oauth/service/OauthService.kt @@ -1,5 +1,6 @@ package com.ddd.sonnypolabobe.domain.oauth.service +import com.ddd.sonnypolabobe.domain.user.dto.GenderType import com.ddd.sonnypolabobe.domain.user.dto.UserDto import com.ddd.sonnypolabobe.domain.user.repository.UserJooqRepository import com.ddd.sonnypolabobe.domain.user.token.dto.UserTokenDto @@ -37,7 +38,11 @@ class OauthService( ) this.userTokenRepository.updateByUserId(userToken) - return tokenRes.also { it.isNewUser = false } + return tokenRes.also { _ -> + tokenRes.isNewUser = false + tokenRes.birthDt = it.birthDt + tokenRes.gender = it.gender + } } ?: run { val userId = this.userRepository.insertOne(request) @@ -58,7 +63,11 @@ class OauthService( ) this.userTokenRepository.insertOne(userToken) - return tokenRes.also { it.isNewUser = true } + return tokenRes.also { _ -> + tokenRes.isNewUser = true + tokenRes.birthDt = null + tokenRes.gender = GenderType.NONE + } } } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt index d48e9f2..e65d85f 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/controller/UserController.kt @@ -23,7 +23,7 @@ class UserController( 보내는 값을 그대로 디비에 저장합니다. """) @PutMapping("/profile") - fun updateNickname(@RequestBody request: UserDto.Companion.UpdateReq) + fun updateProfile(@RequestBody request: UserDto.Companion.UpdateReq) : ApplicationResponse { val userInfoFromToken = SecurityContextHolder.getContext().authentication.principal as UserDto.Companion.Res this.userService.updateProfile(request, userInfoFromToken.id) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt index f0b20fc..7e626af 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/dto/UserDto.kt @@ -1,6 +1,8 @@ package com.ddd.sonnypolabobe.domain.user.dto import com.fasterxml.jackson.annotation.JsonProperty +import jakarta.validation.constraints.Email +import org.intellij.lang.annotations.RegExp import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.UserDetails @@ -12,22 +14,23 @@ import java.util.stream.Collectors class UserDto { companion object { data class CreateReq( + @field:Email val email: String, val nickName: String, - val birthDt : LocalDate?, - val gender : GenderType? - ) +// val birthDt : LocalDate?, +// val gender : GenderType? + ) data class UpdateReq( @JsonProperty("nickName") - val nickName : String, + val nickName: String, @JsonProperty("birthDt") - val birthDt : LocalDate?, - val gender : GenderType? + val birthDt: LocalDate?, + val gender: GenderType? ) data class CreateTokenReq( - val id : Long, + val id: Long, val email: String, val nickName: String ) @@ -36,9 +39,27 @@ class UserDto { val accessToken: String, val refreshToken: String, val expiredDate: Date, - var isNewUser : Boolean, - val nickName: String - ) + var isNewUser: Boolean, + val nickName: String, + var birthDt: LocalDate?, + var gender: GenderType + ) { + constructor( + accessToken: String, + refreshToken: String, + expiredDate: Date, + isNewUser: Boolean, + nickName: String + ) : this( + accessToken, + refreshToken, + expiredDate, + isNewUser, + nickName, + null, + GenderType.NONE + ) + } data class ProfileRes( val id: Long, @@ -49,7 +70,7 @@ class UserDto { data class WithdrawReq( val type: WithdrawType, - val reason : String? + val reason: String? ) data class Res( @@ -58,7 +79,9 @@ class UserDto { val nickName: String, val yn: Boolean, val createdAt: LocalDateTime, - val updatedAt: LocalDateTime? + val updatedAt: LocalDateTime?, + val birthDt: LocalDate?, + val gender : GenderType ) : UserDetails { override fun getAuthorities(): MutableCollection { val roles = mutableListOf("ROLE_USER") diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt index 939e149..91816a9 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/user/repository/UserJooqRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.ddd.sonnypolabobe.domain.user.repository +import com.ddd.sonnypolabobe.domain.user.dto.GenderType import com.ddd.sonnypolabobe.domain.user.dto.UserDto import com.ddd.sonnypolabobe.global.util.DateConverter import com.ddd.sonnypolabobe.jooq.polabo.enums.UserGender @@ -17,16 +18,12 @@ class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepos jUser.NICK_NAME, jUser.CREATED_AT, jUser.YN, - jUser.BIRTH_DT, - jUser.GENDER ) .values( request.email, request.nickName, DateConverter.convertToKst(LocalDateTime.now()), 1, - request.birthDt, - UserGender.valueOf(request.gender?.name ?: UserGender.NONE.name) ).execute() if(result == 0) throw Exception("Failed to insert user") @@ -49,7 +46,9 @@ class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepos nickName = it.nickName!!, yn = it.yn?.toInt() == 1, createdAt = it.createdAt!!, - updatedAt = it.updatedAt + updatedAt = it.updatedAt, + birthDt = it.birthDt, + gender = GenderType.valueOf(it.gender?.name ?: GenderType.NONE.name) ) } } @@ -67,7 +66,9 @@ class UserJooqRepositoryImpl(private val dslContext: DSLContext) : UserJooqRepos nickName = it.nickName!!, yn = it.yn?.toInt() == 1, createdAt = it.createdAt!!, - updatedAt = it.updatedAt + updatedAt = it.updatedAt, + birthDt = it.birthDt, + gender = GenderType.valueOf(it.gender?.name ?: GenderType.NONE.name) ) } } diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt index 8b8f005..b3ce586 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt @@ -4,7 +4,19 @@ package com.ddd.sonnypolabobe.global.entity data class PageDto( val data: List, val totalCount: Long, - val totalPage: Int, + var totalPage: Int, val currentPage: Int, val size : Int -) \ No newline at end of file +) { + + constructor( + data: List, + totalCount: Long, + page: Int, + size: Int + ) : this(data, totalCount, 0, page, size + ) + init { + totalPage = totalCount.toInt() / size + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt index d00a866..9f6bcfe 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt @@ -62,7 +62,7 @@ class DiscordApiClient( .bodyValue(payload) .retrieve() .bodyToMono(Void::class.java) - .block() + .subscribe() } fun sendErrorTrace(errorCode: String, message: String?, trace: String) { @@ -82,7 +82,7 @@ class DiscordApiClient( field3["name"] = "메시지" field3["value"] = message ?: "메시지 없음" - embedData["fields"] = listOf>(field1) + embedData["fields"] = listOf>(field1, field2, field3) val payload: MutableMap = HashMap() payload["embeds"] = arrayOf(embedData) @@ -93,7 +93,7 @@ class DiscordApiClient( .bodyValue(payload) .retrieve() .bodyToMono(Void::class.java) - .block() + .subscribe() } } \ No newline at end of file From 527e2a1bf774f25b21b3db31aee587a563833e1c Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 18 Aug 2024 17:56:18 +0900 Subject: [PATCH 45/48] =?UTF-8?q?fix(POLABO-108):=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=ED=99=94=20=EC=9E=91=EC=97=85=202=EC=B0=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt index b3ce586..7be6e65 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/entity/PageDto.kt @@ -17,6 +17,7 @@ data class PageDto( ) : this(data, totalCount, 0, page, size ) init { - totalPage = totalCount.toInt() / size + totalPage = if(totalCount % size == 0L) (totalCount / size).toInt() + else (totalCount / size + 1).toInt() } } \ No newline at end of file From a1929816824f7a414d2fe6b2276981fc9937fb80 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 22 Aug 2024 21:32:08 +0900 Subject: [PATCH 46/48] =?UTF-8?q?chore(POLABO-108):=20discord=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=9C=EB=A0=A5=20=ED=8F=AC=EB=A9=A7=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/JwtExceptionFilter.kt | 31 ++++++------------- .../global/util/DiscordApiClient.kt | 29 +++-------------- src/main/resources/application-dev.yml | 5 ++- src/main/resources/application-local.yml | 4 ++- src/main/resources/application.yml | 3 -- 5 files changed, 21 insertions(+), 51 deletions(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt index 02e1cd6..876a7e6 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/security/JwtExceptionFilter.kt @@ -2,7 +2,6 @@ package com.ddd.sonnypolabobe.global.security import com.ddd.sonnypolabobe.global.exception.CustomErrorCode import com.ddd.sonnypolabobe.global.util.DiscordApiClient -import com.ddd.sonnypolabobe.global.util.HttpLog import com.ddd.sonnypolabobe.logger import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.JwtException @@ -16,7 +15,6 @@ import org.springframework.web.util.ContentCachingRequestWrapper import org.springframework.web.util.ContentCachingResponseWrapper import org.springframework.web.util.WebUtils import java.io.UnsupportedEncodingException -import java.time.LocalDateTime import java.util.* @@ -42,30 +40,19 @@ class JwtExceptionFilter( val startedAt = System.currentTimeMillis() filterChain.doFilter(requestWrapper, responseWrapper) val endedAt = System.currentTimeMillis() - logger().info( - "\n" + - "[REQUEST] ${request.method} - ${request.requestURI} ${responseWrapper.status} - ${(endedAt - startedAt) / 10000.0} \n" + - "Headers : ${getHeaders(request)} \n" + - "Parameters : ${getRequestParams(request)} \n" + - "Request body : ${getRequestBody(requestWrapper)} \n" + - "Response body : ${getResponseBody(responseWrapper)}" - ) + + val message = "\n" + + "[REQUEST] ${request.method} - ${request.requestURI} ${responseWrapper.status} - ${(endedAt - startedAt) / 10000.0} \n" + + "Headers : ${getHeaders(request)} \n" + + "Parameters : ${getRequestParams(request)} \n" + + "Request body : ${getRequestBody(requestWrapper)} \n" + + "Response body : ${getResponseBody(responseWrapper)}" + logger().error(message) if (responseWrapper.status >= 400 && getResponseBody(responseWrapper).contains( CustomErrorCode.INTERNAL_SERVER_EXCEPTION.message ) ) { - this.discordApiClient.sendErrorLog( - HttpLog( - request.method, - request.requestURI, - responseWrapper.status, - (endedAt - startedAt) / 10000.0, - getHeaders(request), - getRequestParams(request), - getRequestBody(requestWrapper), - getResponseBody(responseWrapper) - ) - ) + this.discordApiClient.sendErrorLog(message) } } } catch (e: JwtException) { diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt index 9f6bcfe..56b317a 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt @@ -1,5 +1,6 @@ package com.ddd.sonnypolabobe.global.util +import com.sun.jndi.ldap.LdapPoolManager.trace import org.springframework.beans.factory.annotation.Value import org.springframework.http.MediaType import org.springframework.http.client.reactive.ReactorClientHttpConnector @@ -22,36 +23,16 @@ class DiscordApiClient( ) .build() - fun sendErrorLog(req: HttpLog) { + fun sendErrorLog(req: String) { val embedData: MutableMap = HashMap() embedData["title"] = "서버 에러 발생" val field1: MutableMap = HashMap() - field1["name"] = "요청 정보" - field1["value"] = req.requestMethod + " " + req.requestURI + " " + req.elapsedTime + "ms" + field1["name"] = "요청" + field1["value"] = req - val field2: MutableMap = HashMap() - field2["name"] = "응답 코드" - field2["value"] = req.responseStatus.toString() - - val field3: MutableMap = HashMap() - field3["name"] = "요청 헤더" - field3["value"] = req.headers.map { it.key + " : " + it.value }.joinToString("\n") - - val field4: MutableMap = HashMap() - field4["name"] = "요청 본문" - field4["value"] = req.requestBody - - val field5: MutableMap = HashMap() - field5["name"] = "요청 파람" - field5["value"] = req.parameters.map { it.key + " : " + it.value }.joinToString("\n") - - val field6: MutableMap = HashMap() - field6["name"] = "응답 본문" - field6["value"] = req.responseBody - - embedData["fields"] = listOf>(field1, field2, field3, field4, field5, field6) + embedData["fields"] = listOf>(field1) val payload: MutableMap = HashMap() payload["embeds"] = arrayOf(embedData) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index ef237b1..255a97d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -64,4 +64,7 @@ jwt: logging: discord: - webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) \ No newline at end of file + webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) + + level: + root: ERROR diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 138276f..dfdfd02 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -62,4 +62,6 @@ jwt: logging: discord: - webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) \ No newline at end of file + webhook-uri: ENC(yfeX3WHXQdxkVtasNl5WLv6M/YlN+dVFUurjxGIddstjjipt+KryWKvLu1wDmdGjpuEhUHyaABg4gFWRMk9gNlxSQEE/G1twbuvkOvT0pyFWycVVJ6ryU/v9pDBOS1PSKJY7L3NP66gOGnam6nOvf0Y+F45zZvXj8/sdtR6N798U6fGjFDxOLQ==) + level: + root: INFO \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 91403d0..750afff 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,9 +6,6 @@ server: port: 8080 shutdown: graceful -logging: - level: - root: INFO jasypt: encryptor: From d596d516ecd3af2c02895f90b742d2b40b73e446 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Mon, 26 Aug 2024 19:36:43 +0900 Subject: [PATCH 47/48] =?UTF-8?q?fix(POLABO-108):=20=EB=88=84=EC=A0=81=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=88=98=20=EC=A1=B0=EA=B1=B4=EC=A0=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/repository/BoardJooqRepositoryImpl.kt | 1 - 1 file changed, 1 deletion(-) 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 index 3356ff4..0dfaa2b 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/domain/board/repository/BoardJooqRepositoryImpl.kt @@ -67,7 +67,6 @@ class BoardJooqRepositoryImpl( return this.dslContext .selectCount() .from(jBoard) - .where(jBoard.YN.eq(1).and(jBoard.ACTIVEYN.eq(1))) .fetchOne(0, Long::class.java) ?: 0 } From 8eb9af83aca9c3f6d3121cdb9adf8acf11d4f887 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Mon, 26 Aug 2024 19:39:07 +0900 Subject: [PATCH 48/48] =?UTF-8?q?fix(POLABO-108):=20=EB=88=84=EC=A0=81=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=88=98=20=EC=A1=B0=EA=B1=B4=EC=A0=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt index 56b317a..0b8906b 100644 --- a/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt +++ b/src/main/kotlin/com/ddd/sonnypolabobe/global/util/DiscordApiClient.kt @@ -1,6 +1,5 @@ package com.ddd.sonnypolabobe.global.util -import com.sun.jndi.ldap.LdapPoolManager.trace import org.springframework.beans.factory.annotation.Value import org.springframework.http.MediaType import org.springframework.http.client.reactive.ReactorClientHttpConnector