From 18a1e9cc35ad5c70b4645434a71a3a1ffe69048c Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 26 Mar 2025 13:25:23 +0900 Subject: [PATCH 001/542] =?UTF-8?q?feat:=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- src/main/kotlin/codel/domain/Profile.kt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/kotlin/codel/domain/Profile.kt diff --git a/src/main/kotlin/codel/domain/Profile.kt b/src/main/kotlin/codel/domain/Profile.kt new file mode 100644 index 00000000..3ff5e96b --- /dev/null +++ b/src/main/kotlin/codel/domain/Profile.kt @@ -0,0 +1,20 @@ +package codel.domain + +class Profile( + val codeName: String, + val age: Int, + val job: String, + val alcohol: String, + val smoke: String, + val hobby: List, + val style: List, + val bigCity: String, + val smallCity: String, + val mbti: String, + val introduce: String, + val codeImage: List, + val faceImage: List +) { + + val id: Long = 0 +} From 3239abe0b2d03355856af5b96992fa47c746575b Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 26 Mar 2025 13:26:07 +0900 Subject: [PATCH 002/542] =?UTF-8?q?feat:=EB=A9=A4=EB=B2=84=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- src/main/kotlin/codel/domain/Member.kt | 10 ++++++++++ src/main/kotlin/codel/domain/OauthType.kt | 6 ++++++ 2 files changed, 16 insertions(+) create mode 100644 src/main/kotlin/codel/domain/Member.kt create mode 100644 src/main/kotlin/codel/domain/OauthType.kt diff --git a/src/main/kotlin/codel/domain/Member.kt b/src/main/kotlin/codel/domain/Member.kt new file mode 100644 index 00000000..ec3c8517 --- /dev/null +++ b/src/main/kotlin/codel/domain/Member.kt @@ -0,0 +1,10 @@ +package codel.domain + +class Member( + val oauthType: OauthType, + val oauthId: String +) { + + val id: Long = 0 + val profile: Profile? = null +} diff --git a/src/main/kotlin/codel/domain/OauthType.kt b/src/main/kotlin/codel/domain/OauthType.kt new file mode 100644 index 00000000..dee8aff1 --- /dev/null +++ b/src/main/kotlin/codel/domain/OauthType.kt @@ -0,0 +1,6 @@ +package codel.domain + +enum class OauthType { + KAKAO, + APPLE, +} From 6fecdf288309c0c15da04b97c2051dc50cc48583 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 26 Mar 2025 17:26:22 +0900 Subject: [PATCH 003/542] =?UTF-8?q?feat:=20jpa=20repository=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../infrastructure/MemberJpaRepository.kt | 10 ++++++ .../infrastructure/entity/MemberEntity.kt | 33 +++++++++++++++++++ .../infrastructure/entity/ProfileEntity.kt | 28 ++++++++++++++++ .../infrastructure/MemberJpaRepositoryTest.kt | 29 ++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt create mode 100644 src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt create mode 100644 src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt create mode 100644 src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt diff --git a/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt b/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt new file mode 100644 index 00000000..eed565a4 --- /dev/null +++ b/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt @@ -0,0 +1,10 @@ +package codel.infrastructure + +import codel.infrastructure.entity.MemberEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MemberJpaRepository : JpaRepository { + +} diff --git a/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt new file mode 100644 index 00000000..33607587 --- /dev/null +++ b/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt @@ -0,0 +1,33 @@ +package codel.infrastructure.entity + +import codel.domain.Member +import codel.domain.OauthType +import jakarta.persistence.* + +@Entity +@Table( + uniqueConstraints = [ + UniqueConstraint(columnNames = ["oauthType", "oauthId"]) + ] +) +class MemberEntity( + private var oauthType: OauthType, + private var oauthId: String +) { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0 + + @OneToOne + var profileEntity: ProfileEntity? = null + + companion object { + fun toEntity(member: Member): MemberEntity { + return MemberEntity( + oauthType = member.oauthType, + oauthId = member.oauthId + ) + } + } +} diff --git a/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt new file mode 100644 index 00000000..8d58dce4 --- /dev/null +++ b/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt @@ -0,0 +1,28 @@ +package codel.infrastructure.entity + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity +class ProfileEntity( + private var codeName: String, + private var age: Int, + private var job: String, + private var alcohol: String, + private var smoke: String, + private var hobby: String, // 복수 + private var style: String, // 복수 + private var bigCity: String, + private var smallCity: String, + private var mbti: String, + private var introduce: String, + private var codeImage: String, // 복수 + private var faceImage: String // 복수 +) { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long = 0 +} diff --git a/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt b/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt new file mode 100644 index 00000000..90208dcc --- /dev/null +++ b/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt @@ -0,0 +1,29 @@ +package codel.infrastructure + +import codel.domain.OauthType +import codel.infrastructure.entity.MemberEntity +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.dao.DataIntegrityViolationException + +@SpringBootTest +class MemberJpaRepositoryTest ( + @Autowired + private val memberJpaRepository: MemberJpaRepository +){ + + @DisplayName("중복된 멤버에 대해 유니크 제약 조건을 발생시킨다.") + @Test + fun saveMemberTest() { + val memberEntity1 = MemberEntity(OauthType.APPLE, "hogee") + val memberEntity2 = MemberEntity(OauthType.APPLE, "hogee") + memberJpaRepository.save(memberEntity1) + + Assertions.assertThrows(DataIntegrityViolationException::class.java) { + memberJpaRepository.save(memberEntity2) + } + } +} From 8d6cb1dca92eee88ef9a8d4d6fc6cd5ef09060c5 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 26 Mar 2025 17:26:54 +0900 Subject: [PATCH 004/542] =?UTF-8?q?feat:=20member=20repository=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../kotlin/codel/domain/MemberRepository.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/kotlin/codel/domain/MemberRepository.kt diff --git a/src/main/kotlin/codel/domain/MemberRepository.kt b/src/main/kotlin/codel/domain/MemberRepository.kt new file mode 100644 index 00000000..9a2d6689 --- /dev/null +++ b/src/main/kotlin/codel/domain/MemberRepository.kt @@ -0,0 +1,19 @@ +package codel.domain + +import codel.infrastructure.MemberJpaRepository +import codel.infrastructure.entity.MemberEntity +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.stereotype.Component + +@Component +class MemberRepository ( + private val memberJpaRepository: MemberJpaRepository +) { + + fun saveMember(member: Member): Boolean = try { + memberJpaRepository.save(MemberEntity.toEntity(member)) + false + } catch (e: DataIntegrityViolationException){ + true + } +} From 91fc937df75a78c807369cae5e253b28919ee878 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 26 Mar 2025 17:27:23 +0900 Subject: [PATCH 005/542] =?UTF-8?q?feat:=20member=20service=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../kotlin/codel/business/MemberService.kt | 25 +++++++++++++++++ .../request/MemberSavedRequest.kt | 8 ++++++ .../response/MemberSavedResponse.kt | 5 ++++ .../codel/business/MemberServiceTest.kt | 28 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 src/main/kotlin/codel/business/MemberService.kt create mode 100644 src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt create mode 100644 src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt create mode 100644 src/test/kotlin/codel/business/MemberServiceTest.kt diff --git a/src/main/kotlin/codel/business/MemberService.kt b/src/main/kotlin/codel/business/MemberService.kt new file mode 100644 index 00000000..a788a07b --- /dev/null +++ b/src/main/kotlin/codel/business/MemberService.kt @@ -0,0 +1,25 @@ +package codel.business + +import codel.domain.Member +import codel.domain.MemberRepository +import codel.presentation.request.MemberSavedRequest +import codel.presentation.response.MemberSavedResponse +import org.springframework.stereotype.Service + +@Service +class MemberService( + private val memberRepository: MemberRepository +) { + + fun saveMember( + request: MemberSavedRequest + ): MemberSavedResponse { + val member = Member( + oauthType = request.oauthType, + oauthId = request.oauthId + ) + val isUser = memberRepository.saveMember(member) + + return MemberSavedResponse(isUser) + } +} diff --git a/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt b/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt new file mode 100644 index 00000000..7c39612a --- /dev/null +++ b/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt @@ -0,0 +1,8 @@ +package codel.presentation.request + +import codel.domain.OauthType + +data class MemberSavedRequest( + val oauthType: OauthType, + val oauthId: String +) diff --git a/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt b/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt new file mode 100644 index 00000000..fed8a960 --- /dev/null +++ b/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt @@ -0,0 +1,5 @@ +package codel.presentation.response + +data class MemberSavedResponse( + val isUser: Boolean +) diff --git a/src/test/kotlin/codel/business/MemberServiceTest.kt b/src/test/kotlin/codel/business/MemberServiceTest.kt new file mode 100644 index 00000000..93f2ce33 --- /dev/null +++ b/src/test/kotlin/codel/business/MemberServiceTest.kt @@ -0,0 +1,28 @@ +package codel.business + +import codel.domain.OauthType +import codel.presentation.request.MemberSavedRequest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class MemberServiceTest( + @Autowired + private val memberService: MemberService +) { + + @DisplayName("멤버 중복 저장 테스트") + @Test + fun saveMemberSuccessTest() { + val memberSavedRequest = MemberSavedRequest(OauthType.KAKAO, "hogee") + + val newUser = memberService.saveMember(memberSavedRequest).isUser + val duplicatedUser = memberService.saveMember(memberSavedRequest).isUser + + assertThat(newUser).isFalse() + assertThat(duplicatedUser).isTrue() + } +} From 7f853f65b5006d91cf7751d79c0d9fc34384188a Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 26 Mar 2025 17:27:35 +0900 Subject: [PATCH 006/542] =?UTF-8?q?feat:=20member=20controller=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- build.gradle.kts | 1 + .../codel/presentation/MemberController.kt | 25 ++++++++ .../presentation/MemberControllerTest.kt | 60 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/main/kotlin/codel/presentation/MemberController.kt create mode 100644 src/test/kotlin/codel/presentation/MemberControllerTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index c20ab546..59998375 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("io.rest-assured:rest-assured:5.3.1") } kotlin { diff --git a/src/main/kotlin/codel/presentation/MemberController.kt b/src/main/kotlin/codel/presentation/MemberController.kt new file mode 100644 index 00000000..8e578abe --- /dev/null +++ b/src/main/kotlin/codel/presentation/MemberController.kt @@ -0,0 +1,25 @@ +package codel.presentation + +import codel.business.MemberService +import codel.presentation.request.MemberSavedRequest +import codel.presentation.response.MemberSavedResponse +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class MemberController( + private val memberService: MemberService +) { + + @PostMapping("/v1/auth/login") + fun saveMember( + @RequestBody request: MemberSavedRequest + ): ResponseEntity { + val memberSavedResponse = memberService.saveMember(request) + + return ResponseEntity.ok(memberSavedResponse) + } +} diff --git a/src/test/kotlin/codel/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/presentation/MemberControllerTest.kt new file mode 100644 index 00000000..e10338a8 --- /dev/null +++ b/src/test/kotlin/codel/presentation/MemberControllerTest.kt @@ -0,0 +1,60 @@ +package codel.presentation + +import codel.presentation.response.MemberSavedResponse +import io.restassured.RestAssured +import io.restassured.RestAssured.given +import io.restassured.http.ContentType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class MemberControllerTest() { + @LocalServerPort + var port: Int = 0 + @BeforeEach + fun setup() { + RestAssured.port = port + } + + @DisplayName("Member 테스트") + @Test + fun saveMember() { + val request = mapOf( + "oauthType" to "APPLE", + "oauthId" to "hogee" + ) + val newUserResponse = MemberSavedResponse(false) + + given() + .contentType(ContentType.JSON) + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + .extract() + .`as`(MemberSavedResponse::class.java) + .also { response -> + assertEquals(newUserResponse.isUser, response.isUser) + } + + val duplicatedUserResponse = MemberSavedResponse(true) + + given() + .contentType(ContentType.JSON) + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + .extract() + .`as`(MemberSavedResponse::class.java) + .also { response -> + assertEquals(duplicatedUserResponse.isUser, response.isUser) + } + } +} From f1f93b3fb4a1a88ad50941ffb232eedd49f136d4 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Fri, 28 Mar 2025 15:17:18 +0900 Subject: [PATCH 007/542] =?UTF-8?q?style:=20ktlint=20=EC=BB=A8=EB=B2=A4?= =?UTF-8?q?=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- build.gradle.kts | 51 +++++++++--------- src/main/kotlin/codel/CodelApplication.kt | 2 +- .../kotlin/codel/business/MemberService.kt | 14 +++-- src/main/kotlin/codel/domain/Member.kt | 5 +- .../kotlin/codel/domain/MemberRepository.kt | 18 +++---- src/main/kotlin/codel/domain/Profile.kt | 27 +++++----- .../infrastructure/MemberJpaRepository.kt | 4 +- .../infrastructure/entity/MemberEntity.kt | 16 +++--- .../infrastructure/entity/ProfileEntity.kt | 27 +++++----- .../codel/presentation/MemberController.kt | 6 +-- .../request/MemberSavedRequest.kt | 4 +- .../response/MemberSavedResponse.kt | 2 +- .../kotlin/codel/CodelApplicationTests.kt | 8 ++- .../codel/business/MemberServiceTest.kt | 5 +- .../infrastructure/MemberJpaRepositoryTest.kt | 9 ++-- .../presentation/MemberControllerTest.kt | 52 ++++++++++--------- 16 files changed, 119 insertions(+), 131 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 59998375..f3b0ad24 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,48 +1,49 @@ plugins { - kotlin("jvm") version "1.9.25" - kotlin("plugin.spring") version "1.9.25" - id("org.springframework.boot") version "3.4.3" - id("io.spring.dependency-management") version "1.1.7" - kotlin("plugin.jpa") version "1.9.25" + kotlin("jvm") version "1.9.25" + kotlin("plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.4.3" + id("io.spring.dependency-management") version "1.1.7" + kotlin("plugin.jpa") version "1.9.25" + id("org.jlleitschuh.gradle.ktlint") version "12.2.0" } group = "codel" version = "0.0.1-SNAPSHOT" java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-reflect") - runtimeOnly("com.h2database:h2") - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("io.rest-assured:rest-assured:5.3.1") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") + runtimeOnly("com.h2database:h2") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("io.rest-assured:rest-assured:5.3.1") } kotlin { - compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict") - } + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } } allOpen { - annotation("jakarta.persistence.Entity") - annotation("jakarta.persistence.MappedSuperclass") - annotation("jakarta.persistence.Embeddable") + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") } tasks.withType { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/kotlin/codel/CodelApplication.kt b/src/main/kotlin/codel/CodelApplication.kt index 289465ff..132d95ef 100644 --- a/src/main/kotlin/codel/CodelApplication.kt +++ b/src/main/kotlin/codel/CodelApplication.kt @@ -7,5 +7,5 @@ import org.springframework.boot.runApplication class CodelApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } diff --git a/src/main/kotlin/codel/business/MemberService.kt b/src/main/kotlin/codel/business/MemberService.kt index a788a07b..da496c26 100644 --- a/src/main/kotlin/codel/business/MemberService.kt +++ b/src/main/kotlin/codel/business/MemberService.kt @@ -8,16 +8,14 @@ import org.springframework.stereotype.Service @Service class MemberService( - private val memberRepository: MemberRepository + private val memberRepository: MemberRepository, ) { - - fun saveMember( - request: MemberSavedRequest - ): MemberSavedResponse { - val member = Member( + fun saveMember(request: MemberSavedRequest): MemberSavedResponse { + val member = + Member( oauthType = request.oauthType, - oauthId = request.oauthId - ) + oauthId = request.oauthId, + ) val isUser = memberRepository.saveMember(member) return MemberSavedResponse(isUser) diff --git a/src/main/kotlin/codel/domain/Member.kt b/src/main/kotlin/codel/domain/Member.kt index ec3c8517..7b35bf62 100644 --- a/src/main/kotlin/codel/domain/Member.kt +++ b/src/main/kotlin/codel/domain/Member.kt @@ -1,10 +1,9 @@ package codel.domain class Member( - val oauthType: OauthType, - val oauthId: String + val oauthType: OauthType, + val oauthId: String, ) { - val id: Long = 0 val profile: Profile? = null } diff --git a/src/main/kotlin/codel/domain/MemberRepository.kt b/src/main/kotlin/codel/domain/MemberRepository.kt index 9a2d6689..75f948a5 100644 --- a/src/main/kotlin/codel/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/domain/MemberRepository.kt @@ -6,14 +6,14 @@ import org.springframework.dao.DataIntegrityViolationException import org.springframework.stereotype.Component @Component -class MemberRepository ( - private val memberJpaRepository: MemberJpaRepository +class MemberRepository( + private val memberJpaRepository: MemberJpaRepository, ) { - - fun saveMember(member: Member): Boolean = try { - memberJpaRepository.save(MemberEntity.toEntity(member)) - false - } catch (e: DataIntegrityViolationException){ - true - } + fun saveMember(member: Member): Boolean = + try { + memberJpaRepository.save(MemberEntity.toEntity(member)) + false + } catch (e: DataIntegrityViolationException) { + true + } } diff --git a/src/main/kotlin/codel/domain/Profile.kt b/src/main/kotlin/codel/domain/Profile.kt index 3ff5e96b..45714acf 100644 --- a/src/main/kotlin/codel/domain/Profile.kt +++ b/src/main/kotlin/codel/domain/Profile.kt @@ -1,20 +1,19 @@ package codel.domain class Profile( - val codeName: String, - val age: Int, - val job: String, - val alcohol: String, - val smoke: String, - val hobby: List, - val style: List, - val bigCity: String, - val smallCity: String, - val mbti: String, - val introduce: String, - val codeImage: List, - val faceImage: List + val codeName: String, + val age: Int, + val job: String, + val alcohol: String, + val smoke: String, + val hobby: List, + val style: List, + val bigCity: String, + val smallCity: String, + val mbti: String, + val introduce: String, + val codeImage: List, + val faceImage: List, ) { - val id: Long = 0 } diff --git a/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt b/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt index eed565a4..717da404 100644 --- a/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt +++ b/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt @@ -5,6 +5,4 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface MemberJpaRepository : JpaRepository { - -} +interface MemberJpaRepository : JpaRepository diff --git a/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt index 33607587..d06a55fb 100644 --- a/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt @@ -7,14 +7,13 @@ import jakarta.persistence.* @Entity @Table( uniqueConstraints = [ - UniqueConstraint(columnNames = ["oauthType", "oauthId"]) - ] + UniqueConstraint(columnNames = ["oauthType", "oauthId"]), + ], ) class MemberEntity( - private var oauthType: OauthType, - private var oauthId: String + private var oauthType: OauthType, + private var oauthId: String, ) { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0 @@ -23,11 +22,10 @@ class MemberEntity( var profileEntity: ProfileEntity? = null companion object { - fun toEntity(member: Member): MemberEntity { - return MemberEntity( + fun toEntity(member: Member): MemberEntity = + MemberEntity( oauthType = member.oauthType, - oauthId = member.oauthId + oauthId = member.oauthId, ) - } } } diff --git a/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt index 8d58dce4..668586be 100644 --- a/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt +++ b/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt @@ -7,21 +7,20 @@ import jakarta.persistence.Id @Entity class ProfileEntity( - private var codeName: String, - private var age: Int, - private var job: String, - private var alcohol: String, - private var smoke: String, - private var hobby: String, // 복수 - private var style: String, // 복수 - private var bigCity: String, - private var smallCity: String, - private var mbti: String, - private var introduce: String, - private var codeImage: String, // 복수 - private var faceImage: String // 복수 + private var codeName: String, + private var age: Int, + private var job: String, + private var alcohol: String, + private var smoke: String, + private var hobby: String, // 복수 + private var style: String, // 복수 + private var bigCity: String, + private var smallCity: String, + private var mbti: String, + private var introduce: String, + private var codeImage: String, // 복수 + private var faceImage: String, // 복수 ) { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0 diff --git a/src/main/kotlin/codel/presentation/MemberController.kt b/src/main/kotlin/codel/presentation/MemberController.kt index 8e578abe..0f9089ae 100644 --- a/src/main/kotlin/codel/presentation/MemberController.kt +++ b/src/main/kotlin/codel/presentation/MemberController.kt @@ -4,19 +4,17 @@ import codel.business.MemberService import codel.presentation.request.MemberSavedRequest import codel.presentation.response.MemberSavedResponse import org.springframework.http.ResponseEntity -import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController @RestController class MemberController( - private val memberService: MemberService + private val memberService: MemberService, ) { - @PostMapping("/v1/auth/login") fun saveMember( - @RequestBody request: MemberSavedRequest + @RequestBody request: MemberSavedRequest, ): ResponseEntity { val memberSavedResponse = memberService.saveMember(request) diff --git a/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt b/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt index 7c39612a..2811392a 100644 --- a/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt +++ b/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt @@ -3,6 +3,6 @@ package codel.presentation.request import codel.domain.OauthType data class MemberSavedRequest( - val oauthType: OauthType, - val oauthId: String + val oauthType: OauthType, + val oauthId: String, ) diff --git a/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt b/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt index fed8a960..1fa228dc 100644 --- a/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt +++ b/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt @@ -1,5 +1,5 @@ package codel.presentation.response data class MemberSavedResponse( - val isUser: Boolean + val isUser: Boolean, ) diff --git a/src/test/kotlin/codel/CodelApplicationTests.kt b/src/test/kotlin/codel/CodelApplicationTests.kt index f5a1ea6b..276623cc 100644 --- a/src/test/kotlin/codel/CodelApplicationTests.kt +++ b/src/test/kotlin/codel/CodelApplicationTests.kt @@ -5,9 +5,7 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class CodelApplicationTests { - - @Test - fun contextLoads() { - } - + @Test + fun contextLoads() { + } } diff --git a/src/test/kotlin/codel/business/MemberServiceTest.kt b/src/test/kotlin/codel/business/MemberServiceTest.kt index 93f2ce33..0a1e05dd 100644 --- a/src/test/kotlin/codel/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/business/MemberServiceTest.kt @@ -10,10 +10,9 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class MemberServiceTest( - @Autowired - private val memberService: MemberService + @Autowired + private val memberService: MemberService, ) { - @DisplayName("멤버 중복 저장 테스트") @Test fun saveMemberSuccessTest() { diff --git a/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt b/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt index 90208dcc..9856029c 100644 --- a/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt +++ b/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt @@ -10,11 +10,10 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.dao.DataIntegrityViolationException @SpringBootTest -class MemberJpaRepositoryTest ( - @Autowired - private val memberJpaRepository: MemberJpaRepository -){ - +class MemberJpaRepositoryTest( + @Autowired + private val memberJpaRepository: MemberJpaRepository, +) { @DisplayName("중복된 멤버에 대해 유니크 제약 조건을 발생시킨다.") @Test fun saveMemberTest() { diff --git a/src/test/kotlin/codel/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/presentation/MemberControllerTest.kt index e10338a8..64b12747 100644 --- a/src/test/kotlin/codel/presentation/MemberControllerTest.kt +++ b/src/test/kotlin/codel/presentation/MemberControllerTest.kt @@ -15,6 +15,7 @@ import org.springframework.boot.test.web.server.LocalServerPort class MemberControllerTest() { @LocalServerPort var port: Int = 0 + @BeforeEach fun setup() { RestAssured.port = port @@ -23,38 +24,39 @@ class MemberControllerTest() { @DisplayName("Member 테스트") @Test fun saveMember() { - val request = mapOf( + val request = + mapOf( "oauthType" to "APPLE", - "oauthId" to "hogee" - ) + "oauthId" to "hogee", + ) val newUserResponse = MemberSavedResponse(false) given() - .contentType(ContentType.JSON) - .body(request) - .`when`() - .post("/v1/auth/login") - .then() - .statusCode(200) - .extract() - .`as`(MemberSavedResponse::class.java) - .also { response -> - assertEquals(newUserResponse.isUser, response.isUser) - } + .contentType(ContentType.JSON) + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + .extract() + .`as`(MemberSavedResponse::class.java) + .also { response -> + assertEquals(newUserResponse.isUser, response.isUser) + } val duplicatedUserResponse = MemberSavedResponse(true) given() - .contentType(ContentType.JSON) - .body(request) - .`when`() - .post("/v1/auth/login") - .then() - .statusCode(200) - .extract() - .`as`(MemberSavedResponse::class.java) - .also { response -> - assertEquals(duplicatedUserResponse.isUser, response.isUser) - } + .contentType(ContentType.JSON) + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + .extract() + .`as`(MemberSavedResponse::class.java) + .also { response -> + assertEquals(duplicatedUserResponse.isUser, response.isUser) + } } } From 979a98ee204cd172d95fdd22cca366728c98597d Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:05:10 +0900 Subject: [PATCH 008/542] =?UTF-8?q?[Feature/#5]=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B2=80=EC=A6=9D/=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: 패키지 구조 변경 Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> * chore: ktlint plugIn 으로 적용 Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> * feature: 토큰 검증 로직 생성 Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> * feature: 토큰 발급 로직 생성 Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> * feat: MemberStatus 분기 생성 Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --------- Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .gitignore | 1 + build.gradle.kts | 14 ++- src/main/kotlin/codel/auth/TokenProvider.kt | 71 +++++++++++ .../kotlin/codel/auth/business/AuthService.kt | 17 +++ .../codel/auth/exception/AuthException.kt | 9 ++ .../codel/config/filter/JwtAuthFilter.kt | 47 +++++++ src/main/kotlin/codel/domain/Member.kt | 9 -- .../kotlin/codel/domain/MemberRepository.kt | 19 --- .../infrastructure/MemberJpaRepository.kt | 8 -- .../{ => member}/business/MemberService.kt | 14 +-- src/main/kotlin/codel/member/domain/Member.kt | 10 ++ .../codel/member/domain/MemberRepository.kt | 28 +++++ .../codel/member/domain/MemberStatus.kt | 9 ++ .../codel/{ => member}/domain/OauthType.kt | 2 +- .../codel/{ => member}/domain/Profile.kt | 2 +- .../infrastructure/MemberJpaRepository.kt | 14 +++ .../infrastructure/entity/MemberEntity.kt | 19 ++- .../infrastructure/entity/ProfileEntity.kt | 2 +- .../presentation/MemberController.kt | 17 ++- .../request/MemberSavedRequest.kt | 4 +- .../response/MemberSavedResponse.kt | 7 ++ .../response/MemberSavedResponse.kt | 5 - src/main/resources/application.properties | 1 - .../kotlin/codel/auth/TokenProviderTest.kt | 24 ++++ src/test/kotlin/codel/config/TestFixture.kt | 32 +++++ .../business/MemberServiceTest.kt | 15 ++- .../infrastructure/MemberJpaRepositoryTest.kt | 11 +- .../presentation/MemberControllerTest.kt | 115 ++++++++++++++++++ .../presentation/MemberControllerTest.kt | 62 ---------- 29 files changed, 447 insertions(+), 141 deletions(-) create mode 100644 src/main/kotlin/codel/auth/TokenProvider.kt create mode 100644 src/main/kotlin/codel/auth/business/AuthService.kt create mode 100644 src/main/kotlin/codel/auth/exception/AuthException.kt create mode 100644 src/main/kotlin/codel/config/filter/JwtAuthFilter.kt delete mode 100644 src/main/kotlin/codel/domain/Member.kt delete mode 100644 src/main/kotlin/codel/domain/MemberRepository.kt delete mode 100644 src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt rename src/main/kotlin/codel/{ => member}/business/MemberService.kt (50%) create mode 100644 src/main/kotlin/codel/member/domain/Member.kt create mode 100644 src/main/kotlin/codel/member/domain/MemberRepository.kt create mode 100644 src/main/kotlin/codel/member/domain/MemberStatus.kt rename src/main/kotlin/codel/{ => member}/domain/OauthType.kt (63%) rename src/main/kotlin/codel/{ => member}/domain/Profile.kt (93%) create mode 100644 src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt rename src/main/kotlin/codel/{ => member}/infrastructure/entity/MemberEntity.kt (54%) rename src/main/kotlin/codel/{ => member}/infrastructure/entity/ProfileEntity.kt (94%) rename src/main/kotlin/codel/{ => member}/presentation/MemberController.kt (53%) rename src/main/kotlin/codel/{ => member}/presentation/request/MemberSavedRequest.kt (53%) create mode 100644 src/main/kotlin/codel/member/presentation/response/MemberSavedResponse.kt delete mode 100644 src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt delete mode 100644 src/main/resources/application.properties create mode 100644 src/test/kotlin/codel/auth/TokenProviderTest.kt create mode 100644 src/test/kotlin/codel/config/TestFixture.kt rename src/test/kotlin/codel/{ => member}/business/MemberServiceTest.kt (55%) rename src/test/kotlin/codel/{ => member}/infrastructure/MemberJpaRepositoryTest.kt (68%) create mode 100644 src/test/kotlin/codel/member/presentation/MemberControllerTest.kt delete mode 100644 src/test/kotlin/codel/presentation/MemberControllerTest.kt diff --git a/.gitignore b/.gitignore index b09902e1..72f48a02 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ gradle/ +src/main/resources/*.yml ### STS ### .apt_generated diff --git a/build.gradle.kts b/build.gradle.kts index f3b0ad24..75d51300 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,6 @@ plugins { id("org.springframework.boot") version "3.4.3" id("io.spring.dependency-management") version "1.1.7" kotlin("plugin.jpa") version "1.9.25" - id("org.jlleitschuh.gradle.ktlint") version "12.2.0" } group = "codel" @@ -21,15 +20,24 @@ repositories { } dependencies { - implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") + + // db + implementation("org.springframework.boot:spring-boot-starter-data-jpa") runtimeOnly("com.h2database:h2") + + // test + testImplementation("io.rest-assured:rest-assured:5.3.1") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("io.rest-assured:rest-assured:5.3.1") + + // jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") } kotlin { diff --git a/src/main/kotlin/codel/auth/TokenProvider.kt b/src/main/kotlin/codel/auth/TokenProvider.kt new file mode 100644 index 00000000..ed60a1b6 --- /dev/null +++ b/src/main/kotlin/codel/auth/TokenProvider.kt @@ -0,0 +1,71 @@ +package codel.auth + +import codel.auth.exception.AuthException +import codel.member.domain.Member +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.Keys +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import java.security.Key +import java.util.Date + +@Component +class TokenProvider( + @Value("\${security.jwt.token.secret-key}") + private val secretKey: String, + @Value("\${security.jwt.token.expire-length}") + private val validityInMilliseconds: Long, +) { + companion object { + private const val MEMBER_ID_CLAIM_KEY = "id" + private const val SOCIAL_LOGIN_ID_CLAIM_KEY = "oauthId" + private const val OAUTH_TYPE = "oauthType" + } + + private val key: Key + get() = Keys.hmacShaKeyFor(secretKey.toByteArray()) + + fun provide(member: Member): String { + val now = Date() + val validity = Date(now.time + validityInMilliseconds) + + return Jwts + .builder() + .claim(MEMBER_ID_CLAIM_KEY, member.id) + .claim(SOCIAL_LOGIN_ID_CLAIM_KEY, member.oauthId) + .claim(OAUTH_TYPE, member.oauthType) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(key, SignatureAlgorithm.HS256) + .compact() + } + + fun validateToken(token: String): Boolean = + runCatching { + val claims = getPayload(token) + validateExpireTime(claims) + }.isSuccess + + private fun getPayload(token: String): Claims = + try { + Jwts + .parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .body + } catch (e: Exception) { + throw AuthException(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다.") + } + + private fun validateExpireTime(claims: Claims) { + if (claims.expiration.before(Date())) { + throw AuthException(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다.") + } + } + + fun extractMemberId(token: String): Long = getPayload(token)[MEMBER_ID_CLAIM_KEY].toString().toLong() +} diff --git a/src/main/kotlin/codel/auth/business/AuthService.kt b/src/main/kotlin/codel/auth/business/AuthService.kt new file mode 100644 index 00000000..f2dcc68c --- /dev/null +++ b/src/main/kotlin/codel/auth/business/AuthService.kt @@ -0,0 +1,17 @@ +package codel.auth.business + +import codel.auth.TokenProvider +import codel.member.domain.MemberRepository +import codel.member.presentation.request.MemberSavedRequest +import org.springframework.stereotype.Service + +@Service +class AuthService( + val tokenProvider: TokenProvider, + val memberRepository: MemberRepository, +) { + fun provideToken(request: MemberSavedRequest): String { + val member = memberRepository.findMember(request.oauthType, request.oauthId) + return tokenProvider.provide(member) + } +} diff --git a/src/main/kotlin/codel/auth/exception/AuthException.kt b/src/main/kotlin/codel/auth/exception/AuthException.kt new file mode 100644 index 00000000..564edb9b --- /dev/null +++ b/src/main/kotlin/codel/auth/exception/AuthException.kt @@ -0,0 +1,9 @@ +package codel.auth.exception + +import org.springframework.http.HttpStatus + +class AuthException( + val statusCode: HttpStatus, + override val message: String, + override val cause: Throwable? = null, +) : RuntimeException(message, cause) diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt new file mode 100644 index 00000000..d10f6751 --- /dev/null +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -0,0 +1,47 @@ +package codel.config.filter + +import codel.auth.TokenProvider +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtAuthFilter( + private val tokenProvider: TokenProvider, +) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + if (request.requestURI == "/v1/auth/login") { + filterChain.doFilter(request, response) + return + } + + val token = resolveToken(request) + + if (token == null || !tokenProvider.validateToken(token)) { + response.status = HttpServletResponse.SC_UNAUTHORIZED + response.contentType = "application/json" + response.characterEncoding = "UTF-8" + response.writer.write("""{"message": "인증되지 않은 사용자입니다."}""") + return + } + val memberId = tokenProvider.extractMemberId(token) + request.setAttribute("memberId", memberId) + + filterChain.doFilter(request, response) + } + + private fun resolveToken(request: HttpServletRequest): String? { + val bearer = request.getHeader("Authorization") + return if (bearer != null && bearer.startsWith("Bearer ")) { + bearer.substring(7) + } else { + null + } + } +} diff --git a/src/main/kotlin/codel/domain/Member.kt b/src/main/kotlin/codel/domain/Member.kt deleted file mode 100644 index 7b35bf62..00000000 --- a/src/main/kotlin/codel/domain/Member.kt +++ /dev/null @@ -1,9 +0,0 @@ -package codel.domain - -class Member( - val oauthType: OauthType, - val oauthId: String, -) { - val id: Long = 0 - val profile: Profile? = null -} diff --git a/src/main/kotlin/codel/domain/MemberRepository.kt b/src/main/kotlin/codel/domain/MemberRepository.kt deleted file mode 100644 index 75f948a5..00000000 --- a/src/main/kotlin/codel/domain/MemberRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package codel.domain - -import codel.infrastructure.MemberJpaRepository -import codel.infrastructure.entity.MemberEntity -import org.springframework.dao.DataIntegrityViolationException -import org.springframework.stereotype.Component - -@Component -class MemberRepository( - private val memberJpaRepository: MemberJpaRepository, -) { - fun saveMember(member: Member): Boolean = - try { - memberJpaRepository.save(MemberEntity.toEntity(member)) - false - } catch (e: DataIntegrityViolationException) { - true - } -} diff --git a/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt b/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt deleted file mode 100644 index 717da404..00000000 --- a/src/main/kotlin/codel/infrastructure/MemberJpaRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package codel.infrastructure - -import codel.infrastructure.entity.MemberEntity -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository - -@Repository -interface MemberJpaRepository : JpaRepository diff --git a/src/main/kotlin/codel/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt similarity index 50% rename from src/main/kotlin/codel/business/MemberService.kt rename to src/main/kotlin/codel/member/business/MemberService.kt index da496c26..df934f1f 100644 --- a/src/main/kotlin/codel/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -1,9 +1,9 @@ -package codel.business +package codel.member.business -import codel.domain.Member -import codel.domain.MemberRepository -import codel.presentation.request.MemberSavedRequest -import codel.presentation.response.MemberSavedResponse +import codel.member.domain.Member +import codel.member.domain.MemberRepository +import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.response.MemberSavedResponse import org.springframework.stereotype.Service @Service @@ -16,8 +16,8 @@ class MemberService( oauthType = request.oauthType, oauthId = request.oauthId, ) - val isUser = memberRepository.saveMember(member) + val savedMember = memberRepository.saveMember(member) - return MemberSavedResponse(isUser) + return MemberSavedResponse(savedMember.memberStatus) } } diff --git a/src/main/kotlin/codel/member/domain/Member.kt b/src/main/kotlin/codel/member/domain/Member.kt new file mode 100644 index 00000000..76e944af --- /dev/null +++ b/src/main/kotlin/codel/member/domain/Member.kt @@ -0,0 +1,10 @@ +package codel.member.domain + +class Member( + val id: Long? = null, + val oauthType: OauthType, + val oauthId: String, + val memberStatus: MemberStatus = MemberStatus.SIGNUP, +) { + val profile: Profile? = null +} diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt new file mode 100644 index 00000000..aaa6a18e --- /dev/null +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -0,0 +1,28 @@ +package codel.member.domain + +import codel.member.infrastructure.MemberJpaRepository +import codel.member.infrastructure.entity.MemberEntity +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.stereotype.Component + +@Component +class MemberRepository( + private val memberJpaRepository: MemberJpaRepository, +) { + fun saveMember(member: Member): Member = + try { + val memberEntity = memberJpaRepository.save(MemberEntity.toEntity(member)) + memberEntity.toDomain() + } catch (e: DataIntegrityViolationException) { + val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(member.oauthType, member.oauthId) + memberEntity.toDomain() + } + + fun findMember( + oauthType: OauthType, + oauthId: String, + ): Member { + val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(oauthType, oauthId) + return memberEntity.toDomain() + } +} diff --git a/src/main/kotlin/codel/member/domain/MemberStatus.kt b/src/main/kotlin/codel/member/domain/MemberStatus.kt new file mode 100644 index 00000000..2d516c27 --- /dev/null +++ b/src/main/kotlin/codel/member/domain/MemberStatus.kt @@ -0,0 +1,9 @@ +package codel.member.domain + +enum class MemberStatus { + SIGNUP, + CODE_SURVEY, + CODE_PROFILE_IMAGE, + PENDING, + DONE, +} diff --git a/src/main/kotlin/codel/domain/OauthType.kt b/src/main/kotlin/codel/member/domain/OauthType.kt similarity index 63% rename from src/main/kotlin/codel/domain/OauthType.kt rename to src/main/kotlin/codel/member/domain/OauthType.kt index dee8aff1..3bb4a6c0 100644 --- a/src/main/kotlin/codel/domain/OauthType.kt +++ b/src/main/kotlin/codel/member/domain/OauthType.kt @@ -1,4 +1,4 @@ -package codel.domain +package codel.member.domain enum class OauthType { KAKAO, diff --git a/src/main/kotlin/codel/domain/Profile.kt b/src/main/kotlin/codel/member/domain/Profile.kt similarity index 93% rename from src/main/kotlin/codel/domain/Profile.kt rename to src/main/kotlin/codel/member/domain/Profile.kt index 45714acf..463104b6 100644 --- a/src/main/kotlin/codel/domain/Profile.kt +++ b/src/main/kotlin/codel/member/domain/Profile.kt @@ -1,4 +1,4 @@ -package codel.domain +package codel.member.domain class Profile( val codeName: String, diff --git a/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt new file mode 100644 index 00000000..367c7333 --- /dev/null +++ b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt @@ -0,0 +1,14 @@ +package codel.member.infrastructure + +import codel.member.domain.OauthType +import codel.member.infrastructure.entity.MemberEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MemberJpaRepository : JpaRepository { + fun findByOauthTypeAndOauthId( + oauthType: OauthType, + oauthId: String, + ): MemberEntity +} diff --git a/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt similarity index 54% rename from src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt rename to src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index d06a55fb..7a6806f1 100644 --- a/src/main/kotlin/codel/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -1,7 +1,10 @@ -package codel.infrastructure.entity +@file:Suppress("ktlint:standard:no-wildcard-imports") -import codel.domain.Member -import codel.domain.OauthType +package codel.member.infrastructure.entity + +import codel.member.domain.Member +import codel.member.domain.MemberStatus +import codel.member.domain.OauthType import jakarta.persistence.* @Entity @@ -13,6 +16,7 @@ import jakarta.persistence.* class MemberEntity( private var oauthType: OauthType, private var oauthId: String, + private var memberStatus: MemberStatus, ) { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -26,6 +30,15 @@ class MemberEntity( MemberEntity( oauthType = member.oauthType, oauthId = member.oauthId, + memberStatus = member.memberStatus, ) } + + fun toDomain(): Member = + Member( + id = this.id, + oauthType = this.oauthType, + oauthId = this.oauthId, + memberStatus = this.memberStatus, + ) } diff --git a/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt similarity index 94% rename from src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt rename to src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt index 668586be..43b9366e 100644 --- a/src/main/kotlin/codel/infrastructure/entity/ProfileEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt @@ -1,4 +1,4 @@ -package codel.infrastructure.entity +package codel.member.infrastructure.entity import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue diff --git a/src/main/kotlin/codel/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt similarity index 53% rename from src/main/kotlin/codel/presentation/MemberController.kt rename to src/main/kotlin/codel/member/presentation/MemberController.kt index 0f9089ae..7cb89265 100644 --- a/src/main/kotlin/codel/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -1,8 +1,9 @@ -package codel.presentation +package codel.member.presentation -import codel.business.MemberService -import codel.presentation.request.MemberSavedRequest -import codel.presentation.response.MemberSavedResponse +import codel.auth.business.AuthService +import codel.member.business.MemberService +import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.response.MemberSavedResponse import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -11,13 +12,17 @@ import org.springframework.web.bind.annotation.RestController @RestController class MemberController( private val memberService: MemberService, + private val authService: AuthService, ) { @PostMapping("/v1/auth/login") fun saveMember( @RequestBody request: MemberSavedRequest, ): ResponseEntity { val memberSavedResponse = memberService.saveMember(request) - - return ResponseEntity.ok(memberSavedResponse) + val token = authService.provideToken(request) + return ResponseEntity + .ok() + .header("Authorization", "Bearer $token") + .body(memberSavedResponse) } } diff --git a/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt b/src/main/kotlin/codel/member/presentation/request/MemberSavedRequest.kt similarity index 53% rename from src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt rename to src/main/kotlin/codel/member/presentation/request/MemberSavedRequest.kt index 2811392a..a718e4cd 100644 --- a/src/main/kotlin/codel/presentation/request/MemberSavedRequest.kt +++ b/src/main/kotlin/codel/member/presentation/request/MemberSavedRequest.kt @@ -1,6 +1,6 @@ -package codel.presentation.request +package codel.member.presentation.request -import codel.domain.OauthType +import codel.member.domain.OauthType data class MemberSavedRequest( val oauthType: OauthType, diff --git a/src/main/kotlin/codel/member/presentation/response/MemberSavedResponse.kt b/src/main/kotlin/codel/member/presentation/response/MemberSavedResponse.kt new file mode 100644 index 00000000..39531e59 --- /dev/null +++ b/src/main/kotlin/codel/member/presentation/response/MemberSavedResponse.kt @@ -0,0 +1,7 @@ +package codel.member.presentation.response + +import codel.member.domain.MemberStatus + +data class MemberSavedResponse( + val memberStatus: MemberStatus, +) diff --git a/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt b/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt deleted file mode 100644 index 1fa228dc..00000000 --- a/src/main/kotlin/codel/presentation/response/MemberSavedResponse.kt +++ /dev/null @@ -1,5 +0,0 @@ -package codel.presentation.response - -data class MemberSavedResponse( - val isUser: Boolean, -) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index b1f198a0..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=reunion diff --git a/src/test/kotlin/codel/auth/TokenProviderTest.kt b/src/test/kotlin/codel/auth/TokenProviderTest.kt new file mode 100644 index 00000000..ef72bf04 --- /dev/null +++ b/src/test/kotlin/codel/auth/TokenProviderTest.kt @@ -0,0 +1,24 @@ +package codel.auth + +import codel.config.TestFixture +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class TokenProviderTest : TestFixture() { + @DisplayName("토큰 정상 생성 테스트") + @Test + fun provideTest() { + val token = tokenProvider.provide(hogee) + + Assertions.assertThat(token).isNotNull() + } + + @DisplayName("토큰 유효성 검증 테스트") + @Test + fun validateTokenTest() { + val isToken = tokenProvider.validateToken("hogee") + + Assertions.assertThat(isToken).isFalse() + } +} diff --git a/src/test/kotlin/codel/config/TestFixture.kt b/src/test/kotlin/codel/config/TestFixture.kt new file mode 100644 index 00000000..8c4c1af5 --- /dev/null +++ b/src/test/kotlin/codel/config/TestFixture.kt @@ -0,0 +1,32 @@ +package codel.config + +import codel.auth.TokenProvider +import codel.member.domain.Member +import codel.member.domain.MemberRepository +import codel.member.domain.OauthType +import org.junit.jupiter.api.BeforeEach +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class TestFixture { + lateinit var hogee: Member + lateinit var token: String + + @Autowired + lateinit var tokenProvider: TokenProvider + + @Autowired + lateinit var memberRepository: MemberRepository + + @BeforeEach + fun setUp() { + hogee = + Member( + oauthType = OauthType.APPLE, + oauthId = "hogee", + ) + memberRepository.saveMember(hogee) + token = tokenProvider.provide(hogee) + } +} diff --git a/src/test/kotlin/codel/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt similarity index 55% rename from src/test/kotlin/codel/business/MemberServiceTest.kt rename to src/test/kotlin/codel/member/business/MemberServiceTest.kt index 0a1e05dd..f990b507 100644 --- a/src/test/kotlin/codel/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -1,7 +1,8 @@ -package codel.business +package codel.member.business -import codel.domain.OauthType -import codel.presentation.request.MemberSavedRequest +import codel.member.domain.MemberStatus +import codel.member.domain.OauthType +import codel.member.presentation.request.MemberSavedRequest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -13,15 +14,13 @@ class MemberServiceTest( @Autowired private val memberService: MemberService, ) { - @DisplayName("멤버 중복 저장 테스트") + @DisplayName("첫 로그인을 한 멤버의 상태는 SignUp 이다.") @Test fun saveMemberSuccessTest() { val memberSavedRequest = MemberSavedRequest(OauthType.KAKAO, "hogee") - val newUser = memberService.saveMember(memberSavedRequest).isUser - val duplicatedUser = memberService.saveMember(memberSavedRequest).isUser + val memberStatus = memberService.saveMember(memberSavedRequest).memberStatus - assertThat(newUser).isFalse() - assertThat(duplicatedUser).isTrue() + assertThat(memberStatus).isEqualTo(MemberStatus.SIGNUP) } } diff --git a/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt similarity index 68% rename from src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt rename to src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt index 9856029c..8ada045b 100644 --- a/src/test/kotlin/codel/infrastructure/MemberJpaRepositoryTest.kt +++ b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt @@ -1,7 +1,8 @@ -package codel.infrastructure +package codel.member.infrastructure -import codel.domain.OauthType -import codel.infrastructure.entity.MemberEntity +import codel.member.domain.MemberStatus +import codel.member.domain.OauthType +import codel.member.infrastructure.entity.MemberEntity import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -17,8 +18,8 @@ class MemberJpaRepositoryTest( @DisplayName("중복된 멤버에 대해 유니크 제약 조건을 발생시킨다.") @Test fun saveMemberTest() { - val memberEntity1 = MemberEntity(OauthType.APPLE, "hogee") - val memberEntity2 = MemberEntity(OauthType.APPLE, "hogee") + val memberEntity1 = MemberEntity(OauthType.APPLE, "hoho", MemberStatus.SIGNUP) + val memberEntity2 = MemberEntity(OauthType.APPLE, "hoho", MemberStatus.SIGNUP) memberJpaRepository.save(memberEntity1) Assertions.assertThrows(DataIntegrityViolationException::class.java) { diff --git a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt new file mode 100644 index 00000000..0009f3dc --- /dev/null +++ b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt @@ -0,0 +1,115 @@ +package codel.member.presentation + +import codel.config.TestFixture +import codel.member.domain.MemberStatus +import codel.member.presentation.response.MemberSavedResponse +import io.restassured.RestAssured +import io.restassured.RestAssured.given +import io.restassured.http.ContentType +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class MemberControllerTest : TestFixture() { + @LocalServerPort + var port: Int = 0 + + @BeforeEach + fun setup() { + RestAssured.port = port + } + + @DisplayName("Member 중복 로그인 테스트") + @Test + fun saveMember() { + val request = + mapOf( + "oauthType" to "APPLE", + "oauthId" to "hogee", + ) + val expectedResponse = MemberSavedResponse(MemberStatus.SIGNUP) + + val token = + given() + .contentType(ContentType.JSON) + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + .extract() + .header("Authorization") + .removePrefix("Bearer ") + + val response = + given() + .contentType(ContentType.JSON) + .header("Authorization", "Bearer $token") + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + .extract() + .`as`(MemberSavedResponse::class.java) + + Assertions.assertThat(expectedResponse.memberStatus).isEqualTo(response.memberStatus) + } + + @DisplayName("허용된 URL은 인증 없이 접근 가능해야 한다") + @Test + fun excludePathTest() { + val request = + mapOf( + "oauthType" to "APPLE", + "oauthId" to "hogee", + ) + + given() + .contentType(ContentType.JSON) + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + } + + @DisplayName("토큰 없으면 401을 응답해야 한다") + @Test + fun unAuthorizeUserTest() { + given() + .contentType(ContentType.JSON) + .`when`() + .get("/v1/user") + .then() + .statusCode(401) + } + + @DisplayName("로그인 성공 시 accessToken을 발급한다") + @Test + fun issueTokenOnLoginSuccess() { + val request = + mapOf( + "oauthType" to "APPLE", + "oauthId" to "hogee", + ) + + val response = + given() + .contentType(ContentType.JSON) + .body(request) + .`when`() + .post("/v1/auth/login") + .then() + .statusCode(200) + .extract() + + val authHeader = response.header("Authorization") + assertTrue(authHeader != null && authHeader.startsWith("Bearer ")) + } +} diff --git a/src/test/kotlin/codel/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/presentation/MemberControllerTest.kt deleted file mode 100644 index 64b12747..00000000 --- a/src/test/kotlin/codel/presentation/MemberControllerTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package codel.presentation - -import codel.presentation.response.MemberSavedResponse -import io.restassured.RestAssured -import io.restassured.RestAssured.given -import io.restassured.http.ContentType -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.server.LocalServerPort - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class MemberControllerTest() { - @LocalServerPort - var port: Int = 0 - - @BeforeEach - fun setup() { - RestAssured.port = port - } - - @DisplayName("Member 테스트") - @Test - fun saveMember() { - val request = - mapOf( - "oauthType" to "APPLE", - "oauthId" to "hogee", - ) - val newUserResponse = MemberSavedResponse(false) - - given() - .contentType(ContentType.JSON) - .body(request) - .`when`() - .post("/v1/auth/login") - .then() - .statusCode(200) - .extract() - .`as`(MemberSavedResponse::class.java) - .also { response -> - assertEquals(newUserResponse.isUser, response.isUser) - } - - val duplicatedUserResponse = MemberSavedResponse(true) - - given() - .contentType(ContentType.JSON) - .body(request) - .`when`() - .post("/v1/auth/login") - .then() - .statusCode(200) - .extract() - .`as`(MemberSavedResponse::class.java) - .also { response -> - assertEquals(duplicatedUserResponse.isUser, response.isUser) - } - } -} From 1177a410e5931559cd1beb94402a7c72f14aa8f6 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:33:00 +0900 Subject: [PATCH 009/542] =?UTF-8?q?feat:=20memberControllerSwagger=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: memberControllerSwagger 명세 추가 Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> * feat: swagger configuration 추가 Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --------- Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- build.gradle.kts | 3 ++ src/main/kotlin/codel/config/SwaggerConfig.kt | 30 +++++++++++++++++++ .../codel/config/filter/JwtAuthFilter.kt | 12 +++++++- .../member/presentation/MemberController.kt | 5 ++-- .../swagger/MemberControllerSwagger.kt | 25 ++++++++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/codel/config/SwaggerConfig.kt create mode 100644 src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt diff --git a/build.gradle.kts b/build.gradle.kts index 75d51300..eb39c634 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,9 @@ dependencies { implementation("io.jsonwebtoken:jjwt-api:0.11.5") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + + // swagger + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4") } kotlin { diff --git a/src/main/kotlin/codel/config/SwaggerConfig.kt b/src/main/kotlin/codel/config/SwaggerConfig.kt new file mode 100644 index 00000000..0f370a69 --- /dev/null +++ b/src/main/kotlin/codel/config/SwaggerConfig.kt @@ -0,0 +1,30 @@ +package codel.config + +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Contact +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.info.License +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class SwaggerConfig { + @Bean + fun openAPI(): OpenAPI = + OpenAPI() + .info( + Info() + .title("Code:L API") + .description("API 문서입니다.") + .version("v1.0.0") + .contact( + Contact() + .name("code") + .email("codel@gmail.com"), + ).license( + License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0"), + ), + ) +} diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt index d10f6751..3c5a7144 100644 --- a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -11,12 +11,22 @@ import org.springframework.web.filter.OncePerRequestFilter class JwtAuthFilter( private val tokenProvider: TokenProvider, ) : OncePerRequestFilter() { + companion object { + private val EXCLUDE_URIS = + listOf( + "/v1/auth/login", + "/v1/health", + "/swagger-ui/", + "/v3/api-docs", + ) + } + override fun doFilterInternal( request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain, ) { - if (request.requestURI == "/v1/auth/login") { + if (EXCLUDE_URIS.any { request.requestURI.startsWith(it) }) { filterChain.doFilter(request, response) return } diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 7cb89265..74e20dcc 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -4,6 +4,7 @@ import codel.auth.business.AuthService import codel.member.business.MemberService import codel.member.presentation.request.MemberSavedRequest import codel.member.presentation.response.MemberSavedResponse +import codel.member.presentation.swagger.MemberControllerSwagger import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -13,9 +14,9 @@ import org.springframework.web.bind.annotation.RestController class MemberController( private val memberService: MemberService, private val authService: AuthService, -) { +) : MemberControllerSwagger { @PostMapping("/v1/auth/login") - fun saveMember( + override fun saveMember( @RequestBody request: MemberSavedRequest, ): ResponseEntity { val memberSavedResponse = memberService.saveMember(request) diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt new file mode 100644 index 00000000..7871921e --- /dev/null +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -0,0 +1,25 @@ +package codel.member.presentation.swagger + +import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.response.MemberSavedResponse +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.RequestBody + +@Tag(name = "Member", description = "회원 관련 API") +interface MemberControllerSwagger { + @Operation(summary = "로그인 및 회원 저장 후 분기 반환", description = "소셜 로그인 정보를 기반으로 회원을 저장하고, JWT 토큰과 회원 분기를 반환합니다.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "성공적으로 로그인 및 회원 저장됨"), + ApiResponse(responseCode = "400", description = "요청 값이 잘못됨"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + fun saveMember( + @RequestBody request: MemberSavedRequest, + ): ResponseEntity +} From 0f644ef061ebdd8bd339a230286b5d3bf7653f2f Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:13:24 +0900 Subject: [PATCH 010/542] =?UTF-8?q?feat:=20gradle=20=ED=8C=8C=EC=9D=BC=20g?= =?UTF-8?q?itignore=20=EC=A0=9C=EA=B1=B0=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .gitignore | 1 - gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 7 +++++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties diff --git a/.gitignore b/.gitignore index 72f48a02..060a1cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ -gradle/ src/main/resources/*.yml ### STS ### diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..a4b76b9530d66f5e68d973ea569d8e19de379189 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e18bc253 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists From 446ca7e404ddc7794f4ffa18f903f2da8b742aa2 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:26:39 +0900 Subject: [PATCH 011/542] =?UTF-8?q?chore:=20ci=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..64267bbb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI - PR to develop + +on: + pull_request: + branches: [ develop ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Generate application-dev.yml + run: | + mkdir -p src/main/resources + cat < src/main/resources/application.yml + server: + port: 8080 + security: + jwt: + token: + expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} + secret-key: ${{ secrets.JWT_SECRET_KEY }} + EOF + + - name: Build with Gradle + run: ./gradlew build + + - name: Run tests + run: ./gradlew test From 606473de4175916a7fc11325b5ff2aa4a9117ce8 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 2 Apr 2025 14:30:30 +0900 Subject: [PATCH 012/542] =?UTF-8?q?feat:=20CodeImage,=20FaceImage=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../kotlin/codel/member/domain/CodeImage.kt | 5 +++ .../kotlin/codel/member/domain/FaceImage.kt | 5 +++ .../kotlin/codel/member/domain/Profile.kt | 2 -- .../infrastructure/entity/ProfileEntity.kt | 32 +++++++++++++++++-- 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/codel/member/domain/CodeImage.kt create mode 100644 src/main/kotlin/codel/member/domain/FaceImage.kt diff --git a/src/main/kotlin/codel/member/domain/CodeImage.kt b/src/main/kotlin/codel/member/domain/CodeImage.kt new file mode 100644 index 00000000..b79db006 --- /dev/null +++ b/src/main/kotlin/codel/member/domain/CodeImage.kt @@ -0,0 +1,5 @@ +package codel.member.domain + +class CodeImage( + val urls: List, +) diff --git a/src/main/kotlin/codel/member/domain/FaceImage.kt b/src/main/kotlin/codel/member/domain/FaceImage.kt new file mode 100644 index 00000000..a37c1f30 --- /dev/null +++ b/src/main/kotlin/codel/member/domain/FaceImage.kt @@ -0,0 +1,5 @@ +package codel.member.domain + +class FaceImage( + val urls: List, +) diff --git a/src/main/kotlin/codel/member/domain/Profile.kt b/src/main/kotlin/codel/member/domain/Profile.kt index 463104b6..595dfdfc 100644 --- a/src/main/kotlin/codel/member/domain/Profile.kt +++ b/src/main/kotlin/codel/member/domain/Profile.kt @@ -12,8 +12,6 @@ class Profile( val smallCity: String, val mbti: String, val introduce: String, - val codeImage: List, - val faceImage: List, ) { val id: Long = 0 } diff --git a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt index 43b9366e..5d6269f5 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt @@ -1,5 +1,6 @@ package codel.member.infrastructure.entity +import codel.member.domain.Profile import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue import jakarta.persistence.GenerationType @@ -18,10 +19,37 @@ class ProfileEntity( private var smallCity: String, private var mbti: String, private var introduce: String, - private var codeImage: String, // 복수 - private var faceImage: String, // 복수 + private var codeImage: String? = null, // 복수 + private var faceImage: String? = null, // 복수 ) { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0 + + companion object { + fun toEntity(profile: Profile): ProfileEntity = + ProfileEntity( + codeName = profile.codeName, + age = profile.age, + job = profile.job, + alcohol = profile.alcohol, + smoke = profile.smoke, + hobby = serializeAttribute(profile.hobby), + style = serializeAttribute(profile.style), + bigCity = profile.bigCity, + smallCity = profile.smallCity, + mbti = profile.mbti, + introduce = profile.introduce, + ) + + private fun serializeAttribute(attribute: List): String = attribute.joinToString(separator = ",") + } + + fun updateCodeImage(codeImages: List) { + codeImage = serializeAttribute(codeImages) + } + + fun updateFaceImage(faceImages: List) { + faceImage = serializeAttribute(faceImages) + } } From 6a8cfe5e8d51920b095053711a8bc02e526b866c Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 2 Apr 2025 14:30:57 +0900 Subject: [PATCH 013/542] =?UTF-8?q?feat:=20saveProfile=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- src/main/kotlin/codel/member/domain/MemberRepository.kt | 7 +++++++ .../codel/member/infrastructure/ProfileJpaRepository.kt | 6 ++++++ 2 files changed, 13 insertions(+) create mode 100644 src/main/kotlin/codel/member/infrastructure/ProfileJpaRepository.kt diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index aaa6a18e..deb1fd84 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -1,13 +1,16 @@ package codel.member.domain import codel.member.infrastructure.MemberJpaRepository +import codel.member.infrastructure.ProfileJpaRepository import codel.member.infrastructure.entity.MemberEntity +import codel.member.infrastructure.entity.ProfileEntity import org.springframework.dao.DataIntegrityViolationException import org.springframework.stereotype.Component @Component class MemberRepository( private val memberJpaRepository: MemberJpaRepository, + private val profileJpaRepository: ProfileJpaRepository, ) { fun saveMember(member: Member): Member = try { @@ -25,4 +28,8 @@ class MemberRepository( val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(oauthType, oauthId) return memberEntity.toDomain() } + + fun saveProfile(profile: Profile) { + profileJpaRepository.save(ProfileEntity.toEntity(profile)) + } } diff --git a/src/main/kotlin/codel/member/infrastructure/ProfileJpaRepository.kt b/src/main/kotlin/codel/member/infrastructure/ProfileJpaRepository.kt new file mode 100644 index 00000000..6612834f --- /dev/null +++ b/src/main/kotlin/codel/member/infrastructure/ProfileJpaRepository.kt @@ -0,0 +1,6 @@ +package codel.member.infrastructure + +import codel.member.infrastructure.entity.ProfileEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface ProfileJpaRepository : JpaRepository From 2d03a630eee4e62d9f0be07de64aadd64d3150fb Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 2 Apr 2025 14:31:13 +0900 Subject: [PATCH 014/542] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=83=9D=EC=84=B1=20api=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../codel/config/filter/JwtAuthFilter.kt | 2 +- .../codel/member/business/MemberService.kt | 20 +++++++++++++++++++ .../member/presentation/MemberController.kt | 11 +++++++++- .../request/ProfileSavedRequest.kt | 15 ++++++++++++++ .../swagger/MemberControllerSwagger.kt | 13 ++++++++++++ .../presentation/MemberControllerTest.kt | 8 ++++---- 6 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt index 3c5a7144..fc79ba33 100644 --- a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -14,7 +14,7 @@ class JwtAuthFilter( companion object { private val EXCLUDE_URIS = listOf( - "/v1/auth/login", + "/v1/member/login", "/v1/health", "/swagger-ui/", "/v3/api-docs", diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index df934f1f..7a5d64e1 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -2,7 +2,9 @@ package codel.member.business import codel.member.domain.Member import codel.member.domain.MemberRepository +import codel.member.domain.Profile import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberSavedResponse import org.springframework.stereotype.Service @@ -20,4 +22,22 @@ class MemberService( return MemberSavedResponse(savedMember.memberStatus) } + + fun saveProfile(request: ProfileSavedRequest) { + val profile = + Profile( + codeName = request.codeName, + age = request.age, + job = request.job, + alcohol = request.alcohol, + smoke = request.smoke, + hobby = request.hobby, + style = request.style, + bigCity = request.bigCity, + smallCity = request.smallCity, + mbti = request.mbti, + introduce = request.introduce, + ) + memberRepository.saveProfile(profile) + } } diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 74e20dcc..4db86be3 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -3,6 +3,7 @@ package codel.member.presentation import codel.auth.business.AuthService import codel.member.business.MemberService import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberSavedResponse import codel.member.presentation.swagger.MemberControllerSwagger import org.springframework.http.ResponseEntity @@ -15,7 +16,7 @@ class MemberController( private val memberService: MemberService, private val authService: AuthService, ) : MemberControllerSwagger { - @PostMapping("/v1/auth/login") + @PostMapping("/v1/member/login") override fun saveMember( @RequestBody request: MemberSavedRequest, ): ResponseEntity { @@ -26,4 +27,12 @@ class MemberController( .header("Authorization", "Bearer $token") .body(memberSavedResponse) } + + @PostMapping("/v1/member/profile") + override fun saveProfile( + @RequestBody request: ProfileSavedRequest, + ): ResponseEntity { + memberService.saveProfile(request) + return ResponseEntity.ok().build() + } } diff --git a/src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt b/src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt new file mode 100644 index 00000000..91e484de --- /dev/null +++ b/src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt @@ -0,0 +1,15 @@ +package codel.member.presentation.request + +data class ProfileSavedRequest( + val codeName: String, + val age: Int, + val job: String, + val alcohol: String, + val smoke: String, + val hobby: List, + val style: List, + val bigCity: String, + val smallCity: String, + val mbti: String, + val introduce: String, +) diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index 7871921e..6d3e3b09 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -1,6 +1,7 @@ package codel.member.presentation.swagger import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberSavedResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -22,4 +23,16 @@ interface MemberControllerSwagger { fun saveMember( @RequestBody request: MemberSavedRequest, ): ResponseEntity + + @Operation(summary = "이미지를 제외한 프로필 받기", description = "이미지를 제외한 프로필을 입력받습니다.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "프로필 성공적으로 저장됨"), + ApiResponse(responseCode = "400", description = "요청 값이 잘못됨"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + fun saveProfile( + @RequestBody request: ProfileSavedRequest, + ): ResponseEntity } diff --git a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt index 0009f3dc..9d102471 100644 --- a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt +++ b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt @@ -39,7 +39,7 @@ class MemberControllerTest : TestFixture() { .contentType(ContentType.JSON) .body(request) .`when`() - .post("/v1/auth/login") + .post("/v1/member/login") .then() .statusCode(200) .extract() @@ -52,7 +52,7 @@ class MemberControllerTest : TestFixture() { .header("Authorization", "Bearer $token") .body(request) .`when`() - .post("/v1/auth/login") + .post("/v1/member/login") .then() .statusCode(200) .extract() @@ -74,7 +74,7 @@ class MemberControllerTest : TestFixture() { .contentType(ContentType.JSON) .body(request) .`when`() - .post("/v1/auth/login") + .post("/v1/member/login") .then() .statusCode(200) } @@ -104,7 +104,7 @@ class MemberControllerTest : TestFixture() { .contentType(ContentType.JSON) .body(request) .`when`() - .post("/v1/auth/login") + .post("/v1/member/login") .then() .statusCode(200) .extract() From affcb17297269bdb3dfa878f0ee524d840e58a9e Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:54:41 +0900 Subject: [PATCH 015/542] =?UTF-8?q?chore:=20cd=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..9f306b0b --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,65 @@ +name: CD - Deploy to Dev + +on: + push: + branches: + - develop + +jobs: + deploy: + name: Deploy to Dev Server + runs-on: ubuntu-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Grant permission to gradlew + run: chmod +x ./gradlew + + - name: Generate application.yml + run: | + mkdir -p src/main/resources + cat < src/main/resources/application.yml + server: + port: 8080 + security: + jwt: + token: + expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} + secret-key: ${{ secrets.JWT_SECRET_KEY }} + EOF + + - name: Build without tests + run: ./gradlew clean build -x test + + - name: Upload jar to dev server + uses: appleboy/scp-action@v0.1.4 + with: + host: ${{ secrets.DEV_SERVER_HOST }} + username: ${{ secrets.DEV_SERVER_USER }} + key: ${{ secrets.DEV_SERVER_KEY }} + source: "build/libs/*.jar" + target: ${{ secrets.DEV_DEPLOY_PATH }} + + - name: Clean old jars and restart app + uses: appleboy/ssh-action@v0.1.10 + with: + host: ${{ secrets.DEV_SERVER_HOST }} + username: ${{ secrets.DEV_SERVER_USER }} + key: ${{ secrets.DEV_SERVER_KEY }} + script: | + echo "Stopping existing application..." + pkill -f 'java -jar' || true + echo "Removing old jar backups..." + rm -f ${{ secrets.DEV_DEPLOY_PATH }}/*.jar.old || true + echo "Backing up current jar..." + for f in ${{ secrets.DEV_DEPLOY_PATH }}/*.jar; do mv "$f" "$f.old"; done + echo "Starting new jar..." + nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/*.jar --spring.profiles.active=dev > app.log 2>&1 & From a385e68d157b25fba8162196bf699c0a7a4dace0 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:06:05 +0900 Subject: [PATCH 016/542] =?UTF-8?q?chore:=20cd=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=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.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9f306b0b..9fde2d3f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -62,4 +62,4 @@ jobs: echo "Backing up current jar..." for f in ${{ secrets.DEV_DEPLOY_PATH }}/*.jar; do mv "$f" "$f.old"; done echo "Starting new jar..." - nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/*.jar --spring.profiles.active=dev > app.log 2>&1 & + nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev > app.log 2>&1 & From 30a6b52402b5b236ce5822f83a0a2774e985c027 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:10:02 +0900 Subject: [PATCH 017/542] Update cd.yml --- .github/workflows/cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9fde2d3f..232a7fad 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -58,8 +58,8 @@ jobs: echo "Stopping existing application..." pkill -f 'java -jar' || true echo "Removing old jar backups..." - rm -f ${{ secrets.DEV_DEPLOY_PATH }}/*.jar.old || true + rm -f ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar.old || true echo "Backing up current jar..." - for f in ${{ secrets.DEV_DEPLOY_PATH }}/*.jar; do mv "$f" "$f.old"; done + for f in ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar; do mv "$f" "$f.old"; done echo "Starting new jar..." nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev > app.log 2>&1 & From d34e0b3bb5d35c7ea67aadc797df83c7b8fafd8a Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:21:25 +0900 Subject: [PATCH 018/542] Update cd.yml --- .github/workflows/cd.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 232a7fad..863ee25d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -57,9 +57,5 @@ jobs: script: | echo "Stopping existing application..." pkill -f 'java -jar' || true - echo "Removing old jar backups..." - rm -f ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar.old || true - echo "Backing up current jar..." - for f in ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar; do mv "$f" "$f.old"; done echo "Starting new jar..." - nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev > app.log 2>&1 & + nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & From a7e32bcb99d8077989c66faec3a3a248fac6bed7 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:30:01 +0900 Subject: [PATCH 019/542] Update cd.yml --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 863ee25d..d742947f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -58,4 +58,4 @@ jobs: echo "Stopping existing application..." pkill -f 'java -jar' || true echo "Starting new jar..." - nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & + setsid nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 < /dev/null & From cb0139e438805da7d19ced4d8e225351d47a8cf5 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:38:22 +0900 Subject: [PATCH 020/542] Update cd.yml --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d742947f..c5a9e56d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -58,4 +58,4 @@ jobs: echo "Stopping existing application..." pkill -f 'java -jar' || true echo "Starting new jar..." - setsid nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 < /dev/null & + nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & disown From 8668d72d8b6de0b5f00e68e6d8aa23ba23fa7e4f Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:42:09 +0900 Subject: [PATCH 021/542] Update cd.yml --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c5a9e56d..eb3ef9fd 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -58,4 +58,4 @@ jobs: echo "Stopping existing application..." pkill -f 'java -jar' || true echo "Starting new jar..." - nohup java -jar ${{ secrets.DEV_DEPLOY_PATH }}/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & disown + nohup java -jar ~/app/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & From 1fed9d333571155fb343b198a1b9efbdd2199606 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 15:49:57 +0900 Subject: [PATCH 022/542] Update cd.yml --- .github/workflows/cd.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index eb3ef9fd..81930041 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -58,4 +58,5 @@ jobs: echo "Stopping existing application..." pkill -f 'java -jar' || true echo "Starting new jar..." - nohup java -jar ~/app/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & + nohup bash -c "source /home/ubuntu/.bashrc && java -jar /home/ubuntu/app/build/libs/codel-0.0.1-SNAPSHOT.jar" > /home/ubuntu/app.log 2>&1 & + From b1466f9ee873071eaf2acd303bb30c8c5ec39c76 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 16:02:27 +0900 Subject: [PATCH 023/542] Update cd.yml --- .github/workflows/cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 81930041..d4ae0ebc 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -49,7 +49,7 @@ jobs: target: ${{ secrets.DEV_DEPLOY_PATH }} - name: Clean old jars and restart app - uses: appleboy/ssh-action@v0.1.10 + uses: garygrossgarten/github-action-ssh@release with: host: ${{ secrets.DEV_SERVER_HOST }} username: ${{ secrets.DEV_SERVER_USER }} @@ -58,5 +58,5 @@ jobs: echo "Stopping existing application..." pkill -f 'java -jar' || true echo "Starting new jar..." - nohup bash -c "source /home/ubuntu/.bashrc && java -jar /home/ubuntu/app/build/libs/codel-0.0.1-SNAPSHOT.jar" > /home/ubuntu/app.log 2>&1 & + nohup java -jar ~/app/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & From f9d4f0fdab701c3c09fce729a03d8485c54d97b4 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 16:23:00 +0900 Subject: [PATCH 024/542] Update cd.yml --- .github/workflows/cd.yml | 51 ++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d4ae0ebc..032c90ec 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,4 +1,4 @@ -name: CD - Deploy to Dev +name: CD - Docker Hub Deploy to Dev on: push: @@ -7,11 +7,10 @@ on: jobs: deploy: - name: Deploy to Dev Server runs-on: ubuntu-latest steps: - - name: Checkout source code + - name: Checkout code uses: actions/checkout@v3 - name: Set up JDK 17 @@ -36,27 +35,43 @@ jobs: secret-key: ${{ secrets.JWT_SECRET_KEY }} EOF - - name: Build without tests + - name: Build jar run: ./gradlew clean build -x test - - name: Upload jar to dev server - uses: appleboy/scp-action@v0.1.4 - with: - host: ${{ secrets.DEV_SERVER_HOST }} - username: ${{ secrets.DEV_SERVER_USER }} - key: ${{ secrets.DEV_SERVER_KEY }} - source: "build/libs/*.jar" - target: ${{ secrets.DEV_DEPLOY_PATH }} + - name: Build Docker image + run: docker build -t ${{ secrets.DOCKER_USERNAME }}/codel-app:latest . + + - name: Log in to Docker Hub + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - name: Clean old jars and restart app - uses: garygrossgarten/github-action-ssh@release + - name: Push image to Docker Hub + run: docker push ${{ secrets.DOCKER_USERNAME }}/codel-app:latest + + - name: Deploy to dev server (install docker + run) + uses: appleboy/ssh-action@v0.1.10 with: host: ${{ secrets.DEV_SERVER_HOST }} username: ${{ secrets.DEV_SERVER_USER }} key: ${{ secrets.DEV_SERVER_KEY }} script: | - echo "Stopping existing application..." - pkill -f 'java -jar' || true - echo "Starting new jar..." - nohup java -jar ~/app/build/libs/codel-0.0.1-SNAPSHOT.jar > app.log 2>&1 & + echo "🛠 Checking Docker..." + if ! command -v docker &> /dev/null + then + echo "📦 Docker not found. Installing..." + curl -fsSL https://get.docker.com | sudo bash + else + echo "✅ Docker already installed." + fi + + echo "🧼 Cleaning up old container..." + docker stop codel || true + docker rm codel || true + + echo "🐳 Pulling latest image from Docker Hub..." + docker pull ${{ secrets.DOCKER_USERNAME }}/codel-app:latest + + echo "🚀 Starting container..." + docker run -d --name codel -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/codel-app:latest + echo "📄 Tail log output:" + docker logs --tail=20 codel || true From 7c93cfd0fb8ca1cd3fb3bf63fe9cf909d8b4235b Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 2 Apr 2025 16:27:43 +0900 Subject: [PATCH 025/542] =?UTF-8?q?chore:=20dockerfile=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..8e29811a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:17-jdk-slim +ARG JAR_FILE=build/libs/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] From 80231d64fff1f45e9d92fad667ff37b70531eb4d Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 2 Apr 2025 16:51:03 +0900 Subject: [PATCH 026/542] Update cd.yml --- .github/workflows/cd.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 032c90ec..e0a12dd4 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -64,14 +64,14 @@ jobs: fi echo "🧼 Cleaning up old container..." - docker stop codel || true - docker rm codel || true + sudo docker stop codel || true + sudo docker rm codel || true echo "🐳 Pulling latest image from Docker Hub..." - docker pull ${{ secrets.DOCKER_USERNAME }}/codel-app:latest + sudo docker pull ${{ secrets.DOCKER_USERNAME }}/codel-app:latest echo "🚀 Starting container..." - docker run -d --name codel -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/codel-app:latest + sudo docker run -d --name codel -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/codel-app:latest echo "📄 Tail log output:" - docker logs --tail=20 codel || true + sudo docker logs --tail=20 codel || true From 2691e11e0a80fdc8ea420f516b548c2a003e6654 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Thu, 3 Apr 2025 20:15:09 +0900 Subject: [PATCH 027/542] =?UTF-8?q?refactor:=20=ED=95=84=EB=93=9C=EB=A5=BC?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EC=9E=90=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/main/kotlin/codel/auth/business/AuthService.kt | 4 ++-- .../kotlin/codel/member/business/MemberService.kt | 10 +++++----- src/main/kotlin/codel/member/domain/Member.kt | 5 ++--- .../kotlin/codel/member/domain/MemberRepository.kt | 2 +- src/main/kotlin/codel/member/domain/Profile.kt | 5 ++--- .../member/infrastructure/entity/MemberEntity.kt | 12 +++++------- .../member/infrastructure/entity/ProfileEntity.kt | 7 +++---- .../codel/member/presentation/MemberController.kt | 12 ++++++------ .../{MemberSavedRequest.kt => MemberLoginRequest.kt} | 2 +- ...MemberSavedResponse.kt => MemberLoginResponse.kt} | 2 +- .../presentation/swagger/MemberControllerSwagger.kt | 10 +++++----- 11 files changed, 33 insertions(+), 38 deletions(-) rename src/main/kotlin/codel/member/presentation/request/{MemberSavedRequest.kt => MemberLoginRequest.kt} (81%) rename src/main/kotlin/codel/member/presentation/response/{MemberSavedResponse.kt => MemberLoginResponse.kt} (79%) diff --git a/src/main/kotlin/codel/auth/business/AuthService.kt b/src/main/kotlin/codel/auth/business/AuthService.kt index f2dcc68c..d5b6bb27 100644 --- a/src/main/kotlin/codel/auth/business/AuthService.kt +++ b/src/main/kotlin/codel/auth/business/AuthService.kt @@ -2,7 +2,7 @@ package codel.auth.business import codel.auth.TokenProvider import codel.member.domain.MemberRepository -import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.MemberLoginRequest import org.springframework.stereotype.Service @Service @@ -10,7 +10,7 @@ class AuthService( val tokenProvider: TokenProvider, val memberRepository: MemberRepository, ) { - fun provideToken(request: MemberSavedRequest): String { + fun provideToken(request: MemberLoginRequest): String { val member = memberRepository.findMember(request.oauthType, request.oauthId) return tokenProvider.provide(member) } diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 7a5d64e1..743ba65e 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -3,24 +3,24 @@ package codel.member.business import codel.member.domain.Member import codel.member.domain.MemberRepository import codel.member.domain.Profile -import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest -import codel.member.presentation.response.MemberSavedResponse +import codel.member.presentation.response.MemberLoginResponse import org.springframework.stereotype.Service @Service class MemberService( private val memberRepository: MemberRepository, ) { - fun saveMember(request: MemberSavedRequest): MemberSavedResponse { + fun loginMember(request: MemberLoginRequest): MemberLoginResponse { val member = Member( oauthType = request.oauthType, oauthId = request.oauthId, ) - val savedMember = memberRepository.saveMember(member) + val loginMember = memberRepository.loginMember(member) - return MemberSavedResponse(savedMember.memberStatus) + return MemberLoginResponse(loginMember.memberStatus) } fun saveProfile(request: ProfileSavedRequest) { diff --git a/src/main/kotlin/codel/member/domain/Member.kt b/src/main/kotlin/codel/member/domain/Member.kt index 76e944af..661f2384 100644 --- a/src/main/kotlin/codel/member/domain/Member.kt +++ b/src/main/kotlin/codel/member/domain/Member.kt @@ -2,9 +2,8 @@ package codel.member.domain class Member( val id: Long? = null, + val profile: Profile? = null, val oauthType: OauthType, val oauthId: String, val memberStatus: MemberStatus = MemberStatus.SIGNUP, -) { - val profile: Profile? = null -} +) diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index deb1fd84..ab001336 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -12,7 +12,7 @@ class MemberRepository( private val memberJpaRepository: MemberJpaRepository, private val profileJpaRepository: ProfileJpaRepository, ) { - fun saveMember(member: Member): Member = + fun loginMember(member: Member): Member = try { val memberEntity = memberJpaRepository.save(MemberEntity.toEntity(member)) memberEntity.toDomain() diff --git a/src/main/kotlin/codel/member/domain/Profile.kt b/src/main/kotlin/codel/member/domain/Profile.kt index 595dfdfc..a2f18d70 100644 --- a/src/main/kotlin/codel/member/domain/Profile.kt +++ b/src/main/kotlin/codel/member/domain/Profile.kt @@ -1,6 +1,7 @@ package codel.member.domain class Profile( + val id: Long? = null, val codeName: String, val age: Int, val job: String, @@ -12,6 +13,4 @@ class Profile( val smallCity: String, val mbti: String, val introduce: String, -) { - val id: Long = 0 -} +) diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index 7a6806f1..f01f168a 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -14,17 +14,15 @@ import jakarta.persistence.* ], ) class MemberEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private var id: Long? = null, + @OneToOne + private var profileEntity: ProfileEntity? = null, private var oauthType: OauthType, private var oauthId: String, private var memberStatus: MemberStatus, ) { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0 - - @OneToOne - var profileEntity: ProfileEntity? = null - companion object { fun toEntity(member: Member): MemberEntity = MemberEntity( diff --git a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt index 5d6269f5..60eeffff 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt @@ -8,6 +8,9 @@ import jakarta.persistence.Id @Entity class ProfileEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private var id: Long? = null, private var codeName: String, private var age: Int, private var job: String, @@ -22,10 +25,6 @@ class ProfileEntity( private var codeImage: String? = null, // 복수 private var faceImage: String? = null, // 복수 ) { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long = 0 - companion object { fun toEntity(profile: Profile): ProfileEntity = ProfileEntity( diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 4db86be3..904ddacd 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -2,9 +2,9 @@ package codel.member.presentation import codel.auth.business.AuthService import codel.member.business.MemberService -import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest -import codel.member.presentation.response.MemberSavedResponse +import codel.member.presentation.response.MemberLoginResponse import codel.member.presentation.swagger.MemberControllerSwagger import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping @@ -17,10 +17,10 @@ class MemberController( private val authService: AuthService, ) : MemberControllerSwagger { @PostMapping("/v1/member/login") - override fun saveMember( - @RequestBody request: MemberSavedRequest, - ): ResponseEntity { - val memberSavedResponse = memberService.saveMember(request) + override fun loginMember( + @RequestBody request: MemberLoginRequest, + ): ResponseEntity { + val memberSavedResponse = memberService.loginMember(request) val token = authService.provideToken(request) return ResponseEntity .ok() diff --git a/src/main/kotlin/codel/member/presentation/request/MemberSavedRequest.kt b/src/main/kotlin/codel/member/presentation/request/MemberLoginRequest.kt similarity index 81% rename from src/main/kotlin/codel/member/presentation/request/MemberSavedRequest.kt rename to src/main/kotlin/codel/member/presentation/request/MemberLoginRequest.kt index a718e4cd..00d6a199 100644 --- a/src/main/kotlin/codel/member/presentation/request/MemberSavedRequest.kt +++ b/src/main/kotlin/codel/member/presentation/request/MemberLoginRequest.kt @@ -2,7 +2,7 @@ package codel.member.presentation.request import codel.member.domain.OauthType -data class MemberSavedRequest( +data class MemberLoginRequest( val oauthType: OauthType, val oauthId: String, ) diff --git a/src/main/kotlin/codel/member/presentation/response/MemberSavedResponse.kt b/src/main/kotlin/codel/member/presentation/response/MemberLoginResponse.kt similarity index 79% rename from src/main/kotlin/codel/member/presentation/response/MemberSavedResponse.kt rename to src/main/kotlin/codel/member/presentation/response/MemberLoginResponse.kt index 39531e59..68fb71b7 100644 --- a/src/main/kotlin/codel/member/presentation/response/MemberSavedResponse.kt +++ b/src/main/kotlin/codel/member/presentation/response/MemberLoginResponse.kt @@ -2,6 +2,6 @@ package codel.member.presentation.response import codel.member.domain.MemberStatus -data class MemberSavedResponse( +data class MemberLoginResponse( val memberStatus: MemberStatus, ) diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index 6d3e3b09..323038f9 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -1,8 +1,8 @@ package codel.member.presentation.swagger -import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest -import codel.member.presentation.response.MemberSavedResponse +import codel.member.presentation.response.MemberLoginResponse import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponses @@ -20,9 +20,9 @@ interface MemberControllerSwagger { ApiResponse(responseCode = "500", description = "서버 내부 오류"), ], ) - fun saveMember( - @RequestBody request: MemberSavedRequest, - ): ResponseEntity + fun loginMember( + @RequestBody request: MemberLoginRequest, + ): ResponseEntity @Operation(summary = "이미지를 제외한 프로필 받기", description = "이미지를 제외한 프로필을 입력받습니다.") @ApiResponses( From 6ea8335c5b924eaf6a645db76d79ebd54781270c Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Thu, 3 Apr 2025 20:15:28 +0900 Subject: [PATCH 028/542] =?UTF-8?q?test:=20TestFixture=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/test/kotlin/codel/config/TestFixture.kt | 20 ++++++++++++++++++- .../member/business/MemberServiceTest.kt | 8 ++++---- .../infrastructure/MemberJpaRepositoryTest.kt | 19 +++++++++--------- .../presentation/MemberControllerTest.kt | 6 +++--- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/test/kotlin/codel/config/TestFixture.kt b/src/test/kotlin/codel/config/TestFixture.kt index 8c4c1af5..91eb8f97 100644 --- a/src/test/kotlin/codel/config/TestFixture.kt +++ b/src/test/kotlin/codel/config/TestFixture.kt @@ -3,7 +3,10 @@ package codel.config import codel.auth.TokenProvider import codel.member.domain.Member import codel.member.domain.MemberRepository +import codel.member.domain.MemberStatus import codel.member.domain.OauthType +import codel.member.infrastructure.MemberJpaRepository +import codel.member.infrastructure.entity.MemberEntity import org.junit.jupiter.api.BeforeEach import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -11,6 +14,7 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class TestFixture { lateinit var hogee: Member + lateinit var seokEntity: MemberEntity lateinit var token: String @Autowired @@ -19,6 +23,9 @@ class TestFixture { @Autowired lateinit var memberRepository: MemberRepository + @Autowired + lateinit var memberJpaRepository: MemberJpaRepository + @BeforeEach fun setUp() { hogee = @@ -26,7 +33,18 @@ class TestFixture { oauthType = OauthType.APPLE, oauthId = "hogee", ) - memberRepository.saveMember(hogee) + + memberRepository.loginMember(hogee) + + seokEntity = + MemberEntity( + oauthType = OauthType.KAKAO, + oauthId = "seok", + memberStatus = MemberStatus.SIGNUP, + ) + + memberJpaRepository.save(seokEntity) + token = tokenProvider.provide(hogee) } } diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index f990b507..f4a243ec 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -2,7 +2,7 @@ package codel.member.business import codel.member.domain.MemberStatus import codel.member.domain.OauthType -import codel.member.presentation.request.MemberSavedRequest +import codel.member.presentation.request.MemberLoginRequest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test @@ -16,10 +16,10 @@ class MemberServiceTest( ) { @DisplayName("첫 로그인을 한 멤버의 상태는 SignUp 이다.") @Test - fun saveMemberSuccessTest() { - val memberSavedRequest = MemberSavedRequest(OauthType.KAKAO, "hogee") + fun loginMemberSuccessTest() { + val memberLoginRequest = MemberLoginRequest(OauthType.KAKAO, "hogee") - val memberStatus = memberService.saveMember(memberSavedRequest).memberStatus + val memberStatus = memberService.loginMember(memberLoginRequest).memberStatus assertThat(memberStatus).isEqualTo(MemberStatus.SIGNUP) } diff --git a/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt index 8ada045b..1dbfd766 100644 --- a/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt +++ b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt @@ -1,29 +1,28 @@ package codel.member.infrastructure +import codel.config.TestFixture import codel.member.domain.MemberStatus import codel.member.domain.OauthType import codel.member.infrastructure.entity.MemberEntity import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.dao.DataIntegrityViolationException @SpringBootTest -class MemberJpaRepositoryTest( - @Autowired - private val memberJpaRepository: MemberJpaRepository, -) { +class MemberJpaRepositoryTest : TestFixture() { @DisplayName("중복된 멤버에 대해 유니크 제약 조건을 발생시킨다.") @Test fun saveMemberTest() { - val memberEntity1 = MemberEntity(OauthType.APPLE, "hoho", MemberStatus.SIGNUP) - val memberEntity2 = MemberEntity(OauthType.APPLE, "hoho", MemberStatus.SIGNUP) - memberJpaRepository.save(memberEntity1) - + val newEntity = + MemberEntity( + oauthType = OauthType.KAKAO, + oauthId = "seok", + memberStatus = MemberStatus.SIGNUP, + ) Assertions.assertThrows(DataIntegrityViolationException::class.java) { - memberJpaRepository.save(memberEntity2) + memberJpaRepository.save(newEntity) } } } diff --git a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt index 9d102471..0a0c0986 100644 --- a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt +++ b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt @@ -2,7 +2,7 @@ package codel.member.presentation import codel.config.TestFixture import codel.member.domain.MemberStatus -import codel.member.presentation.response.MemberSavedResponse +import codel.member.presentation.response.MemberLoginResponse import io.restassured.RestAssured import io.restassured.RestAssured.given import io.restassured.http.ContentType @@ -32,7 +32,7 @@ class MemberControllerTest : TestFixture() { "oauthType" to "APPLE", "oauthId" to "hogee", ) - val expectedResponse = MemberSavedResponse(MemberStatus.SIGNUP) + val expectedResponse = MemberLoginResponse(MemberStatus.SIGNUP) val token = given() @@ -56,7 +56,7 @@ class MemberControllerTest : TestFixture() { .then() .statusCode(200) .extract() - .`as`(MemberSavedResponse::class.java) + .`as`(MemberLoginResponse::class.java) Assertions.assertThat(expectedResponse.memberStatus).isEqualTo(response.memberStatus) } From e1abaa55171cebae6f5adb56160225616b2dc4dd Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Thu, 3 Apr 2025 20:30:02 +0900 Subject: [PATCH 029/542] =?UTF-8?q?test:=20memberJpaRepository=20deleteAll?= =?UTF-8?q?=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/test/kotlin/codel/config/TestFixture.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/kotlin/codel/config/TestFixture.kt b/src/test/kotlin/codel/config/TestFixture.kt index 91eb8f97..fe5db2d6 100644 --- a/src/test/kotlin/codel/config/TestFixture.kt +++ b/src/test/kotlin/codel/config/TestFixture.kt @@ -28,6 +28,7 @@ class TestFixture { @BeforeEach fun setUp() { + memberJpaRepository.deleteAll() hogee = Member( oauthType = OauthType.APPLE, From 742a1009b4615c75fb8c1e35d866f040598c3025 Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:18:12 +0900 Subject: [PATCH 030/542] Update ci.yml --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64267bbb..f046163f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,16 @@ jobs: token: expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} secret-key: ${{ secrets.JWT_SECRET_KEY }} + + cloud: + aws: + region: + static: ap-northeast-2 + s3: + bucket: code-l-bucket + credentials: + access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} + secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} EOF - name: Build with Gradle From 33acdc8502b1a3f773dda244c40b2eb559df037c Mon Sep 17 00:00:00 2001 From: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Date: Wed, 9 Apr 2025 16:18:49 +0900 Subject: [PATCH 031/542] Update cd.yml --- .github/workflows/cd.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index e0a12dd4..ed0d0872 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -33,6 +33,15 @@ jobs: token: expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} secret-key: ${{ secrets.JWT_SECRET_KEY }} + cloud: + aws: + region: + static: ap-northeast-2 + s3: + bucket: code-l-bucket + credentials: + access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} + secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} EOF - name: Build jar From a029f0d5b6065e1fe89aa70bef915768bcab9dd4 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Thu, 3 Apr 2025 21:51:55 +0900 Subject: [PATCH 032/542] =?UTF-8?q?feat:=20s3=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- build.gradle.kts | 3 ++ src/main/kotlin/codel/config/S3Config.kt | 28 ++++++++++++++++ .../codel/member/business/MemberService.kt | 15 +++++++++ .../codel/member/domain/MemberRepository.kt | 3 ++ .../kotlin/codel/member/domain/S3Uploader.kt | 32 +++++++++++++++++++ .../member/presentation/MemberController.kt | 8 +++++ .../request/CodeImageSavedRequest.kt | 7 ++++ .../swagger/MemberControllerSwagger.kt | 13 ++++++++ 8 files changed, 109 insertions(+) create mode 100644 src/main/kotlin/codel/config/S3Config.kt create mode 100644 src/main/kotlin/codel/member/domain/S3Uploader.kt create mode 100644 src/main/kotlin/codel/member/presentation/request/CodeImageSavedRequest.kt diff --git a/build.gradle.kts b/build.gradle.kts index eb39c634..6db02fc0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,9 @@ dependencies { // swagger implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4") + + // s3 + implementation("software.amazon.awssdk:s3:2.20.148") } kotlin { diff --git a/src/main/kotlin/codel/config/S3Config.kt b/src/main/kotlin/codel/config/S3Config.kt new file mode 100644 index 00000000..47357136 --- /dev/null +++ b/src/main/kotlin/codel/config/S3Config.kt @@ -0,0 +1,28 @@ +package codel.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.s3.S3Client + +@Configuration +class S3Config( + @Value("\${cloud.aws.credentials.access-key}") private val accessKey: String, + @Value("\${cloud.aws.credentials.secret-key}") private val secretKey: String, + @Value("\${cloud.aws.region.static}") private val region: String, +) { + @Bean + fun s3Client(): S3Client { + val credentials = AwsBasicCredentials.create(accessKey, secretKey) + val provider = StaticCredentialsProvider.create(credentials) + + return S3Client + .builder() + .region(Region.of(region)) + .credentialsProvider(provider) + .build() + } +} diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 743ba65e..16c80831 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -1,16 +1,21 @@ package codel.member.business +import codel.member.domain.CodeImage import codel.member.domain.Member import codel.member.domain.MemberRepository import codel.member.domain.Profile +import codel.member.domain.S3Uploader +import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse +import jakarta.transaction.Transactional import org.springframework.stereotype.Service @Service class MemberService( private val memberRepository: MemberRepository, + private val s3Uploader: S3Uploader, ) { fun loginMember(request: MemberLoginRequest): MemberLoginResponse { val member = @@ -40,4 +45,14 @@ class MemberService( ) memberRepository.saveProfile(profile) } + + @Transactional + fun saveCodeImage(request: CodeImageSavedRequest) { + val imageFiles = request.imageFiles + val codeImages = CodeImage(imageFiles.map { file -> s3Uploader.uploadFile(file) }) + + memberRepository.saveImagePath(codeImages) + + // memberStatus 수정 + } } diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index ab001336..80a07de8 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -32,4 +32,7 @@ class MemberRepository( fun saveProfile(profile: Profile) { profileJpaRepository.save(ProfileEntity.toEntity(profile)) } + + fun saveImagePath(codeImage: CodeImage) { + } } diff --git a/src/main/kotlin/codel/member/domain/S3Uploader.kt b/src/main/kotlin/codel/member/domain/S3Uploader.kt new file mode 100644 index 00000000..68f86541 --- /dev/null +++ b/src/main/kotlin/codel/member/domain/S3Uploader.kt @@ -0,0 +1,32 @@ +package codel.member.domain + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile +import software.amazon.awssdk.core.sync.RequestBody +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.PutObjectRequest +import java.util.* + +@Component +class S3Uploader( + private val s3Client: S3Client, + @Value("\${cloud.aws.s3.bucket}") private val bucket: String, +) { + fun uploadFile(file: MultipartFile): String { + val fileName = "images/${UUID.randomUUID()}-${file.originalFilename}" + + val putObjectRequest = + PutObjectRequest + .builder() + .bucket(bucket) + .key(fileName) + .contentType(file.contentType) + .acl("public-read") + .build() + + s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.bytes)) + + return "https://$bucket.s3.amazonaws.com/$fileName" + } +} diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 904ddacd..036b4461 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -2,6 +2,7 @@ package codel.member.presentation import codel.auth.business.AuthService import codel.member.business.MemberService +import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse @@ -35,4 +36,11 @@ class MemberController( memberService.saveProfile(request) return ResponseEntity.ok().build() } + + @PostMapping("/v1/member/codeimage") + override fun saveCodeImage( + @RequestBody request: CodeImageSavedRequest, + ): ResponseEntity { + memberService.saveCodeImage(request) + } } diff --git a/src/main/kotlin/codel/member/presentation/request/CodeImageSavedRequest.kt b/src/main/kotlin/codel/member/presentation/request/CodeImageSavedRequest.kt new file mode 100644 index 00000000..a4a98b86 --- /dev/null +++ b/src/main/kotlin/codel/member/presentation/request/CodeImageSavedRequest.kt @@ -0,0 +1,7 @@ +package codel.member.presentation.request + +import org.springframework.web.multipart.MultipartFile + +data class CodeImageSavedRequest( + val imageFiles: List, +) diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index 323038f9..c04024c2 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -1,5 +1,6 @@ package codel.member.presentation.swagger +import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse @@ -35,4 +36,16 @@ interface MemberControllerSwagger { fun saveProfile( @RequestBody request: ProfileSavedRequest, ): ResponseEntity + + @Operation(summary = "코드 프로필 이미지 받기", description = "코드 프로필 이미지 받습니다.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "코드 프로필 이미지 성공적으로 저장됨"), + ApiResponse(responseCode = "400", description = "요청 값이 잘못됨"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + fun saveCodeImage( + @RequestBody request: CodeImageSavedRequest, + ): ResponseEntity } From 15394440a7222c16c5e78162d0f85f23c270a6b6 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Sat, 5 Apr 2025 23:34:58 +0900 Subject: [PATCH 033/542] =?UTF-8?q?feat:=20MemberArgumentResolver=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/main/kotlin/codel/config/WebMvcConfig.kt | 15 +++++++ .../config/argumentresolver/LoginMember.kt | 5 +++ .../MemberArgumentResolver.kt | 43 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 src/main/kotlin/codel/config/WebMvcConfig.kt create mode 100644 src/main/kotlin/codel/config/argumentresolver/LoginMember.kt create mode 100644 src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt diff --git a/src/main/kotlin/codel/config/WebMvcConfig.kt b/src/main/kotlin/codel/config/WebMvcConfig.kt new file mode 100644 index 00000000..9f0e4cd0 --- /dev/null +++ b/src/main/kotlin/codel/config/WebMvcConfig.kt @@ -0,0 +1,15 @@ +package codel.config + +import codel.config.argumentresolver.MemberArgumentResolver +import org.springframework.context.annotation.Configuration +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebMvcConfig( + private val memberArgumentResolver: MemberArgumentResolver, +) : WebMvcConfigurer { + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(memberArgumentResolver) + } +} diff --git a/src/main/kotlin/codel/config/argumentresolver/LoginMember.kt b/src/main/kotlin/codel/config/argumentresolver/LoginMember.kt new file mode 100644 index 00000000..cec43756 --- /dev/null +++ b/src/main/kotlin/codel/config/argumentresolver/LoginMember.kt @@ -0,0 +1,5 @@ +package codel.config.argumentresolver + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class LoginMember diff --git a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt new file mode 100644 index 00000000..ff718e31 --- /dev/null +++ b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt @@ -0,0 +1,43 @@ +package codel.config.argumentresolver + +import codel.auth.TokenProvider +import codel.auth.exception.AuthException +import codel.member.business.MemberService +import codel.member.domain.Member +import org.springframework.core.MethodParameter +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class MemberArgumentResolver( + private val memberService: MemberService, + private val tokenProvider: TokenProvider, +) : HandlerMethodArgumentResolver { + override fun supportsParameter(parameter: MethodParameter): Boolean = + parameter.hasParameterAnnotation(LoginMember::class.java) && + parameter.parameterType == Member::class.java + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: org.springframework.web.bind.support.WebDataBinderFactory?, + ): Any? { + val token = resolveToken(webRequest) + val memberId = tokenProvider.extractMemberId(token) + + return memberService.findMember(memberId) + } + + private fun resolveToken(webRequest: NativeWebRequest): String { + val bearer = webRequest.getHeader("Authorization") + return if (bearer != null && bearer.startsWith("Bearer ")) { + bearer.substring(7) + } else { + throw AuthException(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다.") + } + } +} From 3d627580f869f0b9980ae70a9c841abe5848e747 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Sat, 5 Apr 2025 23:52:28 +0900 Subject: [PATCH 034/542] =?UTF-8?q?refactor:=20S3Uploader=20=EC=B6=94?= =?UTF-8?q?=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../codel/member/business/MemberService.kt | 19 +++++++++++++------ .../codel/member/domain/ImageUploader.kt | 7 +++++++ .../{domain => infrastructure}/S3Uploader.kt | 7 ++++--- 3 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/codel/member/domain/ImageUploader.kt rename src/main/kotlin/codel/member/{domain => infrastructure}/S3Uploader.kt (86%) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 16c80831..1d261a3c 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -1,10 +1,10 @@ package codel.member.business import codel.member.domain.CodeImage +import codel.member.domain.ImageUploader import codel.member.domain.Member import codel.member.domain.MemberRepository import codel.member.domain.Profile -import codel.member.domain.S3Uploader import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest @@ -15,7 +15,7 @@ import org.springframework.stereotype.Service @Service class MemberService( private val memberRepository: MemberRepository, - private val s3Uploader: S3Uploader, + private val imageUploader: ImageUploader, ) { fun loginMember(request: MemberLoginRequest): MemberLoginResponse { val member = @@ -47,12 +47,19 @@ class MemberService( } @Transactional - fun saveCodeImage(request: CodeImageSavedRequest) { - val imageFiles = request.imageFiles - val codeImages = CodeImage(imageFiles.map { file -> s3Uploader.uploadFile(file) }) + fun saveCodeImage( + member: Member, + request: CodeImageSavedRequest, + ) { + val codeImage = uploadFile(request) - memberRepository.saveImagePath(codeImages) + memberRepository.saveImagePath(member, codeImage) // memberStatus 수정 } + + private fun uploadFile(request: CodeImageSavedRequest): CodeImage { + val imageFiles = request.imageFiles + return CodeImage(imageFiles.map { file -> imageUploader.uploadFile(file) }) + } } diff --git a/src/main/kotlin/codel/member/domain/ImageUploader.kt b/src/main/kotlin/codel/member/domain/ImageUploader.kt new file mode 100644 index 00000000..519d54b6 --- /dev/null +++ b/src/main/kotlin/codel/member/domain/ImageUploader.kt @@ -0,0 +1,7 @@ +package codel.member.domain + +import org.springframework.web.multipart.MultipartFile + +interface ImageUploader { + fun uploadFile(file: MultipartFile): String +} diff --git a/src/main/kotlin/codel/member/domain/S3Uploader.kt b/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt similarity index 86% rename from src/main/kotlin/codel/member/domain/S3Uploader.kt rename to src/main/kotlin/codel/member/infrastructure/S3Uploader.kt index 68f86541..fab9372e 100644 --- a/src/main/kotlin/codel/member/domain/S3Uploader.kt +++ b/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt @@ -1,5 +1,6 @@ -package codel.member.domain +package codel.member.infrastructure +import codel.member.domain.ImageUploader import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile @@ -12,8 +13,8 @@ import java.util.* class S3Uploader( private val s3Client: S3Client, @Value("\${cloud.aws.s3.bucket}") private val bucket: String, -) { - fun uploadFile(file: MultipartFile): String { +) : ImageUploader { + override fun uploadFile(file: MultipartFile): String { val fileName = "images/${UUID.randomUUID()}-${file.originalFilename}" val putObjectRequest = From e7267f50a279aeaa2e3c17f1db98cb490991f0b2 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Sun, 6 Apr 2025 00:18:45 +0900 Subject: [PATCH 035/542] =?UTF-8?q?refactor:=20AuthService=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=84=20MemberRepository=EC=97=90?= =?UTF-8?q?=EC=84=9C=20MemberService=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/main/kotlin/codel/auth/business/AuthService.kt | 6 +++--- src/main/kotlin/codel/member/business/MemberService.kt | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/codel/auth/business/AuthService.kt b/src/main/kotlin/codel/auth/business/AuthService.kt index d5b6bb27..c74f6746 100644 --- a/src/main/kotlin/codel/auth/business/AuthService.kt +++ b/src/main/kotlin/codel/auth/business/AuthService.kt @@ -1,17 +1,17 @@ package codel.auth.business import codel.auth.TokenProvider -import codel.member.domain.MemberRepository +import codel.member.business.MemberService import codel.member.presentation.request.MemberLoginRequest import org.springframework.stereotype.Service @Service class AuthService( val tokenProvider: TokenProvider, - val memberRepository: MemberRepository, + val memberService: MemberService, ) { fun provideToken(request: MemberLoginRequest): String { - val member = memberRepository.findMember(request.oauthType, request.oauthId) + val member = memberService.findMember(request.oauthType, request.oauthId) return tokenProvider.provide(member) } } diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 1d261a3c..6d34a282 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -4,6 +4,7 @@ import codel.member.domain.CodeImage import codel.member.domain.ImageUploader import codel.member.domain.Member import codel.member.domain.MemberRepository +import codel.member.domain.OauthType import codel.member.domain.Profile import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest @@ -46,6 +47,11 @@ class MemberService( memberRepository.saveProfile(profile) } + fun findMember( + oauthType: OauthType, + oauthId: String, + ): Member = memberRepository.findMember(oauthType, oauthId) + @Transactional fun saveCodeImage( member: Member, From 7790a3e0361cc1f966bd34970ddd71d9ad33d906 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Sun, 6 Apr 2025 00:21:44 +0900 Subject: [PATCH 036/542] =?UTF-8?q?refactor:=20MemberArgumentResolver=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/main/kotlin/codel/auth/TokenProvider.kt | 7 ++++-- .../MemberArgumentResolver.kt | 24 +++++++------------ .../codel/config/filter/JwtAuthFilter.kt | 6 +++-- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/codel/auth/TokenProvider.kt b/src/main/kotlin/codel/auth/TokenProvider.kt index ed60a1b6..818650f1 100644 --- a/src/main/kotlin/codel/auth/TokenProvider.kt +++ b/src/main/kotlin/codel/auth/TokenProvider.kt @@ -2,6 +2,7 @@ package codel.auth import codel.auth.exception.AuthException import codel.member.domain.Member +import codel.member.domain.OauthType import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm @@ -10,7 +11,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import java.security.Key -import java.util.Date +import java.util.* @Component class TokenProvider( @@ -67,5 +68,7 @@ class TokenProvider( } } - fun extractMemberId(token: String): Long = getPayload(token)[MEMBER_ID_CLAIM_KEY].toString().toLong() + fun extractOauthId(token: String): String = getPayload(token)[SOCIAL_LOGIN_ID_CLAIM_KEY].toString() + + fun extractOauthType(token: String): OauthType = OauthType.valueOf(getPayload(token)[OAUTH_TYPE].toString()) } diff --git a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt index ff718e31..bcaeaa0e 100644 --- a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt +++ b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt @@ -1,20 +1,18 @@ package codel.config.argumentresolver -import codel.auth.TokenProvider -import codel.auth.exception.AuthException import codel.member.business.MemberService import codel.member.domain.Member +import codel.member.domain.OauthType import org.springframework.core.MethodParameter -import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.context.request.RequestAttributes import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer @Component class MemberArgumentResolver( private val memberService: MemberService, - private val tokenProvider: TokenProvider, ) : HandlerMethodArgumentResolver { override fun supportsParameter(parameter: MethodParameter): Boolean = parameter.hasParameterAnnotation(LoginMember::class.java) && @@ -26,18 +24,14 @@ class MemberArgumentResolver( webRequest: NativeWebRequest, binderFactory: org.springframework.web.bind.support.WebDataBinderFactory?, ): Any? { - val token = resolveToken(webRequest) - val memberId = tokenProvider.extractMemberId(token) + val oauthId = + webRequest.getAttribute("oauthId", RequestAttributes.SCOPE_REQUEST) as? String + ?: throw IllegalArgumentException("oauthId가 요청이 없습니다.") - return memberService.findMember(memberId) - } + val oauthType = + webRequest.getAttribute("oauthType", RequestAttributes.SCOPE_REQUEST) as? OauthType + ?: throw IllegalArgumentException("oauthType이 요청이 없습니다.") - private fun resolveToken(webRequest: NativeWebRequest): String { - val bearer = webRequest.getHeader("Authorization") - return if (bearer != null && bearer.startsWith("Bearer ")) { - bearer.substring(7) - } else { - throw AuthException(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다.") - } + return memberService.findMember(oauthType, oauthId) } } diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt index fc79ba33..fbbcd449 100644 --- a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -40,8 +40,10 @@ class JwtAuthFilter( response.writer.write("""{"message": "인증되지 않은 사용자입니다."}""") return } - val memberId = tokenProvider.extractMemberId(token) - request.setAttribute("memberId", memberId) + val oauthId = tokenProvider.extractOauthId(token) + val oauthType = tokenProvider.extractOauthType(token) + request.setAttribute("oauthId", oauthId) + request.setAttribute("oauthType", oauthType) filterChain.doFilter(request, response) } From 0f138517410f9f8bd89a4bbade99e3d302287360 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Sun, 6 Apr 2025 00:28:27 +0900 Subject: [PATCH 037/542] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/main/kotlin/codel/member/domain/Member.kt | 12 +++++++++++- .../kotlin/codel/member/domain/MemberRepository.kt | 8 +++++++- .../codel/member/presentation/MemberController.kt | 8 +++++++- .../presentation/swagger/MemberControllerSwagger.kt | 3 +++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/codel/member/domain/Member.kt b/src/main/kotlin/codel/member/domain/Member.kt index 661f2384..d8c48d77 100644 --- a/src/main/kotlin/codel/member/domain/Member.kt +++ b/src/main/kotlin/codel/member/domain/Member.kt @@ -5,5 +5,15 @@ class Member( val profile: Profile? = null, val oauthType: OauthType, val oauthId: String, + private var codeImage: CodeImage? = null, + private var faceImage: FaceImage? = null, val memberStatus: MemberStatus = MemberStatus.SIGNUP, -) +) { + fun saveCodeImage(codeImage: CodeImage) { + this.codeImage = codeImage + } + + fun saveFaceImage(faceImage: FaceImage) { + this.faceImage = faceImage + } +} diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 80a07de8..d2bd7fb5 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -33,6 +33,12 @@ class MemberRepository( profileJpaRepository.save(ProfileEntity.toEntity(profile)) } - fun saveImagePath(codeImage: CodeImage) { + fun saveImagePath( + member: Member, + codeImage: CodeImage, + ) { + member.saveCodeImage(codeImage) + + val memberEntity = MemberEntity.toEntity(member) } } diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 036b4461..51996ae9 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -1,7 +1,9 @@ package codel.member.presentation import codel.auth.business.AuthService +import codel.config.argumentresolver.LoginMember import codel.member.business.MemberService +import codel.member.domain.Member import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest @@ -39,8 +41,12 @@ class MemberController( @PostMapping("/v1/member/codeimage") override fun saveCodeImage( + @LoginMember member: Member, @RequestBody request: CodeImageSavedRequest, ): ResponseEntity { - memberService.saveCodeImage(request) + // 이미지를 엔티티로 저장 + // s3 통신 방식 변경 필요 + memberService.saveCodeImage(member, request) + return ResponseEntity.ok().build() } } diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index c04024c2..ac036f94 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -1,5 +1,7 @@ package codel.member.presentation.swagger +import codel.config.argumentresolver.LoginMember +import codel.member.domain.Member import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest @@ -46,6 +48,7 @@ interface MemberControllerSwagger { ], ) fun saveCodeImage( + @LoginMember member: Member, @RequestBody request: CodeImageSavedRequest, ): ResponseEntity } From 083e01d5fec1fce5dd59338897389f3d601e1e93 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Sun, 6 Apr 2025 00:48:00 +0900 Subject: [PATCH 038/542] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../codel/member/business/MemberService.kt | 11 ++++++-- .../codel/member/domain/MemberRepository.kt | 27 +++++++++++++++++-- .../infrastructure/entity/MemberEntity.kt | 17 +++++++++++- .../infrastructure/entity/ProfileEntity.kt | 18 +++++++++++++ .../member/presentation/MemberController.kt | 3 ++- .../swagger/MemberControllerSwagger.kt | 1 + 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 6d34a282..3d45c690 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -4,6 +4,7 @@ import codel.member.domain.CodeImage import codel.member.domain.ImageUploader import codel.member.domain.Member import codel.member.domain.MemberRepository +import codel.member.domain.MemberStatus import codel.member.domain.OauthType import codel.member.domain.Profile import codel.member.presentation.request.CodeImageSavedRequest @@ -29,7 +30,11 @@ class MemberService( return MemberLoginResponse(loginMember.memberStatus) } - fun saveProfile(request: ProfileSavedRequest) { + @Transactional + fun saveProfile( + member: Member, + request: ProfileSavedRequest, + ) { val profile = Profile( codeName = request.codeName, @@ -44,7 +49,9 @@ class MemberService( mbti = request.mbti, introduce = request.introduce, ) - memberRepository.saveProfile(profile) + + memberRepository.saveProfile(member, profile) + memberRepository.changeMemberStatus(member, MemberStatus.CODE_SURVEY) } fun findMember( diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index d2bd7fb5..33f501c9 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -5,6 +5,7 @@ import codel.member.infrastructure.ProfileJpaRepository import codel.member.infrastructure.entity.MemberEntity import codel.member.infrastructure.entity.ProfileEntity import org.springframework.dao.DataIntegrityViolationException +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component @Component @@ -29,8 +30,30 @@ class MemberRepository( return memberEntity.toDomain() } - fun saveProfile(profile: Profile) { - profileJpaRepository.save(ProfileEntity.toEntity(profile)) + fun saveProfile( + member: Member, + profile: Profile, + ) { + val memberEntity = findMemberEntity(member) + val profileEntity = ProfileEntity.toEntity(profile) + + profileJpaRepository.save(profileEntity) + memberEntity.saveProfileEntity(profileEntity) + } + + fun changeMemberStatus( + member: Member, + status: MemberStatus, + ) { + val memberEntity = findMemberEntity(member) + + memberEntity.changeMemberStatus(status) + } + + private fun findMemberEntity(member: Member): MemberEntity { + val memberId = member.id ?: throw IllegalArgumentException("member id가 비어있습니다.") + + return memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("멤버가 존재하지 않습니다.") } fun saveImagePath( diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index f01f168a..bdca965d 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -5,7 +5,13 @@ package codel.member.infrastructure.entity import codel.member.domain.Member import codel.member.domain.MemberStatus import codel.member.domain.OauthType -import jakarta.persistence.* +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.OneToOne +import jakarta.persistence.Table +import jakarta.persistence.UniqueConstraint @Entity @Table( @@ -35,8 +41,17 @@ class MemberEntity( fun toDomain(): Member = Member( id = this.id, + profile = this.profileEntity?.toDomain(), oauthType = this.oauthType, oauthId = this.oauthId, memberStatus = this.memberStatus, ) + + fun saveProfileEntity(profileEntity: ProfileEntity) { + this.profileEntity = profileEntity + } + + fun changeMemberStatus(status: MemberStatus) { + this.memberStatus = status + } } diff --git a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt index 60eeffff..c2ddd773 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt @@ -42,8 +42,26 @@ class ProfileEntity( ) private fun serializeAttribute(attribute: List): String = attribute.joinToString(separator = ",") + + private fun deserializeAttribute(attribute: String): List = attribute.split(",") } + fun toDomain(): Profile = + Profile( + id = this.id, + codeName = this.codeName, + age = this.age, + job = this.job, + alcohol = this.alcohol, + smoke = this.smoke, + hobby = deserializeAttribute(this.hobby), + style = deserializeAttribute(this.style), + bigCity = this.bigCity, + smallCity = this.smallCity, + mbti = this.mbti, + introduce = this.introduce, + ) + fun updateCodeImage(codeImages: List) { codeImage = serializeAttribute(codeImages) } diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 51996ae9..f2f019b4 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -33,9 +33,10 @@ class MemberController( @PostMapping("/v1/member/profile") override fun saveProfile( + @LoginMember member: Member, @RequestBody request: ProfileSavedRequest, ): ResponseEntity { - memberService.saveProfile(request) + memberService.saveProfile(member, request) return ResponseEntity.ok().build() } diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index ac036f94..73560fad 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -36,6 +36,7 @@ interface MemberControllerSwagger { ], ) fun saveProfile( + @LoginMember member: Member, @RequestBody request: ProfileSavedRequest, ): ResponseEntity From 591b4b043b5541b2c79cf3657eeafd492cd52ae1 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Sun, 6 Apr 2025 00:57:49 +0900 Subject: [PATCH 039/542] =?UTF-8?q?test:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- src/test/kotlin/codel/config/TestFixture.kt | 2 +- .../member/business/MemberServiceTest.kt | 42 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/codel/config/TestFixture.kt b/src/test/kotlin/codel/config/TestFixture.kt index fe5db2d6..a25b76ae 100644 --- a/src/test/kotlin/codel/config/TestFixture.kt +++ b/src/test/kotlin/codel/config/TestFixture.kt @@ -35,7 +35,7 @@ class TestFixture { oauthId = "hogee", ) - memberRepository.loginMember(hogee) + hogee = memberRepository.loginMember(hogee) seokEntity = MemberEntity( diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index f4a243ec..7b3d482b 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -1,9 +1,12 @@ package codel.member.business +import codel.config.TestFixture import codel.member.domain.MemberStatus import codel.member.domain.OauthType import codel.member.presentation.request.MemberLoginRequest +import codel.member.presentation.request.ProfileSavedRequest import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -13,14 +16,49 @@ import org.springframework.boot.test.context.SpringBootTest class MemberServiceTest( @Autowired private val memberService: MemberService, -) { +) : TestFixture() { @DisplayName("첫 로그인을 한 멤버의 상태는 SignUp 이다.") @Test fun loginMemberSuccessTest() { - val memberLoginRequest = MemberLoginRequest(OauthType.KAKAO, "hogee") + val memberLoginRequest = + MemberLoginRequest( + oauthType = OauthType.KAKAO, + oauthId = "hogee", + ) val memberStatus = memberService.loginMember(memberLoginRequest).memberStatus assertThat(memberStatus).isEqualTo(MemberStatus.SIGNUP) } + + @DisplayName("프로필을 저장에 성공한 후 멤버 상태는 CODE_SURVEY 이다.") + @Test + fun saveProfileSuccessTest() { + val profileSavedRequest = + ProfileSavedRequest( + codeName = "seok", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = listOf("영화 & 드라마", "여행 & 캠핑"), + style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + ) + memberService.saveProfile(hogee, profileSavedRequest) + + val findMember = + memberService.findMember( + oauthType = hogee.oauthType, + oauthId = hogee.oauthId, + ) + + Assertions.assertAll( + { assertThat(findMember.profile).isNotNull }, + { assertThat(findMember.memberStatus).isEqualTo(MemberStatus.CODE_SURVEY) }, + ) + } } From ac3aeb4da8a2a24db053521b315c86005ec7a6e3 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Mon, 7 Apr 2025 00:25:49 +0900 Subject: [PATCH 040/542] refactor: NativeWebRequest -> HttpServletRequest Co-authored-by: hoyeonyy --- .../config/argumentresolver/MemberArgumentResolver.kt | 9 ++++++--- .../kotlin/codel/member/infrastructure/S3Uploader.kt | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt index bcaeaa0e..cb62d071 100644 --- a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt +++ b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt @@ -3,10 +3,10 @@ package codel.config.argumentresolver import codel.member.business.MemberService import codel.member.domain.Member import codel.member.domain.OauthType +import jakarta.servlet.http.HttpServletRequest import org.springframework.core.MethodParameter import org.springframework.stereotype.Component import org.springframework.web.context.request.NativeWebRequest -import org.springframework.web.context.request.RequestAttributes import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer @@ -24,12 +24,15 @@ class MemberArgumentResolver( webRequest: NativeWebRequest, binderFactory: org.springframework.web.bind.support.WebDataBinderFactory?, ): Any? { + val httpServletRequest = + webRequest.getNativeRequest(HttpServletRequest::class.java) + ?: throw IllegalStateException("HttpServletRequest를 가져올 수 없습니다.") val oauthId = - webRequest.getAttribute("oauthId", RequestAttributes.SCOPE_REQUEST) as? String + httpServletRequest.getAttribute("oauthId") as? String ?: throw IllegalArgumentException("oauthId가 요청이 없습니다.") val oauthType = - webRequest.getAttribute("oauthType", RequestAttributes.SCOPE_REQUEST) as? OauthType + httpServletRequest.getAttribute("oauthType") as? OauthType ?: throw IllegalArgumentException("oauthType이 요청이 없습니다.") return memberService.findMember(oauthType, oauthId) diff --git a/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt b/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt index fab9372e..e1d432dd 100644 --- a/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt +++ b/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt @@ -7,7 +7,7 @@ import org.springframework.web.multipart.MultipartFile import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.PutObjectRequest -import java.util.* +import java.util.UUID @Component class S3Uploader( From 2a91bc6b3c7d3f84b39e78a98a7d4fe645cea3be Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Mon, 7 Apr 2025 00:46:49 +0900 Subject: [PATCH 041/542] =?UTF-8?q?test:=20saveProfile(),=20changeMemberSt?= =?UTF-8?q?atus()=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../member/domain/MemberRepositoryTest.kt | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt new file mode 100644 index 00000000..b79a0f85 --- /dev/null +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -0,0 +1,84 @@ +package codel.member.domain + +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class MemberRepositoryTest( + @Autowired + private val memberRepository: MemberRepository, +) { + @DisplayName("멤버 아이디가 없으면 예외를 반환한다.") + @Test + fun saveProfileWithoutMemberIdTest() { + val seokMember = createMemberWithoutMemberId() + + val seokProfile = createProfile() + + assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("member id가 비어있습니다.") + } + + @DisplayName("멤버 아이디에 해당하는 멤버가 없으면 예외를 반환한다.") + @Test + fun saveProfileWithoutMemberTest() { + val seokMember = createMember() + val seokProfile = createProfile() + + assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("멤버가 존재하지 않습니다.") + } + + @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") + @Test + fun changeMemberStatusWithoutMemberIdTest() { + val seokMember = createMemberWithoutMemberId() + + assertThatThrownBy { memberRepository.changeMemberStatus(seokMember, MemberStatus.CODE_SURVEY) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("member id가 비어있습니다.") + } + + @DisplayName("바꾸고자 하는 멤버 아이디에 해당하는 멤버가 없으면 예외를 반환한다.") + @Test + fun changeMemberStatusWithoutMemberTest() { + val seokMember = createMember() + + assertThatThrownBy { memberRepository.changeMemberStatus(seokMember, MemberStatus.CODE_SURVEY) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("멤버가 존재하지 않습니다.") + } + + private fun createMemberWithoutMemberId(): Member = + Member( + oauthType = OauthType.APPLE, + oauthId = "seok", + ) + + private fun createMember(): Member = + Member( + id = 100L, + oauthType = OauthType.APPLE, + oauthId = "seok", + ) + + private fun createProfile(): Profile = + Profile( + codeName = "seok", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = listOf("영화 & 드라마", "여행 & 캠핑"), + style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + ) +} From e35e5bbc4cef04e6cdcd2cf3abdcf16ac3b00f40 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Mon, 7 Apr 2025 01:18:23 +0900 Subject: [PATCH 042/542] =?UTF-8?q?test:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../presentation/MemberControllerTest.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt index 0a0c0986..d9060d18 100644 --- a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt +++ b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt @@ -112,4 +112,33 @@ class MemberControllerTest : TestFixture() { val authHeader = response.header("Authorization") assertTrue(authHeader != null && authHeader.startsWith("Bearer ")) } + + @DisplayName("프로필을 등록 테스트") + @Test + fun saveProfile() { + val request = + mapOf( + "codeName" to "seok", + "age" to 28, + "job" to "백엔드 개발자", + "alcohol" to "자주 마심", + "smoke" to "비흡연자 - 흡연자와 교류 NO", + "hobby" to listOf("영화 & 드라마", "여행 & 캠핑"), + "style" to listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + "bigCity" to "경기도", + "smallCity" to "성남시", + "mbti" to "isfj", + "introduce" to "잘부탁드립니다!", + ) + + given() + .contentType(ContentType.JSON) + .header("Authorization", "Bearer $token") + .body(request) + .`when`() + .post("/v1/member/profile") + .then() + .statusCode(200) + .extract() + } } From 2dcf155585a81981752728aaa5b499d72d260886 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Mon, 7 Apr 2025 16:34:12 +0900 Subject: [PATCH 043/542] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../codel/member/business/MemberService.kt | 5 ----- .../codel/member/domain/MemberRepository.kt | 17 +++++------------ .../infrastructure/MemberJpaRepository.kt | 11 ++++++++--- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 3d45c690..7f8da246 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -4,14 +4,12 @@ import codel.member.domain.CodeImage import codel.member.domain.ImageUploader import codel.member.domain.Member import codel.member.domain.MemberRepository -import codel.member.domain.MemberStatus import codel.member.domain.OauthType import codel.member.domain.Profile import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse -import jakarta.transaction.Transactional import org.springframework.stereotype.Service @Service @@ -30,7 +28,6 @@ class MemberService( return MemberLoginResponse(loginMember.memberStatus) } - @Transactional fun saveProfile( member: Member, request: ProfileSavedRequest, @@ -51,7 +48,6 @@ class MemberService( ) memberRepository.saveProfile(member, profile) - memberRepository.changeMemberStatus(member, MemberStatus.CODE_SURVEY) } fun findMember( @@ -59,7 +55,6 @@ class MemberService( oauthId: String, ): Member = memberRepository.findMember(oauthType, oauthId) - @Transactional fun saveCodeImage( member: Member, request: CodeImageSavedRequest, diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 33f501c9..be83a6e8 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -7,8 +7,10 @@ import codel.member.infrastructure.entity.ProfileEntity import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional @Component +@Transactional class MemberRepository( private val memberJpaRepository: MemberJpaRepository, private val profileJpaRepository: ProfileJpaRepository, @@ -18,15 +20,14 @@ class MemberRepository( val memberEntity = memberJpaRepository.save(MemberEntity.toEntity(member)) memberEntity.toDomain() } catch (e: DataIntegrityViolationException) { - val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(member.oauthType, member.oauthId) - memberEntity.toDomain() + findMember(member.oauthType, member.oauthId) } fun findMember( oauthType: OauthType, oauthId: String, ): Member { - val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(oauthType, oauthId) + val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(oauthType.name, oauthId) return memberEntity.toDomain() } @@ -39,15 +40,7 @@ class MemberRepository( profileJpaRepository.save(profileEntity) memberEntity.saveProfileEntity(profileEntity) - } - - fun changeMemberStatus( - member: Member, - status: MemberStatus, - ) { - val memberEntity = findMemberEntity(member) - - memberEntity.changeMemberStatus(status) + memberEntity.changeMemberStatus(MemberStatus.CODE_SURVEY) } private fun findMemberEntity(member: Member): MemberEntity { diff --git a/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt index 367c7333..beb7e015 100644 --- a/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt +++ b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt @@ -1,14 +1,19 @@ package codel.member.infrastructure -import codel.member.domain.OauthType import codel.member.infrastructure.entity.MemberEntity import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository @Repository interface MemberJpaRepository : JpaRepository { + @Query( + value = "SELECT * FROM member_entity WHERE oauth_type = :oauthType AND oauth_id = :oauthId", + nativeQuery = true, + ) fun findByOauthTypeAndOauthId( - oauthType: OauthType, - oauthId: String, + @Param("oauthType") oauthType: String, + @Param("oauthId") oauthId: String, ): MemberEntity } From 7b8b13f389829f240fd0a5370c4b53570874ab7d Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Mon, 7 Apr 2025 16:34:25 +0900 Subject: [PATCH 044/542] =?UTF-8?q?test:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../member/domain/MemberRepositoryTest.kt | 102 +++++------------- 1 file changed, 28 insertions(+), 74 deletions(-) diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index b79a0f85..0ec40876 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -1,84 +1,38 @@ package codel.member.domain -import org.assertj.core.api.Assertions.assertThatThrownBy +import codel.config.TestFixture +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired +import org.junit.jupiter.api.assertAll import org.springframework.boot.test.context.SpringBootTest @SpringBootTest -class MemberRepositoryTest( - @Autowired - private val memberRepository: MemberRepository, -) { - @DisplayName("멤버 아이디가 없으면 예외를 반환한다.") +class MemberRepositoryTest : TestFixture() { + @DisplayName("프로필 저장 테스트") @Test - fun saveProfileWithoutMemberIdTest() { - val seokMember = createMemberWithoutMemberId() - - val seokProfile = createProfile() - - assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("member id가 비어있습니다.") - } - - @DisplayName("멤버 아이디에 해당하는 멤버가 없으면 예외를 반환한다.") - @Test - fun saveProfileWithoutMemberTest() { - val seokMember = createMember() - val seokProfile = createProfile() - - assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("멤버가 존재하지 않습니다.") - } - - @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") - @Test - fun changeMemberStatusWithoutMemberIdTest() { - val seokMember = createMemberWithoutMemberId() - - assertThatThrownBy { memberRepository.changeMemberStatus(seokMember, MemberStatus.CODE_SURVEY) } - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("member id가 비어있습니다.") - } - - @DisplayName("바꾸고자 하는 멤버 아이디에 해당하는 멤버가 없으면 예외를 반환한다.") - @Test - fun changeMemberStatusWithoutMemberTest() { - val seokMember = createMember() - - assertThatThrownBy { memberRepository.changeMemberStatus(seokMember, MemberStatus.CODE_SURVEY) } - .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("멤버가 존재하지 않습니다.") - } - - private fun createMemberWithoutMemberId(): Member = - Member( - oauthType = OauthType.APPLE, - oauthId = "seok", - ) - - private fun createMember(): Member = - Member( - id = 100L, - oauthType = OauthType.APPLE, - oauthId = "seok", - ) - - private fun createProfile(): Profile = - Profile( - codeName = "seok", - age = 28, - job = "백엔드 개발자", - alcohol = "자주 마심", - smoke = "비흡연자 - 흡연자와 교류 NO", - hobby = listOf("영화 & 드라마", "여행 & 캠핑"), - style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), - bigCity = "경기도", - smallCity = "성남시", - mbti = "isfj", - introduce = "잘부탁드립니다!", + fun saveProfileTest() { + val seokProfile = + Profile( + codeName = "seok", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = listOf("영화 & 드라마", "여행 & 캠핑"), + style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + ) + + memberRepository.saveProfile(hogee, seokProfile) + val findMember = memberRepository.findMember(hogee.oauthType, hogee.oauthId) + + assertAll( + { assertThat(findMember).isNotNull }, + { assertThat(findMember.memberStatus).isEqualTo(MemberStatus.CODE_SURVEY) }, ) + } } From 562df7482eb4e2de24cb81152ebe4b2fb96a4c66 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Mon, 7 Apr 2025 17:47:51 +0900 Subject: [PATCH 045/542] =?UTF-8?q?refactor:=20MemberRepository=20?= =?UTF-8?q?=EA=B3=84=EC=B8=B5=EC=97=90=EC=84=9C=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=A0=9D=EC=85=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../codel/member/domain/MemberRepository.kt | 12 ++++++++---- .../member/infrastructure/MemberJpaRepository.kt | 16 ++++++++-------- .../member/infrastructure/entity/MemberEntity.kt | 2 -- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index be83a6e8..fc43cb04 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -15,19 +15,23 @@ class MemberRepository( private val memberJpaRepository: MemberJpaRepository, private val profileJpaRepository: ProfileJpaRepository, ) { - fun loginMember(member: Member): Member = - try { + fun loginMember(member: Member): Member { + if (memberJpaRepository.existsByOauthTypeAndOauthId(member.oauthType, member.oauthId)) { + return findMember(member.oauthType, member.oauthId) + } + return try { val memberEntity = memberJpaRepository.save(MemberEntity.toEntity(member)) memberEntity.toDomain() } catch (e: DataIntegrityViolationException) { - findMember(member.oauthType, member.oauthId) + throw IllegalArgumentException("이미 회원이 존재합니다.") } + } fun findMember( oauthType: OauthType, oauthId: String, ): Member { - val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(oauthType.name, oauthId) + val memberEntity = memberJpaRepository.findByOauthTypeAndOauthId(oauthType, oauthId) return memberEntity.toDomain() } diff --git a/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt index beb7e015..24583f36 100644 --- a/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt +++ b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt @@ -1,19 +1,19 @@ package codel.member.infrastructure +import codel.member.domain.OauthType import codel.member.infrastructure.entity.MemberEntity import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query -import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository @Repository interface MemberJpaRepository : JpaRepository { - @Query( - value = "SELECT * FROM member_entity WHERE oauth_type = :oauthType AND oauth_id = :oauthId", - nativeQuery = true, - ) + fun existsByOauthTypeAndOauthId( + oauthType: OauthType, + oauthId: String, + ): Boolean + fun findByOauthTypeAndOauthId( - @Param("oauthType") oauthType: String, - @Param("oauthId") oauthId: String, + oauthType: OauthType, + oauthId: String, ): MemberEntity } diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index bdca965d..6c3f7901 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -1,5 +1,3 @@ -@file:Suppress("ktlint:standard:no-wildcard-imports") - package codel.member.infrastructure.entity import codel.member.domain.Member From 2cf50dede4e814772a5eebcd237951d671d37a5f Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Mon, 7 Apr 2025 17:50:18 +0900 Subject: [PATCH 046/542] =?UTF-8?q?test:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=98=88=EC=99=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .../member/domain/MemberRepositoryTest.kt | 66 +++++++++++++++---- 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index 0ec40876..289bac87 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -2,6 +2,7 @@ package codel.member.domain import codel.config.TestFixture import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll @@ -12,20 +13,7 @@ class MemberRepositoryTest : TestFixture() { @DisplayName("프로필 저장 테스트") @Test fun saveProfileTest() { - val seokProfile = - Profile( - codeName = "seok", - age = 28, - job = "백엔드 개발자", - alcohol = "자주 마심", - smoke = "비흡연자 - 흡연자와 교류 NO", - hobby = listOf("영화 & 드라마", "여행 & 캠핑"), - style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), - bigCity = "경기도", - smallCity = "성남시", - mbti = "isfj", - introduce = "잘부탁드립니다!", - ) + val seokProfile = createProfile() memberRepository.saveProfile(hogee, seokProfile) val findMember = memberRepository.findMember(hogee.oauthType, hogee.oauthId) @@ -35,4 +23,54 @@ class MemberRepositoryTest : TestFixture() { { assertThat(findMember.memberStatus).isEqualTo(MemberStatus.CODE_SURVEY) }, ) } + + @DisplayName("멤버 아이디에 해당하는 멤버가 없으면 예외를 반환한다.") + @Test + fun saveProfileWithoutMemberTest() { + val seokMember = createMember() + val seokProfile = createProfile() + + assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("멤버가 존재하지 않습니다.") + } + + @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") + @Test + fun changeMemberStatusWithoutMemberIdTest() { + val seokMember = createMemberWithoutMemberId() + val seokProfile = createProfile() + + assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage("member id가 비어있습니다.") + } + + private fun createProfile(): Profile = + Profile( + codeName = "seok", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = listOf("영화 & 드라마", "여행 & 캠핑"), + style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + ) + + private fun createMember(): Member = + Member( + id = 100L, + oauthType = OauthType.APPLE, + oauthId = "seok", + ) + + private fun createMemberWithoutMemberId(): Member = + Member( + oauthType = OauthType.APPLE, + oauthId = "seok", + ) } From eb469e01524152b201befa3e3e2d4f1dec54c050 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Mon, 7 Apr 2025 20:04:10 +0900 Subject: [PATCH 047/542] =?UTF-8?q?test:=20MemberArgumentResolverTest=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../MemberArgumentResolver.kt | 5 +- .../MemberArgumentResolverTest.kt | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt diff --git a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt index cb62d071..993a3c3b 100644 --- a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt +++ b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt @@ -6,6 +6,7 @@ import codel.member.domain.OauthType import jakarta.servlet.http.HttpServletRequest import org.springframework.core.MethodParameter import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer @@ -22,15 +23,15 @@ class MemberArgumentResolver( parameter: MethodParameter, mavContainer: ModelAndViewContainer?, webRequest: NativeWebRequest, - binderFactory: org.springframework.web.bind.support.WebDataBinderFactory?, + binderFactory: WebDataBinderFactory?, ): Any? { val httpServletRequest = webRequest.getNativeRequest(HttpServletRequest::class.java) ?: throw IllegalStateException("HttpServletRequest를 가져올 수 없습니다.") + val oauthId = httpServletRequest.getAttribute("oauthId") as? String ?: throw IllegalArgumentException("oauthId가 요청이 없습니다.") - val oauthType = httpServletRequest.getAttribute("oauthType") as? OauthType ?: throw IllegalArgumentException("oauthType이 요청이 없습니다.") diff --git a/src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt b/src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt new file mode 100644 index 00000000..b2af11aa --- /dev/null +++ b/src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt @@ -0,0 +1,60 @@ +package codel.config.argumentresolver + +import codel.member.business.MemberService +import codel.member.domain.Member +import codel.member.domain.MemberStatus +import codel.member.domain.OauthType +import jakarta.servlet.http.HttpServletRequest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import org.springframework.core.MethodParameter +import org.springframework.web.context.request.NativeWebRequest + +class MemberArgumentResolverTest { + private lateinit var memberService: MemberService + private lateinit var resolver: MemberArgumentResolver + + @BeforeEach + fun setUp() { + memberService = mock(MemberService::class.java) + resolver = MemberArgumentResolver(memberService) + } + + @DisplayName("ArgumentResolver는 Member 정보를 반환한다.") + @Test + fun resolveArgumentTest() { + val oauthId = "seok" + val oauthType = OauthType.KAKAO + + val fakeMember = + Member( + id = 1L, + oauthId = oauthId, + oauthType = oauthType, + memberStatus = MemberStatus.SIGNUP, + ) + + val httpRequest = mock(HttpServletRequest::class.java) + `when`(httpRequest.getAttribute("oauthId")).thenReturn(oauthId) + `when`(httpRequest.getAttribute("oauthType")).thenReturn(oauthType) + + val webRequest = mock(NativeWebRequest::class.java) + `when`(webRequest.getNativeRequest(HttpServletRequest::class.java)).thenReturn(httpRequest) + + `when`(memberService.findMember(oauthType, oauthId)).thenReturn(fakeMember) + + val result = + resolver.resolveArgument( + mock(MethodParameter::class.java), + null, + webRequest, + null, + ) + + assertEquals(fakeMember, result) + } +} From 372d920028eb0ed40721f7f84c190d7b8d4db10a Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Mon, 7 Apr 2025 20:04:31 +0900 Subject: [PATCH 048/542] =?UTF-8?q?test:=20TestFixture=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../codel/member/domain/MemberRepository.kt | 3 +- .../kotlin/codel/auth/TokenProviderTest.kt | 2 +- src/test/kotlin/codel/config/TestFixture.kt | 60 +++++++++++++++---- .../member/business/MemberServiceTest.kt | 6 +- .../member/domain/MemberRepositoryTest.kt | 53 ++++------------ 5 files changed, 68 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index fc43cb04..23b9112e 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -9,8 +9,8 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional -@Component @Transactional +@Component class MemberRepository( private val memberJpaRepository: MemberJpaRepository, private val profileJpaRepository: ProfileJpaRepository, @@ -27,6 +27,7 @@ class MemberRepository( } } + @Transactional(readOnly = true) fun findMember( oauthType: OauthType, oauthId: String, diff --git a/src/test/kotlin/codel/auth/TokenProviderTest.kt b/src/test/kotlin/codel/auth/TokenProviderTest.kt index ef72bf04..d933b5f0 100644 --- a/src/test/kotlin/codel/auth/TokenProviderTest.kt +++ b/src/test/kotlin/codel/auth/TokenProviderTest.kt @@ -9,7 +9,7 @@ class TokenProviderTest : TestFixture() { @DisplayName("토큰 정상 생성 테스트") @Test fun provideTest() { - val token = tokenProvider.provide(hogee) + val token = tokenProvider.provide(member) Assertions.assertThat(token).isNotNull() } diff --git a/src/test/kotlin/codel/config/TestFixture.kt b/src/test/kotlin/codel/config/TestFixture.kt index a25b76ae..6a3e87b6 100644 --- a/src/test/kotlin/codel/config/TestFixture.kt +++ b/src/test/kotlin/codel/config/TestFixture.kt @@ -5,17 +5,23 @@ import codel.member.domain.Member import codel.member.domain.MemberRepository import codel.member.domain.MemberStatus import codel.member.domain.OauthType +import codel.member.domain.Profile import codel.member.infrastructure.MemberJpaRepository +import codel.member.infrastructure.ProfileJpaRepository import codel.member.infrastructure.entity.MemberEntity +import codel.member.infrastructure.entity.ProfileEntity import org.junit.jupiter.api.BeforeEach import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class TestFixture { - lateinit var hogee: Member - lateinit var seokEntity: MemberEntity + lateinit var member: Member + lateinit var nonSavedMember: Member + lateinit var memberEntity: MemberEntity lateinit var token: String + lateinit var nonSavedProfile: Profile + lateinit var profile: Profile @Autowired lateinit var tokenProvider: TokenProvider @@ -23,29 +29,63 @@ class TestFixture { @Autowired lateinit var memberRepository: MemberRepository + @Autowired + lateinit var profileJpaRepository: ProfileJpaRepository + @Autowired lateinit var memberJpaRepository: MemberJpaRepository @BeforeEach fun setUp() { memberJpaRepository.deleteAll() - hogee = + member = Member( oauthType = OauthType.APPLE, oauthId = "hogee", ) - - hogee = memberRepository.loginMember(hogee) - - seokEntity = + nonSavedMember = + Member( + oauthType = OauthType.APPLE, + oauthId = "seok", + ) + memberEntity = MemberEntity( oauthType = OauthType.KAKAO, oauthId = "seok", memberStatus = MemberStatus.SIGNUP, ) + nonSavedProfile = + Profile( + codeName = "hogee", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = listOf("영화 & 드라마", "여행 & 캠핑"), + style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + ) + profile = + Profile( + codeName = "hogee", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = listOf("영화 & 드라마", "여행 & 캠핑"), + style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + ) - memberJpaRepository.save(seokEntity) - - token = tokenProvider.provide(hogee) + member = memberRepository.loginMember(member) + memberJpaRepository.save(memberEntity) + profileJpaRepository.save(ProfileEntity.toEntity(profile)) + token = tokenProvider.provide(member) } } diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index 7b3d482b..5a1ef113 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -48,12 +48,12 @@ class MemberServiceTest( mbti = "isfj", introduce = "잘부탁드립니다!", ) - memberService.saveProfile(hogee, profileSavedRequest) + memberService.saveProfile(member, profileSavedRequest) val findMember = memberService.findMember( - oauthType = hogee.oauthType, - oauthId = hogee.oauthId, + oauthType = member.oauthType, + oauthId = member.oauthId, ) Assertions.assertAll( diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index 289bac87..2acd3e6d 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -10,13 +10,11 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class MemberRepositoryTest : TestFixture() { - @DisplayName("프로필 저장 테스트") + @DisplayName("프로필을 저장하면 memberStatus 가 CODE_SURVEY 로 바뀐다.") @Test fun saveProfileTest() { - val seokProfile = createProfile() - - memberRepository.saveProfile(hogee, seokProfile) - val findMember = memberRepository.findMember(hogee.oauthType, hogee.oauthId) + memberRepository.saveProfile(member, nonSavedProfile) + val findMember = memberRepository.findMember(member.oauthType, member.oauthId) assertAll( { assertThat(findMember).isNotNull }, @@ -27,10 +25,14 @@ class MemberRepositoryTest : TestFixture() { @DisplayName("멤버 아이디에 해당하는 멤버가 없으면 예외를 반환한다.") @Test fun saveProfileWithoutMemberTest() { - val seokMember = createMember() - val seokProfile = createProfile() - - assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } + val seokMember = + Member( + id = 100L, + oauthType = OauthType.APPLE, + oauthId = "seok", + ) + + assertThatThrownBy { memberRepository.saveProfile(seokMember, nonSavedProfile) } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage("멤버가 존재하지 않습니다.") } @@ -38,39 +40,8 @@ class MemberRepositoryTest : TestFixture() { @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") @Test fun changeMemberStatusWithoutMemberIdTest() { - val seokMember = createMemberWithoutMemberId() - val seokProfile = createProfile() - - assertThatThrownBy { memberRepository.saveProfile(seokMember, seokProfile) } + assertThatThrownBy { memberRepository.saveProfile(nonSavedMember, nonSavedProfile) } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage("member id가 비어있습니다.") } - - private fun createProfile(): Profile = - Profile( - codeName = "seok", - age = 28, - job = "백엔드 개발자", - alcohol = "자주 마심", - smoke = "비흡연자 - 흡연자와 교류 NO", - hobby = listOf("영화 & 드라마", "여행 & 캠핑"), - style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), - bigCity = "경기도", - smallCity = "성남시", - mbti = "isfj", - introduce = "잘부탁드립니다!", - ) - - private fun createMember(): Member = - Member( - id = 100L, - oauthType = OauthType.APPLE, - oauthId = "seok", - ) - - private fun createMemberWithoutMemberId(): Member = - Member( - oauthType = OauthType.APPLE, - oauthId = "seok", - ) } From 91dd98a9729335de66d240932f250a8d6025e004 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Mon, 7 Apr 2025 21:15:36 +0900 Subject: [PATCH 049/542] =?UTF-8?q?feat:=20S3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=86=B5=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../codel/member/business/MemberService.kt | 5 ++- .../codel/member/domain/MemberRepository.kt | 7 ++-- .../infrastructure/entity/MemberEntity.kt | 5 +++ .../infrastructure/entity/ProfileEntity.kt | 10 +++--- .../member/business/MemberServiceTest.kt | 33 +++++++++++++++++++ 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 7f8da246..76976438 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -11,6 +11,7 @@ import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class MemberService( @@ -55,15 +56,13 @@ class MemberService( oauthId: String, ): Member = memberRepository.findMember(oauthType, oauthId) + @Transactional fun saveCodeImage( member: Member, request: CodeImageSavedRequest, ) { val codeImage = uploadFile(request) - memberRepository.saveImagePath(member, codeImage) - - // memberStatus 수정 } private fun uploadFile(request: CodeImageSavedRequest): CodeImage { diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 23b9112e..1d7d7636 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -58,8 +58,9 @@ class MemberRepository( member: Member, codeImage: CodeImage, ) { - member.saveCodeImage(codeImage) - - val memberEntity = MemberEntity.toEntity(member) + val memberEntity = + memberJpaRepository.findByIdOrNull(member.id) ?: throw IllegalArgumentException("해당 id 멤버 없음") + memberEntity.updateCodeImage(codeImage) + memberEntity.changeMemberStatus(MemberStatus.CODE_PROFILE_IMAGE) } } diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index 6c3f7901..16444e4b 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -1,5 +1,6 @@ package codel.member.infrastructure.entity +import codel.member.domain.CodeImage import codel.member.domain.Member import codel.member.domain.MemberStatus import codel.member.domain.OauthType @@ -49,6 +50,10 @@ class MemberEntity( this.profileEntity = profileEntity } + fun updateCodeImage(codeImage: CodeImage) { + profileEntity!!.updateCodeImage(codeImage) + } + fun changeMemberStatus(status: MemberStatus) { this.memberStatus = status } diff --git a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt index c2ddd773..5161001a 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt @@ -1,5 +1,7 @@ package codel.member.infrastructure.entity +import codel.member.domain.CodeImage +import codel.member.domain.FaceImage import codel.member.domain.Profile import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue @@ -62,11 +64,11 @@ class ProfileEntity( introduce = this.introduce, ) - fun updateCodeImage(codeImages: List) { - codeImage = serializeAttribute(codeImages) + fun updateCodeImage(codeImage: CodeImage) { + this.codeImage = serializeAttribute(codeImage.urls) } - fun updateFaceImage(faceImages: List) { - faceImage = serializeAttribute(faceImages) + fun updateFaceImage(faceImage: FaceImage) { + this.faceImage = serializeAttribute(faceImage.urls) } } diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index 5a1ef113..5c8ae73f 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -1,16 +1,25 @@ package codel.member.business import codel.config.TestFixture +import codel.member.domain.CodeImage +import codel.member.domain.ImageUploader import codel.member.domain.MemberStatus import codel.member.domain.OauthType +import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.mock.web.MockMultipartFile @SpringBootTest class MemberServiceTest( @@ -61,4 +70,28 @@ class MemberServiceTest( { assertThat(findMember.memberStatus).isEqualTo(MemberStatus.CODE_SURVEY) }, ) } + + @Test + fun `saveCodeImage는 S3에 업로드하고 저장소에 이미지 경로 저장한다`() { + val imageUploader = mock(ImageUploader::class.java) + val mockMemberService = MemberService(memberRepository, imageUploader) + // given + val member = member + + val file1 = MockMultipartFile("image1", "image1.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) + val file2 = MockMultipartFile("image2", "image2.jpg", "image/jpeg", byteArrayOf(4, 5, 6)) + val request = CodeImageSavedRequest(listOf(file1, file2)) + + // mock imageUploader.uploadFile + `when`(imageUploader.uploadFile(file1)).thenReturn("https://s3.amazonaws.com/image1.jpg") + `when`(imageUploader.uploadFile(file2)).thenReturn("https://s3.amazonaws.com/image2.jpg") + + // when + mockMemberService.saveCodeImage(member, request) + + // then + verify(imageUploader).uploadFile(file1) + verify(imageUploader).uploadFile(file2) + verify(memberRepository).saveImagePath(eq(member), any(CodeImage::class.java)) + } } From 611d4e9ed411f5624d40def950fe27f45e7bccfe Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Mon, 7 Apr 2025 21:15:54 +0900 Subject: [PATCH 050/542] =?UTF-8?q?test:=20TestFixture=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- src/test/kotlin/codel/config/TestFixture.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/kotlin/codel/config/TestFixture.kt b/src/test/kotlin/codel/config/TestFixture.kt index 6a3e87b6..c4ca1579 100644 --- a/src/test/kotlin/codel/config/TestFixture.kt +++ b/src/test/kotlin/codel/config/TestFixture.kt @@ -18,6 +18,7 @@ import org.springframework.boot.test.context.SpringBootTest class TestFixture { lateinit var member: Member lateinit var nonSavedMember: Member + lateinit var memberWithProfile: Member lateinit var memberEntity: MemberEntity lateinit var token: String lateinit var nonSavedProfile: Profile @@ -35,6 +36,21 @@ class TestFixture { @Autowired lateinit var memberJpaRepository: MemberJpaRepository + /** + * gpt야 부탁해 + * 1. 프로필 없는 회원 + * 2. 프로필만 있는 회원 + * 3. 코드이미지만 있는 회원 + * 4. 얼굴이미지까지 다 있는 회원 + * member, memberEntity, 저장안한 멤버까지 + * 5. 정말 프로필만 있는 프로필 객체 + * 6. 코드이미지까지 저장된 프로필 객체 + * 7. 페이스이미지까지 저장된 프로필 객체 + * profile, profileEntity, 저장안한 프로필까지 + * + * + * + */ @BeforeEach fun setUp() { memberJpaRepository.deleteAll() @@ -86,6 +102,7 @@ class TestFixture { member = memberRepository.loginMember(member) memberJpaRepository.save(memberEntity) profileJpaRepository.save(ProfileEntity.toEntity(profile)) + memberRepository.saveProfile(member, profile) token = tokenProvider.provide(member) } } From c7ed31018bc0623bda097f22ada4d2f25d1fe7e4 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 9 Apr 2025 15:20:13 +0900 Subject: [PATCH 051/542] =?UTF-8?q?test:=20TestFixture=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../kotlin/codel/auth/TokenProviderTest.kt | 2 +- src/test/kotlin/codel/config/TestFixture.kt | 161 ++++++++++++------ .../member/business/MemberServiceTest.kt | 39 +---- .../member/domain/MemberRepositoryTest.kt | 35 +++- .../infrastructure/MemberJpaRepositoryTest.kt | 5 +- 5 files changed, 146 insertions(+), 96 deletions(-) diff --git a/src/test/kotlin/codel/auth/TokenProviderTest.kt b/src/test/kotlin/codel/auth/TokenProviderTest.kt index d933b5f0..062b919e 100644 --- a/src/test/kotlin/codel/auth/TokenProviderTest.kt +++ b/src/test/kotlin/codel/auth/TokenProviderTest.kt @@ -9,7 +9,7 @@ class TokenProviderTest : TestFixture() { @DisplayName("토큰 정상 생성 테스트") @Test fun provideTest() { - val token = tokenProvider.provide(member) + val token = tokenProvider.provide(memberSignup) Assertions.assertThat(token).isNotNull() } diff --git a/src/test/kotlin/codel/config/TestFixture.kt b/src/test/kotlin/codel/config/TestFixture.kt index c4ca1579..21864a0b 100644 --- a/src/test/kotlin/codel/config/TestFixture.kt +++ b/src/test/kotlin/codel/config/TestFixture.kt @@ -2,10 +2,8 @@ package codel.config import codel.auth.TokenProvider import codel.member.domain.Member -import codel.member.domain.MemberRepository import codel.member.domain.MemberStatus import codel.member.domain.OauthType -import codel.member.domain.Profile import codel.member.infrastructure.MemberJpaRepository import codel.member.infrastructure.ProfileJpaRepository import codel.member.infrastructure.entity.MemberEntity @@ -16,93 +14,156 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class TestFixture { - lateinit var member: Member - lateinit var nonSavedMember: Member - lateinit var memberWithProfile: Member - lateinit var memberEntity: MemberEntity + lateinit var memberSignup: Member + lateinit var memberCodeSurvey: Member + lateinit var memberCodeProfileImage: Member + lateinit var memberPending: Member + lateinit var memberDone: Member + lateinit var token: String - lateinit var nonSavedProfile: Profile - lateinit var profile: Profile @Autowired lateinit var tokenProvider: TokenProvider - @Autowired - lateinit var memberRepository: MemberRepository - @Autowired lateinit var profileJpaRepository: ProfileJpaRepository @Autowired lateinit var memberJpaRepository: MemberJpaRepository - /** - * gpt야 부탁해 - * 1. 프로필 없는 회원 - * 2. 프로필만 있는 회원 - * 3. 코드이미지만 있는 회원 - * 4. 얼굴이미지까지 다 있는 회원 - * member, memberEntity, 저장안한 멤버까지 - * 5. 정말 프로필만 있는 프로필 객체 - * 6. 코드이미지까지 저장된 프로필 객체 - * 7. 페이스이미지까지 저장된 프로필 객체 - * profile, profileEntity, 저장안한 프로필까지 - * - * - * - */ @BeforeEach fun setUp() { memberJpaRepository.deleteAll() - member = - Member( - oauthType = OauthType.APPLE, - oauthId = "hogee", + memberSignup = saveMemberSignup() + memberCodeSurvey = saveMemberCodeSurvey() + memberCodeProfileImage = saveMemberCodeProfileImage() + memberPending = saveMemberPending() + memberDone = saveMemberDone() + + token = tokenProvider.provide(memberSignup) + } + + private fun saveMemberSignup(): Member { + val memberEntity = + MemberEntity( + oauthType = OauthType.KAKAO, + oauthId = "hogee1", + memberStatus = MemberStatus.SIGNUP, ) - nonSavedMember = - Member( - oauthType = OauthType.APPLE, - oauthId = "seok", + val saveEntity = memberJpaRepository.save(memberEntity) + return saveEntity.toDomain() + } + + private fun saveMemberCodeSurvey(): Member { + val profileEntity = + ProfileEntity( + codeName = "hogee", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = "영화 & 드라마,여행 & 캠핑", + style = "표현을 잘하는 직진형,상대가 필요할 때 항상 먼저 연락하는 스타일", + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", ) - memberEntity = + val saveProfileEntity = profileJpaRepository.save(profileEntity) + val memberEntity = MemberEntity( + profileEntity = saveProfileEntity, oauthType = OauthType.KAKAO, - oauthId = "seok", - memberStatus = MemberStatus.SIGNUP, + oauthId = "hogee2", + memberStatus = MemberStatus.CODE_SURVEY, ) - nonSavedProfile = - Profile( + val saveEntity = memberJpaRepository.save(memberEntity) + return saveEntity.toDomain() + } + + private fun saveMemberCodeProfileImage(): Member { + val profileEntity = + ProfileEntity( codeName = "hogee", age = 28, job = "백엔드 개발자", alcohol = "자주 마심", smoke = "비흡연자 - 흡연자와 교류 NO", - hobby = listOf("영화 & 드라마", "여행 & 캠핑"), - style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + hobby = "영화 & 드라마,여행 & 캠핑", + style = "표현을 잘하는 직진형,상대가 필요할 때 항상 먼저 연락하는 스타일", bigCity = "경기도", smallCity = "성남시", mbti = "isfj", introduce = "잘부탁드립니다!", + codeImage = "www.s3.aws.com1", ) - profile = - Profile( + val saveProfileEntity = profileJpaRepository.save(profileEntity) + val memberEntity = + MemberEntity( + profileEntity = saveProfileEntity, + oauthType = OauthType.KAKAO, + oauthId = "hogee3", + memberStatus = MemberStatus.CODE_PROFILE_IMAGE, + ) + val saveEntity = memberJpaRepository.save(memberEntity) + return saveEntity.toDomain() + } + + private fun saveMemberPending(): Member { + val profileEntity = + ProfileEntity( codeName = "hogee", age = 28, job = "백엔드 개발자", alcohol = "자주 마심", smoke = "비흡연자 - 흡연자와 교류 NO", - hobby = listOf("영화 & 드라마", "여행 & 캠핑"), - style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + hobby = "영화 & 드라마,여행 & 캠핑", + style = "표현을 잘하는 직진형,상대가 필요할 때 항상 먼저 연락하는 스타일", bigCity = "경기도", smallCity = "성남시", mbti = "isfj", introduce = "잘부탁드립니다!", + codeImage = "www.s3.aws.com1", + faceImage = "www.s3.aws1,www.s3.aws2,www.s3.aws3", ) + val saveProfileEntity = profileJpaRepository.save(profileEntity) + val memberEntity = + MemberEntity( + profileEntity = saveProfileEntity, + oauthType = OauthType.KAKAO, + oauthId = "hogee4", + memberStatus = MemberStatus.PENDING, + ) + val saveEntity = memberJpaRepository.save(memberEntity) + return saveEntity.toDomain() + } - member = memberRepository.loginMember(member) - memberJpaRepository.save(memberEntity) - profileJpaRepository.save(ProfileEntity.toEntity(profile)) - memberRepository.saveProfile(member, profile) - token = tokenProvider.provide(member) + private fun saveMemberDone(): Member { + val profileEntity = + ProfileEntity( + codeName = "hogee", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = "영화 & 드라마,여행 & 캠핑", + style = "표현을 잘하는 직진형,상대가 필요할 때 항상 먼저 연락하는 스타일", + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + codeImage = "www.s3.aws.com1", + faceImage = "www.s3.aws1,www.s3.aws2,www.s3.aws3", + ) + val saveProfileEntity = profileJpaRepository.save(profileEntity) + val memberEntity = + MemberEntity( + profileEntity = saveProfileEntity, + oauthType = OauthType.KAKAO, + oauthId = "hogee5", + memberStatus = MemberStatus.DONE, + ) + val saveEntity = memberJpaRepository.save(memberEntity) + return saveEntity.toDomain() } } diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index 5c8ae73f..66e4339f 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -1,25 +1,16 @@ package codel.member.business import codel.config.TestFixture -import codel.member.domain.CodeImage -import codel.member.domain.ImageUploader import codel.member.domain.MemberStatus import codel.member.domain.OauthType -import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import org.springframework.mock.web.MockMultipartFile @SpringBootTest class MemberServiceTest( @@ -57,12 +48,12 @@ class MemberServiceTest( mbti = "isfj", introduce = "잘부탁드립니다!", ) - memberService.saveProfile(member, profileSavedRequest) + memberService.saveProfile(memberSignup, profileSavedRequest) val findMember = memberService.findMember( - oauthType = member.oauthType, - oauthId = member.oauthId, + oauthType = memberSignup.oauthType, + oauthId = memberSignup.oauthId, ) Assertions.assertAll( @@ -70,28 +61,4 @@ class MemberServiceTest( { assertThat(findMember.memberStatus).isEqualTo(MemberStatus.CODE_SURVEY) }, ) } - - @Test - fun `saveCodeImage는 S3에 업로드하고 저장소에 이미지 경로 저장한다`() { - val imageUploader = mock(ImageUploader::class.java) - val mockMemberService = MemberService(memberRepository, imageUploader) - // given - val member = member - - val file1 = MockMultipartFile("image1", "image1.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) - val file2 = MockMultipartFile("image2", "image2.jpg", "image/jpeg", byteArrayOf(4, 5, 6)) - val request = CodeImageSavedRequest(listOf(file1, file2)) - - // mock imageUploader.uploadFile - `when`(imageUploader.uploadFile(file1)).thenReturn("https://s3.amazonaws.com/image1.jpg") - `when`(imageUploader.uploadFile(file2)).thenReturn("https://s3.amazonaws.com/image2.jpg") - - // when - mockMemberService.saveCodeImage(member, request) - - // then - verify(imageUploader).uploadFile(file1) - verify(imageUploader).uploadFile(file2) - verify(memberRepository).saveImagePath(eq(member), any(CodeImage::class.java)) - } } diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index 2acd3e6d..9c283b38 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -6,15 +6,34 @@ import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @SpringBootTest -class MemberRepositoryTest : TestFixture() { +class MemberRepositoryTest( + @Autowired + private val memberRepository: MemberRepository, +) : TestFixture() { + var profile: Profile = + Profile( + codeName = "hogee", + age = 28, + job = "백엔드 개발자", + alcohol = "자주 마심", + smoke = "비흡연자 - 흡연자와 교류 NO", + hobby = listOf("영화 & 드라마", "여행 & 캠핑"), + style = listOf("표현을 잘하는 직진형", "상대가 필요할 때 항상 먼저 연락하는 스타일"), + bigCity = "경기도", + smallCity = "성남시", + mbti = "isfj", + introduce = "잘부탁드립니다!", + ) + @DisplayName("프로필을 저장하면 memberStatus 가 CODE_SURVEY 로 바뀐다.") @Test fun saveProfileTest() { - memberRepository.saveProfile(member, nonSavedProfile) - val findMember = memberRepository.findMember(member.oauthType, member.oauthId) + memberRepository.saveProfile(memberSignup, profile) + val findMember = memberRepository.findMember(memberSignup.oauthType, memberSignup.oauthId) assertAll( { assertThat(findMember).isNotNull }, @@ -31,8 +50,7 @@ class MemberRepositoryTest : TestFixture() { oauthType = OauthType.APPLE, oauthId = "seok", ) - - assertThatThrownBy { memberRepository.saveProfile(seokMember, nonSavedProfile) } + assertThatThrownBy { memberRepository.saveProfile(seokMember, profile) } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage("멤버가 존재하지 않습니다.") } @@ -40,7 +58,12 @@ class MemberRepositoryTest : TestFixture() { @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") @Test fun changeMemberStatusWithoutMemberIdTest() { - assertThatThrownBy { memberRepository.saveProfile(nonSavedMember, nonSavedProfile) } + val seokMember = + Member( + oauthType = OauthType.APPLE, + oauthId = "seok", + ) + assertThatThrownBy { memberRepository.saveProfile(seokMember, profile) } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage("member id가 비어있습니다.") } diff --git a/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt index 1dbfd766..d813025c 100644 --- a/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt +++ b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt @@ -2,7 +2,6 @@ package codel.member.infrastructure import codel.config.TestFixture import codel.member.domain.MemberStatus -import codel.member.domain.OauthType import codel.member.infrastructure.entity.MemberEntity import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName @@ -17,8 +16,8 @@ class MemberJpaRepositoryTest : TestFixture() { fun saveMemberTest() { val newEntity = MemberEntity( - oauthType = OauthType.KAKAO, - oauthId = "seok", + oauthType = memberSignup.oauthType, + oauthId = memberSignup.oauthId, memberStatus = MemberStatus.SIGNUP, ) Assertions.assertThrows(DataIntegrityViolationException::class.java) { From 5887daf898035afc2ef572330f7820b72c83f02e Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 9 Apr 2025 15:57:48 +0900 Subject: [PATCH 052/542] =?UTF-8?q?test:=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- src/main/kotlin/codel/member/domain/Member.kt | 14 ++------ .../infrastructure/entity/MemberEntity.kt | 9 ++--- .../infrastructure/entity/ProfileEntity.kt | 30 +++++++++-------- .../member/business/MemberServiceTest.kt | 33 +++++++++++++++++++ 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/codel/member/domain/Member.kt b/src/main/kotlin/codel/member/domain/Member.kt index d8c48d77..87978239 100644 --- a/src/main/kotlin/codel/member/domain/Member.kt +++ b/src/main/kotlin/codel/member/domain/Member.kt @@ -5,15 +5,7 @@ class Member( val profile: Profile? = null, val oauthType: OauthType, val oauthId: String, - private var codeImage: CodeImage? = null, - private var faceImage: FaceImage? = null, + val codeImage: CodeImage? = null, + val faceImage: FaceImage? = null, val memberStatus: MemberStatus = MemberStatus.SIGNUP, -) { - fun saveCodeImage(codeImage: CodeImage) { - this.codeImage = codeImage - } - - fun saveFaceImage(faceImage: FaceImage) { - this.faceImage = faceImage - } -} +) diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index 16444e4b..ab0ac39d 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -23,10 +23,10 @@ class MemberEntity( @GeneratedValue(strategy = GenerationType.IDENTITY) private var id: Long? = null, @OneToOne - private var profileEntity: ProfileEntity? = null, - private var oauthType: OauthType, - private var oauthId: String, - private var memberStatus: MemberStatus, + var profileEntity: ProfileEntity? = null, + var oauthType: OauthType, + var oauthId: String, + var memberStatus: MemberStatus, ) { companion object { fun toEntity(member: Member): MemberEntity = @@ -44,6 +44,7 @@ class MemberEntity( oauthType = this.oauthType, oauthId = this.oauthId, memberStatus = this.memberStatus, + codeImage = profileEntity?.getCodeImage()?.let { CodeImage(it) }, ) fun saveProfileEntity(profileEntity: ProfileEntity) { diff --git a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt index 5161001a..6d294097 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/ProfileEntity.kt @@ -13,19 +13,19 @@ class ProfileEntity( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private var id: Long? = null, - private var codeName: String, - private var age: Int, - private var job: String, - private var alcohol: String, - private var smoke: String, - private var hobby: String, // 복수 - private var style: String, // 복수 - private var bigCity: String, - private var smallCity: String, - private var mbti: String, - private var introduce: String, - private var codeImage: String? = null, // 복수 - private var faceImage: String? = null, // 복수 + var codeName: String, + var age: Int, + var job: String, + var alcohol: String, + var smoke: String, + var hobby: String, // 복수 + var style: String, // 복수 + var bigCity: String, + var smallCity: String, + var mbti: String, + var introduce: String, + var codeImage: String? = null, // 복수 + var faceImage: String? = null, // 복수 ) { companion object { fun toEntity(profile: Profile): ProfileEntity = @@ -48,6 +48,10 @@ class ProfileEntity( private fun deserializeAttribute(attribute: String): List = attribute.split(",") } + fun getCodeImage(): List? = this.codeImage?.let { deserializeAttribute(it) } + + fun getFaceImage(): List? = this.faceImage?.let { deserializeAttribute(it) } + fun toDomain(): Profile = Profile( id = this.id, diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index 66e4339f..1a67448d 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -1,21 +1,30 @@ package codel.member.business import codel.config.TestFixture +import codel.member.domain.ImageUploader +import codel.member.domain.MemberRepository import codel.member.domain.MemberStatus import codel.member.domain.OauthType +import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.mock.web.MockMultipartFile @SpringBootTest class MemberServiceTest( @Autowired private val memberService: MemberService, + @Autowired + private val memberRepository: MemberRepository, ) : TestFixture() { @DisplayName("첫 로그인을 한 멤버의 상태는 SignUp 이다.") @Test @@ -61,4 +70,28 @@ class MemberServiceTest( { assertThat(findMember.memberStatus).isEqualTo(MemberStatus.CODE_SURVEY) }, ) } + + @DisplayName("saveCodeImage는 S3에 업로드하고 저장소에 이미지 경로 저장한다") + @Test + fun saveCodeImageTest() { + val imageUploader = mock(ImageUploader::class.java) + val mockMemberService = MemberService(memberRepository, imageUploader) + val member = memberCodeSurvey + + val file1 = MockMultipartFile("image1", "image1.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) + val file2 = MockMultipartFile("image2", "image2.jpg", "image/jpeg", byteArrayOf(4, 5, 6)) + val request = CodeImageSavedRequest(listOf(file1, file2)) + + `when`(imageUploader.uploadFile(file1)).thenReturn("https://s3.amazonaws.com/image1.jpg") + `when`(imageUploader.uploadFile(file2)).thenReturn("https://s3.amazonaws.com/image2.jpg") + + mockMemberService.saveCodeImage(member, request) + + verify(imageUploader).uploadFile(file1) + verify(imageUploader).uploadFile(file2) + val memberEntity = memberJpaRepository.findById(member.id!!).get() + val savedMember = memberEntity.toDomain() + assertThat(savedMember.codeImage).isNotNull + assertThat(savedMember.memberStatus).isEqualTo(MemberStatus.CODE_PROFILE_IMAGE) + } } From cbbe43cd14a24ff780c3827ffe4d1d84f1b6454b Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 9 Apr 2025 17:07:34 +0900 Subject: [PATCH 053/542] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SongGwanSeok <20003204@sju.ac.kr> --- .../kotlin/codel/member/business/MemberService.kt | 11 ++++------- .../kotlin/codel/member/infrastructure/S3Uploader.kt | 1 - .../codel/member/presentation/MemberController.kt | 9 ++++----- .../presentation/swagger/MemberControllerSwagger.kt | 5 +++-- .../kotlin/codel/member/business/MemberServiceTest.kt | 3 +-- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 76976438..c25f0ebf 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -6,12 +6,12 @@ import codel.member.domain.Member import codel.member.domain.MemberRepository import codel.member.domain.OauthType import codel.member.domain.Profile -import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile @Service class MemberService( @@ -59,14 +59,11 @@ class MemberService( @Transactional fun saveCodeImage( member: Member, - request: CodeImageSavedRequest, + files: List, ) { - val codeImage = uploadFile(request) + val codeImage = uploadFile(files) memberRepository.saveImagePath(member, codeImage) } - private fun uploadFile(request: CodeImageSavedRequest): CodeImage { - val imageFiles = request.imageFiles - return CodeImage(imageFiles.map { file -> imageUploader.uploadFile(file) }) - } + private fun uploadFile(files: List): CodeImage = CodeImage(files.map { file -> imageUploader.uploadFile(file) }) } diff --git a/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt b/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt index e1d432dd..1f11a779 100644 --- a/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt +++ b/src/main/kotlin/codel/member/infrastructure/S3Uploader.kt @@ -23,7 +23,6 @@ class S3Uploader( .bucket(bucket) .key(fileName) .contentType(file.contentType) - .acl("public-read") .build() s3Client.putObject(putObjectRequest, RequestBody.fromBytes(file.bytes)) diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index f2f019b4..1cb00b01 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -4,7 +4,6 @@ import codel.auth.business.AuthService import codel.config.argumentresolver.LoginMember import codel.member.business.MemberService import codel.member.domain.Member -import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse @@ -12,7 +11,9 @@ import codel.member.presentation.swagger.MemberControllerSwagger import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @RestController class MemberController( @@ -43,11 +44,9 @@ class MemberController( @PostMapping("/v1/member/codeimage") override fun saveCodeImage( @LoginMember member: Member, - @RequestBody request: CodeImageSavedRequest, + @RequestPart files: List, ): ResponseEntity { - // 이미지를 엔티티로 저장 - // s3 통신 방식 변경 필요 - memberService.saveCodeImage(member, request) + memberService.saveCodeImage(member, files) return ResponseEntity.ok().build() } } diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index 73560fad..d0e759eb 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -2,7 +2,6 @@ package codel.member.presentation.swagger import codel.config.argumentresolver.LoginMember import codel.member.domain.Member -import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import codel.member.presentation.response.MemberLoginResponse @@ -12,6 +11,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.multipart.MultipartFile @Tag(name = "Member", description = "회원 관련 API") interface MemberControllerSwagger { @@ -50,6 +51,6 @@ interface MemberControllerSwagger { ) fun saveCodeImage( @LoginMember member: Member, - @RequestBody request: CodeImageSavedRequest, + @RequestPart files: List, ): ResponseEntity } diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index 1a67448d..cbf30615 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -5,7 +5,6 @@ import codel.member.domain.ImageUploader import codel.member.domain.MemberRepository import codel.member.domain.MemberStatus import codel.member.domain.OauthType -import codel.member.presentation.request.CodeImageSavedRequest import codel.member.presentation.request.MemberLoginRequest import codel.member.presentation.request.ProfileSavedRequest import org.assertj.core.api.Assertions.assertThat @@ -80,7 +79,7 @@ class MemberServiceTest( val file1 = MockMultipartFile("image1", "image1.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) val file2 = MockMultipartFile("image2", "image2.jpg", "image/jpeg", byteArrayOf(4, 5, 6)) - val request = CodeImageSavedRequest(listOf(file1, file2)) + val request = listOf(file1, file2) `when`(imageUploader.uploadFile(file1)).thenReturn("https://s3.amazonaws.com/image1.jpg") `when`(imageUploader.uploadFile(file2)).thenReturn("https://s3.amazonaws.com/image2.jpg") From 9141165c244d5eb4cd9e701628ba20538a6bea28 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Fri, 11 Apr 2025 14:58:11 +0900 Subject: [PATCH 054/542] =?UTF-8?q?feat:=20spring=20actuator=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- build.gradle.kts | 3 +++ src/main/kotlin/codel/config/filter/JwtAuthFilter.kt | 1 + 2 files changed, 4 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 6db02fc0..99addf0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,6 +44,9 @@ dependencies { // s3 implementation("software.amazon.awssdk:s3:2.20.148") + + // monitoring + implementation("org.springframework.boot:spring-boot-starter-actuator") } kotlin { diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt index fbbcd449..ae7f1efd 100644 --- a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -18,6 +18,7 @@ class JwtAuthFilter( "/v1/health", "/swagger-ui/", "/v3/api-docs", + "/actuator/", ) } From d5280828a7e79431e0bda218446dda868e124a9a Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Fri, 11 Apr 2025 15:15:23 +0900 Subject: [PATCH 055/542] =?UTF-8?q?feat:=20saveFaceImage=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/codel/member/domain/MemberRepository.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 1d7d7636..2a8e3694 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -54,7 +54,7 @@ class MemberRepository( return memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("멤버가 존재하지 않습니다.") } - fun saveImagePath( + fun saveCodeImage( member: Member, codeImage: CodeImage, ) { @@ -63,4 +63,14 @@ class MemberRepository( memberEntity.updateCodeImage(codeImage) memberEntity.changeMemberStatus(MemberStatus.CODE_PROFILE_IMAGE) } + + fun saveFaceImage( + member: Member, + faceImage: FaceImage, + ) { + val memberEntity = + memberJpaRepository.findByIdOrNull(member.id) ?: throw IllegalArgumentException("해당 id 멤버 없음") + memberEntity.updateFaceImage(faceImage) + memberEntity.changeMemberStatus(MemberStatus.PENDING) + } } From 24f2590f9bb2c0a5a7d900fabaae8d8c0c80fc0d Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Fri, 11 Apr 2025 15:15:51 +0900 Subject: [PATCH 056/542] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=A0=80=EC=9E=A5=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/member/business/MemberService.kt | 18 ++++++++++--- .../kotlin/codel/member/domain/FaceImage.kt | 6 ++++- .../infrastructure/entity/MemberEntity.kt | 7 ++++- .../member/presentation/MemberController.kt | 9 +++++++ .../swagger/MemberControllerSwagger.kt | 13 +++++++++ .../member/business/MemberServiceTest.kt | 27 +++++++++++++++++++ 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index c25f0ebf..c401e1e6 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -1,6 +1,7 @@ package codel.member.business import codel.member.domain.CodeImage +import codel.member.domain.FaceImage import codel.member.domain.ImageUploader import codel.member.domain.Member import codel.member.domain.MemberRepository @@ -61,9 +62,20 @@ class MemberService( member: Member, files: List, ) { - val codeImage = uploadFile(files) - memberRepository.saveImagePath(member, codeImage) + val codeImage = uploadCodeImage(files) + memberRepository.saveCodeImage(member, codeImage) } - private fun uploadFile(files: List): CodeImage = CodeImage(files.map { file -> imageUploader.uploadFile(file) }) + private fun uploadCodeImage(files: List): CodeImage = CodeImage(files.map { file -> imageUploader.uploadFile(file) }) + + @Transactional + fun saveFaceImage( + member: Member, + files: List, + ) { + val faceImage = uploadFaceImage(files) + memberRepository.saveFaceImage(member, faceImage) + } + + private fun uploadFaceImage(files: List): FaceImage = FaceImage(files.map { file -> imageUploader.uploadFile(file) }) } diff --git a/src/main/kotlin/codel/member/domain/FaceImage.kt b/src/main/kotlin/codel/member/domain/FaceImage.kt index a37c1f30..22a95b2e 100644 --- a/src/main/kotlin/codel/member/domain/FaceImage.kt +++ b/src/main/kotlin/codel/member/domain/FaceImage.kt @@ -2,4 +2,8 @@ package codel.member.domain class FaceImage( val urls: List, -) +) { + init { + require(urls.size == 3) { "얼굴 이미지 URL은 정확히 3개여야 합니다." } + } +} diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index ab0ac39d..d0fe15cb 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -1,6 +1,7 @@ package codel.member.infrastructure.entity import codel.member.domain.CodeImage +import codel.member.domain.FaceImage import codel.member.domain.Member import codel.member.domain.MemberStatus import codel.member.domain.OauthType @@ -52,7 +53,11 @@ class MemberEntity( } fun updateCodeImage(codeImage: CodeImage) { - profileEntity!!.updateCodeImage(codeImage) + profileEntity?.updateCodeImage(codeImage) + } + + fun updateFaceImage(faceImage: FaceImage) { + profileEntity?.updateFaceImage(faceImage) } fun changeMemberStatus(status: MemberStatus) { diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 1cb00b01..08cd28c7 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -49,4 +49,13 @@ class MemberController( memberService.saveCodeImage(member, files) return ResponseEntity.ok().build() } + + @PostMapping("/v1/member/faceimage") + override fun saveFaceImage( + @LoginMember member: Member, + @RequestPart files: List, + ): ResponseEntity { + memberService.saveFaceImage(member, files) + return ResponseEntity.ok().build() + } } diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index d0e759eb..23cfae38 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -53,4 +53,17 @@ interface MemberControllerSwagger { @LoginMember member: Member, @RequestPart files: List, ): ResponseEntity + + @Operation(summary = "페이지 이미지 받기", description = "페이즈 이미지를 3장 받습니다.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "페이스 이미지 성공적으로 저장됨"), + ApiResponse(responseCode = "400", description = "요청 값이 잘못됨"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + fun saveFaceImage( + @LoginMember member: Member, + @RequestPart files: List, + ): ResponseEntity } diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index cbf30615..d1c02caf 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -93,4 +93,31 @@ class MemberServiceTest( assertThat(savedMember.codeImage).isNotNull assertThat(savedMember.memberStatus).isEqualTo(MemberStatus.CODE_PROFILE_IMAGE) } + + @DisplayName("saveFaceImage는 S3에 업로드하고 저장소에 이미지 경로 저장한다") + @Test + fun saveFaceImageTest() { + val imageUploader = mock(ImageUploader::class.java) + val mockMemberService = MemberService(memberRepository, imageUploader) + val member = memberCodeProfileImage + + val file1 = MockMultipartFile("image1", "image1.jpg", "image/jpeg", byteArrayOf(1, 2, 3)) + val file2 = MockMultipartFile("image2", "image2.jpg", "image/jpeg", byteArrayOf(4, 5, 6)) + val file3 = MockMultipartFile("image3", "image3.jpg", "image/jpeg", byteArrayOf(7, 8, 9)) + val request = listOf(file1, file2, file3) + + `when`(imageUploader.uploadFile(file1)).thenReturn("https://s3.amazonaws.com/image1.jpg") + `when`(imageUploader.uploadFile(file2)).thenReturn("https://s3.amazonaws.com/image2.jpg") + `when`(imageUploader.uploadFile(file3)).thenReturn("https://s3.amazonaws.com/image3.jpg") + + mockMemberService.saveFaceImage(member, request) + + verify(imageUploader).uploadFile(file1) + verify(imageUploader).uploadFile(file2) + verify(imageUploader).uploadFile(file3) + val memberEntity = memberJpaRepository.findById(member.id!!).get() + val savedMember = memberEntity.toDomain() + assertThat(savedMember.codeImage).isNotNull + assertThat(savedMember.memberStatus).isEqualTo(MemberStatus.PENDING) + } } From 600b2c0a4c3c60e4c107bd2534a61c663226f14f Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Fri, 11 Apr 2025 15:16:16 +0900 Subject: [PATCH 057/542] =?UTF-8?q?test:=20=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/member/domain/FaceImageTest.kt | 31 +++++++++++++++++++ .../infrastructure/MemberJpaRepositoryTest.kt | 4 +-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/test/kotlin/codel/member/domain/FaceImageTest.kt diff --git a/src/test/kotlin/codel/member/domain/FaceImageTest.kt b/src/test/kotlin/codel/member/domain/FaceImageTest.kt new file mode 100644 index 00000000..1ebe246f --- /dev/null +++ b/src/test/kotlin/codel/member/domain/FaceImageTest.kt @@ -0,0 +1,31 @@ +package codel.member.domain + +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class FaceImageTest { + @DisplayName("페이스 이미지는 3장이여야 한다.") + @Test + fun initTest() { + assertAll( + { + assertDoesNotThrow { + FaceImage(listOf("url1", "url2", "url3")) + } + }, + { + assertThrows(IllegalArgumentException::class.java) { + FaceImage(listOf("url1", "url2")) + } + }, + { + assertThrows(IllegalArgumentException::class.java) { + FaceImage(listOf("url1", "url2", "url3", "url4")) + } + }, + ) + } +} diff --git a/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt index d813025c..11b79f45 100644 --- a/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt +++ b/src/test/kotlin/codel/member/infrastructure/MemberJpaRepositoryTest.kt @@ -3,7 +3,7 @@ package codel.member.infrastructure import codel.config.TestFixture import codel.member.domain.MemberStatus import codel.member.infrastructure.entity.MemberEntity -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest @@ -20,7 +20,7 @@ class MemberJpaRepositoryTest : TestFixture() { oauthId = memberSignup.oauthId, memberStatus = MemberStatus.SIGNUP, ) - Assertions.assertThrows(DataIntegrityViolationException::class.java) { + assertThrows(DataIntegrityViolationException::class.java) { memberJpaRepository.save(newEntity) } } From e40e87f8ee987452d6d497468b6420c0bd82330e Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Fri, 11 Apr 2025 16:02:15 +0900 Subject: [PATCH 058/542] =?UTF-8?q?chore:=20ci/cd=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=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.yml | 5 +++++ .github/workflows/ci.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ed0d0872..224af6d9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -42,6 +42,11 @@ jobs: credentials: access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} + management: + endpoints: + web: + exposure: + include: health, metrics, prometheus EOF - name: Build jar diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f046163f..a99234a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,11 @@ jobs: credentials: access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} + management: + endpoints: + web: + exposure: + include: health, metrics, prometheus EOF - name: Build with Gradle From 13893fbc7480d8415e24919d221e5d0930bb1054 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Fri, 11 Apr 2025 16:14:08 +0900 Subject: [PATCH 059/542] =?UTF-8?q?chore:=20prometheus=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 99addf0f..8c638481 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { // monitoring implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") } kotlin { From 628f60aa876b9a2f725d4c50344a88a5771f87f3 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Fri, 11 Apr 2025 17:03:27 +0900 Subject: [PATCH 060/542] =?UTF-8?q?refactor:=20toDomain=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/member/infrastructure/entity/MemberEntity.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index d0fe15cb..28392af7 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -46,6 +46,7 @@ class MemberEntity( oauthId = this.oauthId, memberStatus = this.memberStatus, codeImage = profileEntity?.getCodeImage()?.let { CodeImage(it) }, + faceImage = profileEntity?.getFaceImage()?.let { FaceImage(it) }, ) fun saveProfileEntity(profileEntity: ProfileEntity) { @@ -53,11 +54,11 @@ class MemberEntity( } fun updateCodeImage(codeImage: CodeImage) { - profileEntity?.updateCodeImage(codeImage) + profileEntity!!.updateCodeImage(codeImage) } fun updateFaceImage(faceImage: FaceImage) { - profileEntity?.updateFaceImage(faceImage) + profileEntity!!.updateFaceImage(faceImage) } fun changeMemberStatus(status: MemberStatus) { From b83738ee259560675e8495608a60761fb015249c Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Fri, 11 Apr 2025 17:03:40 +0900 Subject: [PATCH 061/542] =?UTF-8?q?refactor:=20CodeImage=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/codel/member/domain/CodeImage.kt | 6 +++- .../codel/member/domain/CodeImageTest.kt | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/codel/member/domain/CodeImageTest.kt diff --git a/src/main/kotlin/codel/member/domain/CodeImage.kt b/src/main/kotlin/codel/member/domain/CodeImage.kt index b79db006..cb24931e 100644 --- a/src/main/kotlin/codel/member/domain/CodeImage.kt +++ b/src/main/kotlin/codel/member/domain/CodeImage.kt @@ -2,4 +2,8 @@ package codel.member.domain class CodeImage( val urls: List, -) +) { + init { + require(urls.size in 1..3) { "얼굴 이미지 URL은 1개 이상 3개 이하이어야 합니다." } + } +} diff --git a/src/test/kotlin/codel/member/domain/CodeImageTest.kt b/src/test/kotlin/codel/member/domain/CodeImageTest.kt new file mode 100644 index 00000000..4ee35bdb --- /dev/null +++ b/src/test/kotlin/codel/member/domain/CodeImageTest.kt @@ -0,0 +1,31 @@ +package codel.member.domain + +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class CodeImageTest { + @DisplayName("페이스 이미지는 3장이여야 한다.") + @Test + fun initTest() { + assertAll( + { + assertDoesNotThrow { + CodeImage(listOf("url1", "url2", "url3")) + } + }, + { + assertThrows(IllegalArgumentException::class.java) { + CodeImage(listOf()) + } + }, + { + assertThrows(IllegalArgumentException::class.java) { + CodeImage(listOf("url1", "url2", "url3", "url4")) + } + }, + ) + } +} From f7866b758ba0450dc75d0defec5203c1dccca0a6 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Fri, 11 Apr 2025 19:10:33 +0900 Subject: [PATCH 062/542] =?UTF-8?q?chore:=20logback=20appender=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .github/workflows/cd.yml | 22 ++++++++ .github/workflows/ci.yml | 105 +++++++++++++++++++++++---------------- build.gradle.kts | 4 ++ 3 files changed, 89 insertions(+), 42 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 224af6d9..61048614 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -49,6 +49,28 @@ jobs: include: health, metrics, prometheus EOF + - name: Generate logback-spring.xml + run: | + mkdir -p src/main/resources + cat < src/main/resources/logback-spring.xml + + + + http://52.79.233.93:3100/loki/api/v1/push + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + EOF + + - name: Build jar run: ./gradlew clean build -x test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a99234a1..be8e0799 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,48 +9,69 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - - name: Grant execute permission for gradlew - run: chmod +x ./gradlew - - - name: Generate application-dev.yml - run: | - mkdir -p src/main/resources - cat < src/main/resources/application.yml - server: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Generate application-dev.yml + run: | + mkdir -p src/main/resources + cat < src/main/resources/application.yml + server: port: 8080 - security: + security: jwt: - token: - expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} - secret-key: ${{ secrets.JWT_SECRET_KEY }} - - cloud: + token: + expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} + secret-key: ${{ secrets.JWT_SECRET_KEY }} + + cloud: aws: - region: - static: ap-northeast-2 - s3: - bucket: code-l-bucket - credentials: - access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} - secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} - management: - endpoints: - web: - exposure: - include: health, metrics, prometheus - EOF - - - name: Build with Gradle - run: ./gradlew build - - - name: Run tests - run: ./gradlew test + region: + static: ap-northeast-2 + s3: + bucket: code-l-bucket + credentials: + access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} + secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} + management: + endpoints: + web: + exposure: + include: health, metrics, prometheus + EOF + + - name: Generate logback-spring.xml + run: | + mkdir -p src/main/resources + cat < src/main/resources/logback-spring.xml + + + + http://52.79.233.93:3100/loki/api/v1/push + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + EOF + + - name: Build with Gradle + run: ./gradlew build + + - name: Run tests + run: ./gradlew test diff --git a/build.gradle.kts b/build.gradle.kts index 8c638481..112b8619 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,10 @@ dependencies { // monitoring implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("io.micrometer:micrometer-registry-prometheus") + implementation("com.github.loki4j:loki-logback-appender:1.4.0") + + // logging + implementation("io.github.oshai:kotlin-logging-jvm:5.1.1") } kotlin { From c32313161fe5a6db4270902300fa4c30f55b88f1 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Fri, 11 Apr 2025 19:25:18 +0900 Subject: [PATCH 063/542] =?UTF-8?q?chore:=20=EC=A0=95=EB=A0=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .github/workflows/ci.yml | 73 ++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be8e0799..c943ebe3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,49 +26,50 @@ jobs: mkdir -p src/main/resources cat < src/main/resources/application.yml server: - port: 8080 + port: 8080 security: - jwt: - token: - expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} - secret-key: ${{ secrets.JWT_SECRET_KEY }} + jwt: + token: + expire-length: ${{ secrets.JWT_EXPIRE_LENGTH }} + secret-key: ${{ secrets.JWT_SECRET_KEY }} cloud: - aws: - region: - static: ap-northeast-2 - s3: - bucket: code-l-bucket - credentials: - access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} - secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} + aws: + region: + static: ap-northeast-2 + s3: + bucket: code-l-bucket + credentials: + access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} + secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} management: - endpoints: - web: - exposure: - include: health, metrics, prometheus + endpoints: + web: + exposure: + include: health, metrics, prometheus EOF - - name: Generate logback-spring.xml - run: | - mkdir -p src/main/resources - cat < src/main/resources/logback-spring.xml - - - - http://52.79.233.93:3100/loki/api/v1/push - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - + - name: Generate logback-spring.xml + run: | + mkdir -p src/main/resources + cat < src/main/resources/logback-spring.xml + + + + http://52.79.233.93:3100/loki/api/v1/push + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + - - - - - EOF + + + + + EOF + - name: Build with Gradle run: ./gradlew build From a0b495b8f7e4cb9a9c14f29f642b5f2c02fd784b Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Fri, 11 Apr 2025 19:54:46 +0900 Subject: [PATCH 064/542] =?UTF-8?q?chore:=20=ED=94=84=EB=A1=9C=EB=A9=94?= =?UTF-8?q?=ED=85=8C=EC=9A=B0=EC=8A=A4=20=EC=A7=80=ED=91=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: hoyeonyy --- .github/workflows/cd.yml | 3 +++ .github/workflows/ci.yml | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 61048614..0737b80f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -47,6 +47,9 @@ jobs: web: exposure: include: health, metrics, prometheus + metrics: + enable: + all: true EOF - name: Generate logback-spring.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c943ebe3..cf435abf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,13 @@ jobs: access-key: ${{ secrets.DEV_S3_ACCESS_KEY }} secret-key: ${{ secrets.DEV_S3_SECRET_KEY }} management: - endpoints: - web: - exposure: - include: health, metrics, prometheus + endpoints: + web: + exposure: + include: health, metrics, prometheus + metrics: + enable: + all: true EOF - name: Generate logback-spring.xml From dbaea782beb6e724828bc663c49e9ed0b7b59090 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 17:41:13 +0900 Subject: [PATCH 065/542] =?UTF-8?q?chore:=20gitignore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 060a1cf9..9119f86d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ src/main/resources/*.yml +src/main/resources/*.json + ### STS ### .apt_generated From e3020ad979c9a9a750a2678fb25533152de0fd9e Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 17:42:07 +0900 Subject: [PATCH 066/542] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 ++ src/main/kotlin/codel/config/FcmConfig.kt | 29 +++++++++++++++++ .../business/NotificationService.kt | 31 +++++++++++++++++++ .../business/request/NotificationRequest.kt | 7 +++++ 4 files changed, 70 insertions(+) create mode 100644 src/main/kotlin/codel/config/FcmConfig.kt create mode 100644 src/main/kotlin/codel/notification/business/NotificationService.kt create mode 100644 src/main/kotlin/codel/notification/business/request/NotificationRequest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 112b8619..967c9011 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,9 @@ dependencies { // monitoring implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("io.micrometer:micrometer-registry-prometheus") + + // fcm + implementation("com.google.firebase:firebase-admin:9.2.0") implementation("com.github.loki4j:loki-logback-appender:1.4.0") // logging diff --git a/src/main/kotlin/codel/config/FcmConfig.kt b/src/main/kotlin/codel/config/FcmConfig.kt new file mode 100644 index 00000000..9201e78d --- /dev/null +++ b/src/main/kotlin/codel/config/FcmConfig.kt @@ -0,0 +1,29 @@ +package codel.config + +import com.google.auth.oauth2.GoogleCredentials +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import jakarta.annotation.PostConstruct +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile +import org.springframework.core.io.ClassPathResource + +@Configuration +@Profile("!test") +class FcmConfig { + @PostConstruct + fun initialize() { + try { + val options = + FirebaseOptions + .builder() + .setCredentials( + GoogleCredentials.fromStream(ClassPathResource("code-l-b109b-firebase-adminsdk-fbsvc-8c4eb2e6f2.json").inputStream), + ).build() + + FirebaseApp.initializeApp(options) + } catch (e: Exception) { + throw IllegalArgumentException("fcm 연결에 실패하였습니다.") + } + } +} diff --git a/src/main/kotlin/codel/notification/business/NotificationService.kt b/src/main/kotlin/codel/notification/business/NotificationService.kt new file mode 100644 index 00000000..ff72363e --- /dev/null +++ b/src/main/kotlin/codel/notification/business/NotificationService.kt @@ -0,0 +1,31 @@ +package codel.notification.business + +import codel.notification.business.request.NotificationRequest +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.FirebaseMessagingException +import com.google.firebase.messaging.Message +import com.google.firebase.messaging.Notification +import org.springframework.stereotype.Service + +@Service +class NotificationService { + fun sendPushNotification(notificationRequest: NotificationRequest): String { + val message = + Message + .builder() + .setToken(notificationRequest.token) + .setNotification( + Notification + .builder() + .setTitle(notificationRequest.title) + .setBody(notificationRequest.body) + .build(), + ).build() + + return try { + FirebaseMessaging.getInstance().send(message) + } catch (e: FirebaseMessagingException) { + throw IllegalArgumentException("알림 전송중 오류가 발생했습니다.") + } + } +} diff --git a/src/main/kotlin/codel/notification/business/request/NotificationRequest.kt b/src/main/kotlin/codel/notification/business/request/NotificationRequest.kt new file mode 100644 index 00000000..258e33be --- /dev/null +++ b/src/main/kotlin/codel/notification/business/request/NotificationRequest.kt @@ -0,0 +1,7 @@ +package codel.notification.business.request + +data class NotificationRequest( + val token: String, + val title: String, + val body: String, +) From 3a9198b99e9bbb3ef050fd0224db37142359bdd8 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 17:42:26 +0900 Subject: [PATCH 067/542] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=20=ED=9B=84=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/member/business/MemberService.kt | 18 +++++-- src/main/kotlin/codel/member/domain/Member.kt | 51 ++++++++++++++++++- .../codel/member/domain/MemberRepository.kt | 40 ++------------- .../infrastructure/entity/MemberEntity.kt | 32 +++++++----- .../member/presentation/MemberController.kt | 9 ++++ .../swagger/MemberControllerSwagger.kt | 13 +++++ .../member/domain/MemberRepositoryTest.kt | 15 +++--- .../presentation/MemberControllerTest.kt | 2 + 8 files changed, 122 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index c401e1e6..c4c7c4dc 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -48,8 +48,8 @@ class MemberService( mbti = request.mbti, introduce = request.introduce, ) - - memberRepository.saveProfile(member, profile) + val updateMember = member.updateProfile(profile) + memberRepository.saveMember(updateMember) } fun findMember( @@ -63,7 +63,8 @@ class MemberService( files: List, ) { val codeImage = uploadCodeImage(files) - memberRepository.saveCodeImage(member, codeImage) + val updateMember = member.updateCodeImage(codeImage) + memberRepository.saveMember(updateMember) } private fun uploadCodeImage(files: List): CodeImage = CodeImage(files.map { file -> imageUploader.uploadFile(file) }) @@ -74,8 +75,17 @@ class MemberService( files: List, ) { val faceImage = uploadFaceImage(files) - memberRepository.saveFaceImage(member, faceImage) + val updateMember = member.updateFaceImage(faceImage) + memberRepository.saveMember(updateMember) } private fun uploadFaceImage(files: List): FaceImage = FaceImage(files.map { file -> imageUploader.uploadFile(file) }) + + fun saveFcmToken( + member: Member, + fcmToken: String, + ) { + val updateMember = member.updateFcmToken(fcmToken) + memberRepository.saveMember(updateMember) + } } diff --git a/src/main/kotlin/codel/member/domain/Member.kt b/src/main/kotlin/codel/member/domain/Member.kt index 87978239..8980934a 100644 --- a/src/main/kotlin/codel/member/domain/Member.kt +++ b/src/main/kotlin/codel/member/domain/Member.kt @@ -8,4 +8,53 @@ class Member( val codeImage: CodeImage? = null, val faceImage: FaceImage? = null, val memberStatus: MemberStatus = MemberStatus.SIGNUP, -) + val fcmToken: String? = null, +) { + fun updateProfile(profile: Profile): Member = + Member( + id = this.id, + profile = profile, + oauthType = this.oauthType, + oauthId = this.oauthId, + codeImage = this.codeImage, + faceImage = this.faceImage, + memberStatus = MemberStatus.CODE_SURVEY, + fcmToken = this.fcmToken, + ) + + fun updateCodeImage(codeImage: CodeImage): Member = + Member( + id = this.id, + profile = this.profile, + oauthType = this.oauthType, + oauthId = this.oauthId, + codeImage = codeImage, + faceImage = this.faceImage, + memberStatus = MemberStatus.CODE_PROFILE_IMAGE, + fcmToken = this.fcmToken, + ) + + fun updateFaceImage(faceImage: FaceImage): Member = + Member( + id = this.id, + profile = this.profile, + oauthType = this.oauthType, + oauthId = this.oauthId, + codeImage = this.codeImage, + faceImage = faceImage, + memberStatus = MemberStatus.PENDING, + fcmToken = this.fcmToken, + ) + + fun updateFcmToken(fcmToken: String): Member = + Member( + id = this.id, + profile = this.profile, + oauthType = this.oauthType, + oauthId = this.oauthId, + codeImage = this.codeImage, + faceImage = this.faceImage, + memberStatus = this.memberStatus, + fcmToken = fcmToken, + ) +} diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 2a8e3694..9ae7588b 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -36,41 +36,11 @@ class MemberRepository( return memberEntity.toDomain() } - fun saveProfile( - member: Member, - profile: Profile, - ) { - val memberEntity = findMemberEntity(member) - val profileEntity = ProfileEntity.toEntity(profile) + fun saveMember(member: Member) { + val memberId = member.id ?: throw IllegalArgumentException("id가 없는 멤버 입니다.") + val memberEntity = memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("해당 id 멤버 없음") + val profileEntity = member.profile?.let { profileJpaRepository.save(ProfileEntity.toEntity(member.profile)) } - profileJpaRepository.save(profileEntity) - memberEntity.saveProfileEntity(profileEntity) - memberEntity.changeMemberStatus(MemberStatus.CODE_SURVEY) - } - - private fun findMemberEntity(member: Member): MemberEntity { - val memberId = member.id ?: throw IllegalArgumentException("member id가 비어있습니다.") - - return memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("멤버가 존재하지 않습니다.") - } - - fun saveCodeImage( - member: Member, - codeImage: CodeImage, - ) { - val memberEntity = - memberJpaRepository.findByIdOrNull(member.id) ?: throw IllegalArgumentException("해당 id 멤버 없음") - memberEntity.updateCodeImage(codeImage) - memberEntity.changeMemberStatus(MemberStatus.CODE_PROFILE_IMAGE) - } - - fun saveFaceImage( - member: Member, - faceImage: FaceImage, - ) { - val memberEntity = - memberJpaRepository.findByIdOrNull(member.id) ?: throw IllegalArgumentException("해당 id 멤버 없음") - memberEntity.updateFaceImage(faceImage) - memberEntity.changeMemberStatus(MemberStatus.PENDING) + memberEntity.updateEntity(member, profileEntity) } } diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index 28392af7..d08b02d6 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -25,6 +25,7 @@ class MemberEntity( private var id: Long? = null, @OneToOne var profileEntity: ProfileEntity? = null, + var fcmToken: String? = null, var oauthType: OauthType, var oauthId: String, var memberStatus: MemberStatus, @@ -49,19 +50,26 @@ class MemberEntity( faceImage = profileEntity?.getFaceImage()?.let { FaceImage(it) }, ) - fun saveProfileEntity(profileEntity: ProfileEntity) { - this.profileEntity = profileEntity - } - - fun updateCodeImage(codeImage: CodeImage) { - profileEntity!!.updateCodeImage(codeImage) - } - - fun updateFaceImage(faceImage: FaceImage) { - profileEntity!!.updateFaceImage(faceImage) - } - fun changeMemberStatus(status: MemberStatus) { this.memberStatus = status } + + fun updateEntity( + member: Member, + profileEntity: ProfileEntity?, + ) { + profileEntity?.let { + this.profileEntity = profileEntity + } + member.codeImage?.let { + this.profileEntity?.updateCodeImage(it) + } + member.faceImage?.let { + this.profileEntity?.updateFaceImage(it) + } + member.fcmToken?.let { + this.fcmToken = it + } + this.memberStatus = member.memberStatus + } } diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 08cd28c7..80cea06e 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -58,4 +58,13 @@ class MemberController( memberService.saveFaceImage(member, files) return ResponseEntity.ok().build() } + + @PostMapping("/v1/member/fcmtoken") + override fun saveFcmToken( + @LoginMember member: Member, + @RequestBody fcmToken: String, + ): ResponseEntity { + memberService.saveFcmToken(member, fcmToken) + return ResponseEntity.ok().build() + } } diff --git a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt index 23cfae38..5bc9ae4f 100644 --- a/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt +++ b/src/main/kotlin/codel/member/presentation/swagger/MemberControllerSwagger.kt @@ -66,4 +66,17 @@ interface MemberControllerSwagger { @LoginMember member: Member, @RequestPart files: List, ): ResponseEntity + + @Operation(summary = "사용자별 fcm 토큰 받기", description = "사용자의 디바이스 별 fcm 토큰을 저장합니다.") + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "fcm 토큰 저장됨"), + ApiResponse(responseCode = "400", description = "요청 값이 잘못됨"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + fun saveFcmToken( + @LoginMember member: Member, + @RequestBody fcmToken: String, + ): ResponseEntity } diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index 9c283b38..ad6f39d5 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -32,8 +32,9 @@ class MemberRepositoryTest( @DisplayName("프로필을 저장하면 memberStatus 가 CODE_SURVEY 로 바뀐다.") @Test fun saveProfileTest() { - memberRepository.saveProfile(memberSignup, profile) - val findMember = memberRepository.findMember(memberSignup.oauthType, memberSignup.oauthId) + val updateMember = memberSignup.updateProfile(profile) + memberRepository.saveMember(updateMember) + val findMember = memberRepository.findMember(updateMember.oauthType, updateMember.oauthId) assertAll( { assertThat(findMember).isNotNull }, @@ -50,9 +51,10 @@ class MemberRepositoryTest( oauthType = OauthType.APPLE, oauthId = "seok", ) - assertThatThrownBy { memberRepository.saveProfile(seokMember, profile) } + seokMember.updateProfile(profile) + assertThatThrownBy { memberRepository.saveMember(seokMember) } .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("멤버가 존재하지 않습니다.") + .hasMessage("해당 id 멤버 없음") } @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") @@ -63,8 +65,9 @@ class MemberRepositoryTest( oauthType = OauthType.APPLE, oauthId = "seok", ) - assertThatThrownBy { memberRepository.saveProfile(seokMember, profile) } + seokMember.updateProfile(profile) + assertThatThrownBy { memberRepository.saveMember(seokMember) } .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("member id가 비어있습니다.") + .hasMessage("id가 없는 멤버 입니다.") } } diff --git a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt index d9060d18..ef48081d 100644 --- a/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt +++ b/src/test/kotlin/codel/member/presentation/MemberControllerTest.kt @@ -13,7 +13,9 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.context.ActiveProfiles +@ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class MemberControllerTest : TestFixture() { @LocalServerPort From cff0cb8731f8e6033c7dff70695b94f3e43e9963 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 19:25:26 +0900 Subject: [PATCH 068/542] =?UTF-8?q?chore:=20ci/cd=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=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.yml | 6 ++++++ .github/workflows/ci.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 0737b80f..847d63c2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -51,6 +51,12 @@ jobs: enable: all: true EOF + + - name: Restore firebase-adminsdk.json + run: | + echo "$FIREBASE_CONFIG_JSON" > ./src/main/resources/firebase-adminsdk.json + env: + FIREBASE_CONFIG_JSON: ${{ secrets.FIREBASE_CONFIG_JSON }} - name: Generate logback-spring.xml run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf435abf..4a5d577b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,12 @@ jobs: all: true EOF + - name: Restore firebase-adminsdk.json + run: | + echo "$FIREBASE_CONFIG_JSON" > ./src/main/resources/code-l-b109b-firebase-adminsdk-fbsvc-8c4eb2e6f2.json + env: + FIREBASE_CONFIG_JSON: ${{ secrets.FIREBASE_CONFIG_JSON }} + - name: Generate logback-spring.xml run: | mkdir -p src/main/resources From a47c2763c1b79381ddaac78d642b79ee659dcf6b Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 20:46:11 +0900 Subject: [PATCH 069/542] =?UTF-8?q?chore:=20cd=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=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.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 847d63c2..3c602e27 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -54,7 +54,7 @@ jobs: - name: Restore firebase-adminsdk.json run: | - echo "$FIREBASE_CONFIG_JSON" > ./src/main/resources/firebase-adminsdk.json + echo "$FIREBASE_CONFIG_JSON" > ./src/main/resources/code-l-b109b-firebase-adminsdk-fbsvc-8c4eb2e6f2.json env: FIREBASE_CONFIG_JSON: ${{ secrets.FIREBASE_CONFIG_JSON }} From 8ae4929521824b1825b965a43401c2aa608ea349 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 20:46:23 +0900 Subject: [PATCH 070/542] =?UTF-8?q?chore:=20build.gradle.kts=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 967c9011..c59d0bfa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,10 +51,10 @@ dependencies { // fcm implementation("com.google.firebase:firebase-admin:9.2.0") - implementation("com.github.loki4j:loki-logback-appender:1.4.0") // logging implementation("io.github.oshai:kotlin-logging-jvm:5.1.1") + implementation("com.github.loki4j:loki-logback-appender:1.4.0") } kotlin { From c5664d3665c2a64d4a5ed63b36e92abfb2b7a5d2 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 20:46:42 +0900 Subject: [PATCH 071/542] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/codel/member/domain/MemberRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 9ae7588b..79eb86a4 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -38,7 +38,7 @@ class MemberRepository( fun saveMember(member: Member) { val memberId = member.id ?: throw IllegalArgumentException("id가 없는 멤버 입니다.") - val memberEntity = memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("해당 id 멤버 없음") + val memberEntity = memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("해당 id 멤버가 존재하지 않습니다.") val profileEntity = member.profile?.let { profileJpaRepository.save(ProfileEntity.toEntity(member.profile)) } memberEntity.updateEntity(member, profileEntity) From e586865e5089334aaec7f37f3c80183d4a66c786 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 20:47:13 +0900 Subject: [PATCH 072/542] =?UTF-8?q?style:=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/codel/member/business/MemberService.kt | 8 ++++---- src/main/kotlin/codel/member/domain/MemberRepository.kt | 2 +- .../kotlin/codel/member/domain/MemberRepositoryTest.kt | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index c4c7c4dc..10e75eb6 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -49,7 +49,7 @@ class MemberService( introduce = request.introduce, ) val updateMember = member.updateProfile(profile) - memberRepository.saveMember(updateMember) + memberRepository.updateMember(updateMember) } fun findMember( @@ -64,7 +64,7 @@ class MemberService( ) { val codeImage = uploadCodeImage(files) val updateMember = member.updateCodeImage(codeImage) - memberRepository.saveMember(updateMember) + memberRepository.updateMember(updateMember) } private fun uploadCodeImage(files: List): CodeImage = CodeImage(files.map { file -> imageUploader.uploadFile(file) }) @@ -76,7 +76,7 @@ class MemberService( ) { val faceImage = uploadFaceImage(files) val updateMember = member.updateFaceImage(faceImage) - memberRepository.saveMember(updateMember) + memberRepository.updateMember(updateMember) } private fun uploadFaceImage(files: List): FaceImage = FaceImage(files.map { file -> imageUploader.uploadFile(file) }) @@ -86,6 +86,6 @@ class MemberService( fcmToken: String, ) { val updateMember = member.updateFcmToken(fcmToken) - memberRepository.saveMember(updateMember) + memberRepository.updateMember(updateMember) } } diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 79eb86a4..0988ce70 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -36,7 +36,7 @@ class MemberRepository( return memberEntity.toDomain() } - fun saveMember(member: Member) { + fun updateMember(member: Member) { val memberId = member.id ?: throw IllegalArgumentException("id가 없는 멤버 입니다.") val memberEntity = memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("해당 id 멤버가 존재하지 않습니다.") val profileEntity = member.profile?.let { profileJpaRepository.save(ProfileEntity.toEntity(member.profile)) } diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index ad6f39d5..b10798c8 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -33,7 +33,7 @@ class MemberRepositoryTest( @Test fun saveProfileTest() { val updateMember = memberSignup.updateProfile(profile) - memberRepository.saveMember(updateMember) + memberRepository.updateMember(updateMember) val findMember = memberRepository.findMember(updateMember.oauthType, updateMember.oauthId) assertAll( @@ -52,7 +52,7 @@ class MemberRepositoryTest( oauthId = "seok", ) seokMember.updateProfile(profile) - assertThatThrownBy { memberRepository.saveMember(seokMember) } + assertThatThrownBy { memberRepository.updateMember(seokMember) } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage("해당 id 멤버 없음") } @@ -66,7 +66,7 @@ class MemberRepositoryTest( oauthId = "seok", ) seokMember.updateProfile(profile) - assertThatThrownBy { memberRepository.saveMember(seokMember) } + assertThatThrownBy { memberRepository.updateMember(seokMember) } .isInstanceOf(IllegalArgumentException::class.java) .hasMessage("id가 없는 멤버 입니다.") } From 49b351134f9cf2e25df50afa1672904f9c68bc43 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 20 Apr 2025 20:47:58 +0900 Subject: [PATCH 073/542] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/codel/member/infrastructure/entity/MemberEntity.kt | 4 ---- src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index d08b02d6..6097397c 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -50,10 +50,6 @@ class MemberEntity( faceImage = profileEntity?.getFaceImage()?.let { FaceImage(it) }, ) - fun changeMemberStatus(status: MemberStatus) { - this.memberStatus = status - } - fun updateEntity( member: Member, profileEntity: ProfileEntity?, diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index b10798c8..1e97faf8 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -54,7 +54,7 @@ class MemberRepositoryTest( seokMember.updateProfile(profile) assertThatThrownBy { memberRepository.updateMember(seokMember) } .isInstanceOf(IllegalArgumentException::class.java) - .hasMessage("해당 id 멤버 없음") + .hasMessage("해당 id 멤버가 존재하지 않습니다.") } @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") From b43c0de50272093e8feeef463214d49ed93cce1d Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Tue, 22 Apr 2025 21:10:16 +0900 Subject: [PATCH 074/542] =?UTF-8?q?refactor:=20dto=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/member/business/MemberService.kt | 29 +++---------------- .../member/presentation/MemberController.kt | 6 ++-- .../request/MemberLoginRequest.kt | 9 +++++- .../request/ProfileSavedRequest.kt | 19 +++++++++++- .../member/business/MemberServiceTest.kt | 17 +++++------ 5 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 10e75eb6..95c43a3d 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -5,11 +5,9 @@ import codel.member.domain.FaceImage import codel.member.domain.ImageUploader import codel.member.domain.Member import codel.member.domain.MemberRepository +import codel.member.domain.MemberStatus import codel.member.domain.OauthType import codel.member.domain.Profile -import codel.member.presentation.request.MemberLoginRequest -import codel.member.presentation.request.ProfileSavedRequest -import codel.member.presentation.response.MemberLoginResponse import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile @@ -19,35 +17,16 @@ class MemberService( private val memberRepository: MemberRepository, private val imageUploader: ImageUploader, ) { - fun loginMember(request: MemberLoginRequest): MemberLoginResponse { - val member = - Member( - oauthType = request.oauthType, - oauthId = request.oauthId, - ) + fun loginMember(member: Member): MemberStatus { val loginMember = memberRepository.loginMember(member) - return MemberLoginResponse(loginMember.memberStatus) + return loginMember.memberStatus } fun saveProfile( member: Member, - request: ProfileSavedRequest, + profile: Profile, ) { - val profile = - Profile( - codeName = request.codeName, - age = request.age, - job = request.job, - alcohol = request.alcohol, - smoke = request.smoke, - hobby = request.hobby, - style = request.style, - bigCity = request.bigCity, - smallCity = request.smallCity, - mbti = request.mbti, - introduce = request.introduce, - ) val updateMember = member.updateProfile(profile) memberRepository.updateMember(updateMember) } diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 80cea06e..3cb0426c 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -24,12 +24,12 @@ class MemberController( override fun loginMember( @RequestBody request: MemberLoginRequest, ): ResponseEntity { - val memberSavedResponse = memberService.loginMember(request) + val memberStatus = memberService.loginMember(request.toMember()) val token = authService.provideToken(request) return ResponseEntity .ok() .header("Authorization", "Bearer $token") - .body(memberSavedResponse) + .body(MemberLoginResponse(memberStatus)) } @PostMapping("/v1/member/profile") @@ -37,7 +37,7 @@ class MemberController( @LoginMember member: Member, @RequestBody request: ProfileSavedRequest, ): ResponseEntity { - memberService.saveProfile(member, request) + memberService.saveProfile(member, request.toProfile()) return ResponseEntity.ok().build() } diff --git a/src/main/kotlin/codel/member/presentation/request/MemberLoginRequest.kt b/src/main/kotlin/codel/member/presentation/request/MemberLoginRequest.kt index 00d6a199..285bcfc3 100644 --- a/src/main/kotlin/codel/member/presentation/request/MemberLoginRequest.kt +++ b/src/main/kotlin/codel/member/presentation/request/MemberLoginRequest.kt @@ -1,8 +1,15 @@ package codel.member.presentation.request +import codel.member.domain.Member import codel.member.domain.OauthType data class MemberLoginRequest( val oauthType: OauthType, val oauthId: String, -) +) { + fun toMember(): Member = + Member( + oauthType = this.oauthType, + oauthId = this.oauthId, + ) +} diff --git a/src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt b/src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt index 91e484de..72a09d06 100644 --- a/src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt +++ b/src/main/kotlin/codel/member/presentation/request/ProfileSavedRequest.kt @@ -1,5 +1,7 @@ package codel.member.presentation.request +import codel.member.domain.Profile + data class ProfileSavedRequest( val codeName: String, val age: Int, @@ -12,4 +14,19 @@ data class ProfileSavedRequest( val smallCity: String, val mbti: String, val introduce: String, -) +) { + fun toProfile(): Profile = + Profile( + codeName = this.codeName, + age = this.age, + job = this.job, + alcohol = this.alcohol, + smoke = this.smoke, + hobby = this.hobby, + style = this.style, + bigCity = this.bigCity, + smallCity = this.smallCity, + mbti = this.mbti, + introduce = this.introduce, + ) +} diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index d1c02caf..71a72da2 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -2,11 +2,11 @@ package codel.member.business import codel.config.TestFixture import codel.member.domain.ImageUploader +import codel.member.domain.Member import codel.member.domain.MemberRepository import codel.member.domain.MemberStatus import codel.member.domain.OauthType -import codel.member.presentation.request.MemberLoginRequest -import codel.member.presentation.request.ProfileSavedRequest +import codel.member.domain.Profile import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName @@ -28,13 +28,12 @@ class MemberServiceTest( @DisplayName("첫 로그인을 한 멤버의 상태는 SignUp 이다.") @Test fun loginMemberSuccessTest() { - val memberLoginRequest = - MemberLoginRequest( + val member = + Member( oauthType = OauthType.KAKAO, oauthId = "hogee", ) - - val memberStatus = memberService.loginMember(memberLoginRequest).memberStatus + val memberStatus = memberService.loginMember(member) assertThat(memberStatus).isEqualTo(MemberStatus.SIGNUP) } @@ -42,8 +41,8 @@ class MemberServiceTest( @DisplayName("프로필을 저장에 성공한 후 멤버 상태는 CODE_SURVEY 이다.") @Test fun saveProfileSuccessTest() { - val profileSavedRequest = - ProfileSavedRequest( + val profile = + Profile( codeName = "seok", age = 28, job = "백엔드 개발자", @@ -56,7 +55,7 @@ class MemberServiceTest( mbti = "isfj", introduce = "잘부탁드립니다!", ) - memberService.saveProfile(memberSignup, profileSavedRequest) + memberService.saveProfile(memberSignup, profile) val findMember = memberService.findMember( From 42a03c43145768bff1fb4209dcb7671ceccf45b7 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Tue, 22 Apr 2025 21:41:26 +0900 Subject: [PATCH 075/542] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B8=80?= =?UTF-8?q?=EB=A1=9C=EB=B2=8C=20=ED=97=A8=EB=93=A4=EB=9F=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/config/exception/CodelException.kt | 8 +++ .../exception/GlobalExceptionHandler.kt | 52 +++++++++++++++++++ .../codel/member/domain/MemberRepository.kt | 9 ++-- .../codel/member/exception/MemberException.kt | 9 ++++ .../member/domain/MemberRepositoryTest.kt | 5 +- 5 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/codel/config/exception/CodelException.kt create mode 100644 src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt create mode 100644 src/main/kotlin/codel/member/exception/MemberException.kt diff --git a/src/main/kotlin/codel/config/exception/CodelException.kt b/src/main/kotlin/codel/config/exception/CodelException.kt new file mode 100644 index 00000000..7a6dfde2 --- /dev/null +++ b/src/main/kotlin/codel/config/exception/CodelException.kt @@ -0,0 +1,8 @@ +package codel.config.exception + +import org.springframework.http.HttpStatus + +open class CodelException( + val status: HttpStatus, + override val message: String, +) : RuntimeException(message) diff --git a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt new file mode 100644 index 00000000..efb5abf3 --- /dev/null +++ b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt @@ -0,0 +1,52 @@ +package codel.config.exception + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@RestControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(CodelException::class) + fun handleCodelException( + e: CodelException, + request: HttpServletRequest, + ): ResponseEntity { + val response = + ErrorResponse( + timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + status = e.status.value(), + path = request.requestURI, + message = e.message, + stackTrace = e.stackTraceToString(), + ) + return ResponseEntity.status(e.status).body(response) + } + + @ExceptionHandler(Exception::class) + fun handleUnexpectedException( + e: Exception, + request: HttpServletRequest, + ): ResponseEntity { + val response = + ErrorResponse( + timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + status = HttpStatus.INTERNAL_SERVER_ERROR.value(), + path = request.requestURI, + message = e.message ?: "예상치 못한 오류가 발생했습니다.", + stackTrace = e.stackTraceToString(), + ) + return ResponseEntity.status(500).body(response) + } + + data class ErrorResponse( + val timestamp: String, + val status: Int, + val path: String, + val message: String, + val stackTrace: String, + ) +} diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 0988ce70..ac64fdd2 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -1,11 +1,13 @@ package codel.member.domain +import codel.member.exception.MemberException import codel.member.infrastructure.MemberJpaRepository import codel.member.infrastructure.ProfileJpaRepository import codel.member.infrastructure.entity.MemberEntity import codel.member.infrastructure.entity.ProfileEntity import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional @@ -23,7 +25,7 @@ class MemberRepository( val memberEntity = memberJpaRepository.save(MemberEntity.toEntity(member)) memberEntity.toDomain() } catch (e: DataIntegrityViolationException) { - throw IllegalArgumentException("이미 회원이 존재합니다.") + throw MemberException(HttpStatus.BAD_REQUEST, "이미 회원이 존재합니다.") } } @@ -37,8 +39,9 @@ class MemberRepository( } fun updateMember(member: Member) { - val memberId = member.id ?: throw IllegalArgumentException("id가 없는 멤버 입니다.") - val memberEntity = memberJpaRepository.findByIdOrNull(memberId) ?: throw IllegalArgumentException("해당 id 멤버가 존재하지 않습니다.") + val memberId = member.id ?: throw MemberException(HttpStatus.BAD_REQUEST, "id가 없는 멤버 입니다.") + val memberEntity = + memberJpaRepository.findByIdOrNull(memberId) ?: throw MemberException(HttpStatus.BAD_REQUEST, "해당 id 멤버가 존재하지 않습니다.") val profileEntity = member.profile?.let { profileJpaRepository.save(ProfileEntity.toEntity(member.profile)) } memberEntity.updateEntity(member, profileEntity) diff --git a/src/main/kotlin/codel/member/exception/MemberException.kt b/src/main/kotlin/codel/member/exception/MemberException.kt new file mode 100644 index 00000000..6adee43b --- /dev/null +++ b/src/main/kotlin/codel/member/exception/MemberException.kt @@ -0,0 +1,9 @@ +package codel.member.exception + +import codel.config.exception.CodelException +import org.springframework.http.HttpStatus + +class MemberException( + status: HttpStatus, + message: String, +) : CodelException(status, message) diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index 1e97faf8..63d55710 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -1,6 +1,7 @@ package codel.member.domain import codel.config.TestFixture +import codel.member.exception.MemberException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.jupiter.api.DisplayName @@ -53,7 +54,7 @@ class MemberRepositoryTest( ) seokMember.updateProfile(profile) assertThatThrownBy { memberRepository.updateMember(seokMember) } - .isInstanceOf(IllegalArgumentException::class.java) + .isInstanceOf(MemberException::class.java) .hasMessage("해당 id 멤버가 존재하지 않습니다.") } @@ -67,7 +68,7 @@ class MemberRepositoryTest( ) seokMember.updateProfile(profile) assertThatThrownBy { memberRepository.updateMember(seokMember) } - .isInstanceOf(IllegalArgumentException::class.java) + .isInstanceOf(MemberException::class.java) .hasMessage("id가 없는 멤버 입니다.") } } From b2895a2511446abe518be19b943edf8e36e03663 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Tue, 22 Apr 2025 22:16:43 +0900 Subject: [PATCH 076/542] =?UTF-8?q?refactor:=20ArgumentResolver=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/codel/auth/exception/AuthException.kt | 8 ++++---- .../config/argumentresolver/MemberArgumentResolver.kt | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/codel/auth/exception/AuthException.kt b/src/main/kotlin/codel/auth/exception/AuthException.kt index 564edb9b..d545fd44 100644 --- a/src/main/kotlin/codel/auth/exception/AuthException.kt +++ b/src/main/kotlin/codel/auth/exception/AuthException.kt @@ -1,9 +1,9 @@ package codel.auth.exception +import codel.config.exception.CodelException import org.springframework.http.HttpStatus class AuthException( - val statusCode: HttpStatus, - override val message: String, - override val cause: Throwable? = null, -) : RuntimeException(message, cause) + status: HttpStatus, + message: String, +) : CodelException(status, message) diff --git a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt index 993a3c3b..84a4aca1 100644 --- a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt +++ b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt @@ -1,10 +1,12 @@ package codel.config.argumentresolver +import codel.auth.exception.AuthException import codel.member.business.MemberService import codel.member.domain.Member import codel.member.domain.OauthType import jakarta.servlet.http.HttpServletRequest import org.springframework.core.MethodParameter +import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import org.springframework.web.bind.support.WebDataBinderFactory import org.springframework.web.context.request.NativeWebRequest @@ -27,14 +29,14 @@ class MemberArgumentResolver( ): Any? { val httpServletRequest = webRequest.getNativeRequest(HttpServletRequest::class.java) - ?: throw IllegalStateException("HttpServletRequest를 가져올 수 없습니다.") + ?: throw AuthException(HttpStatus.UNAUTHORIZED, "HttpServletRequest를 가져올 수 없습니다.") val oauthId = httpServletRequest.getAttribute("oauthId") as? String - ?: throw IllegalArgumentException("oauthId가 요청이 없습니다.") + ?: throw AuthException(HttpStatus.UNAUTHORIZED, "oauthId가 요청이 없습니다.") val oauthType = httpServletRequest.getAttribute("oauthType") as? OauthType - ?: throw IllegalArgumentException("oauthType이 요청이 없습니다.") + ?: throw AuthException(HttpStatus.UNAUTHORIZED, "oauthType이 요청이 없습니다.") return memberService.findMember(oauthType, oauthId) } From 32a4e9b53f49033904924bcd0fed7ec1030154f1 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Tue, 22 Apr 2025 22:20:02 +0900 Subject: [PATCH 077/542] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=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/codel/config/exception/GlobalExceptionHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt index efb5abf3..611b52a0 100644 --- a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt @@ -39,7 +39,7 @@ class GlobalExceptionHandler { message = e.message ?: "예상치 못한 오류가 발생했습니다.", stackTrace = e.stackTraceToString(), ) - return ResponseEntity.status(500).body(response) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body(response) } data class ErrorResponse( From 746c219fd49e22d1cfcd4e3b6110db80ddb9d5f4 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Tue, 22 Apr 2025 22:24:18 +0900 Subject: [PATCH 078/542] =?UTF-8?q?refactor:=20handleUnexpectedException?= =?UTF-8?q?=20status=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/codel/config/exception/GlobalExceptionHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt index 611b52a0..b3d4ed15 100644 --- a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt @@ -39,7 +39,7 @@ class GlobalExceptionHandler { message = e.message ?: "예상치 못한 오류가 발생했습니다.", stackTrace = e.stackTraceToString(), ) - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body(response) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) } data class ErrorResponse( From 7d198665198f6cf9b8a668bd5ece331accfd0ffd Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Tue, 22 Apr 2025 22:54:00 +0900 Subject: [PATCH 079/542] =?UTF-8?q?feat:=20NotificationException=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/business/NotificationService.kt | 14 ++++++++------ .../business/request/NotificationRequest.kt | 7 ------- .../codel/notification/domain/Notification.kt | 7 +++++++ .../exception/NotificationException.kt | 9 +++++++++ 4 files changed, 24 insertions(+), 13 deletions(-) delete mode 100644 src/main/kotlin/codel/notification/business/request/NotificationRequest.kt create mode 100644 src/main/kotlin/codel/notification/domain/Notification.kt create mode 100644 src/main/kotlin/codel/notification/exception/NotificationException.kt diff --git a/src/main/kotlin/codel/notification/business/NotificationService.kt b/src/main/kotlin/codel/notification/business/NotificationService.kt index ff72363e..8e9adcf3 100644 --- a/src/main/kotlin/codel/notification/business/NotificationService.kt +++ b/src/main/kotlin/codel/notification/business/NotificationService.kt @@ -1,31 +1,33 @@ package codel.notification.business -import codel.notification.business.request.NotificationRequest +import codel.notification.exception.NotificationException import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessagingException import com.google.firebase.messaging.Message import com.google.firebase.messaging.Notification +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service +import codel.notification.domain.Notification as CodelNotification @Service class NotificationService { - fun sendPushNotification(notificationRequest: NotificationRequest): String { + fun sendPushNotification(notification: CodelNotification): String { val message = Message .builder() - .setToken(notificationRequest.token) + .setToken(notification.token) .setNotification( Notification .builder() - .setTitle(notificationRequest.title) - .setBody(notificationRequest.body) + .setTitle(notification.title) + .setBody(notification.body) .build(), ).build() return try { FirebaseMessaging.getInstance().send(message) } catch (e: FirebaseMessagingException) { - throw IllegalArgumentException("알림 전송중 오류가 발생했습니다.") + throw NotificationException(HttpStatus.BAD_GATEWAY, "알림 전송중 오류가 발생했습니다.") } } } diff --git a/src/main/kotlin/codel/notification/business/request/NotificationRequest.kt b/src/main/kotlin/codel/notification/business/request/NotificationRequest.kt deleted file mode 100644 index 258e33be..00000000 --- a/src/main/kotlin/codel/notification/business/request/NotificationRequest.kt +++ /dev/null @@ -1,7 +0,0 @@ -package codel.notification.business.request - -data class NotificationRequest( - val token: String, - val title: String, - val body: String, -) diff --git a/src/main/kotlin/codel/notification/domain/Notification.kt b/src/main/kotlin/codel/notification/domain/Notification.kt new file mode 100644 index 00000000..b9dc92a0 --- /dev/null +++ b/src/main/kotlin/codel/notification/domain/Notification.kt @@ -0,0 +1,7 @@ +package codel.notification.domain + +data class Notification( + val token: String, + val title: String, + val body: String, +) diff --git a/src/main/kotlin/codel/notification/exception/NotificationException.kt b/src/main/kotlin/codel/notification/exception/NotificationException.kt new file mode 100644 index 00000000..efac76ed --- /dev/null +++ b/src/main/kotlin/codel/notification/exception/NotificationException.kt @@ -0,0 +1,9 @@ +package codel.notification.exception + +import codel.config.exception.CodelException +import org.springframework.http.HttpStatus + +class NotificationException( + status: HttpStatus, + message: String, +) : CodelException(status, message) From 86f482a851f1908624c6a98991ee32c6c6128aac Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 23 Apr 2025 17:40:07 +0900 Subject: [PATCH 080/542] =?UTF-8?q?feat:=20GlobalExceptionHandler=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=A1=9C=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/codel/config/Loggable.kt | 8 ++++++ .../exception/GlobalExceptionHandler.kt | 26 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/codel/config/Loggable.kt diff --git a/src/main/kotlin/codel/config/Loggable.kt b/src/main/kotlin/codel/config/Loggable.kt new file mode 100644 index 00000000..c1f9ab3e --- /dev/null +++ b/src/main/kotlin/codel/config/Loggable.kt @@ -0,0 +1,8 @@ +package codel.config + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging + +interface Loggable { + val log: KLogger get() = KotlinLogging.logger {} +} diff --git a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt index b3d4ed15..ed0dd5e0 100644 --- a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt @@ -1,5 +1,6 @@ package codel.config.exception +import codel.config.Loggable import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -9,7 +10,7 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter @RestControllerAdvice -class GlobalExceptionHandler { +class GlobalExceptionHandler : Loggable { @ExceptionHandler(CodelException::class) fun handleCodelException( e: CodelException, @@ -23,6 +24,18 @@ class GlobalExceptionHandler { message = e.message, stackTrace = e.stackTraceToString(), ) + log.info { + """ + ❗ [CodelException 발생] + - 예외명: ${e::class.simpleName} + - 메시지: ${e.message} + - 상태코드: ${e.status} + - 요청 URI: ${request.requestURI} + - 요청 방식: ${request.method} + - 스택트레이스: + ${e.stackTraceToString()} + """.trimIndent() + } return ResponseEntity.status(e.status).body(response) } @@ -39,6 +52,17 @@ class GlobalExceptionHandler { message = e.message ?: "예상치 못한 오류가 발생했습니다.", stackTrace = e.stackTraceToString(), ) + log.warn { + """ + 💥 [Unhandled Exception] + - 예외명: ${e::class.simpleName} + - 메시지: ${e.message} + - 요청 URI: ${request.requestURI} + - 요청 방식: ${request.method} + - 스택트레이스: + ${e.stackTraceToString()} + """.trimIndent() + } return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) } From 54c5aec12c53673aa63a2a79c043c95558f9f6ba Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 23 Apr 2025 18:00:01 +0900 Subject: [PATCH 081/542] =?UTF-8?q?feat:=20MDC=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 2 +- src/main/kotlin/codel/auth/TokenProvider.kt | 4 ++- .../codel/config/filter/JwtAuthFilter.kt | 4 +++ .../config/filter/MemberLoggingFilter.kt | 33 +++++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/codel/config/filter/MemberLoggingFilter.kt diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3c602e27..4fae4dfb 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -69,7 +69,7 @@ jobs: - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - memberId=%X{memberId} - %msg%n diff --git a/src/main/kotlin/codel/auth/TokenProvider.kt b/src/main/kotlin/codel/auth/TokenProvider.kt index 818650f1..2e89bc48 100644 --- a/src/main/kotlin/codel/auth/TokenProvider.kt +++ b/src/main/kotlin/codel/auth/TokenProvider.kt @@ -11,7 +11,7 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpStatus import org.springframework.stereotype.Component import java.security.Key -import java.util.* +import java.util.Date @Component class TokenProvider( @@ -71,4 +71,6 @@ class TokenProvider( fun extractOauthId(token: String): String = getPayload(token)[SOCIAL_LOGIN_ID_CLAIM_KEY].toString() fun extractOauthType(token: String): OauthType = OauthType.valueOf(getPayload(token)[OAUTH_TYPE].toString()) + + fun extractMemberId(token: String): String = getPayload(token)[MEMBER_ID_CLAIM_KEY].toString() } diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt index ae7f1efd..b55bfbdc 100644 --- a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -4,10 +4,12 @@ import codel.auth.TokenProvider import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import org.springframework.core.annotation.Order import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter @Component +@Order(1) class JwtAuthFilter( private val tokenProvider: TokenProvider, ) : OncePerRequestFilter() { @@ -43,8 +45,10 @@ class JwtAuthFilter( } val oauthId = tokenProvider.extractOauthId(token) val oauthType = tokenProvider.extractOauthType(token) + val memberId = tokenProvider.extractMemberId(token) request.setAttribute("oauthId", oauthId) request.setAttribute("oauthType", oauthType) + request.setAttribute("memberId", memberId) filterChain.doFilter(request, response) } diff --git a/src/main/kotlin/codel/config/filter/MemberLoggingFilter.kt b/src/main/kotlin/codel/config/filter/MemberLoggingFilter.kt new file mode 100644 index 00000000..67ce47fc --- /dev/null +++ b/src/main/kotlin/codel/config/filter/MemberLoggingFilter.kt @@ -0,0 +1,33 @@ +package codel.config.filter + +import codel.config.Loggable +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.slf4j.MDC +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +@Order(2) +class MemberLoggingFilter : + OncePerRequestFilter(), + Loggable { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain, + ) { + val memberId = request.getAttribute("memberId")?.toString() + if (memberId != null) { + MDC.put("memberId", memberId) + } + try { + log.info { "HTTP ${request.method} ${request.requestURI} memberId=$memberId" } + filterChain.doFilter(request, response) + } finally { + MDC.clear() + } + } +} From d86cd05a8e06eb4fe83cd71fad1071575a1a234d Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Wed, 23 Apr 2025 18:06:52 +0900 Subject: [PATCH 082/542] =?UTF-8?q?feat:=20Error=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=EC=9D=98=20=EC=98=88=EC=99=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt index ed0dd5e0..a13af8ff 100644 --- a/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/codel/config/exception/GlobalExceptionHandler.kt @@ -66,6 +66,35 @@ class GlobalExceptionHandler : Loggable { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) } + @ExceptionHandler(Error::class) + fun handleFatalError( + e: Error, + request: HttpServletRequest, + ): ResponseEntity { + val response = + ErrorResponse( + timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + status = HttpStatus.INTERNAL_SERVER_ERROR.value(), + path = request.requestURI, + message = e.message ?: "치명적인 오류가 발생했습니다.", + stackTrace = e.stackTraceToString(), + ) + + log.error { + """ + 💩 [Fatal Error 발생] + - 예외명: ${e::class.simpleName} + - 메시지: ${e.message} + - 요청 URI: ${request.requestURI} + - 요청 방식: ${request.method} + - 스택트레이스: + ${e.stackTraceToString()} + """.trimIndent() + } + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) + } + data class ErrorResponse( val timestamp: String, val status: Int, From 0973b0277903fe3136a3d8dcda3777695d12c03e Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Sun, 27 Apr 2025 19:58:12 +0900 Subject: [PATCH 083/542] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../argumentresolver/MemberArgumentResolver.kt | 12 ++++-------- src/main/kotlin/codel/config/filter/JwtAuthFilter.kt | 4 ---- .../kotlin/codel/member/business/MemberService.kt | 2 ++ .../kotlin/codel/member/domain/MemberRepository.kt | 9 +++++++++ .../argumentresolver/MemberArgumentResolverTest.kt | 8 ++++---- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt index 84a4aca1..d117e9bf 100644 --- a/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt +++ b/src/main/kotlin/codel/config/argumentresolver/MemberArgumentResolver.kt @@ -3,7 +3,6 @@ package codel.config.argumentresolver import codel.auth.exception.AuthException import codel.member.business.MemberService import codel.member.domain.Member -import codel.member.domain.OauthType import jakarta.servlet.http.HttpServletRequest import org.springframework.core.MethodParameter import org.springframework.http.HttpStatus @@ -31,13 +30,10 @@ class MemberArgumentResolver( webRequest.getNativeRequest(HttpServletRequest::class.java) ?: throw AuthException(HttpStatus.UNAUTHORIZED, "HttpServletRequest를 가져올 수 없습니다.") - val oauthId = - httpServletRequest.getAttribute("oauthId") as? String - ?: throw AuthException(HttpStatus.UNAUTHORIZED, "oauthId가 요청이 없습니다.") - val oauthType = - httpServletRequest.getAttribute("oauthType") as? OauthType - ?: throw AuthException(HttpStatus.UNAUTHORIZED, "oauthType이 요청이 없습니다.") + val memberId = + httpServletRequest.getAttribute("memberId") as? String + ?: throw AuthException(HttpStatus.UNAUTHORIZED, "memberId가 요청이 없습니다.") - return memberService.findMember(oauthType, oauthId) + return memberService.findMember(memberId.toLong()) } } diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt index b55bfbdc..6831633d 100644 --- a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -43,11 +43,7 @@ class JwtAuthFilter( response.writer.write("""{"message": "인증되지 않은 사용자입니다."}""") return } - val oauthId = tokenProvider.extractOauthId(token) - val oauthType = tokenProvider.extractOauthType(token) val memberId = tokenProvider.extractMemberId(token) - request.setAttribute("oauthId", oauthId) - request.setAttribute("oauthType", oauthType) request.setAttribute("memberId", memberId) filterChain.doFilter(request, response) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 95c43a3d..cfe8b53a 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -36,6 +36,8 @@ class MemberService( oauthId: String, ): Member = memberRepository.findMember(oauthType, oauthId) + fun findMember(memberId: Long): Member = memberRepository.findMember(memberId) + @Transactional fun saveCodeImage( member: Member, diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index ac64fdd2..853a77dc 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -38,6 +38,15 @@ class MemberRepository( return memberEntity.toDomain() } + @Transactional(readOnly = true) + fun findMember(memberId: Long): Member { + val memberEntity = + memberJpaRepository + .findById(memberId) + .orElseThrow { throw MemberException(HttpStatus.BAD_REQUEST, "회원이 존재하지 않습니다.") } + return memberEntity.toDomain() + } + fun updateMember(member: Member) { val memberId = member.id ?: throw MemberException(HttpStatus.BAD_REQUEST, "id가 없는 멤버 입니다.") val memberEntity = diff --git a/src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt b/src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt index b2af11aa..2a50ad9c 100644 --- a/src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt +++ b/src/test/kotlin/codel/config/argumentresolver/MemberArgumentResolverTest.kt @@ -27,25 +27,25 @@ class MemberArgumentResolverTest { @DisplayName("ArgumentResolver는 Member 정보를 반환한다.") @Test fun resolveArgumentTest() { + val memberId = 1L val oauthId = "seok" val oauthType = OauthType.KAKAO val fakeMember = Member( - id = 1L, + id = memberId, oauthId = oauthId, oauthType = oauthType, memberStatus = MemberStatus.SIGNUP, ) val httpRequest = mock(HttpServletRequest::class.java) - `when`(httpRequest.getAttribute("oauthId")).thenReturn(oauthId) - `when`(httpRequest.getAttribute("oauthType")).thenReturn(oauthType) + `when`(httpRequest.getAttribute("memberId")).thenReturn(memberId.toString()) val webRequest = mock(NativeWebRequest::class.java) `when`(webRequest.getNativeRequest(HttpServletRequest::class.java)).thenReturn(httpRequest) - `when`(memberService.findMember(oauthType, oauthId)).thenReturn(fakeMember) + `when`(memberService.findMember(memberId)).thenReturn(fakeMember) val result = resolver.resolveArgument( From 53baf6f75acb5685102a80173d386c2a1299d959 Mon Sep 17 00:00:00 2001 From: hoyeonyy Date: Mon, 28 Apr 2025 14:04:41 +0900 Subject: [PATCH 084/542] =?UTF-8?q?refactor:=20AuthService=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/codel/auth/TokenProvider.kt | 9 --------- src/main/kotlin/codel/auth/business/AuthService.kt | 9 ++------- src/main/kotlin/codel/member/business/MemberService.kt | 5 ++--- .../kotlin/codel/member/presentation/MemberController.kt | 6 +++--- .../kotlin/codel/member/business/MemberServiceTest.kt | 4 ++-- 5 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/codel/auth/TokenProvider.kt b/src/main/kotlin/codel/auth/TokenProvider.kt index 2e89bc48..b05e47b3 100644 --- a/src/main/kotlin/codel/auth/TokenProvider.kt +++ b/src/main/kotlin/codel/auth/TokenProvider.kt @@ -2,7 +2,6 @@ package codel.auth import codel.auth.exception.AuthException import codel.member.domain.Member -import codel.member.domain.OauthType import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm @@ -22,8 +21,6 @@ class TokenProvider( ) { companion object { private const val MEMBER_ID_CLAIM_KEY = "id" - private const val SOCIAL_LOGIN_ID_CLAIM_KEY = "oauthId" - private const val OAUTH_TYPE = "oauthType" } private val key: Key @@ -36,8 +33,6 @@ class TokenProvider( return Jwts .builder() .claim(MEMBER_ID_CLAIM_KEY, member.id) - .claim(SOCIAL_LOGIN_ID_CLAIM_KEY, member.oauthId) - .claim(OAUTH_TYPE, member.oauthType) .setIssuedAt(now) .setExpiration(validity) .signWith(key, SignatureAlgorithm.HS256) @@ -68,9 +63,5 @@ class TokenProvider( } } - fun extractOauthId(token: String): String = getPayload(token)[SOCIAL_LOGIN_ID_CLAIM_KEY].toString() - - fun extractOauthType(token: String): OauthType = OauthType.valueOf(getPayload(token)[OAUTH_TYPE].toString()) - fun extractMemberId(token: String): String = getPayload(token)[MEMBER_ID_CLAIM_KEY].toString() } diff --git a/src/main/kotlin/codel/auth/business/AuthService.kt b/src/main/kotlin/codel/auth/business/AuthService.kt index c74f6746..b21987a3 100644 --- a/src/main/kotlin/codel/auth/business/AuthService.kt +++ b/src/main/kotlin/codel/auth/business/AuthService.kt @@ -1,17 +1,12 @@ package codel.auth.business import codel.auth.TokenProvider -import codel.member.business.MemberService -import codel.member.presentation.request.MemberLoginRequest +import codel.member.domain.Member import org.springframework.stereotype.Service @Service class AuthService( val tokenProvider: TokenProvider, - val memberService: MemberService, ) { - fun provideToken(request: MemberLoginRequest): String { - val member = memberService.findMember(request.oauthType, request.oauthId) - return tokenProvider.provide(member) - } + fun provideToken(member: Member): String = tokenProvider.provide(member) } diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index cfe8b53a..cbfb9d94 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -5,7 +5,6 @@ import codel.member.domain.FaceImage import codel.member.domain.ImageUploader import codel.member.domain.Member import codel.member.domain.MemberRepository -import codel.member.domain.MemberStatus import codel.member.domain.OauthType import codel.member.domain.Profile import org.springframework.stereotype.Service @@ -17,10 +16,10 @@ class MemberService( private val memberRepository: MemberRepository, private val imageUploader: ImageUploader, ) { - fun loginMember(member: Member): MemberStatus { + fun loginMember(member: Member): Member { val loginMember = memberRepository.loginMember(member) - return loginMember.memberStatus + return loginMember } fun saveProfile( diff --git a/src/main/kotlin/codel/member/presentation/MemberController.kt b/src/main/kotlin/codel/member/presentation/MemberController.kt index 3cb0426c..7df5514d 100644 --- a/src/main/kotlin/codel/member/presentation/MemberController.kt +++ b/src/main/kotlin/codel/member/presentation/MemberController.kt @@ -24,12 +24,12 @@ class MemberController( override fun loginMember( @RequestBody request: MemberLoginRequest, ): ResponseEntity { - val memberStatus = memberService.loginMember(request.toMember()) - val token = authService.provideToken(request) + val member = memberService.loginMember(request.toMember()) + val token = authService.provideToken(member) return ResponseEntity .ok() .header("Authorization", "Bearer $token") - .body(MemberLoginResponse(memberStatus)) + .body(MemberLoginResponse(member.memberStatus)) } @PostMapping("/v1/member/profile") diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index 71a72da2..f33eddc6 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -33,9 +33,9 @@ class MemberServiceTest( oauthType = OauthType.KAKAO, oauthId = "hogee", ) - val memberStatus = memberService.loginMember(member) + val loginMember = memberService.loginMember(member) - assertThat(memberStatus).isEqualTo(MemberStatus.SIGNUP) + assertThat(loginMember.memberStatus).isEqualTo(MemberStatus.SIGNUP) } @DisplayName("프로필을 저장에 성공한 후 멤버 상태는 CODE_SURVEY 이다.") From 725b01d242c3b894561bfa71c4f75a1ab632f747 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 16:06:44 +0900 Subject: [PATCH 085/542] =?UTF-8?q?chore:=20mysql=20rds=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 13 +++++++++++++ .github/workflows/ci.yml | 13 +++++++++++++ build.gradle.kts | 1 + 3 files changed, 27 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4fae4dfb..7f030dcc 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -50,6 +50,19 @@ jobs: metrics: enable: all: true + spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://codel-db.cbu0ugiswpor.ap-northeast-2.rds.amazonaws.com:3306/codel + username: ${{ secrets.DEV_RDS_USER_NAME }} + password: ${{ secrets.DEV_RDS_USER_PASSWORD }} + + jpa: + open-in-view: false + hibernate: + ddl-auto: create + show-sql: true + database-platform: org.hibernate.dialect.MySQL8Dialect EOF - name: Restore firebase-adminsdk.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a5d577b..b71b1095 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,19 @@ jobs: metrics: enable: all: true + spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://codel-db.cbu0ugiswpor.ap-northeast-2.rds.amazonaws.com:3306/codel + username: ${{ secrets.DEV_RDS_USER_NAME }} + password: ${{ secrets.DEV_RDS_USER_PASSWORD }} + + jpa: + open-in-view: false + hibernate: + ddl-auto: create + show-sql: true + database-platform: org.hibernate.dialect.MySQL8Dialect EOF - name: Restore firebase-adminsdk.json diff --git a/build.gradle.kts b/build.gradle.kts index c59d0bfa..d4477528 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { // db implementation("org.springframework.boot:spring-boot-starter-data-jpa") + runtimeOnly("com.mysql:mysql-connector-j") runtimeOnly("com.h2database:h2") // test From 33219b2bd21726458e60985138486f175b238be8 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 16:07:16 +0900 Subject: [PATCH 086/542] =?UTF-8?q?chore:=20firebase=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=ED=83=80=EC=9E=84=EB=A6=AC?= =?UTF-8?q?=ED=94=84=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d4477528..1a9fbdfb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,11 +51,14 @@ dependencies { implementation("io.micrometer:micrometer-registry-prometheus") // fcm - implementation("com.google.firebase:firebase-admin:9.2.0") + implementation("com.google.firebase:firebase-admin:9.4.3") // logging implementation("io.github.oshai:kotlin-logging-jvm:5.1.1") implementation("com.github.loki4j:loki-logback-appender:1.4.0") + + // web + implementation("org.springframework.boot:spring-boot-starter-thymeleaf") } kotlin { From 596b7c2db306026e2d1e5a774eec989b7f4b3ea6 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 16:38:09 +0900 Subject: [PATCH 087/542] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=EC=8B=AC=EC=82=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/static/image/codel-image.png | Bin 0 -> 11685 bytes src/main/resources/templates/home.html | 72 ++++++++++++++++++ src/main/resources/templates/login.html | 23 ++++++ 3 files changed, 95 insertions(+) create mode 100644 src/main/resources/static/image/codel-image.png create mode 100644 src/main/resources/templates/home.html create mode 100644 src/main/resources/templates/login.html diff --git a/src/main/resources/static/image/codel-image.png b/src/main/resources/static/image/codel-image.png new file mode 100644 index 0000000000000000000000000000000000000000..46b4c43967ec13b6e10bb76e5a85ab24aef2ef46 GIT binary patch literal 11685 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 + + + + 회원 승인 페이지 + + + +

회원 목록

+ + + + + + + + + + + + + + + + + +
ID이메일이름상태
1email@example.com홍길동ACTIVE
+ + + + + + + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 00000000..56e7532d --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,23 @@ + + + + + codel 관리자 로그인 + + +
+ code:l logo + +
+ + +

관리자만 접근할 수 있습니다.

+
+
+ + From 45f63fdf33f138ab79304c358c5842d8f3e0a4a1 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 16:44:40 +0900 Subject: [PATCH 088/542] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20w?= =?UTF-8?q?eb=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/admin/business/AdminService.kt | 52 ++++++++++++ src/main/kotlin/codel/admin/domain/Admin.kt | 17 ++++ .../kotlin/codel/admin/domain/ValidateCode.kt | 6 ++ .../admin/presentation/AdminController.kt | 81 +++++++++++++++++++ .../presentation/request/AdminLoginRequest.kt | 5 ++ .../presentation/request/ValidateRequest.kt | 10 +++ .../kotlin/codel/auth/business/AuthService.kt | 2 +- .../codel/config/filter/JwtAuthFilter.kt | 1 + .../codel/member/business/MemberService.kt | 10 +++ src/main/kotlin/codel/member/domain/Member.kt | 12 +++ .../codel/member/domain/MemberRepository.kt | 12 ++- .../codel/member/domain/MemberStatus.kt | 1 + .../kotlin/codel/member/domain/OauthType.kt | 1 + .../infrastructure/MemberJpaRepository.kt | 3 + 14 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/codel/admin/business/AdminService.kt create mode 100644 src/main/kotlin/codel/admin/domain/Admin.kt create mode 100644 src/main/kotlin/codel/admin/domain/ValidateCode.kt create mode 100644 src/main/kotlin/codel/admin/presentation/AdminController.kt create mode 100644 src/main/kotlin/codel/admin/presentation/request/AdminLoginRequest.kt create mode 100644 src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt diff --git a/src/main/kotlin/codel/admin/business/AdminService.kt b/src/main/kotlin/codel/admin/business/AdminService.kt new file mode 100644 index 00000000..861b86fe --- /dev/null +++ b/src/main/kotlin/codel/admin/business/AdminService.kt @@ -0,0 +1,52 @@ +package codel.admin.business + +import codel.admin.domain.Admin +import codel.admin.domain.ValidateCode +import codel.member.business.MemberService +import codel.member.domain.Member +import codel.member.domain.MemberStatus +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AdminService( + private val memberService: MemberService, + @Value("\${security.admin.password}") + private val answerPassword: String, +) { + fun loginAdmin(admin: Admin) { + admin.validatePassword(answerPassword) + + val member = + Member( + oauthType = admin.oauthType, + oauthId = admin.oauthId, + memberStatus = admin.memberStatus, + ) + + memberService.loginMember(member) + } + + fun findPendingMemberFaceImage(): List = memberService.findPendingMembers() + + @Transactional + fun validateFaceImage( + target: Member, + request: ValidateCode, + ) { + val member = + memberService.findMember( + oauthType = target.oauthType, + oauthId = target.oauthId, + ) + + val memberStatus = + when (request) { + ValidateCode.APPROVE -> MemberStatus.DONE + ValidateCode.REJECT -> MemberStatus.REJECT + } + + memberService.updateMemberStatus(member, memberStatus) + } +} diff --git a/src/main/kotlin/codel/admin/domain/Admin.kt b/src/main/kotlin/codel/admin/domain/Admin.kt new file mode 100644 index 00000000..494fa3c8 --- /dev/null +++ b/src/main/kotlin/codel/admin/domain/Admin.kt @@ -0,0 +1,17 @@ +package codel.admin.domain + +import codel.member.domain.MemberStatus +import codel.member.domain.OauthType + +class Admin( + val password: String, + val oauthType: OauthType = OauthType.ADMIN, + val oauthId: String = "admin", + val memberStatus: MemberStatus = MemberStatus.DONE, +) { + fun validatePassword(targetPassword: String) { + if (password != targetPassword) { + throw IllegalArgumentException("패스워드가 일치하지 않습니다.") + } + } +} diff --git a/src/main/kotlin/codel/admin/domain/ValidateCode.kt b/src/main/kotlin/codel/admin/domain/ValidateCode.kt new file mode 100644 index 00000000..e534c04d --- /dev/null +++ b/src/main/kotlin/codel/admin/domain/ValidateCode.kt @@ -0,0 +1,6 @@ +package codel.admin.domain + +enum class ValidateCode { + APPROVE, + REJECT, +} diff --git a/src/main/kotlin/codel/admin/presentation/AdminController.kt b/src/main/kotlin/codel/admin/presentation/AdminController.kt new file mode 100644 index 00000000..1c3fe3a3 --- /dev/null +++ b/src/main/kotlin/codel/admin/presentation/AdminController.kt @@ -0,0 +1,81 @@ +package codel.admin.presentation + +import codel.admin.business.AdminService +import codel.admin.domain.Admin +import codel.admin.presentation.request.AdminLoginRequest +import codel.admin.presentation.request.ValidateRequest +import codel.auth.business.AuthService +import codel.member.domain.Member +import codel.member.presentation.request.MemberLoginRequest +import jakarta.servlet.http.Cookie +import jakarta.servlet.http.HttpServletResponse +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody + +@Controller +class AdminController( + private val adminService: AdminService, + private val authService: AuthService, +) { + @GetMapping("/v1/admin/login") + fun login(): String = "login" + + @PostMapping("/v1/admin/login") + fun login( + @RequestBody adminLoginRequest: AdminLoginRequest, + response: HttpServletResponse, + ): String { + val admin = Admin(adminLoginRequest.password) + adminService.loginAdmin(admin) + + val token = + authService.provideToken( + MemberLoginRequest( + oauthType = admin.oauthType, + oauthId = admin.oauthId, + ), + ) + + addCookie(token, response) + + return "redirect:/v1/admin/home" + } + + private fun addCookie( + token: String, + response: HttpServletResponse, + ) { + val cookie = Cookie("access_token", token) + cookie.path = "/v1/admin" + cookie.isHttpOnly = true + cookie.maxAge = 86400 + + response.addCookie(cookie) + } + + @GetMapping("/v1/admin/home") + fun home(model: Model): String { + val members = adminService.findPendingMemberFaceImage() + + model.addAttribute("members", members) + + return "home" + } + + @PostMapping("/v1/admin/validation/faceimage") + fun validateFaceImage( + @RequestBody request: ValidateRequest, + ): String { + val member = + Member( + oauthType = request.targetOauthType, + oauthId = request.targetOauthId, + ) + adminService.validateFaceImage(member, request.validateCode) + + return "redirect:/v1/admin/home" + } +} diff --git a/src/main/kotlin/codel/admin/presentation/request/AdminLoginRequest.kt b/src/main/kotlin/codel/admin/presentation/request/AdminLoginRequest.kt new file mode 100644 index 00000000..bd5e9ebf --- /dev/null +++ b/src/main/kotlin/codel/admin/presentation/request/AdminLoginRequest.kt @@ -0,0 +1,5 @@ +package codel.admin.presentation.request + +data class AdminLoginRequest( + val password: String, +) diff --git a/src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt b/src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt new file mode 100644 index 00000000..db9020be --- /dev/null +++ b/src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt @@ -0,0 +1,10 @@ +package codel.admin.presentation.request + +import codel.admin.domain.ValidateCode +import codel.member.domain.OauthType + +class ValidateRequest( + val targetOauthType: OauthType, + val targetOauthId: String, + val validateCode: ValidateCode, +) diff --git a/src/main/kotlin/codel/auth/business/AuthService.kt b/src/main/kotlin/codel/auth/business/AuthService.kt index b21987a3..659ec2ea 100644 --- a/src/main/kotlin/codel/auth/business/AuthService.kt +++ b/src/main/kotlin/codel/auth/business/AuthService.kt @@ -6,7 +6,7 @@ import org.springframework.stereotype.Service @Service class AuthService( - val tokenProvider: TokenProvider, + private val tokenProvider: TokenProvider, ) { fun provideToken(member: Member): String = tokenProvider.provide(member) } diff --git a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt index 6831633d..437f8267 100644 --- a/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt +++ b/src/main/kotlin/codel/config/filter/JwtAuthFilter.kt @@ -17,6 +17,7 @@ class JwtAuthFilter( private val EXCLUDE_URIS = listOf( "/v1/member/login", + "/v1/admin/login", "/v1/health", "/swagger-ui/", "/v3/api-docs", diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index cbfb9d94..0b43e951 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -68,4 +68,14 @@ class MemberService( val updateMember = member.updateFcmToken(fcmToken) memberRepository.updateMember(updateMember) } + + fun findPendingMembers(): List = memberRepository.findPendingMembers() + + fun updateMemberStatus( + member: Member, + memberStatus: MemberStatus, + ) { + val updateMember = member.updateMemberStatus(memberStatus) + memberRepository.updateMember(updateMember) + } } diff --git a/src/main/kotlin/codel/member/domain/Member.kt b/src/main/kotlin/codel/member/domain/Member.kt index 8980934a..92be1516 100644 --- a/src/main/kotlin/codel/member/domain/Member.kt +++ b/src/main/kotlin/codel/member/domain/Member.kt @@ -57,4 +57,16 @@ class Member( memberStatus = this.memberStatus, fcmToken = fcmToken, ) + + fun updateMemberStatus(memberStatus: MemberStatus): Member = + Member( + id = this.id, + profile = this.profile, + oauthType = this.oauthType, + oauthId = this.oauthId, + codeImage = this.codeImage, + faceImage = this.faceImage, + memberStatus = memberStatus, + fcmToken = this.fcmToken, + ) } diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index 853a77dc..dfba720f 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -50,9 +50,19 @@ class MemberRepository( fun updateMember(member: Member) { val memberId = member.id ?: throw MemberException(HttpStatus.BAD_REQUEST, "id가 없는 멤버 입니다.") val memberEntity = - memberJpaRepository.findByIdOrNull(memberId) ?: throw MemberException(HttpStatus.BAD_REQUEST, "해당 id 멤버가 존재하지 않습니다.") + memberJpaRepository.findByIdOrNull(memberId) ?: throw MemberException( + HttpStatus.BAD_REQUEST, + "해당 id 멤버가 존재하지 않습니다.", + ) val profileEntity = member.profile?.let { profileJpaRepository.save(ProfileEntity.toEntity(member.profile)) } memberEntity.updateEntity(member, profileEntity) } + + @Transactional(readOnly = true) + fun findPendingMembers(): List { + val memberEntities = memberJpaRepository.findByMemberStatus(MemberStatus.PENDING) + + return memberEntities.map { memberEntity -> memberEntity.toDomain() } + } } diff --git a/src/main/kotlin/codel/member/domain/MemberStatus.kt b/src/main/kotlin/codel/member/domain/MemberStatus.kt index 2d516c27..f7ad97ec 100644 --- a/src/main/kotlin/codel/member/domain/MemberStatus.kt +++ b/src/main/kotlin/codel/member/domain/MemberStatus.kt @@ -5,5 +5,6 @@ enum class MemberStatus { CODE_SURVEY, CODE_PROFILE_IMAGE, PENDING, + REJECT, DONE, } diff --git a/src/main/kotlin/codel/member/domain/OauthType.kt b/src/main/kotlin/codel/member/domain/OauthType.kt index 3bb4a6c0..e3f5817b 100644 --- a/src/main/kotlin/codel/member/domain/OauthType.kt +++ b/src/main/kotlin/codel/member/domain/OauthType.kt @@ -3,4 +3,5 @@ package codel.member.domain enum class OauthType { KAKAO, APPLE, + ADMIN, } diff --git a/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt index 24583f36..6e294d1e 100644 --- a/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt +++ b/src/main/kotlin/codel/member/infrastructure/MemberJpaRepository.kt @@ -1,5 +1,6 @@ package codel.member.infrastructure +import codel.member.domain.MemberStatus import codel.member.domain.OauthType import codel.member.infrastructure.entity.MemberEntity import org.springframework.data.jpa.repository.JpaRepository @@ -16,4 +17,6 @@ interface MemberJpaRepository : JpaRepository { oauthType: OauthType, oauthId: String, ): MemberEntity + + fun findByMemberStatus(memberStatus: MemberStatus): List } From 7a803052b9c7cace138138e4082248a6840b66e2 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 17:05:09 +0900 Subject: [PATCH 089/542] =?UTF-8?q?feat:=20AdminException=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/codel/admin/domain/Admin.kt | 4 +++- src/main/kotlin/codel/admin/exception/AdminException.kt | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/codel/admin/exception/AdminException.kt diff --git a/src/main/kotlin/codel/admin/domain/Admin.kt b/src/main/kotlin/codel/admin/domain/Admin.kt index 494fa3c8..45888eb4 100644 --- a/src/main/kotlin/codel/admin/domain/Admin.kt +++ b/src/main/kotlin/codel/admin/domain/Admin.kt @@ -1,7 +1,9 @@ package codel.admin.domain +import codel.admin.exception.AdminException import codel.member.domain.MemberStatus import codel.member.domain.OauthType +import org.springframework.http.HttpStatus class Admin( val password: String, @@ -11,7 +13,7 @@ class Admin( ) { fun validatePassword(targetPassword: String) { if (password != targetPassword) { - throw IllegalArgumentException("패스워드가 일치하지 않습니다.") + throw AdminException(HttpStatus.UNAUTHORIZED, "패스워드가 일치하지 않습니다.") } } } diff --git a/src/main/kotlin/codel/admin/exception/AdminException.kt b/src/main/kotlin/codel/admin/exception/AdminException.kt new file mode 100644 index 00000000..3badaad2 --- /dev/null +++ b/src/main/kotlin/codel/admin/exception/AdminException.kt @@ -0,0 +1,9 @@ +package codel.admin.exception + +import codel.config.exception.CodelException +import org.springframework.http.HttpStatus + +class AdminException( + status: HttpStatus, + message: String, +) : CodelException(status, message) From 910a647785a55d0ed7bd5f9249a8fa5a80e54ae4 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 17:05:32 +0900 Subject: [PATCH 090/542] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/codel/member/domain/CodeImage.kt | 7 ++++++- src/main/kotlin/codel/member/domain/FaceImage.kt | 7 ++++++- src/test/kotlin/codel/member/domain/CodeImageTest.kt | 5 +++-- src/test/kotlin/codel/member/domain/FaceImageTest.kt | 5 +++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/codel/member/domain/CodeImage.kt b/src/main/kotlin/codel/member/domain/CodeImage.kt index cb24931e..327f5ac4 100644 --- a/src/main/kotlin/codel/member/domain/CodeImage.kt +++ b/src/main/kotlin/codel/member/domain/CodeImage.kt @@ -1,9 +1,14 @@ package codel.member.domain +import codel.member.exception.MemberException +import org.springframework.http.HttpStatus + class CodeImage( val urls: List, ) { init { - require(urls.size in 1..3) { "얼굴 이미지 URL은 1개 이상 3개 이하이어야 합니다." } + require(urls.size in 1..3) { + throw MemberException(HttpStatus.BAD_REQUEST, "코드 이미지 URL은 1개 이상 3개 이하이어야 합니다.") + } } } diff --git a/src/main/kotlin/codel/member/domain/FaceImage.kt b/src/main/kotlin/codel/member/domain/FaceImage.kt index 22a95b2e..d51f670b 100644 --- a/src/main/kotlin/codel/member/domain/FaceImage.kt +++ b/src/main/kotlin/codel/member/domain/FaceImage.kt @@ -1,9 +1,14 @@ package codel.member.domain +import codel.member.exception.MemberException +import org.springframework.http.HttpStatus + class FaceImage( val urls: List, ) { init { - require(urls.size == 3) { "얼굴 이미지 URL은 정확히 3개여야 합니다." } + if (urls.size != 3) { + throw MemberException(HttpStatus.BAD_REQUEST, "얼굴 이미지 URL은 정확히 3개여야 합니다.") + } } } diff --git a/src/test/kotlin/codel/member/domain/CodeImageTest.kt b/src/test/kotlin/codel/member/domain/CodeImageTest.kt index 4ee35bdb..44e9a1f1 100644 --- a/src/test/kotlin/codel/member/domain/CodeImageTest.kt +++ b/src/test/kotlin/codel/member/domain/CodeImageTest.kt @@ -1,5 +1,6 @@ package codel.member.domain +import codel.member.exception.MemberException import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Assertions.assertDoesNotThrow import org.junit.jupiter.api.Assertions.assertThrows @@ -17,12 +18,12 @@ class CodeImageTest { } }, { - assertThrows(IllegalArgumentException::class.java) { + assertThrows(MemberException::class.java) { CodeImage(listOf()) } }, { - assertThrows(IllegalArgumentException::class.java) { + assertThrows(MemberException::class.java) { CodeImage(listOf("url1", "url2", "url3", "url4")) } }, diff --git a/src/test/kotlin/codel/member/domain/FaceImageTest.kt b/src/test/kotlin/codel/member/domain/FaceImageTest.kt index 1ebe246f..75f53cea 100644 --- a/src/test/kotlin/codel/member/domain/FaceImageTest.kt +++ b/src/test/kotlin/codel/member/domain/FaceImageTest.kt @@ -1,5 +1,6 @@ package codel.member.domain +import codel.member.exception.MemberException import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Assertions.assertDoesNotThrow import org.junit.jupiter.api.Assertions.assertThrows @@ -17,12 +18,12 @@ class FaceImageTest { } }, { - assertThrows(IllegalArgumentException::class.java) { + assertThrows(MemberException::class.java) { FaceImage(listOf("url1", "url2")) } }, { - assertThrows(IllegalArgumentException::class.java) { + assertThrows(MemberException::class.java) { FaceImage(listOf("url1", "url2", "url3", "url4")) } }, From 7b562bcb6bff57b428b551dab31d69bc646bf6bd Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 18:03:18 +0900 Subject: [PATCH 091/542] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/admin/business/AdminService.kt | 23 +++++++++++++++---- .../admin/presentation/AdminController.kt | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/codel/admin/business/AdminService.kt b/src/main/kotlin/codel/admin/business/AdminService.kt index 861b86fe..6d6dc09c 100644 --- a/src/main/kotlin/codel/admin/business/AdminService.kt +++ b/src/main/kotlin/codel/admin/business/AdminService.kt @@ -5,13 +5,15 @@ import codel.admin.domain.ValidateCode import codel.member.business.MemberService import codel.member.domain.Member import codel.member.domain.MemberStatus +import codel.notification.business.NotificationService +import codel.notification.domain.Notification import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional @Service class AdminService( private val memberService: MemberService, + private val notificationService: NotificationService, @Value("\${security.admin.password}") private val answerPassword: String, ) { @@ -28,10 +30,9 @@ class AdminService( memberService.loginMember(member) } - fun findPendingMemberFaceImage(): List = memberService.findPendingMembers() + fun findPendingMembers(): List = memberService.findPendingMembers() - @Transactional - fun validateFaceImage( + fun reviewMemberProfile( target: Member, request: ValidateCode, ) { @@ -48,5 +49,19 @@ class AdminService( } memberService.updateMemberStatus(member, memberStatus) + sendNotification(member) + } + + private fun sendNotification(member: Member) { + member.fcmToken?.let { fcmToken -> + notificationService.sendPushNotification( + notification = + Notification( + token = fcmToken, + title = "심사가 완료되었습니다.", + body = "code:L 프로필 심사가 완료되었습니다.", + ), + ) + } } } diff --git a/src/main/kotlin/codel/admin/presentation/AdminController.kt b/src/main/kotlin/codel/admin/presentation/AdminController.kt index 1c3fe3a3..c6e2f422 100644 --- a/src/main/kotlin/codel/admin/presentation/AdminController.kt +++ b/src/main/kotlin/codel/admin/presentation/AdminController.kt @@ -58,7 +58,7 @@ class AdminController( @GetMapping("/v1/admin/home") fun home(model: Model): String { - val members = adminService.findPendingMemberFaceImage() + val members = adminService.findPendingMembers() model.addAttribute("members", members) @@ -74,7 +74,7 @@ class AdminController( oauthType = request.targetOauthType, oauthId = request.targetOauthId, ) - adminService.validateFaceImage(member, request.validateCode) + adminService.reviewMemberProfile(member, request.validateCode) return "redirect:/v1/admin/home" } From a1142903a5572ea8af87215cd813d22fa50a944f Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Wed, 23 Apr 2025 18:03:37 +0900 Subject: [PATCH 092/542] =?UTF-8?q?test:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/admin/business/AdminServiceTest.kt | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/test/kotlin/codel/admin/business/AdminServiceTest.kt diff --git a/src/test/kotlin/codel/admin/business/AdminServiceTest.kt b/src/test/kotlin/codel/admin/business/AdminServiceTest.kt new file mode 100644 index 00000000..c23f12bb --- /dev/null +++ b/src/test/kotlin/codel/admin/business/AdminServiceTest.kt @@ -0,0 +1,87 @@ +package codel.admin.business + +import codel.admin.domain.Admin +import codel.admin.domain.ValidateCode +import codel.admin.exception.AdminException +import codel.config.TestFixture +import codel.member.business.MemberService +import codel.member.domain.Member +import codel.member.domain.MemberStatus +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class AdminServiceTest( + @Autowired val adminService: AdminService, + @Autowired val memberService: MemberService, + @Value("\${security.admin.password}") + private val answerPassword: String, +) : TestFixture() { + @DisplayName("정확한 비밀번호를 작성하면 로그인에 성공한다.") + @Test + fun loginAdminSuccessTest() { + val admin = Admin(password = answerPassword) + Assertions.assertDoesNotThrow { adminService.loginAdmin(admin) } + } + + @DisplayName("정확하지 않은 비밀번호를 작성하면 로그인에 실패한다.") + @Test + fun loginAdminFailureTest() { + val admin = Admin(password = "wrong password") + Assertions.assertThrows(AdminException::class.java) { adminService.loginAdmin(admin) } + } + + @DisplayName("PENDING 상태인 회원을 찾아온다.") + @Test + fun findPendingMembersTest() { + val pendingMembers = adminService.findPendingMembers() + + Assertions.assertAll( + { Assertions.assertEquals(1, pendingMembers.size) }, + { Assertions.assertEquals(memberPending.id, pendingMembers[0].id) }, + ) + } + + @DisplayName("사용자의 프로필을 승인하면 사용자의 상태가 DONE 으로 바뀐다.") + @Test + fun reviewMemberProfileApproveTest() { + val target = + Member( + oauthType = memberPending.oauthType, + oauthId = memberPending.oauthId, + ) + + adminService.reviewMemberProfile(target, ValidateCode.APPROVE) + + val findMember = + memberService.findMember( + oauthType = memberPending.oauthType, + oauthId = memberPending.oauthId, + ) + assertThat(findMember.memberStatus).isEqualTo(MemberStatus.DONE) + } + + @DisplayName("사용자의 프로필을 거절하면 사용자의 상태가 REJECT 로 바뀐다.") + @Test + fun reviewMemberProfileRejectTest() { + val target = + Member( + oauthType = memberPending.oauthType, + oauthId = memberPending.oauthId, + ) + + adminService.reviewMemberProfile(target, ValidateCode.REJECT) + + val findMember = + memberService.findMember( + oauthType = memberPending.oauthType, + oauthId = memberPending.oauthId, + ) + assertThat(findMember.memberStatus).isEqualTo(MemberStatus.REJECT) + } +} From 381d281b82986d6b4f188b4cee740e0b946e5908 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Thu, 24 Apr 2025 01:27:26 +0900 Subject: [PATCH 093/542] =?UTF-8?q?refactor:=20=EC=8A=B9=EC=9D=B8=20?= =?UTF-8?q?=EA=B1=B0=EC=A0=88=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EA=B1=B0=EC=A0=88=20=EC=9D=B4=EC=9C=A0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/admin/business/AdminService.kt | 29 +++++++------------ .../kotlin/codel/admin/domain/ValidateCode.kt | 6 ---- .../admin/presentation/AdminController.kt | 27 +++++++++-------- .../presentation/request/ValidateRequest.kt | 10 ------- .../codel/member/business/MemberService.kt | 25 ++++++++++++---- src/main/kotlin/codel/member/domain/Member.kt | 19 ++++++++++++ .../codel/member/domain/MemberRepository.kt | 24 ++++++++------- .../infrastructure/RejectReasonRepository.kt | 8 +++++ .../infrastructure/entity/MemberEntity.kt | 7 +++++ .../entity/RejectReasonEntity.kt | 14 +++++++++ 10 files changed, 107 insertions(+), 62 deletions(-) delete mode 100644 src/main/kotlin/codel/admin/domain/ValidateCode.kt delete mode 100644 src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt create mode 100644 src/main/kotlin/codel/member/infrastructure/RejectReasonRepository.kt create mode 100644 src/main/kotlin/codel/member/infrastructure/entity/RejectReasonEntity.kt diff --git a/src/main/kotlin/codel/admin/business/AdminService.kt b/src/main/kotlin/codel/admin/business/AdminService.kt index 6d6dc09c..f1a08192 100644 --- a/src/main/kotlin/codel/admin/business/AdminService.kt +++ b/src/main/kotlin/codel/admin/business/AdminService.kt @@ -1,10 +1,8 @@ package codel.admin.business import codel.admin.domain.Admin -import codel.admin.domain.ValidateCode import codel.member.business.MemberService import codel.member.domain.Member -import codel.member.domain.MemberStatus import codel.notification.business.NotificationService import codel.notification.domain.Notification import org.springframework.beans.factory.annotation.Value @@ -32,24 +30,17 @@ class AdminService( fun findPendingMembers(): List = memberService.findPendingMembers() - fun reviewMemberProfile( - target: Member, - request: ValidateCode, - ) { - val member = - memberService.findMember( - oauthType = target.oauthType, - oauthId = target.oauthId, - ) - - val memberStatus = - when (request) { - ValidateCode.APPROVE -> MemberStatus.DONE - ValidateCode.REJECT -> MemberStatus.REJECT - } + fun approveMemberProfile(targetId: Long) { + val approvedMember = memberService.approveMember(targetId) + sendNotification(approvedMember) + } - memberService.updateMemberStatus(member, memberStatus) - sendNotification(member) + fun rejectMemberProfile( + targetId: Long, + reason: String, + ) { + val rejectedMember = memberService.rejectMember(targetId, reason) + sendNotification(rejectedMember) } private fun sendNotification(member: Member) { diff --git a/src/main/kotlin/codel/admin/domain/ValidateCode.kt b/src/main/kotlin/codel/admin/domain/ValidateCode.kt deleted file mode 100644 index e534c04d..00000000 --- a/src/main/kotlin/codel/admin/domain/ValidateCode.kt +++ /dev/null @@ -1,6 +0,0 @@ -package codel.admin.domain - -enum class ValidateCode { - APPROVE, - REJECT, -} diff --git a/src/main/kotlin/codel/admin/presentation/AdminController.kt b/src/main/kotlin/codel/admin/presentation/AdminController.kt index c6e2f422..3a93f8a5 100644 --- a/src/main/kotlin/codel/admin/presentation/AdminController.kt +++ b/src/main/kotlin/codel/admin/presentation/AdminController.kt @@ -3,15 +3,14 @@ package codel.admin.presentation import codel.admin.business.AdminService import codel.admin.domain.Admin import codel.admin.presentation.request.AdminLoginRequest -import codel.admin.presentation.request.ValidateRequest import codel.auth.business.AuthService -import codel.member.domain.Member import codel.member.presentation.request.MemberLoginRequest import jakarta.servlet.http.Cookie import jakarta.servlet.http.HttpServletResponse import org.springframework.stereotype.Controller import org.springframework.ui.Model 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 @@ -59,22 +58,26 @@ class AdminController( @GetMapping("/v1/admin/home") fun home(model: Model): String { val members = adminService.findPendingMembers() - model.addAttribute("members", members) return "home" } - @PostMapping("/v1/admin/validation/faceimage") - fun validateFaceImage( - @RequestBody request: ValidateRequest, + @PostMapping("/v1/admin/approval/{targetId}") + fun approveMember( + @PathVariable targetId: Long, ): String { - val member = - Member( - oauthType = request.targetOauthType, - oauthId = request.targetOauthId, - ) - adminService.reviewMemberProfile(member, request.validateCode) + adminService.approveMemberProfile(targetId) + + return "redirect:/v1/admin/home" + } + + @PostMapping("/v1/admin/reject/{targetId}") + fun rejectMember( + @PathVariable targetId: Long, + @RequestBody rejectReason: String, + ): String { + adminService.rejectMemberProfile(targetId, rejectReason) return "redirect:/v1/admin/home" } diff --git a/src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt b/src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt deleted file mode 100644 index db9020be..00000000 --- a/src/main/kotlin/codel/admin/presentation/request/ValidateRequest.kt +++ /dev/null @@ -1,10 +0,0 @@ -package codel.admin.presentation.request - -import codel.admin.domain.ValidateCode -import codel.member.domain.OauthType - -class ValidateRequest( - val targetOauthType: OauthType, - val targetOauthId: String, - val validateCode: ValidateCode, -) diff --git a/src/main/kotlin/codel/member/business/MemberService.kt b/src/main/kotlin/codel/member/business/MemberService.kt index 0b43e951..7eab9bff 100644 --- a/src/main/kotlin/codel/member/business/MemberService.kt +++ b/src/main/kotlin/codel/member/business/MemberService.kt @@ -71,11 +71,26 @@ class MemberService( fun findPendingMembers(): List = memberRepository.findPendingMembers() - fun updateMemberStatus( - member: Member, - memberStatus: MemberStatus, - ) { - val updateMember = member.updateMemberStatus(memberStatus) + fun approveMember(memberId: Long): Member { + val member = memberRepository.findMember(memberId) + + val updateMember = member.updateMemberStatus(MemberStatus.DONE) + memberRepository.updateMember(updateMember) + return updateMember + } + + @Transactional + fun rejectMember( + memberId: Long, + reason: String, + ): Member { + val member = memberRepository.findMember(memberId) + + val updateMember = + member + .updateMemberStatus(MemberStatus.REJECT) + .updateRejectReason(reason) memberRepository.updateMember(updateMember) + return updateMember } } diff --git a/src/main/kotlin/codel/member/domain/Member.kt b/src/main/kotlin/codel/member/domain/Member.kt index 92be1516..b981b9b8 100644 --- a/src/main/kotlin/codel/member/domain/Member.kt +++ b/src/main/kotlin/codel/member/domain/Member.kt @@ -3,6 +3,7 @@ package codel.member.domain class Member( val id: Long? = null, val profile: Profile? = null, + val rejectReason: String? = null, val oauthType: OauthType, val oauthId: String, val codeImage: CodeImage? = null, @@ -14,6 +15,7 @@ class Member( Member( id = this.id, profile = profile, + rejectReason = this.rejectReason, oauthType = this.oauthType, oauthId = this.oauthId, codeImage = this.codeImage, @@ -26,6 +28,7 @@ class Member( Member( id = this.id, profile = this.profile, + rejectReason = this.rejectReason, oauthType = this.oauthType, oauthId = this.oauthId, codeImage = codeImage, @@ -38,6 +41,7 @@ class Member( Member( id = this.id, profile = this.profile, + rejectReason = this.rejectReason, oauthType = this.oauthType, oauthId = this.oauthId, codeImage = this.codeImage, @@ -50,6 +54,7 @@ class Member( Member( id = this.id, profile = this.profile, + rejectReason = this.rejectReason, oauthType = this.oauthType, oauthId = this.oauthId, codeImage = this.codeImage, @@ -62,6 +67,20 @@ class Member( Member( id = this.id, profile = this.profile, + rejectReason = this.rejectReason, + oauthType = this.oauthType, + oauthId = this.oauthId, + codeImage = this.codeImage, + faceImage = this.faceImage, + memberStatus = memberStatus, + fcmToken = this.fcmToken, + ) + + fun updateRejectReason(rejectReason: String): Member = + Member( + id = this.id, + profile = this.profile, + rejectReason = rejectReason, oauthType = this.oauthType, oauthId = this.oauthId, codeImage = this.codeImage, diff --git a/src/main/kotlin/codel/member/domain/MemberRepository.kt b/src/main/kotlin/codel/member/domain/MemberRepository.kt index dfba720f..f75f8fca 100644 --- a/src/main/kotlin/codel/member/domain/MemberRepository.kt +++ b/src/main/kotlin/codel/member/domain/MemberRepository.kt @@ -3,8 +3,10 @@ package codel.member.domain import codel.member.exception.MemberException import codel.member.infrastructure.MemberJpaRepository import codel.member.infrastructure.ProfileJpaRepository +import codel.member.infrastructure.RejectReasonRepository import codel.member.infrastructure.entity.MemberEntity import codel.member.infrastructure.entity.ProfileEntity +import codel.member.infrastructure.entity.RejectReasonEntity import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpStatus @@ -16,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional class MemberRepository( private val memberJpaRepository: MemberJpaRepository, private val profileJpaRepository: ProfileJpaRepository, + private val rejectReasonRepository: RejectReasonRepository, ) { fun loginMember(member: Member): Member { if (memberJpaRepository.existsByOauthTypeAndOauthId(member.oauthType, member.oauthId)) { @@ -40,23 +43,18 @@ class MemberRepository( @Transactional(readOnly = true) fun findMember(memberId: Long): Member { - val memberEntity = - memberJpaRepository - .findById(memberId) - .orElseThrow { throw MemberException(HttpStatus.BAD_REQUEST, "회원이 존재하지 않습니다.") } + val memberEntity = findMemberEntityByMemberId(memberId) return memberEntity.toDomain() } fun updateMember(member: Member) { val memberId = member.id ?: throw MemberException(HttpStatus.BAD_REQUEST, "id가 없는 멤버 입니다.") - val memberEntity = - memberJpaRepository.findByIdOrNull(memberId) ?: throw MemberException( - HttpStatus.BAD_REQUEST, - "해당 id 멤버가 존재하지 않습니다.", - ) + val memberEntity = findMemberEntityByMemberId(memberId) val profileEntity = member.profile?.let { profileJpaRepository.save(ProfileEntity.toEntity(member.profile)) } + val rejectReasonEntity = + member.rejectReason?.let { rejectReasonRepository.save(RejectReasonEntity(reason = it)) } - memberEntity.updateEntity(member, profileEntity) + memberEntity.updateEntity(member, profileEntity, rejectReasonEntity) } @Transactional(readOnly = true) @@ -65,4 +63,10 @@ class MemberRepository( return memberEntities.map { memberEntity -> memberEntity.toDomain() } } + + private fun findMemberEntityByMemberId(memberId: Long) = + memberJpaRepository.findByIdOrNull(memberId) ?: throw MemberException( + HttpStatus.BAD_REQUEST, + "해당 id에 일치하는 멤버가 없습니다.", + ) } diff --git a/src/main/kotlin/codel/member/infrastructure/RejectReasonRepository.kt b/src/main/kotlin/codel/member/infrastructure/RejectReasonRepository.kt new file mode 100644 index 00000000..109fd4a3 --- /dev/null +++ b/src/main/kotlin/codel/member/infrastructure/RejectReasonRepository.kt @@ -0,0 +1,8 @@ +package codel.member.infrastructure + +import codel.member.infrastructure.entity.RejectReasonEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface RejectReasonRepository : JpaRepository diff --git a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt index 6097397c..46dd26e5 100644 --- a/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt +++ b/src/main/kotlin/codel/member/infrastructure/entity/MemberEntity.kt @@ -25,6 +25,8 @@ class MemberEntity( private var id: Long? = null, @OneToOne var profileEntity: ProfileEntity? = null, + @OneToOne + var rejectReasonEntity: RejectReasonEntity? = null, var fcmToken: String? = null, var oauthType: OauthType, var oauthId: String, @@ -43,6 +45,7 @@ class MemberEntity( Member( id = this.id, profile = this.profileEntity?.toDomain(), + rejectReason = this.rejectReasonEntity?.reason, oauthType = this.oauthType, oauthId = this.oauthId, memberStatus = this.memberStatus, @@ -53,10 +56,14 @@ class MemberEntity( fun updateEntity( member: Member, profileEntity: ProfileEntity?, + rejectReasonEntity: RejectReasonEntity?, ) { profileEntity?.let { this.profileEntity = profileEntity } + rejectReasonEntity?.let { + this.rejectReasonEntity = rejectReasonEntity + } member.codeImage?.let { this.profileEntity?.updateCodeImage(it) } diff --git a/src/main/kotlin/codel/member/infrastructure/entity/RejectReasonEntity.kt b/src/main/kotlin/codel/member/infrastructure/entity/RejectReasonEntity.kt new file mode 100644 index 00000000..4dd9439c --- /dev/null +++ b/src/main/kotlin/codel/member/infrastructure/entity/RejectReasonEntity.kt @@ -0,0 +1,14 @@ +package codel.member.infrastructure.entity + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity +class RejectReasonEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private var id: Long? = null, + var reason: String, +) From 0963aa3d554099f5a456d76d260749be2014363d Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Thu, 24 Apr 2025 01:27:39 +0900 Subject: [PATCH 094/542] =?UTF-8?q?test:=20=EC=8A=B9=EC=9D=B8,=20=EA=B1=B0?= =?UTF-8?q?=EC=A0=88=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../codel/admin/business/AdminServiceTest.kt | 51 +++++++------------ .../member/business/MemberServiceTest.kt | 22 ++++++++ .../member/domain/MemberRepositoryTest.kt | 2 +- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/test/kotlin/codel/admin/business/AdminServiceTest.kt b/src/test/kotlin/codel/admin/business/AdminServiceTest.kt index c23f12bb..76af3814 100644 --- a/src/test/kotlin/codel/admin/business/AdminServiceTest.kt +++ b/src/test/kotlin/codel/admin/business/AdminServiceTest.kt @@ -1,14 +1,15 @@ package codel.admin.business import codel.admin.domain.Admin -import codel.admin.domain.ValidateCode import codel.admin.exception.AdminException import codel.config.TestFixture import codel.member.business.MemberService -import codel.member.domain.Member import codel.member.domain.MemberStatus import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertDoesNotThrow +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -26,14 +27,14 @@ class AdminServiceTest( @Test fun loginAdminSuccessTest() { val admin = Admin(password = answerPassword) - Assertions.assertDoesNotThrow { adminService.loginAdmin(admin) } + assertDoesNotThrow { adminService.loginAdmin(admin) } } @DisplayName("정확하지 않은 비밀번호를 작성하면 로그인에 실패한다.") @Test fun loginAdminFailureTest() { val admin = Admin(password = "wrong password") - Assertions.assertThrows(AdminException::class.java) { adminService.loginAdmin(admin) } + assertThrows(AdminException::class.java) { adminService.loginAdmin(admin) } } @DisplayName("PENDING 상태인 회원을 찾아온다.") @@ -41,47 +42,33 @@ class AdminServiceTest( fun findPendingMembersTest() { val pendingMembers = adminService.findPendingMembers() - Assertions.assertAll( - { Assertions.assertEquals(1, pendingMembers.size) }, - { Assertions.assertEquals(memberPending.id, pendingMembers[0].id) }, + assertAll( + { assertEquals(1, pendingMembers.size) }, + { assertEquals(memberPending.id, pendingMembers[0].id) }, ) } @DisplayName("사용자의 프로필을 승인하면 사용자의 상태가 DONE 으로 바뀐다.") @Test fun reviewMemberProfileApproveTest() { - val target = - Member( - oauthType = memberPending.oauthType, - oauthId = memberPending.oauthId, - ) + adminService.approveMemberProfile(memberPending.id!!) - adminService.reviewMemberProfile(target, ValidateCode.APPROVE) - - val findMember = - memberService.findMember( - oauthType = memberPending.oauthType, - oauthId = memberPending.oauthId, - ) + val findMember = memberService.findMember(memberId = memberPending.id!!) assertThat(findMember.memberStatus).isEqualTo(MemberStatus.DONE) } @DisplayName("사용자의 프로필을 거절하면 사용자의 상태가 REJECT 로 바뀐다.") @Test fun reviewMemberProfileRejectTest() { - val target = - Member( - oauthType = memberPending.oauthType, - oauthId = memberPending.oauthId, - ) + val rejectReason = "페이스 이미지에 얼굴이 제대로 확인되지 않습니다." + + adminService.rejectMemberProfile(memberPending.id!!, rejectReason) - adminService.reviewMemberProfile(target, ValidateCode.REJECT) + val findMember = memberService.findMember(memberId = memberPending.id!!) - val findMember = - memberService.findMember( - oauthType = memberPending.oauthType, - oauthId = memberPending.oauthId, - ) - assertThat(findMember.memberStatus).isEqualTo(MemberStatus.REJECT) + assertAll( + { assertThat(findMember.memberStatus).isEqualTo(MemberStatus.REJECT) }, + { assertThat(findMember.rejectReason).isEqualTo(rejectReason) }, + ) } } diff --git a/src/test/kotlin/codel/member/business/MemberServiceTest.kt b/src/test/kotlin/codel/member/business/MemberServiceTest.kt index f33eddc6..baa7fed9 100644 --- a/src/test/kotlin/codel/member/business/MemberServiceTest.kt +++ b/src/test/kotlin/codel/member/business/MemberServiceTest.kt @@ -11,6 +11,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` @@ -119,4 +120,25 @@ class MemberServiceTest( assertThat(savedMember.codeImage).isNotNull assertThat(savedMember.memberStatus).isEqualTo(MemberStatus.PENDING) } + + @DisplayName("사용자의 프로필을 승인하면 사용자의 상태가 DONE 으로 바뀐다.") + @Test + fun approveMemberTest() { + val approvedMember = memberService.approveMember(memberPending.id!!) + + assertThat(approvedMember.memberStatus).isEqualTo(MemberStatus.DONE) + } + + @DisplayName("사용자의 프로필을 거절하면 사용자의 상태가 REJECT 로 바뀐다.") + @Test + fun rejectMemberTest() { + val rejectReason = "페이스 이미지에 얼굴이 제대로 확인되지 않습니다." + + val rejectedMember = memberService.rejectMember(memberPending.id!!, rejectReason) + + assertAll( + { assertThat(rejectedMember.memberStatus).isEqualTo(MemberStatus.REJECT) }, + { assertThat(rejectedMember.rejectReason).isEqualTo(rejectReason) }, + ) + } } diff --git a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt index 63d55710..f8666d13 100644 --- a/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt +++ b/src/test/kotlin/codel/member/domain/MemberRepositoryTest.kt @@ -55,7 +55,7 @@ class MemberRepositoryTest( seokMember.updateProfile(profile) assertThatThrownBy { memberRepository.updateMember(seokMember) } .isInstanceOf(MemberException::class.java) - .hasMessage("해당 id 멤버가 존재하지 않습니다.") + .hasMessage("해당 id에 일치하는 멤버가 없습니다.") } @DisplayName("바꾸고자 하는 멤버 아이디가 없으면 예외를 반환한다.") From f2f1601f5b242ac835d153a8388abd25f6b12464 Mon Sep 17 00:00:00 2001 From: GS_song98 <20003204@sju.ac.kr> Date: Fri, 25 Apr 2025 23:24:43 +0900 Subject: [PATCH 095/542] =?UTF-8?q?feat:=20ui=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=88=98=EC=A0=95,=20favicon.?= =?UTF-8?q?png=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/static/image/favicon.png | Bin 0 -> 5434 bytes src/main/resources/templates/home.html | 95 ++++++---------- src/main/resources/templates/login.html | 7 +- .../resources/templates/memberDetail.html | 102 ++++++++++++++++++ 4 files changed, 143 insertions(+), 61 deletions(-) create mode 100644 src/main/resources/static/image/favicon.png create mode 100644 src/main/resources/templates/memberDetail.html diff --git a/src/main/resources/static/image/favicon.png b/src/main/resources/static/image/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..f14336c91a2a6fd0f43232b3e745c806c8d4dffc GIT binary patch literal 5434 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 - 회원 승인 페이지 + 관리자 페이지 + -

회원 목록

- - - - - - - - - - - - - - - - - -
ID이메일이름상태
1email@example.com홍길동ACTIVE
- - -